diff options
Diffstat (limited to 'engines/kyra/sound')
-rw-r--r-- | engines/kyra/sound/sound.cpp | 371 | ||||
-rw-r--r-- | engines/kyra/sound/sound.h | 341 | ||||
-rw-r--r-- | engines/kyra/sound/sound_adlib.cpp | 2518 | ||||
-rw-r--r-- | engines/kyra/sound/sound_adlib.h | 115 | ||||
-rw-r--r-- | engines/kyra/sound/sound_amiga.cpp | 232 | ||||
-rw-r--r-- | engines/kyra/sound/sound_digital.cpp | 544 | ||||
-rw-r--r-- | engines/kyra/sound/sound_digital.h | 119 | ||||
-rw-r--r-- | engines/kyra/sound/sound_intern.h | 407 | ||||
-rw-r--r-- | engines/kyra/sound/sound_lok.cpp | 96 | ||||
-rw-r--r-- | engines/kyra/sound/sound_lol.cpp | 307 | ||||
-rw-r--r-- | engines/kyra/sound/sound_midi.cpp | 814 | ||||
-rw-r--r-- | engines/kyra/sound/sound_pcspk.cpp | 366 | ||||
-rw-r--r-- | engines/kyra/sound/sound_towns.cpp | 749 | ||||
-rw-r--r-- | engines/kyra/sound/sound_towns_darkmoon.cpp | 278 |
14 files changed, 7257 insertions, 0 deletions
diff --git a/engines/kyra/sound/sound.cpp b/engines/kyra/sound/sound.cpp new file mode 100644 index 0000000000..39784f4682 --- /dev/null +++ b/engines/kyra/sound/sound.cpp @@ -0,0 +1,371 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + + +#include "kyra/sound/sound.h" +#include "kyra/resource/resource.h" + +#include "audio/mixer.h" +#include "audio/audiostream.h" + +#include "audio/decoders/flac.h" +#include "audio/decoders/mp3.h" +#include "audio/decoders/raw.h" +#include "audio/decoders/voc.h" +#include "audio/decoders/vorbis.h" + +namespace Kyra { + +Sound::Sound(KyraEngine_v1 *vm, Audio::Mixer *mixer) + : _vm(vm), _mixer(mixer), _soundChannels(), _musicEnabled(1), + _sfxEnabled(true) { +} + +Sound::~Sound() { +} + +Sound::kType Sound::getSfxType() const { + return getMusicType(); +} + +bool Sound::isPlaying() const { + return false; +} + +bool Sound::isVoicePresent(const char *file) const { + Common::String filename; + + for (int i = 0; _supportedCodecs[i].fileext; ++i) { + filename = file; + filename += _supportedCodecs[i].fileext; + + if (_vm->resource()->exists(filename.c_str())) + return true; + } + + return false; +} + +int32 Sound::voicePlay(const char *file, Audio::SoundHandle *handle, uint8 volume, uint8 priority, bool isSfx) { + Audio::SeekableAudioStream *audioStream = getVoiceStream(file); + + if (!audioStream) { + return 0; + } + + int playTime = audioStream->getLength().msecs(); + playVoiceStream(audioStream, handle, volume, priority, isSfx); + return playTime; +} + +Audio::SeekableAudioStream *Sound::getVoiceStream(const char *file) const { + Common::String filename; + + Audio::SeekableAudioStream *audioStream = 0; + for (int i = 0; _supportedCodecs[i].fileext; ++i) { + filename = file; + filename += _supportedCodecs[i].fileext; + + Common::SeekableReadStream *stream = _vm->resource()->createReadStream(filename); + if (!stream) + continue; + + audioStream = _supportedCodecs[i].streamFunc(stream, DisposeAfterUse::YES); + break; + } + + if (!audioStream) { + warning("Couldn't load sound file '%s'", file); + return 0; + } else { + return audioStream; + } +} + +bool Sound::playVoiceStream(Audio::AudioStream *stream, Audio::SoundHandle *handle, uint8 volume, uint8 priority, bool isSfx) { + int h = 0; + while (h < kNumChannelHandles && _mixer->isSoundHandleActive(_soundChannels[h].handle)) + ++h; + + if (h >= kNumChannelHandles) { + h = 0; + while (h < kNumChannelHandles && _soundChannels[h].priority > priority) + ++h; + if (h < kNumChannelHandles) + voiceStop(&_soundChannels[h].handle); + } + + if (h >= kNumChannelHandles) { + // When we run out of handles we need to destroy the stream object, + // this is to avoid memory leaks in some scenes where too many sfx + // are started. + // See bug #3427240 "LOL-CD: Memory leak in caves level 3". + delete stream; + return false; + } + + _mixer->playStream(isSfx ? Audio::Mixer::kSFXSoundType : Audio::Mixer::kSpeechSoundType, &_soundChannels[h].handle, stream, -1, volume); + _soundChannels[h].priority = priority; + if (handle) + *handle = _soundChannels[h].handle; + + return true; +} + +void Sound::voiceStop(const Audio::SoundHandle *handle) { + if (!handle) { + for (int h = 0; h < kNumChannelHandles; ++h) { + if (_mixer->isSoundHandleActive(_soundChannels[h].handle)) + _mixer->stopHandle(_soundChannels[h].handle); + } + } else { + _mixer->stopHandle(*handle); + } +} + +bool Sound::voiceIsPlaying(const Audio::SoundHandle *handle) const { + if (!handle) { + for (int h = 0; h < kNumChannelHandles; ++h) { + if (_mixer->isSoundHandleActive(_soundChannels[h].handle)) + return true; + } + } else { + return _mixer->isSoundHandleActive(*handle); + } + + return false; +} + +bool Sound::allVoiceChannelsPlaying() const { + for (int i = 0; i < kNumChannelHandles; ++i) + if (!_mixer->isSoundHandleActive(_soundChannels[i].handle)) + return false; + return true; +} + +#pragma mark - + +MixedSoundDriver::MixedSoundDriver(KyraEngine_v1 *vm, Audio::Mixer *mixer, Sound *music, Sound *sfx) + : Sound(vm, mixer), _music(music), _sfx(sfx) { +} + +MixedSoundDriver::~MixedSoundDriver() { + delete _music; + delete _sfx; +} + +Sound::kType MixedSoundDriver::getMusicType() const { + return _music->getMusicType(); +} + +Sound::kType MixedSoundDriver::getSfxType() const { + return _sfx->getSfxType(); +} + +bool MixedSoundDriver::init() { + return (_music->init() && _sfx->init()); +} + +void MixedSoundDriver::process() { + _music->process(); + _sfx->process(); +} + +void MixedSoundDriver::updateVolumeSettings() { + _music->updateVolumeSettings(); + _sfx->updateVolumeSettings(); +} + +void MixedSoundDriver::initAudioResourceInfo(int set, void *info) { + _music->initAudioResourceInfo(set, info); + _sfx->initAudioResourceInfo(set, info); +} + +void MixedSoundDriver::selectAudioResourceSet(int set) { + _music->selectAudioResourceSet(set); + _sfx->selectAudioResourceSet(set); +} + +bool MixedSoundDriver::hasSoundFile(uint file) const { + return _music->hasSoundFile(file) && _sfx->hasSoundFile(file); +} + +void MixedSoundDriver::loadSoundFile(uint file) { + _music->loadSoundFile(file); + _sfx->loadSoundFile(file); +} + +void MixedSoundDriver::loadSoundFile(Common::String file) { + _music->loadSoundFile(file); + _sfx->loadSoundFile(file); +} + +void MixedSoundDriver::loadSfxFile(Common::String file) { + _sfx->loadSoundFile(file); +} + +void MixedSoundDriver::playTrack(uint8 track) { + _music->playTrack(track); +} + +void MixedSoundDriver::haltTrack() { + _music->haltTrack(); +} + +bool MixedSoundDriver::isPlaying() const { + return _music->isPlaying() | _sfx->isPlaying(); +} + +void MixedSoundDriver::playSoundEffect(uint8 track, uint8 volume) { + _sfx->playSoundEffect(track, volume); +} + +void MixedSoundDriver::stopAllSoundEffects() { + _sfx->stopAllSoundEffects(); +} + +void MixedSoundDriver::beginFadeOut() { + _music->beginFadeOut(); +} + +void MixedSoundDriver::pause(bool paused) { + _music->pause(paused); + _sfx->pause(paused); +} + +#pragma mark - + +void KyraEngine_v1::snd_playTheme(int file, int track) { + if (_curMusicTheme == file) + return; + + _curSfxFile = _curMusicTheme = file; + _sound->loadSoundFile(_curMusicTheme); + + // Kyrandia 2 uses a special file for + // MIDI sound effects, so we load + // this here + if (_flags.gameID == GI_KYRA2) + _sound->loadSfxFile("K2SFX"); + + if (track != -1) + _sound->playTrack(track); +} + +void KyraEngine_v1::snd_playSoundEffect(int track, int volume) { + _sound->playSoundEffect(track, volume); +} + +void KyraEngine_v1::snd_playWanderScoreViaMap(int command, int restart) { + if (restart) + _lastMusicCommand = -1; + + // no track mapping given + // so don't do anything here + if (!_trackMap || !_trackMapSize) + return; + + //if (!_disableSound) { + // XXX + //} + + if (_flags.platform == Common::kPlatformDOS || _flags.platform == Common::kPlatformMacintosh) { + assert(command * 2 + 1 < _trackMapSize); + if (_curMusicTheme != _trackMap[command * 2]) { + if (_trackMap[command * 2] != -1 && _trackMap[command * 2] != -2) + snd_playTheme(_trackMap[command * 2], -1); + } + + if (command != 1) { + if (_lastMusicCommand != command) { + _sound->haltTrack(); + _sound->playTrack(_trackMap[command * 2 + 1]); + } + } else { + _sound->beginFadeOut(); + } + } else if (_flags.platform == Common::kPlatformFMTowns || _flags.platform == Common::kPlatformPC98) { + if (command == -1) { + _sound->haltTrack(); + } else { + assert(command * 2 + 1 < _trackMapSize); + if (_trackMap[command * 2] != -2 && command != _lastMusicCommand) { + _sound->haltTrack(); + _sound->playTrack(command); + } + } + } else if (_flags.platform == Common::kPlatformAmiga) { + if (_curMusicTheme != 1) + snd_playTheme(1, -1); + + assert(command < _trackMapSize); + if (_trackMap[_lastMusicCommand] != _trackMap[command]) + _sound->playTrack(_trackMap[command]); + } + + _lastMusicCommand = command; +} + +void KyraEngine_v1::snd_stopVoice() { + _sound->voiceStop(&_speechHandle); +} + +bool KyraEngine_v1::snd_voiceIsPlaying() { + return _sound->voiceIsPlaying(&_speechHandle); +} + +// static res + +namespace { + +// A simple wrapper to create VOC streams the way like creating MP3, OGG/Vorbis and FLAC streams. +// Possible TODO: Think of making this complete and moving it to sound/voc.cpp ? +Audio::SeekableAudioStream *makeVOCStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { + Audio::SeekableAudioStream *as = Audio::makeVOCStream(stream, Audio::FLAG_UNSIGNED, disposeAfterUse); + return as; +} + +} // end of anonymous namespace + +const Sound::SpeechCodecs Sound::_supportedCodecs[] = { + { ".VOC", makeVOCStream }, + { "", makeVOCStream }, + +#ifdef USE_MAD + { ".VO3", Audio::makeMP3Stream }, + { ".MP3", Audio::makeMP3Stream }, +#endif // USE_MAD + +#ifdef USE_VORBIS + { ".VOG", Audio::makeVorbisStream }, + { ".OGG", Audio::makeVorbisStream }, +#endif // USE_VORBIS + +#ifdef USE_FLAC + { ".VOF", Audio::makeFLACStream }, + { ".FLA", Audio::makeFLACStream }, +#endif // USE_FLAC + + { 0, 0 } +}; + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound.h b/engines/kyra/sound/sound.h new file mode 100644 index 0000000000..3f4216c5fb --- /dev/null +++ b/engines/kyra/sound/sound.h @@ -0,0 +1,341 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef KYRA_SOUND_H +#define KYRA_SOUND_H + +#include "kyra/kyra_v1.h" + +#include "common/scummsys.h" +#include "common/str.h" + +#include "audio/mixer.h" + +namespace Audio { +class AudioStream; +class SeekableAudioStream; +} // End of namespace Audio + +namespace Kyra { + +// Helper structs to format the data passed to the various initAudioResourceInfo() implementations +struct SoundResourceInfo_PC { + SoundResourceInfo_PC(const char *const *files, int numFiles) : fileList(files), fileListSize(numFiles) {} + const char *const *fileList; + uint fileListSize; +}; + +struct SoundResourceInfo_Towns { + SoundResourceInfo_Towns(const char *const *files, int numFiles, const int32 *cdaTbl, int cdaTblSize) : fileList(files), fileListSize(numFiles), cdaTable(cdaTbl), cdaTableSize(cdaTblSize) {} + const char *const *fileList; + uint fileListSize; + const int32 *cdaTable; + uint cdaTableSize; +}; + +struct SoundResourceInfo_PC98 { + SoundResourceInfo_PC98(const char *fileNamePattern) : pattern(fileNamePattern) {} + const char *pattern; +}; + +struct SoundResourceInfo_TownsPC98V2 { + SoundResourceInfo_TownsPC98V2(const char *const *files, int numFiles, const char *fileNamePattern, const uint16 *cdaTbl, int cdaTblSize) : fileList(files), fileListSize(numFiles), pattern(fileNamePattern), cdaTable(cdaTbl), cdaTableSize(cdaTblSize) {} + const char *const *fileList; + uint fileListSize; + const char *pattern; + const uint16 *cdaTable; + uint cdaTableSize; +}; + +struct SoundResourceInfo_TownsEoB { + SoundResourceInfo_TownsEoB(const uint8 *pcmdata, uint dataSize, int pcmvolume) : pcmData(pcmdata), pcmDataSize(dataSize), pcmVolume(pcmvolume) {} + const uint8 *pcmData; + uint pcmDataSize; + int pcmVolume; +}; + +/** + * Analog audio output device API for Kyrandia games. + * It contains functionality to play music tracks, + * sound effects and voices. + */ +class Sound { +public: + Sound(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~Sound(); + + enum kType { + kAdLib, + kMidiMT32, + kMidiGM, + kTowns, + kPC98, + kPCSpkr, + kAmiga + }; + + virtual kType getMusicType() const = 0; + virtual kType getSfxType() const; + + /** + * Initializes the output device. + * + * @return true on success, else false + */ + virtual bool init() = 0; + + /** + * Updates the device, this is needed for some devices. + */ + virtual void process() {} + + /** + * Updates internal volume settings according to ConfigManager. + */ + virtual void updateVolumeSettings() {} + + /** + * Assigns static resource data with information on how to load + * audio resources to + * + * @param set value defined in AudioResourceSet enum + * info various types of resource info data (file list, file name pattern, struct, etc. - depending on the inheriting driver type) + */ + virtual void initAudioResourceInfo(int set, void *info) = 0; + + /** + * Select audio resource set. + * + * @param set value defined in AudioResourceSet enum + */ + virtual void selectAudioResourceSet(int set) = 0; + + /** + * Checks if a given sound file is present. + * + * @param track track number + * @return true if available, false otherwise + */ + virtual bool hasSoundFile(uint file) const = 0; + + /** + * Load a specifc sound file for use of + * playing music and sound effects. + */ + virtual void loadSoundFile(uint file) = 0; + + /** + * Load a sound file for playing music + * (and sometimes sound effects) from. + */ + virtual void loadSoundFile(Common::String file) = 0; + + /** + * Load a sound file for playing sound + * effects from. + */ + virtual void loadSfxFile(Common::String file) {} + + /** + * Plays the specified track. + * + * @param track track number + */ + virtual void playTrack(uint8 track) = 0; + + /** + * Stop playback of the current track. + */ + virtual void haltTrack() = 0; + + /** + * Plays the specified sound effect. + * + * @param track sound effect id + */ + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF) = 0; + + /** + * Stop playback of all sfx tracks. + */ + virtual void stopAllSoundEffects() {} + + /** + * Checks if the sound driver plays any sound. + * + * @return true if playing, false otherwise + */ + virtual bool isPlaying() const; + + /** + * Starts fading out the volume. + * + * This keeps fading out the output until + * it is silenced, but does not change + * the volume set by setVolume! It will + * automatically reset the volume when + * playing a new track or sound effect. + */ + virtual void beginFadeOut() = 0; + + /** + * Stops all audio playback when paused. Continues after end of pause. + */ + virtual void pause(bool paused) {} + + void enableMusic(int enable) { _musicEnabled = enable; } + int musicEnabled() const { return _musicEnabled; } + void enableSFX(bool enable) { _sfxEnabled = enable; } + bool sfxEnabled() const { return _sfxEnabled; } + + /** + * Checks whether a voice file with the given name is present + * + * @param file file name + * @return true if available, false otherwise + */ + bool isVoicePresent(const char *file) const; + + /** + * Plays the specified voice file. + * + * Also before starting to play the + * specified voice file, it stops the + * current voice. + * + * @param file file to be played + * @param volume volume of the voice file + * @param isSfx marks file as sfx instead of voice + * @param handle store a copy of the sound handle + * @return playtime of the voice file (-1 marks unknown playtime) + */ + virtual int32 voicePlay(const char *file, Audio::SoundHandle *handle = 0, uint8 volume = 255, uint8 priority = 255, bool isSfx = false); + + Audio::SeekableAudioStream *getVoiceStream(const char *file) const; + + bool playVoiceStream(Audio::AudioStream *stream, Audio::SoundHandle *handle = 0, uint8 volume = 255, uint8 priority = 255, bool isSfx = false); + + /** + * Checks if a voice is being played. + * + * @return true when playing, else false + */ + bool voiceIsPlaying(const Audio::SoundHandle *handle = 0) const; + + /** + * Checks if all voice handles are used. + * + * @return false when a handle is free, else true + */ + bool allVoiceChannelsPlaying() const; + + /** + * Checks how long a voice has been playing + * + * @return time in milliseconds + */ + uint32 voicePlayedTime(const Audio::SoundHandle &handle) const { + return _mixer->getSoundElapsedTime(handle); + } + + /** + * Stops playback of the current voice. + */ + void voiceStop(const Audio::SoundHandle *handle = 0); + + /** + * Receive notifications from a song at certain points. + */ + virtual int checkTrigger() { return 0; } + + /** + * Reset sound trigger. + */ + virtual void resetTrigger() {} +protected: + enum { + kNumChannelHandles = 4 + }; + + struct SoundChannel { + SoundChannel() : handle(), priority(0) {} + Audio::SoundHandle handle; + int priority; + }; + + SoundChannel _soundChannels[kNumChannelHandles]; + + int _musicEnabled; + bool _sfxEnabled; + + KyraEngine_v1 *_vm; + Audio::Mixer *_mixer; + +private: + struct SpeechCodecs { + const char *fileext; + Audio::SeekableAudioStream *(*streamFunc)( + Common::SeekableReadStream *stream, + DisposeAfterUse::Flag disposeAfterUse); + }; + + static const SpeechCodecs _supportedCodecs[]; +}; + +class MixedSoundDriver : public Sound { +public: + MixedSoundDriver(KyraEngine_v1 *vm, Audio::Mixer *mixer, Sound *music, Sound *sfx); + ~MixedSoundDriver(); + + virtual kType getMusicType() const; + virtual kType getSfxType() const; + + virtual bool init(); + virtual void process(); + + virtual void updateVolumeSettings(); + + virtual void initAudioResourceInfo(int set, void *info); + virtual void selectAudioResourceSet(int set); + virtual bool hasSoundFile(uint file) const; + virtual void loadSoundFile(uint file); + virtual void loadSoundFile(Common::String file); + + virtual void loadSfxFile(Common::String file); + + virtual void playTrack(uint8 track); + virtual void haltTrack(); + virtual bool isPlaying() const; + + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF); + + virtual void stopAllSoundEffects(); + + virtual void beginFadeOut(); + virtual void pause(bool paused); +private: + Sound *_music, *_sfx; +}; + +} // End of namespace Kyra + +#endif diff --git a/engines/kyra/sound/sound_adlib.cpp b/engines/kyra/sound/sound_adlib.cpp new file mode 100644 index 0000000000..7a87e71982 --- /dev/null +++ b/engines/kyra/sound/sound_adlib.cpp @@ -0,0 +1,2518 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * LGPL License + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + + +#include "kyra/sound/sound_intern.h" +#include "kyra/resource/resource.h" + +#include "common/system.h" +#include "common/mutex.h" +#include "common/config-manager.h" + +#include "audio/fmopl.h" + +// Basic AdLib Programming: +// http://www.gamedev.net/reference/articles/article446.asp + +#define CALLBACKS_PER_SECOND 72 + +namespace Audio { +class Mixer; +} + +namespace Kyra { + +class AdLibDriver { +public: + AdLibDriver(Audio::Mixer *mixer, int version); + ~AdLibDriver(); + + void initDriver(); + void setSoundData(uint8 *data, uint32 size); + void queueTrack(int track, int volume); + bool isChannelPlaying(int channel) const; + void stopAllChannels(); + int getSoundTrigger() const { return _soundTrigger; } + void resetSoundTrigger() { _soundTrigger = 0; } + + void callback(); + + void setSyncJumpMask(uint16 mask) { _syncJumpMask = mask; } + + void setMusicVolume(uint8 volume); + void setSfxVolume(uint8 volume); + +private: + // These variables have not yet been named, but some of them are partly + // known nevertheless: + // + // pitchBend - Sound-related. Possibly some sort of pitch bend. + // unk18 - Sound-effect. Used for secondaryEffect1() + // unk19 - Sound-effect. Used for secondaryEffect1() + // unk20 - Sound-effect. Used for secondaryEffect1() + // unk21 - Sound-effect. Used for secondaryEffect1() + // unk22 - Sound-effect. Used for secondaryEffect1() + // unk29 - Sound-effect. Used for primaryEffect1() + // unk30 - Sound-effect. Used for primaryEffect1() + // unk31 - Sound-effect. Used for primaryEffect1() + // unk32 - Sound-effect. Used for primaryEffect2() + // unk33 - Sound-effect. Used for primaryEffect2() + // unk34 - Sound-effect. Used for primaryEffect2() + // unk35 - Sound-effect. Used for primaryEffect2() + // unk36 - Sound-effect. Used for primaryEffect2() + // unk37 - Sound-effect. Used for primaryEffect2() + // unk38 - Sound-effect. Used for primaryEffect2() + // unk39 - Currently unused, except for updateCallback56() + // unk40 - Currently unused, except for updateCallback56() + // unk41 - Sound-effect. Used for primaryEffect2() + + struct Channel { + bool lock; // New to ScummVM + uint8 opExtraLevel2; + const uint8 *dataptr; + uint8 duration; + uint8 repeatCounter; + int8 baseOctave; + uint8 priority; + uint8 dataptrStackPos; + const uint8 *dataptrStack[4]; + int8 baseNote; + uint8 unk29; + uint8 unk31; + uint16 unk30; + uint16 unk37; + uint8 unk33; + uint8 unk34; + uint8 unk35; + uint8 unk36; + uint8 unk32; + uint8 unk41; + uint8 unk38; + uint8 opExtraLevel1; + uint8 spacing2; + uint8 baseFreq; + uint8 tempo; + uint8 position; + uint8 regAx; + uint8 regBx; + typedef void (AdLibDriver::*Callback)(Channel&); + Callback primaryEffect; + Callback secondaryEffect; + uint8 fractionalSpacing; + uint8 opLevel1; + uint8 opLevel2; + uint8 opExtraLevel3; + uint8 twoChan; + uint8 unk39; + uint8 unk40; + uint8 spacing1; + uint8 durationRandomness; + uint8 unk19; + uint8 unk18; + int8 unk20; + int8 unk21; + uint8 unk22; + uint16 offset; + uint8 tempoReset; + uint8 rawNote; + int8 pitchBend; + uint8 volumeModifier; + }; + + void primaryEffect1(Channel &channel); + void primaryEffect2(Channel &channel); + void secondaryEffect1(Channel &channel); + + void resetAdLibState(); + void writeOPL(byte reg, byte val); + void initChannel(Channel &channel); + void noteOff(Channel &channel); + void unkOutput2(uint8 num); + + uint16 getRandomNr(); + void setupDuration(uint8 duration, Channel &channel); + + void setupNote(uint8 rawNote, Channel &channel, bool flag = false); + void setupInstrument(uint8 regOffset, const uint8 *dataptr, Channel &channel); + void noteOn(Channel &channel); + + void adjustVolume(Channel &channel); + + uint8 calculateOpLevel1(Channel &channel); + uint8 calculateOpLevel2(Channel &channel); + + uint16 checkValue(int16 val) { + if (val < 0) + val = 0; + else if (val > 0x3F) + val = 0x3F; + return val; + } + + // The sound data has at least two lookup tables: + // + // * One for programs, starting at offset 0. + // * One for instruments, starting at offset 500. + + uint8 *getProgram(int progId) { + const uint16 offset = READ_LE_UINT16(_soundData + 2 * progId); + + // In case an invalid offset is specified we return nullptr to + // indicate an error. 0xFFFF seems to indicate "this is not a valid + // program/instrument". However, 0 is also invalid because it points + // inside the offset table itself. We also ignore any offsets outside + // of the actual data size. + // The original does not contain any safety checks and will simply + // read outside of the valid sound data in case an invalid offset is + // encountered. + if (offset == 0 || offset >= _soundDataSize) { + return nullptr; + } else { + return _soundData + offset; + } + } + + const uint8 *getInstrument(int instrumentId) { + return getProgram(_numPrograms + instrumentId); + } + + void setupPrograms(); + void executePrograms(); + + struct ParserOpcode { + typedef int (AdLibDriver::*POpcode)(const uint8 *&dataptr, Channel &channel, uint8 value); + POpcode function; + const char *name; + }; + + void setupParserOpcodeTable(); + const ParserOpcode *_parserOpcodeTable; + int _parserOpcodeTableSize; + + int update_setRepeat(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_checkRepeat(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupProgram(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setNoteSpacing(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_jump(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_jumpToSubroutine(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_returnFromSubroutine(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setBaseOctave(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_stopChannel(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_playRest(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_writeAdLib(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupNoteAndDuration(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setBaseNote(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupSecondaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_stopOtherChannel(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_waitForEndOfProgram(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupInstrument(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupPrimaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_removePrimaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setBaseFreq(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupPrimaryEffect2(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setPriority(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback23(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback24(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setExtraLevel1(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupDuration(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_playNote(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setFractionalNoteSpacing(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setTempo(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_removeSecondaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setChannelTempo(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setExtraLevel3(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setExtraLevel2(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_changeExtraLevel2(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setAMDepth(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setVibratoDepth(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_changeExtraLevel1(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback38(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback39(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_removePrimaryEffect2(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_pitchBend(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_resetToGlobalTempo(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_nop(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setDurationRandomness(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_changeChannelTempo(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback46(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setupRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_playRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_removeRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback51(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback52(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback53(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setSoundTrigger(const uint8 *&dataptr, Channel &channel, uint8 value); + int update_setTempoReset(const uint8 *&dataptr, Channel &channel, uint8 value); + int updateCallback56(const uint8 *&dataptr, Channel &channel, uint8 value); +private: + // These variables have not yet been named, but some of them are partly + // known nevertheless: + // + // _unkValue1 - Unknown. Used for updating _unkValue2 + // _unkValue2 - Unknown. Used for updating _unkValue4 + // _unkValue4 - Unknown. Used for updating _unkValue5 + // _unkValue5 - Unknown. Used for controlling updateCallback24(). + // _unkValue6 - Unknown. Rhythm section volume? + // _unkValue7 - Unknown. Rhythm section volume? + // _unkValue8 - Unknown. Rhythm section volume? + // _unkValue9 - Unknown. Rhythm section volume? + // _unkValue10 - Unknown. Rhythm section volume? + // _unkValue11 - Unknown. Rhythm section volume? + // _unkValue12 - Unknown. Rhythm section volume? + // _unkValue13 - Unknown. Rhythm section volume? + // _unkValue14 - Unknown. Rhythm section volume? + // _unkValue15 - Unknown. Rhythm section volume? + // _unkValue16 - Unknown. Rhythm section volume? + // _unkValue17 - Unknown. Rhythm section volume? + // _unkValue18 - Unknown. Rhythm section volume? + // _unkValue19 - Unknown. Rhythm section volume? + // _unkValue20 - Unknown. Rhythm section volume? + // _freqTable[] - Probably frequences for the 12-tone scale. + // _unkTable2[] - Unknown. Currently only used by updateCallback46() + // _unkTable2_1[] - One of the tables in _unkTable2[] + // _unkTable2_2[] - One of the tables in _unkTable2[] + // _unkTable2_3[] - One of the tables in _unkTable2[] + + int _curChannel; + uint8 _soundTrigger; + + uint16 _rnd; + + uint8 _unkValue1; + uint8 _unkValue2; + uint8 _callbackTimer; + uint8 _unkValue4; + uint8 _unkValue5; + uint8 _unkValue6; + uint8 _unkValue7; + uint8 _unkValue8; + uint8 _unkValue9; + uint8 _unkValue10; + uint8 _unkValue11; + uint8 _unkValue12; + uint8 _unkValue13; + uint8 _unkValue14; + uint8 _unkValue15; + uint8 _unkValue16; + uint8 _unkValue17; + uint8 _unkValue18; + uint8 _unkValue19; + uint8 _unkValue20; + + OPL::OPL *_adlib; + + uint8 *_soundData; + uint32 _soundDataSize; + + struct QueueEntry { + QueueEntry() : data(0), id(0), volume(0) {} + QueueEntry(uint8 *ptr, uint8 track, uint8 vol) : data(ptr), id(track), volume(vol) {} + uint8 *data; + uint8 id; + uint8 volume; + }; + + QueueEntry _programQueue[16]; + int _programStartTimeout; + int _programQueueStart, _programQueueEnd; + bool _retrySounds; + + void adjustSfxData(uint8 *data, int volume); + uint8 *_sfxPointer; + int _sfxPriority; + int _sfxVelocity; + + Channel _channels[10]; + + uint8 _vibratoAndAMDepthBits; + uint8 _rhythmSectionBits; + + uint8 _curRegOffset; + uint8 _tempo; + + const uint8 *_tablePtr1; + const uint8 *_tablePtr2; + + static const uint8 _regOffset[]; + static const uint16 _freqTable[]; + static const uint8 *const _unkTable2[]; + static const uint8 _unkTable2_1[]; + static const uint8 _unkTable2_2[]; + static const uint8 _unkTable2_3[]; + static const uint8 _pitchBendTables[][32]; + + uint16 _syncJumpMask; + + Common::Mutex _mutex; + Audio::Mixer *_mixer; + + uint8 _musicVolume, _sfxVolume; + + int _numPrograms; + int _version; +}; + +AdLibDriver::AdLibDriver(Audio::Mixer *mixer, int version) { + setupParserOpcodeTable(); + + _version = version; + _numPrograms = (_version == 1) ? 150 : ((_version == 4) ? 500 : 250); + + _mixer = mixer; + + _adlib = OPL::Config::create(); + if (!_adlib || !_adlib->init()) + error("Failed to create OPL"); + + memset(_channels, 0, sizeof(_channels)); + _soundData = 0; + _soundDataSize = 0; + + _vibratoAndAMDepthBits = _curRegOffset = 0; + + _curChannel = _rhythmSectionBits = 0; + _rnd = 0x1234; + + _tempo = 0; + _soundTrigger = 0; + _programStartTimeout = 0; + + _callbackTimer = 0xFF; + _unkValue1 = _unkValue2 = _unkValue4 = _unkValue5 = 0; + _unkValue6 = _unkValue7 = _unkValue8 = _unkValue9 = _unkValue10 = 0; + _unkValue11 = _unkValue12 = _unkValue13 = _unkValue14 = _unkValue15 = + _unkValue16 = _unkValue17 = _unkValue18 = _unkValue19 = _unkValue20 = 0; + + _tablePtr1 = _tablePtr2 = 0; + + _syncJumpMask = 0; + + _musicVolume = 0; + _sfxVolume = 0; + + _sfxPointer = 0; + + _programQueueStart = _programQueueEnd = 0; + _retrySounds = false; + + _adlib->start(new Common::Functor0Mem<void, AdLibDriver>(this, &AdLibDriver::callback), CALLBACKS_PER_SECOND); +} + +AdLibDriver::~AdLibDriver() { + delete _adlib; + _adlib = 0; +} + +void AdLibDriver::setMusicVolume(uint8 volume) { + Common::StackLock lock(_mutex); + + _musicVolume = volume; + + for (uint i = 0; i < 6; ++i) { + Channel &chan = _channels[i]; + chan.volumeModifier = volume; + + const uint8 regOffset = _regOffset[i]; + + // Level Key Scaling / Total Level + writeOPL(0x40 + regOffset, calculateOpLevel1(chan)); + writeOPL(0x43 + regOffset, calculateOpLevel2(chan)); + } + + // For now we use the music volume for both sfx and music in Kyra1 and EoB + if (_version < 4) { + _sfxVolume = volume; + + for (uint i = 6; i < 9; ++i) { + Channel &chan = _channels[i]; + chan.volumeModifier = volume; + + const uint8 regOffset = _regOffset[i]; + + // Level Key Scaling / Total Level + writeOPL(0x40 + regOffset, calculateOpLevel1(chan)); + writeOPL(0x43 + regOffset, calculateOpLevel2(chan)); + } + } +} + +void AdLibDriver::setSfxVolume(uint8 volume) { + // We only support sfx volume in version 4 games. + if (_version < 4) + return; + + Common::StackLock lock(_mutex); + + _sfxVolume = volume; + + for (uint i = 6; i < 9; ++i) { + Channel &chan = _channels[i]; + chan.volumeModifier = volume; + + const uint8 regOffset = _regOffset[i]; + + // Level Key Scaling / Total Level + writeOPL(0x40 + regOffset, calculateOpLevel1(chan)); + writeOPL(0x43 + regOffset, calculateOpLevel2(chan)); + } +} + +void AdLibDriver::initDriver() { + Common::StackLock lock(_mutex); + resetAdLibState(); +} + +void AdLibDriver::setSoundData(uint8 *data, uint32 size) { + Common::StackLock lock(_mutex); + + // Drop all tracks that are still queued. These would point to the old + // sound data. + _programQueueStart = _programQueueEnd = 0; + memset(_programQueue, 0, sizeof(_programQueue)); + + if (_soundData) { + delete[] _soundData; + _soundData = _sfxPointer = 0; + } + + _soundData = data; + _soundDataSize = size; +} + +void AdLibDriver::queueTrack(int track, int volume) { + Common::StackLock lock(_mutex); + + uint8 *trackData = getProgram(track); + if (!trackData) + return; + + // Don't drop tracks in EoB. The queue is always full there if a couple of monsters are around. + // If we drop the incoming tracks we get no sound effects, but tons of warnings instead. + if (_version >= 3 && _programQueueEnd == _programQueueStart && _programQueue[_programQueueEnd].data != 0) { + warning("AdLibDriver: Program queue full, dropping track %d", track); + return; + } + + _programQueue[_programQueueEnd] = QueueEntry(trackData, track, volume); + _programQueueEnd = (_programQueueEnd + 1) & 15; +} + +bool AdLibDriver::isChannelPlaying(int channel) const { + Common::StackLock lock(_mutex); + + assert(channel >= 0 && channel <= 9); + return (_channels[channel].dataptr != 0); +} + +void AdLibDriver::stopAllChannels() { + Common::StackLock lock(_mutex); + + for (int channel = 0; channel <= 9; ++channel) { + _curChannel = channel; + + Channel &chan = _channels[_curChannel]; + chan.priority = 0; + chan.dataptr = 0; + + if (channel != 9) + noteOff(chan); + } + _retrySounds = false; +} + +// timer callback + +void AdLibDriver::callback() { + Common::StackLock lock(_mutex); + if (_programStartTimeout) + --_programStartTimeout; + else + setupPrograms(); + executePrograms(); + + uint8 temp = _callbackTimer; + _callbackTimer += _tempo; + if (_callbackTimer < temp) { + if (!(--_unkValue2)) { + _unkValue2 = _unkValue1; + ++_unkValue4; + } + } +} + +void AdLibDriver::setupPrograms() { + // If there is no program queued, we skip this. + if (_programQueueStart == _programQueueEnd) + return; + + uint8 *ptr = _programQueue[_programQueueStart].data; + + // The AdLib driver (in its old versions used for EOB) is not suitable for modern (fast) CPUs. + // The stop sound track (track 0 which has a priority of 50) will often still be busy when the + // next sound (with a lower priority) starts which will cause that sound to be skipped. We simply + // restart incoming sounds during stop sound execution. + // UPDATE: This stilly applies after introduction of the _programQueue. + QueueEntry retrySound; + if (_version < 3 && _programQueue[_programQueueStart].id == 0) + _retrySounds = true; + else if (_retrySounds) + retrySound = _programQueue[_programQueueStart]; + + // Adjust data in case we hit a sound effect. + adjustSfxData(ptr, _programQueue[_programQueueStart].volume); + + // Clear the queue entry + _programQueue[_programQueueStart].data = 0; + _programQueueStart = (_programQueueStart + 1) & 15; + + const int chan = *ptr++; + const int priority = *ptr++; + + // Only start this sound if its priority is higher than the one + // already playing. + + Channel &channel = _channels[chan]; + + if (priority >= channel.priority) { + initChannel(channel); + channel.priority = priority; + channel.dataptr = ptr; + channel.tempo = 0xFF; + channel.position = 0xFF; + channel.duration = 1; + + if (chan <= 5) + channel.volumeModifier = _musicVolume; + else + channel.volumeModifier = _sfxVolume; + + unkOutput2(chan); + + // We need to wait two callback calls till we can start another track. + // This is (probably) required to assure that the sfx are started with + // the correct priority and velocity. + _programStartTimeout = 2; + + retrySound = QueueEntry(); + } + + if (retrySound.data) { + debugC(9, kDebugLevelSound, "AdLibDriver::setupPrograms(): WORKAROUND - Restarting skipped sound %d)", retrySound.id); + queueTrack(retrySound.id, retrySound.volume); + } +} + +void AdLibDriver::adjustSfxData(uint8 *ptr, int volume) { + // Check whether we need to reset the data of an old sfx which has been + // started. + if (_sfxPointer) { + _sfxPointer[1] = _sfxPriority; + _sfxPointer[3] = _sfxVelocity; + _sfxPointer = 0; + } + + // Only music tracks are started on channel 9, thus we need to make sure + // we do not have a music track here. + if (*ptr == 9) + return; + + // Store the pointer so we can reset the data when a new program is started. + _sfxPointer = ptr; + + // Store the old values. + _sfxPriority = ptr[1]; + _sfxVelocity = ptr[3]; + + // Adjust the values. + if (volume != 0xFF) { + if (_version >= 3) { + int newVal = ((((ptr[3]) + 63) * volume) >> 8) & 0xFF; + ptr[3] = -newVal + 63; + ptr[1] = ((ptr[1] * volume) >> 8) & 0xFF; + } else { + int newVal = ((_sfxVelocity << 2) ^ 0xFF) * volume; + ptr[3] = (newVal >> 10) ^ 0x3F; + ptr[1] = newVal >> 11; + } + } +} + +// A few words on opcode parsing and timing: +// +// First of all, We simulate a timer callback 72 times per second. Each timeout +// we update each channel that has something to play. +// +// Each channel has its own individual tempo, which is added to its position. +// This will frequently cause the position to "wrap around" but that is +// intentional. In fact, it's the signal to go ahead and do more stuff with +// that channel. +// +// Each channel also has a duration, indicating how much time is left on the +// its current task. This duration is decreased by one. As long as it still has +// not reached zero, the only thing that can happen is that the note is turned +// off depending on manual or automatic note spacing. Once the duration reaches +// zero, a new set of musical opcodes are executed. +// +// An opcode is one byte, followed by a variable number of parameters. Since +// most opcodes have at least one one-byte parameter, we read that as well. Any +// opcode that doesn't have that one parameter is responsible for moving the +// data pointer back again. +// +// If the most significant bit of the opcode is 1, it's a function; call it. +// The opcode functions return either 0 (continue), 1 (stop) or 2 (stop, and do +// not run the effects callbacks). +// +// If the most significant bit of the opcode is 0, it's a note, and the first +// parameter is its duration. (There are cases where the duration is modified +// but that's an exception.) The note opcode is assumed to return 1, and is the +// last opcode unless its duration is zero. +// +// Finally, most of the times that the callback is called, it will invoke the +// effects callbacks. The final opcode in a set can prevent this, if it's a +// function and it returns anything other than 1. + +void AdLibDriver::executePrograms() { + // Each channel runs its own program. There are ten channels: One for + // each AdLib channel (0-8), plus one "control channel" (9) which is + // the one that tells the other channels what to do. + + // This is where we ensure that channels that are made to jump "in + // sync" do so. + + if (_syncJumpMask) { + bool forceUnlock = true; + + for (_curChannel = 9; _curChannel >= 0; --_curChannel) { + if ((_syncJumpMask & (1 << _curChannel)) == 0) + continue; + + if (_channels[_curChannel].dataptr && !_channels[_curChannel].lock) + forceUnlock = false; + } + + if (forceUnlock) { + for (_curChannel = 9; _curChannel >= 0; --_curChannel) + if (_syncJumpMask & (1 << _curChannel)) + _channels[_curChannel].lock = false; + } + } + + for (_curChannel = 9; _curChannel >= 0; --_curChannel) { + int result = 1; + + if (!_channels[_curChannel].dataptr) + continue; + + if (_channels[_curChannel].lock && (_syncJumpMask & (1 << _curChannel))) + continue; + + Channel &channel = _channels[_curChannel]; + if (_curChannel == 9) + _curRegOffset = 0; + else + _curRegOffset = _regOffset[_curChannel]; + + if (channel.tempoReset) + channel.tempo = _tempo; + + uint8 backup = channel.position; + channel.position += channel.tempo; + if (channel.position < backup) { + if (--channel.duration) { + if (channel.duration == channel.spacing2) + noteOff(channel); + if (channel.duration == channel.spacing1 && _curChannel != 9) + noteOff(channel); + } else { + // An opcode is not allowed to modify its own + // data pointer except through the 'dataptr' + // parameter. To enforce that, we have to work + // on a copy of the data pointer. + // + // This fixes a subtle music bug where the + // wrong music would play when getting the + // quill in Kyra 1. + const uint8 *dataptr = channel.dataptr; + while (dataptr) { + uint8 opcode = *dataptr++; + uint8 param = *dataptr++; + + if (opcode & 0x80) { + opcode &= 0x7F; + if (opcode >= _parserOpcodeTableSize) + opcode = _parserOpcodeTableSize - 1; + debugC(9, kDebugLevelSound, "Calling opcode '%s' (%d) (channel: %d)", _parserOpcodeTable[opcode].name, opcode, _curChannel); + result = (this->*(_parserOpcodeTable[opcode].function))(dataptr, channel, param); + channel.dataptr = dataptr; + if (result) + break; + } else { + debugC(9, kDebugLevelSound, "Note on opcode 0x%02X (duration: %d) (channel: %d)", opcode, param, _curChannel); + setupNote(opcode, channel); + noteOn(channel); + setupDuration(param, channel); + if (param) { + // We need to make sure we are always running the + // effects after this. Otherwise some sounds are + // wrong. Like the sfx when bumping into a wall in + // LoL. + result = 1; + channel.dataptr = dataptr; + break; + } + } + } + } + } + + if (result == 1) { + if (channel.primaryEffect) + (this->*(channel.primaryEffect))(channel); + if (channel.secondaryEffect) + (this->*(channel.secondaryEffect))(channel); + } + } +} + +// + +void AdLibDriver::resetAdLibState() { + debugC(9, kDebugLevelSound, "resetAdLibState()"); + _rnd = 0x1234; + + // Authorize the control of the waveforms + writeOPL(0x01, 0x20); + + // Select FM music mode + writeOPL(0x08, 0x00); + + // I would guess the main purpose of this is to turn off the rhythm, + // thus allowing us to use 9 melodic voices instead of 6. + writeOPL(0xBD, 0x00); + + int loop = 10; + while (loop--) { + if (loop != 9) { + // Silence the channel + writeOPL(0x40 + _regOffset[loop], 0x3F); + writeOPL(0x43 + _regOffset[loop], 0x3F); + } + initChannel(_channels[loop]); + } +} + +// Old calling style: output0x388(0xABCD) +// New calling style: writeOPL(0xAB, 0xCD) + +void AdLibDriver::writeOPL(byte reg, byte val) { + _adlib->writeReg(reg, val); +} + +void AdLibDriver::initChannel(Channel &channel) { + debugC(9, kDebugLevelSound, "initChannel(%lu)", (long)(&channel - _channels)); + memset(&channel.dataptr, 0, sizeof(Channel) - ((char *)&channel.dataptr - (char *)&channel)); + + channel.tempo = 0xFF; + channel.priority = 0; + // normally here are nullfuncs but we set 0 for now + channel.primaryEffect = 0; + channel.secondaryEffect = 0; + channel.spacing1 = 1; + channel.lock = false; +} + +void AdLibDriver::noteOff(Channel &channel) { + debugC(9, kDebugLevelSound, "noteOff(%lu)", (long)(&channel - _channels)); + + // The control channel has no corresponding AdLib channel + + if (_curChannel >= 9) + return; + + // When the rhythm section is enabled, channels 6, 7 and 8 are special. + + if (_rhythmSectionBits && _curChannel >= 6) + return; + + // This means the "Key On" bit will always be 0 + channel.regBx &= 0xDF; + + // Octave / F-Number / Key-On + writeOPL(0xB0 + _curChannel, channel.regBx); +} + +void AdLibDriver::unkOutput2(uint8 chan) { + debugC(9, kDebugLevelSound, "unkOutput2(%d)", chan); + + // The control channel has no corresponding AdLib channel + + if (chan >= 9) + return; + + // I believe this has to do with channels 6, 7, and 8 being special + // when AdLib's rhythm section is enabled. + + if (_rhythmSectionBits && chan >= 6) + return; + + uint8 offset = _regOffset[chan]; + + // The channel is cleared: First the attack/delay rate, then the + // sustain level/release rate, and finally the note is turned off. + + writeOPL(0x60 + offset, 0xFF); + writeOPL(0x63 + offset, 0xFF); + + writeOPL(0x80 + offset, 0xFF); + writeOPL(0x83 + offset, 0xFF); + + writeOPL(0xB0 + chan, 0x00); + + // ...and then the note is turned on again, with whatever value is + // still lurking in the A0 + chan register, but everything else - + // including the two most significant frequency bit, and the octave - + // set to zero. + // + // This is very strange behavior, and causes problems with the ancient + // FMOPL code we borrowed from AdPlug. I've added a workaround. See + // audio/softsynth/opl/mame.cpp for more details. + // + // Fortunately, the more modern DOSBox FMOPL code does not seem to have + // any trouble with this. + + writeOPL(0xB0 + chan, 0x20); +} + +// I believe this is a random number generator. It actually does seem to +// generate an even distribution of almost all numbers from 0 through 65535, +// though in my tests some numbers were never generated. + +uint16 AdLibDriver::getRandomNr() { + _rnd += 0x9248; + uint16 lowBits = _rnd & 7; + _rnd >>= 3; + _rnd |= (lowBits << 13); + return _rnd; +} + +void AdLibDriver::setupDuration(uint8 duration, Channel &channel) { + debugC(9, kDebugLevelSound, "setupDuration(%d, %lu)", duration, (long)(&channel - _channels)); + if (channel.durationRandomness) { + channel.duration = duration + (getRandomNr() & channel.durationRandomness); + return; + } + if (channel.fractionalSpacing) + channel.spacing2 = (duration >> 3) * channel.fractionalSpacing; + channel.duration = duration; +} + +// This function may or may not play the note. It's usually followed by a call +// to noteOn(), which will always play the current note. + +void AdLibDriver::setupNote(uint8 rawNote, Channel &channel, bool flag) { + debugC(9, kDebugLevelSound, "setupNote(%d, %lu)", rawNote, (long)(&channel - _channels)); + + if (_curChannel >= 9) + return; + + channel.rawNote = rawNote; + + int8 note = (rawNote & 0x0F) + channel.baseNote; + int8 octave = ((rawNote + channel.baseOctave) >> 4) & 0x0F; + + // There are only twelve notes. If we go outside that, we have to + // adjust the note and octave. + + if (note >= 12) { + note -= 12; + octave++; + } else if (note < 0) { + note += 12; + octave--; + } + + // The calculation of frequency looks quite different from the original + // disassembly at a first glance, but when you consider that the + // largest possible value would be 0x0246 + 0xFF + 0x47 (and that's if + // baseFreq is unsigned), freq is still a 10-bit value, just as it + // should be to fit in the Ax and Bx registers. + // + // If it were larger than that, it could have overflowed into the + // octave bits, and that could possibly have been used in some sound. + // But as it is now, I can't see any way it would happen. + + uint16 freq = _freqTable[note] + channel.baseFreq; + + // When called from callback 41, the behavior is slightly different: + // We adjust the frequency, even when channel.pitchBend is 0. + + if (channel.pitchBend || flag) { + const uint8 *table; + + if (channel.pitchBend >= 0) { + table = _pitchBendTables[(channel.rawNote & 0x0F) + 2]; + freq += table[channel.pitchBend]; + } else { + table = _pitchBendTables[channel.rawNote & 0x0F]; + freq -= table[-channel.pitchBend]; + } + } + + channel.regAx = freq & 0xFF; + channel.regBx = (channel.regBx & 0x20) | (octave << 2) | ((freq >> 8) & 0x03); + + // Keep the note on or off + writeOPL(0xA0 + _curChannel, channel.regAx); + writeOPL(0xB0 + _curChannel, channel.regBx); +} + +void AdLibDriver::setupInstrument(uint8 regOffset, const uint8 *dataptr, Channel &channel) { + debugC(9, kDebugLevelSound, "setupInstrument(%d, %p, %lu)", regOffset, (const void *)dataptr, (long)(&channel - _channels)); + + if (_curChannel >= 9) + return; + + // Amplitude Modulation / Vibrato / Envelope Generator Type / + // Keyboard Scaling Rate / Modulator Frequency Multiple + writeOPL(0x20 + regOffset, *dataptr++); + writeOPL(0x23 + regOffset, *dataptr++); + + uint8 temp = *dataptr++; + + // Feedback / Algorithm + + // It is very likely that _curChannel really does refer to the same + // channel as regOffset, but there's only one Cx register per channel. + + writeOPL(0xC0 + _curChannel, temp); + + // The algorithm bit. I don't pretend to understand this fully, but + // "If set to 0, operator 1 modulates operator 2. In this case, + // operator 2 is the only one producing sound. If set to 1, both + // operators produce sound directly. Complex sounds are more easily + // created if the algorithm is set to 0." + + channel.twoChan = temp & 1; + + // Waveform Select + writeOPL(0xE0 + regOffset, *dataptr++); + writeOPL(0xE3 + regOffset, *dataptr++); + + channel.opLevel1 = *dataptr++; + channel.opLevel2 = *dataptr++; + + // Level Key Scaling / Total Level + writeOPL(0x40 + regOffset, calculateOpLevel1(channel)); + writeOPL(0x43 + regOffset, calculateOpLevel2(channel)); + + // Attack Rate / Decay Rate + writeOPL(0x60 + regOffset, *dataptr++); + writeOPL(0x63 + regOffset, *dataptr++); + + // Sustain Level / Release Rate + writeOPL(0x80 + regOffset, *dataptr++); + writeOPL(0x83 + regOffset, *dataptr++); +} + +// Apart from playing the note, this function also updates the variables for +// primary effect 2. + +void AdLibDriver::noteOn(Channel &channel) { + debugC(9, kDebugLevelSound, "noteOn(%lu)", (long)(&channel - _channels)); + + // The "note on" bit is set, and the current note is played. + + if (_curChannel >= 9) + return; + + channel.regBx |= 0x20; + writeOPL(0xB0 + _curChannel, channel.regBx); + + int8 shift = 9 - channel.unk33; + uint16 temp = channel.regAx | (channel.regBx << 8); + channel.unk37 = ((temp & 0x3FF) >> shift) & 0xFF; + channel.unk38 = channel.unk36; +} + +void AdLibDriver::adjustVolume(Channel &channel) { + debugC(9, kDebugLevelSound, "adjustVolume(%lu)", (long)(&channel - _channels)); + + if (_curChannel >= 9) + return; + + // Level Key Scaling / Total Level + + writeOPL(0x43 + _regOffset[_curChannel], calculateOpLevel2(channel)); + if (channel.twoChan) + writeOPL(0x40 + _regOffset[_curChannel], calculateOpLevel1(channel)); +} + +// This is presumably only used for some sound effects, e.g. Malcolm blowing up +// the trees in the intro (but not the effect where he "booby-traps" the big +// tree) and turning Kallak to stone. Related functions and variables: +// +// update_setupPrimaryEffect1() +// - Initializes unk29, unk30 and unk31 +// - unk29 is not further modified +// - unk30 is not further modified, except by update_removePrimaryEffect1() +// +// update_removePrimaryEffect1() +// - Deinitializes unk30 +// +// unk29 - determines how often the notes are played +// unk30 - modifies the frequency +// unk31 - determines how often the notes are played + +void AdLibDriver::primaryEffect1(Channel &channel) { + debugC(9, kDebugLevelSound, "Calling primaryEffect1 (channel: %d)", _curChannel); + + if (_curChannel >= 9) + return; + + uint8 temp = channel.unk31; + channel.unk31 += channel.unk29; + if (channel.unk31 >= temp) + return; + + // Initialize unk1 to the current frequency + int16 unk1 = ((channel.regBx & 3) << 8) | channel.regAx; + + // This is presumably to shift the "note on" bit so far to the left + // that it won't be affected by any of the calculations below. + int16 unk2 = ((channel.regBx & 0x20) << 8) | (channel.regBx & 0x1C); + + int16 unk3 = (int16)channel.unk30; + + if (unk3 >= 0) { + unk1 += unk3; + if (unk1 >= 734) { + // The new frequency is too high. Shift it down and go + // up one octave. + unk1 >>= 1; + if (!(unk1 & 0x3FF)) + ++unk1; + unk2 = (unk2 & 0xFF00) | ((unk2 + 4) & 0xFF); + unk2 &= 0xFF1C; + } + } else { + unk1 += unk3; + if (unk1 < 388) { + // The new frequency is too low. Shift it up and go + // down one octave. + unk1 <<= 1; + if (!(unk1 & 0x3FF)) + --unk1; + unk2 = (unk2 & 0xFF00) | ((unk2 - 4) & 0xFF); + unk2 &= 0xFF1C; + } + } + + // Make sure that the new frequency is still a 10-bit value. + unk1 &= 0x3FF; + + writeOPL(0xA0 + _curChannel, unk1 & 0xFF); + channel.regAx = unk1 & 0xFF; + + // Shift down the "note on" bit again. + uint8 value = unk1 >> 8; + value |= (unk2 >> 8) & 0xFF; + value |= unk2 & 0xFF; + + writeOPL(0xB0 + _curChannel, value); + channel.regBx = value; +} + +// This is presumably only used for some sound effects, e.g. Malcolm entering +// and leaving Kallak's hut. Related functions and variables: +// +// update_setupPrimaryEffect2() +// - Initializes unk32, unk33, unk34, unk35 and unk36 +// - unk32 is not further modified +// - unk33 is not further modified +// - unk34 is a countdown that gets reinitialized to unk35 on zero +// - unk35 is based on unk34 and not further modified +// - unk36 is not further modified +// +// noteOn() +// - Plays the current note +// - Updates unk37 with a new (lower?) frequency +// - Copies unk36 to unk38. The unk38 variable is a countdown. +// +// unk32 - determines how often the notes are played +// unk33 - modifies the frequency +// unk34 - countdown, updates frequency on zero +// unk35 - initializer for unk34 countdown +// unk36 - initializer for unk38 countdown +// unk37 - frequency +// unk38 - countdown, begins playing on zero +// unk41 - determines how often the notes are played +// +// Note that unk41 is never initialized. Not that it should matter much, but it +// is a bit sloppy. + +void AdLibDriver::primaryEffect2(Channel &channel) { + debugC(9, kDebugLevelSound, "Calling primaryEffect2 (channel: %d)", _curChannel); + + if (_curChannel >= 9) + return; + + if (channel.unk38) { + --channel.unk38; + return; + } + + uint8 temp = channel.unk41; + channel.unk41 += channel.unk32; + if (channel.unk41 < temp) { + uint16 unk1 = channel.unk37; + if (!(--channel.unk34)) { + unk1 ^= 0xFFFF; + ++unk1; + channel.unk37 = unk1; + channel.unk34 = channel.unk35; + } + + uint16 unk2 = (channel.regAx | (channel.regBx << 8)) & 0x3FF; + unk2 += unk1; + + channel.regAx = unk2 & 0xFF; + channel.regBx = (channel.regBx & 0xFC) | (unk2 >> 8); + + // Octave / F-Number / Key-On + writeOPL(0xA0 + _curChannel, channel.regAx); + writeOPL(0xB0 + _curChannel, channel.regBx); + } +} + +// I don't know where this is used. The same operation is performed several +// times on the current channel, using a chunk of the _soundData[] buffer for +// parameters. The parameters are used starting at the end of the chunk. +// +// Since we use _curRegOffset to specify the final register, it's quite +// unlikely that this function is ever used to play notes. It's probably only +// used to modify the sound. Another thing that supports this idea is that it +// can be combined with any of the effects callbacks above. +// +// Related functions and variables: +// +// update_setupSecondaryEffect1() +// - Initialies unk18, unk19, unk20, unk21, unk22 and offset +// - unk19 is not further modified +// - unk20 is not further modified +// - unk22 is not further modified +// - offset is not further modified +// +// unk18 - determines how often the operation is performed +// unk19 - determines how often the operation is performed +// unk20 - the start index into the data chunk +// unk21 - the current index into the data chunk +// unk22 - the operation to perform +// offset - the offset to the data chunk + +void AdLibDriver::secondaryEffect1(Channel &channel) { + debugC(9, kDebugLevelSound, "Calling secondaryEffect1 (channel: %d)", _curChannel); + + if (_curChannel >= 9) + return; + + uint8 temp = channel.unk18; + channel.unk18 += channel.unk19; + if (channel.unk18 < temp) { + if (--channel.unk21 < 0) + channel.unk21 = channel.unk20; + writeOPL(channel.unk22 + _curRegOffset, _soundData[channel.offset + channel.unk21]); + } +} + +uint8 AdLibDriver::calculateOpLevel1(Channel &channel) { + int8 value = channel.opLevel1 & 0x3F; + + if (channel.twoChan) { + value += channel.opExtraLevel1; + value += channel.opExtraLevel2; + + uint16 level3 = (channel.opExtraLevel3 ^ 0x3F) * channel.volumeModifier; + if (level3) { + level3 += 0x3F; + level3 >>= 8; + } + + value += level3 ^ 0x3F; + } + + value = CLIP<int8>(value, 0, 0x3F); + + if (!channel.volumeModifier) + value = 0x3F; + + // Preserve the scaling level bits from opLevel1 + + return checkValue(value) | (channel.opLevel1 & 0xC0); +} + +uint8 AdLibDriver::calculateOpLevel2(Channel &channel) { + int8 value = channel.opLevel2 & 0x3F; + + value += channel.opExtraLevel1; + value += channel.opExtraLevel2; + + uint16 level3 = (channel.opExtraLevel3 ^ 0x3F) * channel.volumeModifier; + if (level3) { + level3 += 0x3F; + level3 >>= 8; + } + + value += level3 ^ 0x3F; + + value = CLIP<int8>(value, 0, 0x3F); + + if (!channel.volumeModifier) + value = 0x3F; + + // Preserve the scaling level bits from opLevel2 + + return checkValue(value) | (channel.opLevel2 & 0xC0); +} + +// parser opcodes + +int AdLibDriver::update_setRepeat(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.repeatCounter = value; + return 0; +} + +int AdLibDriver::update_checkRepeat(const uint8 *&dataptr, Channel &channel, uint8 value) { + ++dataptr; + if (--channel.repeatCounter) { + int16 add = READ_LE_UINT16(dataptr - 2); + dataptr += add; + } + return 0; +} + +int AdLibDriver::update_setupProgram(const uint8 *&dataptr, Channel &channel, uint8 value) { + if (value == 0xFF) + return 0; + + const uint8 *ptr = getProgram(value); + + // In case we encounter an invalid program we simply ignore it and do + // nothing instead. The original did not care about invalid programs and + // simply tried to play them anyway... But to avoid crashes due we ingore + // them. + // This, for example, happens in the Lands of Lore intro when Scotia gets + // the ring in the intro. + if (!ptr) { + debugC(3, kDebugLevelSound, "AdLibDriver::update_setupProgram: Invalid program %d specified", value); + return 0; + } + + uint8 chan = *ptr++; + uint8 priority = *ptr++; + + Channel &channel2 = _channels[chan]; + + if (priority >= channel2.priority) { + // We keep new tracks from being started for two further iterations of + // the callback. This assures the correct velocity is used for this + // program. + _programStartTimeout = 2; + initChannel(channel2); + channel2.priority = priority; + channel2.dataptr = ptr; + channel2.tempo = 0xFF; + channel2.position = 0xFF; + channel2.duration = 1; + + if (chan <= 5) + channel2.volumeModifier = _musicVolume; + else + channel2.volumeModifier = _sfxVolume; + + unkOutput2(chan); + } + + return 0; +} + +int AdLibDriver::update_setNoteSpacing(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.spacing1 = value; + return 0; +} + +int AdLibDriver::update_jump(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + int16 add = READ_LE_UINT16(dataptr); dataptr += 2; + if (_version == 1) + dataptr = _soundData + add - 191; + else + dataptr += add; + if (_syncJumpMask & (1 << (&channel - _channels))) + channel.lock = true; + return 0; +} + +int AdLibDriver::update_jumpToSubroutine(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + int16 add = READ_LE_UINT16(dataptr); dataptr += 2; + channel.dataptrStack[channel.dataptrStackPos++] = dataptr; + if (_version < 3) + dataptr = _soundData + add - 191; + else + dataptr += add; + return 0; +} + +int AdLibDriver::update_returnFromSubroutine(const uint8 *&dataptr, Channel &channel, uint8 value) { + dataptr = channel.dataptrStack[--channel.dataptrStackPos]; + return 0; +} + +int AdLibDriver::update_setBaseOctave(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.baseOctave = value; + return 0; +} + +int AdLibDriver::update_stopChannel(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.priority = 0; + if (_curChannel != 9) + noteOff(channel); + dataptr = 0; + return 2; +} + +int AdLibDriver::update_playRest(const uint8 *&dataptr, Channel &channel, uint8 value) { + setupDuration(value, channel); + noteOff(channel); + return (value != 0); +} + +int AdLibDriver::update_writeAdLib(const uint8 *&dataptr, Channel &channel, uint8 value) { + writeOPL(value, *dataptr++); + return 0; +} + +int AdLibDriver::update_setupNoteAndDuration(const uint8 *&dataptr, Channel &channel, uint8 value) { + setupNote(value, channel); + value = *dataptr++; + setupDuration(value, channel); + return (value != 0); +} + +int AdLibDriver::update_setBaseNote(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.baseNote = value; + return 0; +} + +int AdLibDriver::update_setupSecondaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.unk18 = value; + channel.unk19 = value; + channel.unk20 = channel.unk21 = *dataptr++; + channel.unk22 = *dataptr++; + // WORKAROUND: The original code reads a true offset which later gets translated via xlat (in + // the current segment). This means that the outcome depends on the sound data offset. + // Unfortunately this offset is different in most implementations of the audio driver and + // probably also different from the offset assumed by the sequencer. + // It seems that the driver assumes an offset of 191 which is wrong for all the game driver + // implementations. + // This bug has probably not been noticed, since the effect is hardly used and the sounds are + // not necessarily worse. I noticed the difference between ScummVM and DOSBox for the EOB II + // teleporter sound. I also found the location of the table which is supposed to be used here + // (simple enough: it is located at the end of the track after the 0x88 ending opcode). + // Teleporters in EOB I and II now sound exactly the same which I am sure was the intended way, + // since the sound data is exactly the same. + // In DOSBox the teleporters will sound different in EOB I and II, due to different sound + // data offsets. + channel.offset = READ_LE_UINT16(dataptr) - 191; dataptr += 2; + channel.secondaryEffect = &AdLibDriver::secondaryEffect1; + return 0; +} + +int AdLibDriver::update_stopOtherChannel(const uint8 *&dataptr, Channel &channel, uint8 value) { + Channel &channel2 = _channels[value]; + channel2.duration = 0; + channel2.priority = 0; + channel2.dataptr = 0; + return 0; +} + +int AdLibDriver::update_waitForEndOfProgram(const uint8 *&dataptr, Channel &channel, uint8 value) { + const uint8 *ptr = getProgram(value); + + // Safety check in case an invalid program is specified. This would make + // getProgram return a nullptr and thus cause invalid memory reads. + if (!ptr) { + debugC(3, kDebugLevelSound, "AdLibDriver::update_waitForEndOfProgram: Invalid program %d specified", value); + return 0; + } + + uint8 chan = *ptr; + + if (!_channels[chan].dataptr) + return 0; + + dataptr -= 2; + return 2; +} + +int AdLibDriver::update_setupInstrument(const uint8 *&dataptr, Channel &channel, uint8 value) { + const uint8 *instrument = getInstrument(value); + + // We add a safety check to avoid setting up invalid instruments. This is + // not done in the original. However, to avoid crashes due to invalid + // memory reads we simply ignore the request. + // This happens, for example, in Hand of Fate when using the swampsnake + // potion on Zanthia to scare off the rat in the cave in the first chapter + // of the game. + if (!instrument) { + debugC(3, kDebugLevelSound, "AdLibDriver::update_setupInstrument: Invalid instrument %d specified", value); + return 0; + } + + setupInstrument(_curRegOffset, instrument, channel); + return 0; +} + +int AdLibDriver::update_setupPrimaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.unk29 = value; + channel.unk30 = READ_BE_UINT16(dataptr); + dataptr += 2; + channel.primaryEffect = &AdLibDriver::primaryEffect1; + channel.unk31 = 0xFF; + return 0; +} + +int AdLibDriver::update_removePrimaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + channel.primaryEffect = 0; + channel.unk30 = 0; + return 0; +} + +int AdLibDriver::update_setBaseFreq(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.baseFreq = value; + return 0; +} + +int AdLibDriver::update_setupPrimaryEffect2(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.unk32 = value; + channel.unk33 = *dataptr++; + uint8 temp = *dataptr++; + channel.unk34 = temp + 1; + channel.unk35 = temp << 1; + channel.unk36 = *dataptr++; + channel.primaryEffect = &AdLibDriver::primaryEffect2; + return 0; +} + +int AdLibDriver::update_setPriority(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.priority = value; + return 0; +} + +int AdLibDriver::updateCallback23(const uint8 *&dataptr, Channel &channel, uint8 value) { + value >>= 1; + _unkValue1 = _unkValue2 = value; + _callbackTimer = 0xFF; + _unkValue4 = _unkValue5 = 0; + return 0; +} + +int AdLibDriver::updateCallback24(const uint8 *&dataptr, Channel &channel, uint8 value) { + if (_unkValue5) { + if (_unkValue4 & value) { + _unkValue5 = 0; + return 0; + } + } + + if (!(value & _unkValue4)) + ++_unkValue5; + + dataptr -= 2; + channel.duration = 1; + return 2; +} + +int AdLibDriver::update_setExtraLevel1(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.opExtraLevel1 = value; + adjustVolume(channel); + return 0; +} + +int AdLibDriver::update_setupDuration(const uint8 *&dataptr, Channel &channel, uint8 value) { + setupDuration(value, channel); + return (value != 0); +} + +int AdLibDriver::update_playNote(const uint8 *&dataptr, Channel &channel, uint8 value) { + setupDuration(value, channel); + noteOn(channel); + return (value != 0); +} + +int AdLibDriver::update_setFractionalNoteSpacing(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.fractionalSpacing = value & 7; + return 0; +} + +int AdLibDriver::update_setTempo(const uint8 *&dataptr, Channel &channel, uint8 value) { + _tempo = value; + return 0; +} + +int AdLibDriver::update_removeSecondaryEffect1(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + channel.secondaryEffect = 0; + return 0; +} + +int AdLibDriver::update_setChannelTempo(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.tempo = value; + return 0; +} + +int AdLibDriver::update_setExtraLevel3(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.opExtraLevel3 = value; + return 0; +} + +int AdLibDriver::update_setExtraLevel2(const uint8 *&dataptr, Channel &channel, uint8 value) { + int channelBackUp = _curChannel; + + _curChannel = value; + Channel &channel2 = _channels[value]; + channel2.opExtraLevel2 = *dataptr++; + adjustVolume(channel2); + + _curChannel = channelBackUp; + return 0; +} + +int AdLibDriver::update_changeExtraLevel2(const uint8 *&dataptr, Channel &channel, uint8 value) { + int channelBackUp = _curChannel; + + _curChannel = value; + Channel &channel2 = _channels[value]; + channel2.opExtraLevel2 += *dataptr++; + adjustVolume(channel2); + + _curChannel = channelBackUp; + return 0; +} + +// Apart from initializing to zero, these two functions are the only ones that +// modify _vibratoAndAMDepthBits. + +int AdLibDriver::update_setAMDepth(const uint8 *&dataptr, Channel &channel, uint8 value) { + if (value & 1) + _vibratoAndAMDepthBits |= 0x80; + else + _vibratoAndAMDepthBits &= 0x7F; + + writeOPL(0xBD, _vibratoAndAMDepthBits); + return 0; +} + +int AdLibDriver::update_setVibratoDepth(const uint8 *&dataptr, Channel &channel, uint8 value) { + if (value & 1) + _vibratoAndAMDepthBits |= 0x40; + else + _vibratoAndAMDepthBits &= 0xBF; + + writeOPL(0xBD, _vibratoAndAMDepthBits); + return 0; +} + +int AdLibDriver::update_changeExtraLevel1(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.opExtraLevel1 += value; + adjustVolume(channel); + return 0; +} + +int AdLibDriver::updateCallback38(const uint8 *&dataptr, Channel &channel, uint8 value) { + int channelBackUp = _curChannel; + + _curChannel = value; + Channel &channel2 = _channels[value]; + channel2.duration = channel2.priority = 0; + channel2.dataptr = 0; + channel2.opExtraLevel2 = 0; + + if (value != 9) { + uint8 outValue = _regOffset[value]; + + // Feedback strength / Connection type + writeOPL(0xC0 + _curChannel, 0x00); + + // Key scaling level / Operator output level + writeOPL(0x43 + outValue, 0x3F); + + // Sustain Level / Release Rate + writeOPL(0x83 + outValue, 0xFF); + + // Key On / Octave / Frequency + writeOPL(0xB0 + _curChannel, 0x00); + } + + _curChannel = channelBackUp; + return 0; +} + +int AdLibDriver::updateCallback39(const uint8 *&dataptr, Channel &channel, uint8 value) { + if (_curChannel >= 9) + return 0; + + uint16 unk = *dataptr++; + unk |= value << 8; + unk &= getRandomNr(); + + uint16 unk2 = ((channel.regBx & 0x1F) << 8) | channel.regAx; + unk2 += unk; + unk2 |= ((channel.regBx & 0x20) << 8); + + // Frequency + writeOPL(0xA0 + _curChannel, unk2 & 0xFF); + + // Key On / Octave / Frequency + writeOPL(0xB0 + _curChannel, (unk2 & 0xFF00) >> 8); + + return 0; +} + +int AdLibDriver::update_removePrimaryEffect2(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + channel.primaryEffect = 0; + return 0; +} + +int AdLibDriver::update_pitchBend(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.pitchBend = value; + setupNote(channel.rawNote, channel, true); + return 0; +} + +int AdLibDriver::update_resetToGlobalTempo(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + channel.tempo = _tempo; + return 0; +} + +int AdLibDriver::update_nop(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + return 0; +} + +int AdLibDriver::update_setDurationRandomness(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.durationRandomness = value; + return 0; +} + +int AdLibDriver::update_changeChannelTempo(const uint8 *&dataptr, Channel &channel, uint8 value) { + int tempo = channel.tempo + (int8)value; + + if (tempo <= 0) + tempo = 1; + else if (tempo > 255) + tempo = 255; + + channel.tempo = tempo; + return 0; +} + +int AdLibDriver::updateCallback46(const uint8 *&dataptr, Channel &channel, uint8 value) { + uint8 entry = *dataptr++; + _tablePtr1 = _unkTable2[entry++]; + _tablePtr2 = _unkTable2[entry]; + if (value == 2) { + // Frequency + writeOPL(0xA0, _tablePtr2[0]); + } + return 0; +} + +int AdLibDriver::update_setupRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value) { + int channelBackUp = _curChannel; + int regOffsetBackUp = _curRegOffset; + + _curChannel = 6; + _curRegOffset = _regOffset[6]; + + const uint8 *instrument; + instrument = getInstrument(value); + if (instrument) { + setupInstrument(_curRegOffset, instrument, channel); + } else { + debugC(3, kDebugLevelSound, "AdLibDriver::update_setupRhythmSection: Invalid instrument %d for channel 6 specified", value); + } + _unkValue6 = channel.opLevel2; + + _curChannel = 7; + _curRegOffset = _regOffset[7]; + + instrument = getInstrument(*dataptr++); + if (instrument) { + setupInstrument(_curRegOffset, instrument, channel); + } else { + debugC(3, kDebugLevelSound, "AdLibDriver::update_setupRhythmSection: Invalid instrument %d for channel 7 specified", value); + } + _unkValue7 = channel.opLevel1; + _unkValue8 = channel.opLevel2; + + _curChannel = 8; + _curRegOffset = _regOffset[8]; + + instrument = getInstrument(*dataptr++); + if (instrument) { + setupInstrument(_curRegOffset, instrument, channel); + } else { + debugC(3, kDebugLevelSound, "AdLibDriver::update_setupRhythmSection: Invalid instrument %d for channel 8 specified", value); + } + _unkValue9 = channel.opLevel1; + _unkValue10 = channel.opLevel2; + + // Octave / F-Number / Key-On for channels 6, 7 and 8 + + _channels[6].regBx = *dataptr++ & 0x2F; + writeOPL(0xB6, _channels[6].regBx); + writeOPL(0xA6, *dataptr++); + + _channels[7].regBx = *dataptr++ & 0x2F; + writeOPL(0xB7, _channels[7].regBx); + writeOPL(0xA7, *dataptr++); + + _channels[8].regBx = *dataptr++ & 0x2F; + writeOPL(0xB8, _channels[8].regBx); + writeOPL(0xA8, *dataptr++); + + _rhythmSectionBits = 0x20; + + _curRegOffset = regOffsetBackUp; + _curChannel = channelBackUp; + return 0; +} + +int AdLibDriver::update_playRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value) { + // Any instrument that we want to play, and which was already playing, + // is temporarily keyed off. Instruments that were off already, or + // which we don't want to play, retain their old on/off status. This is + // probably so that the instrument's envelope is played from its + // beginning again... + + writeOPL(0xBD, (_rhythmSectionBits & ~(value & 0x1F)) | 0x20); + + // ...but since we only set the rhythm instrument bits, and never clear + // them (until the entire rhythm section is disabled), I'm not sure how + // useful the cleverness above is. We could perhaps simply turn off all + // the rhythm instruments instead. + + _rhythmSectionBits |= value; + + writeOPL(0xBD, _vibratoAndAMDepthBits | 0x20 | _rhythmSectionBits); + return 0; +} + +int AdLibDriver::update_removeRhythmSection(const uint8 *&dataptr, Channel &channel, uint8 value) { + --dataptr; + _rhythmSectionBits = 0; + + // All the rhythm bits are cleared. The AM and Vibrato depth bits + // remain unchanged. + + writeOPL(0xBD, _vibratoAndAMDepthBits); + return 0; +} + +int AdLibDriver::updateCallback51(const uint8 *&dataptr, Channel &channel, uint8 value) { + uint8 value2 = *dataptr++; + + if (value & 1) { + _unkValue12 = value2; + + // Channel 7, op1: Level Key Scaling / Total Level + writeOPL(0x51, checkValue(value2 + _unkValue7 + _unkValue11 + _unkValue12)); + } + + if (value & 2) { + _unkValue14 = value2; + + // Channel 8, op2: Level Key Scaling / Total Level + writeOPL(0x55, checkValue(value2 + _unkValue10 + _unkValue13 + _unkValue14)); + } + + if (value & 4) { + _unkValue15 = value2; + + // Channel 8, op1: Level Key Scaling / Total Level + writeOPL(0x52, checkValue(value2 + _unkValue9 + _unkValue16 + _unkValue15)); + } + + if (value & 8) { + _unkValue18 = value2; + + // Channel 7, op2: Level Key Scaling / Total Level + writeOPL(0x54, checkValue(value2 + _unkValue8 + _unkValue17 + _unkValue18)); + } + + if (value & 16) { + _unkValue20 = value2; + + // Channel 6, op2: Level Key Scaling / Total Level + writeOPL(0x53, checkValue(value2 + _unkValue6 + _unkValue19 + _unkValue20)); + } + + return 0; +} + +int AdLibDriver::updateCallback52(const uint8 *&dataptr, Channel &channel, uint8 value) { + uint8 value2 = *dataptr++; + + if (value & 1) { + _unkValue11 = checkValue(value2 + _unkValue7 + _unkValue11 + _unkValue12); + + // Channel 7, op1: Level Key Scaling / Total Level + writeOPL(0x51, _unkValue11); + } + + if (value & 2) { + _unkValue13 = checkValue(value2 + _unkValue10 + _unkValue13 + _unkValue14); + + // Channel 8, op2: Level Key Scaling / Total Level + writeOPL(0x55, _unkValue13); + } + + if (value & 4) { + _unkValue16 = checkValue(value2 + _unkValue9 + _unkValue16 + _unkValue15); + + // Channel 8, op1: Level Key Scaling / Total Level + writeOPL(0x52, _unkValue16); + } + + if (value & 8) { + _unkValue17 = checkValue(value2 + _unkValue8 + _unkValue17 + _unkValue18); + + // Channel 7, op2: Level Key Scaling / Total Level + writeOPL(0x54, _unkValue17); + } + + if (value & 16) { + _unkValue19 = checkValue(value2 + _unkValue6 + _unkValue19 + _unkValue20); + + // Channel 6, op2: Level Key Scaling / Total Level + writeOPL(0x53, _unkValue19); + } + + return 0; +} + +int AdLibDriver::updateCallback53(const uint8 *&dataptr, Channel &channel, uint8 value) { + uint8 value2 = *dataptr++; + + if (value & 1) { + _unkValue11 = value2; + + // Channel 7, op1: Level Key Scaling / Total Level + writeOPL(0x51, checkValue(value2 + _unkValue7 + _unkValue12)); + } + + if (value & 2) { + _unkValue13 = value2; + + // Channel 8, op2: Level Key Scaling / Total Level + writeOPL(0x55, checkValue(value2 + _unkValue10 + _unkValue14)); + } + + if (value & 4) { + _unkValue16 = value2; + + // Channel 8, op1: Level Key Scaling / Total Level + writeOPL(0x52, checkValue(value2 + _unkValue9 + _unkValue15)); + } + + if (value & 8) { + _unkValue17 = value2; + + // Channel 7, op2: Level Key Scaling / Total Level + writeOPL(0x54, checkValue(value2 + _unkValue8 + _unkValue18)); + } + + if (value & 16) { + _unkValue19 = value2; + + // Channel 6, op2: Level Key Scaling / Total Level + writeOPL(0x53, checkValue(value2 + _unkValue6 + _unkValue20)); + } + + return 0; +} + +int AdLibDriver::update_setSoundTrigger(const uint8 *&dataptr, Channel &channel, uint8 value) { + _soundTrigger = value; + return 0; +} + +int AdLibDriver::update_setTempoReset(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.tempoReset = value; + return 0; +} + +int AdLibDriver::updateCallback56(const uint8 *&dataptr, Channel &channel, uint8 value) { + channel.unk39 = value; + channel.unk40 = *dataptr++; + return 0; +} + +// static res + +#define COMMAND(x) { &AdLibDriver::x, #x } + +void AdLibDriver::setupParserOpcodeTable() { + static const ParserOpcode parserOpcodeTable[] = { + // 0 + COMMAND(update_setRepeat), + COMMAND(update_checkRepeat), + COMMAND(update_setupProgram), + COMMAND(update_setNoteSpacing), + + // 4 + COMMAND(update_jump), + COMMAND(update_jumpToSubroutine), + COMMAND(update_returnFromSubroutine), + COMMAND(update_setBaseOctave), + + // 8 + COMMAND(update_stopChannel), + COMMAND(update_playRest), + COMMAND(update_writeAdLib), + COMMAND(update_setupNoteAndDuration), + + // 12 + COMMAND(update_setBaseNote), + COMMAND(update_setupSecondaryEffect1), + COMMAND(update_stopOtherChannel), + COMMAND(update_waitForEndOfProgram), + + // 16 + COMMAND(update_setupInstrument), + COMMAND(update_setupPrimaryEffect1), + COMMAND(update_removePrimaryEffect1), + COMMAND(update_setBaseFreq), + + // 20 + COMMAND(update_stopChannel), + COMMAND(update_setupPrimaryEffect2), + COMMAND(update_stopChannel), + COMMAND(update_stopChannel), + + // 24 + COMMAND(update_stopChannel), + COMMAND(update_stopChannel), + COMMAND(update_setPriority), + COMMAND(update_stopChannel), + + // 28 + COMMAND(updateCallback23), + COMMAND(updateCallback24), + COMMAND(update_setExtraLevel1), + COMMAND(update_stopChannel), + + // 32 + COMMAND(update_setupDuration), + COMMAND(update_playNote), + COMMAND(update_stopChannel), + COMMAND(update_stopChannel), + + // 36 + COMMAND(update_setFractionalNoteSpacing), + COMMAND(update_stopChannel), + COMMAND(update_setTempo), + COMMAND(update_removeSecondaryEffect1), + + // 40 + COMMAND(update_stopChannel), + COMMAND(update_setChannelTempo), + COMMAND(update_stopChannel), + COMMAND(update_setExtraLevel3), + + // 44 + COMMAND(update_setExtraLevel2), + COMMAND(update_changeExtraLevel2), + COMMAND(update_setAMDepth), + COMMAND(update_setVibratoDepth), + + // 48 + COMMAND(update_changeExtraLevel1), + COMMAND(update_stopChannel), + COMMAND(update_stopChannel), + COMMAND(updateCallback38), + + // 52 + COMMAND(update_stopChannel), + COMMAND(updateCallback39), + COMMAND(update_removePrimaryEffect2), + COMMAND(update_stopChannel), + + // 56 + COMMAND(update_stopChannel), + COMMAND(update_pitchBend), + COMMAND(update_resetToGlobalTempo), + COMMAND(update_nop), + + // 60 + COMMAND(update_setDurationRandomness), + COMMAND(update_changeChannelTempo), + COMMAND(update_stopChannel), + COMMAND(updateCallback46), + + // 64 + COMMAND(update_nop), + COMMAND(update_setupRhythmSection), + COMMAND(update_playRhythmSection), + COMMAND(update_removeRhythmSection), + + // 68 + COMMAND(updateCallback51), + COMMAND(updateCallback52), + COMMAND(updateCallback53), + COMMAND(update_setSoundTrigger), + + // 72 + COMMAND(update_setTempoReset), + COMMAND(updateCallback56), + COMMAND(update_stopChannel) + }; + + _parserOpcodeTable = parserOpcodeTable; + _parserOpcodeTableSize = ARRAYSIZE(parserOpcodeTable); +} +#undef COMMAND + +// This table holds the register offset for operator 1 for each of the nine +// channels. To get the register offset for operator 2, simply add 3. + +const uint8 AdLibDriver::_regOffset[] = { + 0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, + 0x12 +}; + +//These are the F-Numbers (10 bits) for the notes of the 12-tone scale. +// However, it does not match the table in the AdLib documentation I've seen. + +const uint16 AdLibDriver::_freqTable[] = { + 0x0134, 0x0147, 0x015A, 0x016F, 0x0184, 0x019C, 0x01B4, 0x01CE, 0x01E9, + 0x0207, 0x0225, 0x0246 +}; + +// These tables are currently only used by updateCallback46(), which only ever +// uses the first element of one of the sub-tables. + +const uint8 *const AdLibDriver::_unkTable2[] = { + AdLibDriver::_unkTable2_1, + AdLibDriver::_unkTable2_2, + AdLibDriver::_unkTable2_1, + AdLibDriver::_unkTable2_2, + AdLibDriver::_unkTable2_3, + AdLibDriver::_unkTable2_2 +}; + +const uint8 AdLibDriver::_unkTable2_1[] = { + 0x50, 0x50, 0x4F, 0x4F, 0x4E, 0x4E, 0x4D, 0x4D, + 0x4C, 0x4C, 0x4B, 0x4B, 0x4A, 0x4A, 0x49, 0x49, + 0x48, 0x48, 0x47, 0x47, 0x46, 0x46, 0x45, 0x45, + 0x44, 0x44, 0x43, 0x43, 0x42, 0x42, 0x41, 0x41, + 0x40, 0x40, 0x3F, 0x3F, 0x3E, 0x3E, 0x3D, 0x3D, + 0x3C, 0x3C, 0x3B, 0x3B, 0x3A, 0x3A, 0x39, 0x39, + 0x38, 0x38, 0x37, 0x37, 0x36, 0x36, 0x35, 0x35, + 0x34, 0x34, 0x33, 0x33, 0x32, 0x32, 0x31, 0x31, + 0x30, 0x30, 0x2F, 0x2F, 0x2E, 0x2E, 0x2D, 0x2D, + 0x2C, 0x2C, 0x2B, 0x2B, 0x2A, 0x2A, 0x29, 0x29, + 0x28, 0x28, 0x27, 0x27, 0x26, 0x26, 0x25, 0x25, + 0x24, 0x24, 0x23, 0x23, 0x22, 0x22, 0x21, 0x21, + 0x20, 0x20, 0x1F, 0x1F, 0x1E, 0x1E, 0x1D, 0x1D, + 0x1C, 0x1C, 0x1B, 0x1B, 0x1A, 0x1A, 0x19, 0x19, + 0x18, 0x18, 0x17, 0x17, 0x16, 0x16, 0x15, 0x15, + 0x14, 0x14, 0x13, 0x13, 0x12, 0x12, 0x11, 0x11, + 0x10, 0x10 +}; + +// no don't ask me WHY this table exsits! +const uint8 AdLibDriver::_unkTable2_2[] = { + 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, + 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, + 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, + 0x28, 0x29, 0x2A, 0x2B, 0x2C, 0x2D, 0x2E, 0x2F, + 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, + 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, + 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, + 0x48, 0x49, 0x4A, 0x4B, 0x4C, 0x4D, 0x4E, 0x4F, + 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, + 0x58, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x6F, + 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, + 0x68, 0x69, 0x6A, 0x6B, 0x6C, 0x6D, 0x6E, 0x6F, + 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, + 0x78, 0x79, 0x7A, 0x7B, 0x7C, 0x7D, 0x7E, 0x7F +}; + +const uint8 AdLibDriver::_unkTable2_3[] = { + 0x40, 0x40, 0x40, 0x3F, 0x3F, 0x3F, 0x3E, 0x3E, + 0x3E, 0x3D, 0x3D, 0x3D, 0x3C, 0x3C, 0x3C, 0x3B, + 0x3B, 0x3B, 0x3A, 0x3A, 0x3A, 0x39, 0x39, 0x39, + 0x38, 0x38, 0x38, 0x37, 0x37, 0x37, 0x36, 0x36, + 0x36, 0x35, 0x35, 0x35, 0x34, 0x34, 0x34, 0x33, + 0x33, 0x33, 0x32, 0x32, 0x32, 0x31, 0x31, 0x31, + 0x30, 0x30, 0x30, 0x2F, 0x2F, 0x2F, 0x2E, 0x2E, + 0x2E, 0x2D, 0x2D, 0x2D, 0x2C, 0x2C, 0x2C, 0x2B, + 0x2B, 0x2B, 0x2A, 0x2A, 0x2A, 0x29, 0x29, 0x29, + 0x28, 0x28, 0x28, 0x27, 0x27, 0x27, 0x26, 0x26, + 0x26, 0x25, 0x25, 0x25, 0x24, 0x24, 0x24, 0x23, + 0x23, 0x23, 0x22, 0x22, 0x22, 0x21, 0x21, 0x21, + 0x20, 0x20, 0x20, 0x1F, 0x1F, 0x1F, 0x1E, 0x1E, + 0x1E, 0x1D, 0x1D, 0x1D, 0x1C, 0x1C, 0x1C, 0x1B, + 0x1B, 0x1B, 0x1A, 0x1A, 0x1A, 0x19, 0x19, 0x19, + 0x18, 0x18, 0x18, 0x17, 0x17, 0x17, 0x16, 0x16, + 0x16, 0x15 +}; + +// This table is used to modify the frequency of the notes, depending on the +// note value and the pitch bend value. In theory, we could very well try to +// access memory outside this table, but in reality that probably won't happen. +// + +const uint8 AdLibDriver::_pitchBendTables[][32] = { + // 0 + { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x08, + 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, + 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x19, + 0x1A, 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x21 }, + // 1 + { 0x00, 0x01, 0x02, 0x03, 0x04, 0x06, 0x07, 0x09, + 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, + 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x1A, + 0x1B, 0x1C, 0x1D, 0x1E, 0x1F, 0x20, 0x22, 0x24 }, + // 2 + { 0x00, 0x01, 0x02, 0x03, 0x04, 0x06, 0x08, 0x09, + 0x0A, 0x0C, 0x0D, 0x0E, 0x0F, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x19, 0x1A, 0x1C, 0x1D, + 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x24, 0x25, 0x26 }, + // 3 + { 0x00, 0x01, 0x02, 0x03, 0x04, 0x06, 0x08, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x11, 0x12, 0x13, + 0x14, 0x15, 0x16, 0x17, 0x18, 0x1A, 0x1C, 0x1D, + 0x1E, 0x1F, 0x20, 0x21, 0x23, 0x25, 0x27, 0x28 }, + // 4 + { 0x00, 0x01, 0x02, 0x03, 0x04, 0x06, 0x08, 0x0A, + 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x11, 0x13, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1D, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x28, 0x2A }, + // 5 + { 0x00, 0x01, 0x02, 0x03, 0x05, 0x07, 0x09, 0x0B, + 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x15, + 0x16, 0x17, 0x18, 0x19, 0x1B, 0x1D, 0x1F, 0x20, + 0x21, 0x22, 0x23, 0x25, 0x27, 0x29, 0x2B, 0x2D }, + // 6 + { 0x00, 0x01, 0x02, 0x03, 0x05, 0x07, 0x09, 0x0B, + 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x15, + 0x16, 0x17, 0x18, 0x1A, 0x1C, 0x1E, 0x21, 0x24, + 0x25, 0x26, 0x27, 0x29, 0x2B, 0x2D, 0x2F, 0x30 }, + // 7 + { 0x00, 0x01, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0C, + 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x13, 0x15, 0x18, + 0x19, 0x1A, 0x1C, 0x1D, 0x1F, 0x21, 0x23, 0x25, + 0x26, 0x27, 0x29, 0x2B, 0x2D, 0x2F, 0x30, 0x32 }, + // 8 + { 0x00, 0x01, 0x02, 0x04, 0x06, 0x08, 0x0A, 0x0D, + 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x14, 0x17, 0x1A, + 0x19, 0x1A, 0x1C, 0x1E, 0x20, 0x22, 0x25, 0x28, + 0x29, 0x2A, 0x2B, 0x2D, 0x2F, 0x31, 0x33, 0x35 }, + // 9 + { 0x00, 0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0E, + 0x0F, 0x10, 0x12, 0x14, 0x16, 0x18, 0x1A, 0x1B, + 0x1C, 0x1D, 0x1E, 0x20, 0x22, 0x24, 0x26, 0x29, + 0x2A, 0x2C, 0x2E, 0x30, 0x32, 0x34, 0x36, 0x39 }, + // 10 + { 0x00, 0x01, 0x03, 0x05, 0x07, 0x09, 0x0B, 0x0E, + 0x0F, 0x10, 0x12, 0x14, 0x16, 0x19, 0x1B, 0x1E, + 0x1F, 0x21, 0x23, 0x25, 0x27, 0x29, 0x2B, 0x2D, + 0x2E, 0x2F, 0x31, 0x32, 0x34, 0x36, 0x39, 0x3C }, + // 11 + { 0x00, 0x01, 0x03, 0x05, 0x07, 0x0A, 0x0C, 0x0F, + 0x10, 0x11, 0x13, 0x15, 0x17, 0x19, 0x1B, 0x1E, + 0x1F, 0x20, 0x22, 0x24, 0x26, 0x28, 0x2B, 0x2E, + 0x2F, 0x30, 0x32, 0x34, 0x36, 0x39, 0x3C, 0x3F }, + // 12 + { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0B, 0x0D, 0x10, + 0x11, 0x12, 0x14, 0x16, 0x18, 0x1B, 0x1E, 0x21, + 0x22, 0x23, 0x25, 0x27, 0x29, 0x2C, 0x2F, 0x32, + 0x33, 0x34, 0x36, 0x38, 0x3B, 0x34, 0x41, 0x44 }, + // 13 + { 0x00, 0x02, 0x04, 0x06, 0x08, 0x0B, 0x0D, 0x11, + 0x12, 0x13, 0x15, 0x17, 0x1A, 0x1D, 0x20, 0x23, + 0x24, 0x25, 0x27, 0x29, 0x2C, 0x2F, 0x32, 0x35, + 0x36, 0x37, 0x39, 0x3B, 0x3E, 0x41, 0x44, 0x47 } +}; + +#pragma mark - + +// Kyra 1 sound triggers. Most noticeably, these are used towards the end of +// the game, in the castle, to cycle between different songs. The same music is +// used in other places throughout the game, but the player is less likely to +// spend enough time there to notice. + +const int SoundAdLibPC::_kyra1SoundTriggers[] = { + 0, 4, 5, 3 +}; + +const int SoundAdLibPC::_kyra1NumSoundTriggers = ARRAYSIZE(SoundAdLibPC::_kyra1SoundTriggers); + +SoundAdLibPC::SoundAdLibPC(KyraEngine_v1 *vm, Audio::Mixer *mixer) + : Sound(vm, mixer), _driver(0), _trackEntries(), _soundDataPtr(0) { + memset(_trackEntries, 0, sizeof(_trackEntries)); + + _soundTriggers = 0; + _numSoundTriggers = 0; + _sfxPlayingSound = -1; + _soundFileLoaded.clear(); + _currentResourceSet = 0; + memset(&_resInfo, 0, sizeof(_resInfo)); + + switch (vm->game()) { + case GI_LOL: + _version = _vm->gameFlags().isDemo ? 3 : 4; + break; + case GI_KYRA2: + _version = 4; + break; + case GI_KYRA1: + _version = 3; + _soundTriggers = _kyra1SoundTriggers; + _numSoundTriggers = _kyra1NumSoundTriggers; + break; + case GI_EOB2: + _version = 2; + break; + case GI_EOB1: + _version = 1; + break; + default: + break; + } + + _driver = new AdLibDriver(mixer, _version); + assert(_driver); +} + +SoundAdLibPC::~SoundAdLibPC() { + delete _driver; + delete[] _soundDataPtr; + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); +} + +bool SoundAdLibPC::init() { + _driver->initDriver(); + return true; +} + +void SoundAdLibPC::process() { + int trigger = _driver->getSoundTrigger(); + + if (trigger < _numSoundTriggers) { + int soundId = _soundTriggers[trigger]; + + if (soundId) + playTrack(soundId); + } else { + warning("Unknown sound trigger %d", trigger); + // TODO: At this point, we really want to clear the trigger... + } +} + +void SoundAdLibPC::updateVolumeSettings() { + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); + + int newMusicVolume = mute ? 0 : ConfMan.getInt("music_volume"); + //newMusicVolume = (newMusicVolume * 145) / Audio::Mixer::kMaxMixerVolume + 110; + newMusicVolume = CLIP(newMusicVolume, 0, 255); + + int newSfxVolume = mute ? 0 : ConfMan.getInt("sfx_volume"); + //newSfxVolume = (newSfxVolume * 200) / Audio::Mixer::kMaxMixerVolume + 55; + newSfxVolume = CLIP(newSfxVolume, 0, 255); + + _driver->setMusicVolume(newMusicVolume); + _driver->setSfxVolume(newSfxVolume); +} + +void SoundAdLibPC::playTrack(uint8 track) { + if (_musicEnabled) { + // WORKAROUND: There is a bug in the Kyra 1 "Pool of Sorrow" + // music which causes the channels to get progressively out of + // sync for each loop. To avoid that, we declare that all four + // of the song channels have to jump "in sync". + + if (track == 4 && _soundFileLoaded.equalsIgnoreCase("KYRA1B.ADL")) + _driver->setSyncJumpMask(0x000F); + else + _driver->setSyncJumpMask(0); + play(track, 0xFF); + } +} + +void SoundAdLibPC::haltTrack() { + play(0, 0); + play(0, 0); + //_vm->_system->delayMillis(3 * 60); +} + +bool SoundAdLibPC::isPlaying() const { + return _driver->isChannelPlaying(0); +} + +void SoundAdLibPC::playSoundEffect(uint8 track, uint8 volume) { + if (_sfxEnabled) + play(track, volume); +} + +void SoundAdLibPC::play(uint8 track, uint8 volume) { + uint16 soundId = 0; + + if (_version == 4) + soundId = READ_LE_UINT16(&_trackEntries[track<<1]); + else + soundId = _trackEntries[track]; + + if ((soundId == 0xFFFF && _version == 4) || (soundId == 0xFF && _version < 4) || !_soundDataPtr) + return; + + _driver->queueTrack(soundId, volume); +} + +void SoundAdLibPC::beginFadeOut() { + play(_version > 2 ? 1 : 15, 0xFF); +} + +int SoundAdLibPC::checkTrigger() { + return _driver->getSoundTrigger(); +} + +void SoundAdLibPC::resetTrigger() { + _driver->resetSoundTrigger(); +} + +void SoundAdLibPC::initAudioResourceInfo(int set, void *info) { + if (set >= kMusicIntro && set <= kMusicFinale) { + delete _resInfo[set]; + _resInfo[set] = info ? new SoundResourceInfo_PC(*(SoundResourceInfo_PC*)info) : 0; + } +} + +void SoundAdLibPC::selectAudioResourceSet(int set) { + if (set >= kMusicIntro && set <= kMusicFinale) { + if (_resInfo[set]) + _currentResourceSet = set; + } +} + +bool SoundAdLibPC::hasSoundFile(uint file) const { + if (file < res()->fileListSize) + return (res()->fileList[file] != 0); + return false; +} + +void SoundAdLibPC::loadSoundFile(uint file) { + if (file < res()->fileListSize) + internalLoadFile(res()->fileList[file]); +} + +void SoundAdLibPC::loadSoundFile(Common::String file) { + internalLoadFile(file); +} + +void SoundAdLibPC::internalLoadFile(Common::String file) { + file += ((_version == 1) ? ".DAT" : ".ADL"); + if (_soundFileLoaded == file) + return; + + if (_soundDataPtr) + haltTrack(); + + uint8 *fileData = 0; uint32 fileSize = 0; + + fileData = _vm->resource()->fileData(file.c_str(), &fileSize); + if (!fileData) { + warning("Couldn't find music file: '%s'", file.c_str()); + return; + } + + playSoundEffect(0); + playSoundEffect(0); + + _driver->stopAllChannels(); + _soundDataPtr = 0; + + int soundDataSize = fileSize; + uint8 *p = fileData; + + if (_version == 4) { + memcpy(_trackEntries, p, 500); + p += 500; + soundDataSize -= 500; + } else { + memcpy(_trackEntries, p, 120); + p += 120; + soundDataSize -= 120; + } + + _soundDataPtr = new uint8[soundDataSize]; + assert(_soundDataPtr); + + memcpy(_soundDataPtr, p, soundDataSize); + + delete[] fileData; + fileData = p = 0; + fileSize = 0; + + _driver->setSoundData(_soundDataPtr, soundDataSize); + + _soundFileLoaded = file; +} + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_adlib.h b/engines/kyra/sound/sound_adlib.h new file mode 100644 index 0000000000..a5d6fe3659 --- /dev/null +++ b/engines/kyra/sound/sound_adlib.h @@ -0,0 +1,115 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * LGPL License + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library 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 + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +#ifndef KYRA_SOUND_ADLIB_H +#define KYRA_SOUND_ADLIB_H + +#include "kyra/sound/sound.h" + +#include "common/mutex.h" + +namespace Kyra { +class AdLibDriver; + +/** + * AdLib implementation of the sound output device. + * + * It uses a special sound file format special to + * Dune II, Kyrandia 1 and 2. While Dune II and + * Kyrandia 1 are using exact the same format, the + * one of Kyrandia 2 slightly differs. + * + * See AdLibDriver for more information. + * @see AdLibDriver + */ +class SoundAdLibPC : public Sound { +public: + SoundAdLibPC(KyraEngine_v1 *vm, Audio::Mixer *mixer); + ~SoundAdLibPC(); + + virtual kType getMusicType() const { return kAdLib; } + + virtual bool init(); + virtual void process(); + + virtual void updateVolumeSettings(); + + virtual void initAudioResourceInfo(int set, void *info); + virtual void selectAudioResourceSet(int set); + virtual bool hasSoundFile(uint file) const; + virtual void loadSoundFile(uint file); + virtual void loadSoundFile(Common::String file); + + virtual void playTrack(uint8 track); + virtual void haltTrack(); + virtual bool isPlaying() const; + + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF); + + virtual void beginFadeOut(); + + virtual int checkTrigger(); + virtual void resetTrigger(); +private: + void internalLoadFile(Common::String file); + + void play(uint8 track, uint8 volume); + + const SoundResourceInfo_PC *res() const {return _resInfo[_currentResourceSet]; } + SoundResourceInfo_PC *_resInfo[3]; + int _currentResourceSet; + + AdLibDriver *_driver; + + int _version; + uint8 _trackEntries[500]; + uint8 *_soundDataPtr; + int _sfxPlayingSound; + + Common::String _soundFileLoaded; + + int _numSoundTriggers; + const int *_soundTriggers; + + static const int _kyra1NumSoundTriggers; + static const int _kyra1SoundTriggers[]; +}; + +} // End of namespace Kyra + +#endif diff --git a/engines/kyra/sound/sound_amiga.cpp b/engines/kyra/sound/sound_amiga.cpp new file mode 100644 index 0000000000..110400415f --- /dev/null +++ b/engines/kyra/sound/sound_amiga.cpp @@ -0,0 +1,232 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_intern.h" +#include "kyra/resource/resource.h" + +#include "audio/mixer.h" +#include "audio/mods/maxtrax.h" + +namespace Kyra { + +SoundAmiga::SoundAmiga(KyraEngine_v1 *vm, Audio::Mixer *mixer) + : Sound(vm, mixer), + _driver(0), + _musicHandle(), + _fileLoaded(kFileNone), + _tableSfxIntro(0), + _tableSfxGame(0), + _tableSfxIntro_Size(0), + _tableSfxGame_Size(0) { +} + +SoundAmiga::~SoundAmiga() { + _mixer->stopHandle(_musicHandle); + delete _driver; +} + +bool SoundAmiga::init() { + _driver = new Audio::MaxTrax(_mixer->getOutputRate(), true); + + _tableSfxIntro = _vm->staticres()->loadAmigaSfxTable(k1AmigaIntroSFXTable, _tableSfxIntro_Size); + _tableSfxGame = _vm->staticres()->loadAmigaSfxTable(k1AmigaGameSFXTable, _tableSfxGame_Size); + + return _driver != 0 && _tableSfxIntro && _tableSfxGame; +} + +void SoundAmiga::initAudioResourceInfo(int set, void *info) { + // See comment below +} + +void SoundAmiga::selectAudioResourceSet(int set) { + // It seems that loadSoundFile() is doing what would normally be done in here. + // As long as this driver is only required for one single target (Kyra 1 Amiga) + // this doesn't matter much. +} + +bool SoundAmiga::hasSoundFile(uint file) const { + if (file < 3) + return true; + return false; +} + +void SoundAmiga::loadSoundFile(uint file) { + debugC(5, kDebugLevelSound, "SoundAmiga::loadSoundFile(%d)", file); + + static const char *const tableFilenames[3][2] = { + { "introscr.mx", "introinst.mx" }, + { "kyramusic.mx", 0 }, + { "finalescr.mx", "introinst.mx" } + }; + assert(file < ARRAYSIZE(tableFilenames)); + if (_fileLoaded == (FileType)file) + return; + const char *scoreName = tableFilenames[file][0]; + const char *sampleName = tableFilenames[file][1]; + bool loaded = false; + + Common::SeekableReadStream *scoreIn = _vm->resource()->createReadStream(scoreName); + if (sampleName) { + Common::SeekableReadStream *sampleIn = _vm->resource()->createReadStream(sampleName); + if (scoreIn && sampleIn) { + _fileLoaded = kFileNone; + loaded = _driver->load(*scoreIn, true, false); + loaded = loaded && _driver->load(*sampleIn, false, true); + } else + warning("SoundAmiga: missing atleast one of those music files: %s, %s", scoreName, sampleName); + delete sampleIn; + } else { + if (scoreIn) { + _fileLoaded = kFileNone; + loaded = _driver->load(*scoreIn); + } else + warning("SoundAmiga: missing music file: %s", scoreName); + } + delete scoreIn; + + if (loaded) + _fileLoaded = (FileType)file; +} + +void SoundAmiga::playTrack(uint8 track) { + debugC(5, kDebugLevelSound, "SoundAmiga::playTrack(%d)", track); + + static const byte tempoIntro[] = { 0x46, 0x55, 0x3C, 0x41 }; + static const byte tempoFinal[] = { 0x78, 0x50 }; + static const byte tempoIngame[] = { + 0x64, 0x64, 0x64, 0x64, 0x64, 0x73, 0x4B, 0x64, + 0x64, 0x64, 0x55, 0x9C, 0x6E, 0x91, 0x78, 0x84, + 0x32, 0x64, 0x64, 0x6E, 0x3C, 0xD8, 0xAF + }; + static const byte loopIngame[] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x01, 0x01, 0x01, 0x00, 0x01, 0x01, + 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00 + }; + + int score = -1; + bool loop = false; + byte volume = 0x40; + byte tempo = 0; + + + switch (_fileLoaded) { + case kFileIntro: + if (track >= 2 && track < ARRAYSIZE(tempoIntro) + 2) { + score = track - 2; + tempo = tempoIntro[score]; + } + break; + + case kFileGame: + if (track >= 11 && track < ARRAYSIZE(tempoIngame) + 11) { + score = track - 11; + loop = loopIngame[score] != 0; + tempo = tempoIngame[score]; + } + break; + + case kFileFinal: + // score 0 gets started immediately after loading the music-files with different tempo. + // we need to define a track-value for the fake call of this function + if (track >= 2 && track < ARRAYSIZE(tempoFinal) + 2) { + score = track - 2; + loop = true; + tempo = tempoFinal[score]; + } + break; + + default: + return; + } + + if (score >= 0) { + if (_musicEnabled && _driver->playSong(score, loop)) { + _driver->setVolume(volume); + _driver->setTempo(tempo << 4); + if (!_mixer->isSoundHandleActive(_musicHandle)) + _mixer->playStream(Audio::Mixer::kPlainSoundType, &_musicHandle, _driver, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO); + } + } else if (track == 0) + _driver->stopMusic(); + else if (track == 1) + beginFadeOut(); +} + +void SoundAmiga::haltTrack() { + debugC(5, kDebugLevelSound, "SoundAmiga::haltTrack()"); + _driver->stopMusic(); +} + +void SoundAmiga::beginFadeOut() { + debugC(5, kDebugLevelSound, "SoundAmiga::beginFadeOut()"); + for (int i = 0x3F; i >= 0; --i) { + _driver->setVolume((byte)i); + _vm->delay(_vm->tickLength()); + } + + _driver->stopMusic(); + _vm->delay(_vm->tickLength()); + _driver->setVolume(0x40); +} + +void SoundAmiga::playSoundEffect(uint8 track, uint8) { + debugC(5, kDebugLevelSound, "SoundAmiga::playSoundEffect(%d)", track); + const AmigaSfxTable *sfx = 0; + bool pan = false; + + switch (_fileLoaded) { + case kFileFinal: + case kFileIntro: + // We only allow playing of sound effects, which are included in the table. + if (track < _tableSfxIntro_Size) { + sfx = &_tableSfxIntro[track]; + pan = (sfx->pan != 0); + } + break; + + case kFileGame: + if (0x61 <= track && track <= 0x63) + playTrack(track - 0x4F); + + if (track >= _tableSfxGame_Size) + return; + + if (_tableSfxGame[track].note) { + sfx = &_tableSfxGame[track]; + pan = (sfx->pan != 0) && (sfx->pan != 2); + } + + break; + + default: + return; + } + + if (_sfxEnabled && sfx) { + const bool success = _driver->playNote(sfx->note, sfx->patch, sfx->duration, sfx->volume, pan); + if (success && !_mixer->isSoundHandleActive(_musicHandle)) + _mixer->playStream(Audio::Mixer::kPlainSoundType, &_musicHandle, _driver, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO); + } +} + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_digital.cpp b/engines/kyra/sound/sound_digital.cpp new file mode 100644 index 0000000000..e3586605e8 --- /dev/null +++ b/engines/kyra/sound/sound_digital.cpp @@ -0,0 +1,544 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_digital.h" +#include "kyra/resource/resource.h" +#include "kyra/engine/kyra_mr.h" + +#include "audio/audiostream.h" + +#include "audio/decoders/mp3.h" +#include "audio/decoders/vorbis.h" +#include "audio/decoders/flac.h" + +#include "common/util.h" + +namespace Kyra { + +class KyraAudioStream : public Audio::SeekableAudioStream { +public: + KyraAudioStream(Audio::SeekableAudioStream *impl) : _impl(impl), _rate(impl->getRate()), _fadeSamples(0), _fadeCount(0), _fading(0), _endOfData(false) {} + ~KyraAudioStream() { delete _impl; _impl = 0; } + + int readBuffer(int16 *buffer, const int numSamples); + bool isStereo() const { return _impl->isStereo(); } + bool endOfData() const { return _impl->endOfData() | _endOfData; } + int getRate() const { return _rate; } + + void setRate(int newRate) { _rate = newRate; } + void beginFadeOut(uint32 millis); + + bool seek(const Audio::Timestamp &where) { return _impl->seek(where); } + Audio::Timestamp getLength() const { return _impl->getLength(); } +private: + Audio::SeekableAudioStream *_impl; + + int _rate; + + int32 _fadeSamples; + int32 _fadeCount; + int _fading; + + bool _endOfData; +}; + +void KyraAudioStream::beginFadeOut(uint32 millis) { + _fadeSamples = (millis * getRate()) / 1000; + if (_fading == 0) + _fadeCount = _fadeSamples; + _fading = -1; +} + +int KyraAudioStream::readBuffer(int16 *buffer, const int numSamples) { + int samplesRead = _impl->readBuffer(buffer, numSamples); + + if (_fading) { + int samplesProcessed = 0; + for (; samplesProcessed < samplesRead; ++samplesProcessed) { + // To help avoid overflows for long fade times, we divide both + // _fadeSamples and _fadeCount when calculating the new sample. + + int32 div = _fadeSamples / 256; + if (_fading) { + *buffer = (*buffer * (_fadeCount / 256)) / div; + ++buffer; + + _fadeCount += _fading; + + if (_fadeCount < 0) { + _fadeCount = 0; + _endOfData = true; + } else if (_fadeCount > _fadeSamples) { + _fadeCount = _fadeSamples; + _fading = 0; + } + } + } + + if (_endOfData) { + memset(buffer, 0, (samplesRead - samplesProcessed) * sizeof(int16)); + samplesRead = samplesProcessed; + } + } + + return samplesRead; +} + +// Thanks to Torbjorn Andersson (eriktorbjorn) for his aud player on which +// this code is based on + +// TODO: cleanup of whole AUDStream + +class AUDStream : public Audio::SeekableAudioStream { +public: + AUDStream(Common::SeekableReadStream *stream); + ~AUDStream(); + + int readBuffer(int16 *buffer, const int numSamples); + + bool isStereo() const { return false; } + bool endOfData() const { return _endOfData; } + + int getRate() const { return _rate; } + + bool seek(const Audio::Timestamp &where); + Audio::Timestamp getLength() const { return _length; } +private: + Common::SeekableReadStream *_stream; + uint32 _streamStart; + bool _endOfData; + int _rate; + uint _processedSize; + uint _totalSize; + Audio::Timestamp _length; + + int _bytesLeft; + + byte *_outBuffer; + int _outBufferOffset; + uint _outBufferSize; + + byte *_inBuffer; + uint _inBufferSize; + + int readChunk(int16 *buffer, const int maxSamples); + + static const int8 WSTable2Bit[]; + static const int8 WSTable4Bit[]; +}; + +const int8 AUDStream::WSTable2Bit[] = { -2, -1, 0, 1 }; +const int8 AUDStream::WSTable4Bit[] = { + -9, -8, -6, -5, -4, -3, -2, -1, + 0, 1, 2, 3, 4, 5, 6, 8 +}; + +AUDStream::AUDStream(Common::SeekableReadStream *stream) : _stream(stream), _endOfData(true), _rate(0), + _processedSize(0), _totalSize(0), _length(0, 1), _bytesLeft(0), _outBuffer(0), + _outBufferOffset(0), _outBufferSize(0), _inBuffer(0), _inBufferSize(0) { + + _rate = _stream->readUint16LE(); + _totalSize = _stream->readUint32LE(); + + // TODO?: add checks + int flags = _stream->readByte(); // flags + int type = _stream->readByte(); // type + + _streamStart = stream->pos(); + + debugC(5, kDebugLevelSound, "AUD Info: rate: %d, totalSize: %d, flags: %d, type: %d, streamStart: %d", _rate, _totalSize, flags, type, _streamStart); + + _length = Audio::Timestamp(0, _rate); + for (uint32 i = 0; i < _totalSize;) { + uint16 size = _stream->readUint16LE(); + uint16 outSize = _stream->readUint16LE(); + + _length = _length.addFrames(outSize); + stream->seek(size + 4, SEEK_CUR); + i += size + 8; + } + + stream->seek(_streamStart, SEEK_SET); + + if (type == 1 && !flags) + _endOfData = false; + else + warning("No AUD file (rate: %d, size: %d, flags: 0x%X, type: %d)", _rate, _totalSize, flags, type); +} + +AUDStream::~AUDStream() { + delete[] _outBuffer; + delete[] _inBuffer; + delete _stream; +} + +int AUDStream::readBuffer(int16 *buffer, const int numSamples) { + int samplesRead = 0, samplesLeft = numSamples; + + while (samplesLeft > 0 && !_endOfData) { + int samples = readChunk(buffer, samplesLeft); + samplesRead += samples; + samplesLeft -= samples; + buffer += samples; + } + + return samplesRead; +} + +inline int16 clip8BitSample(int16 sample) { + return CLIP<int16>(sample, 0, 255); +} + +int AUDStream::readChunk(int16 *buffer, const int maxSamples) { + int samplesProcessed = 0; + + // if no bytes of the old chunk are left, read the next one + if (_bytesLeft <= 0) { + if (_processedSize >= _totalSize) { + _endOfData = true; + return 0; + } + + uint16 size = _stream->readUint16LE(); + uint16 outSize = _stream->readUint16LE(); + uint32 id = _stream->readUint32LE(); + + assert(id == 0x0000DEAF); + + _processedSize += 8 + size; + + _outBufferOffset = 0; + if (size == outSize) { + if (outSize > _outBufferSize) { + _outBufferSize = outSize; + delete[] _outBuffer; + _outBuffer = new uint8[_outBufferSize]; + assert(_outBuffer); + } + + _bytesLeft = size; + + _stream->read(_outBuffer, _bytesLeft); + } else { + _bytesLeft = outSize; + + if (outSize > _outBufferSize) { + _outBufferSize = outSize; + delete[] _outBuffer; + _outBuffer = new uint8[_outBufferSize]; + assert(_outBuffer); + } + + if (size > _inBufferSize) { + _inBufferSize = size; + delete[] _inBuffer; + _inBuffer = new uint8[_inBufferSize]; + assert(_inBuffer); + } + + if (_stream->read(_inBuffer, size) != size) { + _endOfData = true; + return 0; + } + + int16 curSample = 0x80; + byte code = 0; + int8 count = 0; + uint16 input = 0; + int j = 0; + int i = 0; + + while (outSize > 0) { + input = _inBuffer[i++] << 2; + code = (input >> 8) & 0xFF; + count = (input & 0xFF) >> 2; + + switch (code) { + case 2: + if (count & 0x20) { + /* NOTE: count is signed! */ + count <<= 3; + curSample += (count >> 3); + _outBuffer[j++] = curSample & 0xFF; + outSize--; + } else { + for (; count >= 0; count--) { + _outBuffer[j++] = _inBuffer[i++]; + outSize--; + } + curSample = _inBuffer[i - 1]; + } + break; + case 1: + for (; count >= 0; count--) { + code = _inBuffer[i++]; + + curSample += WSTable4Bit[code & 0x0F]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample; + + curSample += WSTable4Bit[code >> 4]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample; + + outSize -= 2; + } + break; + case 0: + for (; count >= 0; count--) { + code = (uint8)_inBuffer[i++]; + + curSample += WSTable2Bit[code & 0x03]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample & 0xFF; + + curSample += WSTable2Bit[(code >> 2) & 0x03]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample & 0xFF; + + curSample += WSTable2Bit[(code >> 4) & 0x03]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample & 0xFF; + + curSample += WSTable2Bit[(code >> 6) & 0x03]; + curSample = clip8BitSample(curSample); + _outBuffer[j++] = curSample & 0xFF; + + outSize -= 4; + } + break; + default: + for (; count >= 0; count--) { + _outBuffer[j++] = curSample & 0xFF; + outSize--; + } + } + } + } + } + + // copies the chunk data to the output buffer + if (_bytesLeft > 0) { + int samples = MIN(_bytesLeft, maxSamples); + samplesProcessed += samples; + _bytesLeft -= samples; + + while (samples--) { + int16 sample = (_outBuffer[_outBufferOffset++] << 8) ^ 0x8000; + + *buffer++ = sample; + } + } + + return samplesProcessed; +} + +bool AUDStream::seek(const Audio::Timestamp &where) { + const uint32 seekSample = Audio::convertTimeToStreamPos(where, getRate(), isStereo()).totalNumberOfFrames(); + + _stream->seek(_streamStart); + _processedSize = 0; + _bytesLeft = 0; + _endOfData = false; + + uint32 curSample = 0; + + while (!endOfData()) { + uint16 size = _stream->readUint16LE(); + uint16 outSize = _stream->readUint16LE(); + + if (curSample + outSize > seekSample) { + _stream->seek(-4, SEEK_CUR); + + uint32 samples = seekSample - curSample; + int16 *temp = new int16[samples]; + assert(temp); + + readChunk(temp, samples); + delete[] temp; + curSample += samples; + break; + } else { + curSample += outSize; + _processedSize += 8 + size; + _stream->seek(size + 4, SEEK_CUR); + } + } + + _endOfData = (_processedSize >= _totalSize); + + return (curSample == seekSample); +} + +#pragma mark - + +SoundDigital::SoundDigital(KyraEngine_MR *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) { + for (uint i = 0; i < ARRAYSIZE(_sounds); ++i) + _sounds[i].stream = 0; +} + +SoundDigital::~SoundDigital() { + for (int i = 0; i < ARRAYSIZE(_sounds); ++i) + stopSound(i); +} + +int SoundDigital::playSound(const char *filename, uint8 priority, Audio::Mixer::SoundType type, int volume, bool loop, int channel) { + Sound *use = 0; + if (channel != -1 && channel < ARRAYSIZE(_sounds)) { + stopSound(channel); + use = &_sounds[channel]; + } else { + for (channel = 0; !use && channel < ARRAYSIZE(_sounds); ++channel) { + if (!isPlaying(channel)) { + stopSound(channel); + use = &_sounds[channel]; + break; + } + } + + for (channel = 0; !use && channel < ARRAYSIZE(_sounds); ++channel) { + if (strcmp(_sounds[channel].filename, filename) == 0) { + stopSound(channel); + use = &_sounds[channel]; + break; + } + } + + for (channel = 0; !use && channel < ARRAYSIZE(_sounds); ++channel) { + if (_sounds[channel].priority <= priority) { + stopSound(channel); + use = &_sounds[channel]; + break; + } + } + + if (!use) { + warning("no free sound channel"); + return -1; + } + } + + Common::SeekableReadStream *stream = 0; + int usedCodec = -1; + for (int i = 0; _supportedCodecs[i].fileext; ++i) { + Common::String file = filename; + file += _supportedCodecs[i].fileext; + + if (!_vm->resource()->exists(file.c_str())) + continue; + + stream = _vm->resource()->createReadStream(file); + usedCodec = i; + } + + if (!stream) { + warning("Couldn't find soundfile '%s'", filename); + return -1; + } + + Common::strlcpy(use->filename, filename, sizeof(use->filename)); + use->priority = priority; + debugC(5, kDebugLevelSound, "playSound: \"%s\"", use->filename); + Audio::SeekableAudioStream *audioStream = _supportedCodecs[usedCodec].streamFunc(stream, DisposeAfterUse::YES); + if (!audioStream) { + warning("Couldn't create audio stream for file '%s'", filename); + return -1; + } + use->stream = new KyraAudioStream(audioStream); + assert(use->stream); + if (use->stream->endOfData()) { + delete use->stream; + use->stream = 0; + + return -1; + } + + if (volume > 255) + volume = 255; + volume = (volume * Audio::Mixer::kMaxChannelVolume) / 255; + + if (type == Audio::Mixer::kSpeechSoundType && _vm->heliumMode()) + use->stream->setRate(32765); + + _mixer->playStream(type, &use->handle, makeLoopingAudioStream(use->stream, loop ? 0 : 1), -1, volume); + return use - _sounds; +} + +bool SoundDigital::isPlaying(int channel) { + if (channel == -1) + return false; + + assert(channel >= 0 && channel < ARRAYSIZE(_sounds)); + + if (!_sounds[channel].stream) + return false; + + return _mixer->isSoundHandleActive(_sounds[channel].handle); +} + +void SoundDigital::stopSound(int channel) { + if (channel == -1) + return; + + assert(channel >= 0 && channel < ARRAYSIZE(_sounds)); + _mixer->stopHandle(_sounds[channel].handle); + _sounds[channel].stream = 0; +} + +void SoundDigital::stopAllSounds() { + for (int i = 0; i < ARRAYSIZE(_sounds); ++i) { + if (isPlaying(i)) + stopSound(i); + } +} + +void SoundDigital::beginFadeOut(int channel, int ticks) { + if (isPlaying(channel)) + _sounds[channel].stream->beginFadeOut(ticks * _vm->tickLength()); +} + +// static res + +namespace { + +Audio::SeekableAudioStream *makeAUDStream(Common::SeekableReadStream *stream, DisposeAfterUse::Flag disposeAfterUse) { + return new AUDStream(stream); +} + +} // end of anonymous namespace + +const SoundDigital::AudioCodecs SoundDigital::_supportedCodecs[] = { +#ifdef USE_FLAC + { ".FLA", Audio::makeFLACStream }, +#endif // USE_FLAC +#ifdef USE_VORBIS + { ".OGG", Audio::makeVorbisStream }, +#endif // USE_VORBIS +#ifdef USE_MAD + { ".MP3", Audio::makeMP3Stream }, +#endif // USE_MAD + { ".AUD", makeAUDStream }, + { 0, 0 } +}; + + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_digital.h b/engines/kyra/sound/sound_digital.h new file mode 100644 index 0000000000..271dde6a21 --- /dev/null +++ b/engines/kyra/sound/sound_digital.h @@ -0,0 +1,119 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef KYRA_SOUND_DIGITAL_H +#define KYRA_SOUND_DIGITAL_H + +#include "audio/mixer.h" + +namespace Common { +class SeekableReadStream; +} // End of namespace Common + +namespace Audio { +class SeekableAudioStream; +} // End of namespace Audio + +namespace Kyra { + +// Digital Audio +class KyraAudioStream; +class KyraEngine_MR; + +/** + * Digital audio output device. + * + * This is just used for Kyrandia 3. + */ +class SoundDigital { +public: + SoundDigital(KyraEngine_MR *vm, Audio::Mixer *mixer); + ~SoundDigital(); + + /** + * Plays a sound. + * + * @param filename file to be played + * @param priority priority of the sound + * @param type type + * @param volume channel volume + * @param loop true if the sound should loop (endlessly) + * @param channel tell the sound player to use a specific channel for playback + * + * @return channel playing the sound + */ + int playSound(const char *filename, uint8 priority, Audio::Mixer::SoundType type, int volume = 255, bool loop = false, int channel = -1); + + /** + * Checks if a given channel is playing a sound. + * + * @param channel channel number to check + * @return true if playing, else false + */ + bool isPlaying(int channel); + + /** + * Stop the playback of a sound in the given + * channel. + * + * @param channel channel number + */ + void stopSound(int channel); + + /** + * Stops playback of all sounds. + */ + void stopAllSounds(); + + /** + * Makes the sound in a given channel + * fading out. + * + * @param channel channel number + * @param ticks fadeout time + */ + void beginFadeOut(int channel, int ticks); +private: + KyraEngine_MR *_vm; + Audio::Mixer *_mixer; + + struct Sound { + Audio::SoundHandle handle; + + char filename[16]; + uint8 priority; + KyraAudioStream *stream; + } _sounds[4]; + + struct AudioCodecs { + const char *fileext; + Audio::SeekableAudioStream *(*streamFunc)( + Common::SeekableReadStream *stream, + DisposeAfterUse::Flag disposeAfterUse); + }; + + static const AudioCodecs _supportedCodecs[]; +}; + +} // End of namespace Kyra + +#endif diff --git a/engines/kyra/sound/sound_intern.h b/engines/kyra/sound/sound_intern.h new file mode 100644 index 0000000000..77436d08ee --- /dev/null +++ b/engines/kyra/sound/sound_intern.h @@ -0,0 +1,407 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifndef KYRA_SOUND_INTERN_H +#define KYRA_SOUND_INTERN_H + + + +#include "kyra/sound/sound.h" +#include "kyra/sound/sound_adlib.h" + +#include "common/mutex.h" + +#include "audio/softsynth/fmtowns_pc98/towns_pc98_driver.h" +#include "audio/softsynth/fmtowns_pc98/towns_euphony.h" + +#include "audio/softsynth/emumidi.h" +#include "audio/midiparser.h" + +namespace Audio { +class PCSpeaker; +class MaxTrax; +} // End of namespace Audio + +namespace Kyra { + +class MidiOutput; + +/** + * MIDI output device. + * + * This device supports both MT-32 MIDI, as used in + * Kyrandia 1 and 2, and GM MIDI, as used in Kyrandia 2. + */ +class SoundMidiPC : public Sound { +public: + SoundMidiPC(KyraEngine_v1 *vm, Audio::Mixer *mixer, MidiDriver *driver, kType type); + virtual ~SoundMidiPC(); + + virtual kType getMusicType() const { return _type; } + + virtual bool init(); + + virtual void updateVolumeSettings(); + + virtual void initAudioResourceInfo(int set, void *info); + virtual void selectAudioResourceSet(int set); + virtual bool hasSoundFile(uint file) const; + virtual void loadSoundFile(uint file); + virtual void loadSoundFile(Common::String file); + virtual void loadSfxFile(Common::String file); + + virtual void playTrack(uint8 track); + virtual void haltTrack(); + virtual bool isPlaying() const; + + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF); + virtual void stopAllSoundEffects(); + + virtual void beginFadeOut(); + + virtual void pause(bool paused); +private: + static void onTimer(void *data); + + // Our channel handling + int _musicVolume, _sfxVolume; + + uint32 _fadeStartTime; + bool _fadeMusicOut; + + // Midi file related + Common::String _mFileName, _sFileName; + byte *_musicFile, *_sfxFile; + + MidiParser *_music; + MidiParser *_sfx[3]; + + const SoundResourceInfo_PC *res() const {return _resInfo[_currentResourceSet]; } + SoundResourceInfo_PC *_resInfo[3]; + int _currentResourceSet; + + // misc + kType _type; + Common::String getFileName(const Common::String &str); + + bool _nativeMT32; + MidiDriver *_driver; + MidiOutput *_output; + + Common::Mutex _mutex; +}; + +class SoundTowns : public Sound { +public: + SoundTowns(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~SoundTowns(); + + virtual kType getMusicType() const { return kTowns; } + + virtual bool init(); + virtual void process(); + + virtual void initAudioResourceInfo(int set, void *info); + virtual void selectAudioResourceSet(int set); + virtual bool hasSoundFile(uint file) const; + virtual void loadSoundFile(uint file); + virtual void loadSoundFile(Common::String) {} + + virtual void playTrack(uint8 track); + virtual void haltTrack(); + + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF); + virtual void stopAllSoundEffects(); + + virtual void beginFadeOut(); + + virtual void updateVolumeSettings(); + +private: + bool loadInstruments(); + void playEuphonyTrack(uint32 offset, int loop); + + void fadeOutSoundEffects(); + + int _lastTrack; + Audio::SoundHandle _sfxHandle; + + uint8 *_musicTrackData; + + uint _sfxFileIndex; + uint8 *_sfxFileData; + uint8 _sfxChannel; + + EuphonyPlayer *_player; + + bool _cdaPlaying; + + const SoundResourceInfo_Towns *res() const {return _resInfo[_currentResourceSet]; } + SoundResourceInfo_Towns *_resInfo[3]; + int _currentResourceSet; + + const uint8 *_musicFadeTable; + const uint8 *_sfxBTTable; + const uint8 *_sfxWDTable; +}; + +class SoundPC98 : public Sound { +public: + SoundPC98(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~SoundPC98(); + + virtual kType getMusicType() const override { return kPC98; } + + virtual bool init() override; + + virtual void initAudioResourceInfo(int set, void *info) override; + virtual void selectAudioResourceSet(int set) override; + virtual bool hasSoundFile(uint file) const override; + virtual void loadSoundFile(uint file) override; + virtual void loadSoundFile(Common::String file) override; + + virtual void playTrack(uint8 track) override; + virtual void haltTrack() override; + virtual void beginFadeOut() override; + + virtual int32 voicePlay(const char *file, Audio::SoundHandle *handle, uint8 volume, uint8 priority, bool isSfx) override { return -1; } + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF) override; + + virtual void updateVolumeSettings() override; + +private: + int _lastTrack; + uint8 *_musicTrackData; + uint8 *_sfxTrackData; + TownsPC98_AudioDriver *_driver; + + const char *resPattern() {return _resInfo[_currentResourceSet]->c_str(); } + Common::String *_resInfo[3]; + int _currentResourceSet; +}; + +class SoundTownsPC98_v2 : public Sound { +public: + SoundTownsPC98_v2(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~SoundTownsPC98_v2(); + + virtual kType getMusicType() const override { return _vm->gameFlags().platform == Common::kPlatformFMTowns ? kTowns : kPC98; } + + virtual bool init() override; + virtual void process() override; + + virtual void initAudioResourceInfo(int set, void *info) override; + virtual void selectAudioResourceSet(int set) override; + virtual bool hasSoundFile(uint file) const override; + virtual void loadSoundFile(uint file) override {} + virtual void loadSoundFile(Common::String file) override; + + virtual void playTrack(uint8 track) override; + virtual void haltTrack() override; + virtual void beginFadeOut() override; + + virtual int32 voicePlay(const char *file, Audio::SoundHandle *handle, uint8 volume = 255, uint8 priority = 255, bool isSfx = true) override; + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF) override; + + virtual void updateVolumeSettings() override; + +private: + Audio::AudioStream *_currentSFX; + int _lastTrack; + bool _useFmSfx; + + uint8 *_musicTrackData; + uint8 *_sfxTrackData; + TownsPC98_AudioDriver *_driver; + + const SoundResourceInfo_TownsPC98V2 *res() const {return _resInfo[_currentResourceSet]; } + SoundResourceInfo_TownsPC98V2 *_resInfo[3]; + int _currentResourceSet; +}; + +// PC Speaker MIDI driver +class MidiDriver_PCSpeaker : public MidiDriver_Emulated { +public: + MidiDriver_PCSpeaker(Audio::Mixer *mixer); + ~MidiDriver_PCSpeaker(); + + // MidiDriver interface + virtual void close() {} + + virtual void send(uint32 data); + + virtual MidiChannel *allocateChannel() { return 0; } + virtual MidiChannel *getPercussionChannel() { return 0; } + + // MidiDriver_Emulated interface + void generateSamples(int16 *buffer, int numSamples); + + // AudioStream interface + bool isStereo() const { return false; } + int getRate() const { return _rate; } +private: + Common::Mutex _mutex; + Audio::PCSpeaker *_speaker; + int _rate; + + struct Channel { + uint8 pitchBendLow, pitchBendHigh; + uint8 hold; + uint8 modulation; + uint8 voiceProtect; + uint8 noteCount; + } _channel[2]; + + void resetController(int channel); + + struct Note { + bool enabled; + uint8 hardwareChannel; + uint8 midiChannel; + uint8 note; + bool processHold; + uint8 flags; + uint8 hardwareFlags; + uint16 priority; + int16 modulation; + uint16 precedence; + } _note[2]; + + void noteOn(int channel, int note); + void noteOff(int channel, int note); + + void turnNoteOn(int note); + void overwriteNote(int note); + void turnNoteOff(int note); + + void setupTone(int note); + + uint16 _countdown; + uint8 _hardwareChannel[1]; + bool _modulationFlag; + + uint8 _timerValue; + void onTimer(); + + static const uint8 _noteTable1[]; + static const uint8 _noteTable2[]; +}; + +// for StaticResource (maybe we can find a nicer way to handle it) +struct AmigaSfxTable { + uint8 note; + uint8 patch; + uint16 duration; + uint8 volume; + uint8 pan; +}; + +class SoundAmiga : public Sound { +public: + SoundAmiga(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~SoundAmiga(); + + virtual kType getMusicType() const override { return kAmiga; } //FIXME + + virtual bool init() override; + + virtual void initAudioResourceInfo(int set, void *info) override; + virtual void selectAudioResourceSet(int set) override; + virtual bool hasSoundFile(uint file) const override; + virtual void loadSoundFile(uint file) override; + virtual void loadSoundFile(Common::String) override {} + + virtual void playTrack(uint8 track) override; + virtual void haltTrack() override; + virtual void beginFadeOut() override; + + virtual int32 voicePlay(const char *file, Audio::SoundHandle *handle, uint8 volume, uint8 priority, bool isSfx) override { return -1; } + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF) override; + +protected: + Audio::MaxTrax *_driver; + Audio::SoundHandle _musicHandle; + enum FileType { kFileNone = -1, kFileIntro = 0, kFileGame = 1, kFileFinal = 2 } _fileLoaded; + + const AmigaSfxTable *_tableSfxIntro; + int _tableSfxIntro_Size; + + const AmigaSfxTable *_tableSfxGame; + int _tableSfxGame_Size; +}; + +class SoundTowns_Darkmoon : public Sound, public TownsAudioInterfacePluginDriver { +public: + SoundTowns_Darkmoon(KyraEngine_v1 *vm, Audio::Mixer *mixer); + virtual ~SoundTowns_Darkmoon(); + + virtual kType getMusicType() const { return kTowns; } + + virtual bool init(); + + void timerCallback(int timerId); + + virtual void initAudioResourceInfo(int set, void *info); + virtual void selectAudioResourceSet(int set); + virtual bool hasSoundFile(uint file) const; + virtual void loadSoundFile(uint file) {} + virtual void loadSoundFile(Common::String name); + + virtual void playTrack(uint8 track); + virtual void haltTrack(); + virtual bool isPlaying() const; + + virtual void playSoundEffect(uint8 track, uint8 volume = 0xFF); + virtual void stopAllSoundEffects(); + + virtual void beginFadeOut(); + + virtual void updateVolumeSettings(); + + virtual int checkTrigger(); + + virtual void resetTrigger(); + +private: + struct SoundTableEntry { + int8 type; + int32 para1; + int16 para2; + } _soundTable[120]; + + uint8 _lastSfxChan; + uint8 _lastEnvChan; + uint8 *_pcmData; + uint32 _pcmDataSize; + uint8 _pcmVol; + + int _timer; + int _timerSwitch; + + SoundResourceInfo_TownsEoB *_pcmResource[3]; + + TownsAudioInterface *_intf; +}; + +} // End of namespace Kyra + +#endif diff --git a/engines/kyra/sound/sound_lok.cpp b/engines/kyra/sound/sound_lok.cpp new file mode 100644 index 0000000000..34436637a0 --- /dev/null +++ b/engines/kyra/sound/sound_lok.cpp @@ -0,0 +1,96 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/engine/kyra_lok.h" +#include "kyra/sound/sound.h" + +#include "common/system.h" + +namespace Kyra { + +void KyraEngine_LoK::snd_playSoundEffect(int track, int volume) { + if (_flags.platform == Common::kPlatformPC98) { + if (track < 16 || track > 119) + track = 58; + else + track -= 16; + } + + if (_flags.platform == Common::kPlatformFMTowns && track == 49) { + snd_playWanderScoreViaMap(56, 1); + return; + } + + KyraEngine_v1::snd_playSoundEffect(track); +} + +void KyraEngine_LoK::snd_playWanderScoreViaMap(int command, int restart) { + if (restart) + _lastMusicCommand = -1; + + if (_flags.platform == Common::kPlatformFMTowns) { + if (command >= 35 && command <= 38) { + snd_playSoundEffect(command - 20); + } else if (command >= 2) { + if (_lastMusicCommand != command) + // the original does -2 here we handle this inside _sound->playTrack() + _sound->playTrack(command); + } else { + _sound->beginFadeOut(); + } + _lastMusicCommand = command; + } else if (_flags.platform == Common::kPlatformPC98) { + if (command == 1) { + _sound->beginFadeOut(); + } else if ((command >= 2 && command < 53) || command == 55) { + if (_lastMusicCommand != command) + _sound->playTrack(command); + } else { + _sound->haltTrack(); + } + _lastMusicCommand = command; + } else { + KyraEngine_v1::snd_playWanderScoreViaMap(command, restart); + } +} + +void KyraEngine_LoK::snd_playVoiceFile(int id) { + Common::String vocFile = Common::String::format("%03d", id); + _speechPlayTime = _sound->voicePlay(vocFile.c_str(), &_speechHandle); +} + +void KyraEngine_LoK::snd_voiceWaitForFinish(bool ingame) { + while (_sound->voiceIsPlaying() && !skipFlag()) { + if (ingame) + delay(10, true); + else + _system->delayMillis(10); + } +} + +uint32 KyraEngine_LoK::snd_getVoicePlayTime() { + if (!snd_voiceIsPlaying()) + return 0; + return (_speechPlayTime != -1 ? _speechPlayTime : 0); +} + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_lol.cpp b/engines/kyra/sound/sound_lol.cpp new file mode 100644 index 0000000000..ae64af91b9 --- /dev/null +++ b/engines/kyra/sound/sound_lol.cpp @@ -0,0 +1,307 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#ifdef ENABLE_LOL + +#include "kyra/engine/lol.h" +#include "kyra/sound/sound.h" +#include "kyra/resource/resource.h" + +#include "common/system.h" + +#include "audio/audiostream.h" + +namespace Kyra { + +bool LoLEngine::snd_playCharacterSpeech(int id, int8 speaker, int) { + if (!speechEnabled()) + return false; + + if (speaker < 65) { + if (_characters[speaker].flags & 1) + speaker = (int)_characters[speaker].name[0]; + else + speaker = 0; + } + + if (_lastSpeechId == id && speaker == _lastSpeaker) + return true; + + _lastSpeechId = id; + _lastSpeaker = speaker; + _nextSpeechId = _nextSpeaker = -1; + + Common::String pattern1; + Common::String file1; + Common::String file2; + Common::String file3; + + SpeechList newSpeechList; + + Common::String pattern2 = Common::String::format("%02d", id & 0x4000 ? 0 : _curTlkFile); + + if (id & 0x4000) { + pattern1 = Common::String::format("%03X", id & 0x3FFF); + } else if (id < 1000) { + pattern1 = Common::String::format("%03d", id); + } else { + file3 = Common::String::format("@%04d%c.%s", id - 1000, (char)speaker, pattern2.c_str()); + if (_sound->isVoicePresent(file3.c_str())) + newSpeechList.push_back(_sound->getVoiceStream(file3.c_str())); + } + + if (file3.empty()) { + for (char i = 0; ; i++) { + char symbol = '0' + i; + file1 = Common::String::format("%s%c%c.%s", pattern1.c_str(), (char)speaker, symbol, pattern2.c_str()); + file2 = Common::String::format("%s%c%c.%s", pattern1.c_str(), '_', symbol, pattern2.c_str()); + if (_sound->isVoicePresent(file1.c_str())) + newSpeechList.push_back(_sound->getVoiceStream(file1.c_str())); + else if (_sound->isVoicePresent(file2.c_str())) + newSpeechList.push_back(_sound->getVoiceStream(file2.c_str())); + else + break; + } + } + + if (newSpeechList.empty()) + return false; + + while (_sound->voiceIsPlaying(&_speechHandle)) + delay(_tickLength, true); + + while (_sound->allVoiceChannelsPlaying()) + delay(_tickLength); + + for (SpeechList::iterator i = _speechList.begin(); i != _speechList.end(); ++i) + delete *i; + _speechList.clear(); + _speechList = newSpeechList; + + _activeVoiceFileTotalTime = 0; + for (SpeechList::iterator i = _speechList.begin(); i != _speechList.end();) { + // Just in case any file loading failed: Remove the bad streams here. + if (!*i) + i = _speechList.erase(i); + else + _activeVoiceFileTotalTime += (*i++)->getLength().msecs(); + } + + _sound->playVoiceStream(*_speechList.begin(), &_speechHandle); + _speechList.pop_front(); + + if (!_activeVoiceFileTotalTime) + return false; + + _tim->_abortFlag = 0; + + return true; +} + +int LoLEngine::snd_updateCharacterSpeech() { + if (_sound->voiceIsPlaying(&_speechHandle)) + return 2; + + if (_speechList.begin() != _speechList.end()) { + _sound->playVoiceStream(*_speechList.begin(), &_speechHandle); + _speechList.pop_front(); + return 2; + + } else if (_nextSpeechId != -1) { + _lastSpeechId = _lastSpeaker = -1; + _activeVoiceFileTotalTime = 0; + if (snd_playCharacterSpeech(_nextSpeechId, _nextSpeaker, 0)) + return 2; + } + + _lastSpeechId = _lastSpeaker = -1; + _activeVoiceFileTotalTime = 0; + + return 0; +} + +void LoLEngine::snd_stopSpeech(bool setFlag) { + if (!_sound->voiceIsPlaying(&_speechHandle)) + return; + + //_dlgTimer = 0; + _sound->voiceStop(&_speechHandle); + _activeVoiceFileTotalTime = 0; + _nextSpeechId = _nextSpeaker = -1; + + for (SpeechList::iterator i = _speechList.begin(); i != _speechList.end(); ++i) + delete *i; + _speechList.clear(); + + if (setFlag) + _tim->_abortFlag = 1; +} + +void LoLEngine::snd_playSoundEffect(int track, int volume) { + if ((track == 1 && (_lastSfxTrack == -1 || _lastSfxTrack == 1)) || shouldQuit()) + return; + + _lastSfxTrack = track; + if (track == -1 || track >= _ingameSoundIndexSize / 2) + return; + + volume &= 0xFF; + int16 prIndex = _ingameSoundIndex[track * 2 + 1]; + uint16 priority = (prIndex > 0) ? (prIndex * volume) >> 8 : -prIndex; + + static const uint8 volTable1[] = { 223, 159, 95, 47, 15, 0 }; + static const uint8 volTable2[] = { 255, 191, 127, 63, 30, 0 }; + + for (int i = 0; i < 6; i++) { + if (volTable1[i] < volume) { + volume = volTable2[i]; + break; + } + } + + int16 vocIndex = _ingameSoundIndex[track * 2]; + + bool hasVocFile = false; + if (vocIndex != -1) { + if (scumm_stricmp(_ingameSoundList[vocIndex], "EMPTY")) + hasVocFile = true; + } + + if (hasVocFile) { + if (_sound->isVoicePresent(_ingameSoundList[vocIndex])) + _sound->voicePlay(_ingameSoundList[vocIndex], 0, volume, priority, true); + } else if (_flags.platform == Common::kPlatformDOS) { + if (_sound->getSfxType() == Sound::kMidiMT32) + track = (track < _ingameMT32SoundIndexSize) ? (_ingameMT32SoundIndex[track] - 1) : -1; + else if (_sound->getSfxType() == Sound::kMidiGM) + track = (track < _ingameGMSoundIndexSize) ? (_ingameGMSoundIndex[track] - 1) : -1; + else if (_sound->getSfxType() == Sound::kPCSpkr) + track = (track < _ingamePCSpeakerSoundIndexSize) ? (_ingamePCSpeakerSoundIndex[track] - 1) : -1; + + if (track == 168) + track = 167; + + if (track != -1) + KyraEngine_v1::snd_playSoundEffect(track, volume); + } +} + +bool LoLEngine::snd_processEnvironmentalSoundEffect(int soundId, int block) { + if (!KyraRpgEngine::snd_processEnvironmentalSoundEffect(soundId, block)) + return false; + + if (block != _currentBlock) { + static const int8 blockShiftTable[] = { -32, -31, 1, 33, 32, 31, -1, -33 }; + uint16 cbl = _currentBlock; + + for (int i = 3; i > 0; i--) { + int dir = calcMonsterDirection(cbl & 0x1F, cbl >> 5, block & 0x1F, block >> 5); + cbl = (cbl + blockShiftTable[dir]) & 0x3FF; + if (cbl == block) + break; + if (testWallFlag(cbl, 0, 1)) + _environmentSfxVol >>= 1; + } + } + + if (!soundId || _sceneUpdateRequired) + return false; + + return snd_processEnvironmentalSoundEffect(0, 0); +} + +void LoLEngine::snd_queueEnvironmentalSoundEffect(int soundId, int block) { + if (_envSfxUseQueue && _envSfxNumTracksInQueue < 10) { + _envSfxQueuedTracks[_envSfxNumTracksInQueue] = soundId; + _envSfxQueuedBlocks[_envSfxNumTracksInQueue] = block; + _envSfxNumTracksInQueue++; + } else { + snd_processEnvironmentalSoundEffect(soundId, block); + } +} + +void LoLEngine::snd_playQueuedEffects() { + for (int i = 0; i < _envSfxNumTracksInQueue; i++) + snd_processEnvironmentalSoundEffect(_envSfxQueuedTracks[i], _envSfxQueuedBlocks[i]); + _envSfxNumTracksInQueue = 0; +} + +void LoLEngine::snd_loadSoundFile(int track) { + if (_sound->musicEnabled()) { + if (_flags.platform == Common::kPlatformDOS) { + int t = (track - 250) * 3; + if (_curMusicFileIndex != _musicTrackMap[t] || _curMusicFileExt != (char)_musicTrackMap[t + 1]) { + snd_stopMusic(); + _sound->loadSoundFile(Common::String::format("LORE%02d%c", _musicTrackMap[t], (char)_musicTrackMap[t + 1])); + _curMusicFileIndex = _musicTrackMap[t]; + _curMusicFileExt = (char)_musicTrackMap[t + 1]; + } else { + snd_stopMusic(); + } + } + } +} + +int LoLEngine::snd_playTrack(int track) { + if (track == -1) + return _lastMusicTrack; + + int res = _lastMusicTrack; + _lastMusicTrack = track; + + if (_sound->musicEnabled()) { + if (_flags.platform == Common::kPlatformDOS) { + snd_loadSoundFile(track); + int t = (track - 250) * 3; + _sound->playTrack(_musicTrackMap[t + 2]); + } else { + _sound->playTrack(track - 249); + } + } + + return res; +} + +int LoLEngine::snd_stopMusic() { + if (_sound->musicEnabled()) { + if (_sound->isPlaying()) { + _sound->beginFadeOut(); + _system->delayMillis(3 * _tickLength); + } + + _sound->haltTrack(); + } + return snd_playTrack(-1); +} + +int LoLEngine::convertVolumeToMixer(int value) { + value -= 2; + return (value * Audio::Mixer::kMaxMixerVolume) / 100; +} + +int LoLEngine::convertVolumeFromMixer(int value) { + return (value * 100) / Audio::Mixer::kMaxMixerVolume + 2; +} + +} // End of namespace Kyra + +#endif // ENABLE_LOL diff --git a/engines/kyra/sound/sound_midi.cpp b/engines/kyra/sound/sound_midi.cpp new file mode 100644 index 0000000000..c0cf6c1b16 --- /dev/null +++ b/engines/kyra/sound/sound_midi.cpp @@ -0,0 +1,814 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_intern.h" +#include "kyra/resource/resource.h" + +#include "common/system.h" +#include "common/config-manager.h" +#include "common/translation.h" + +#include "gui/message.h" + +namespace Kyra { + +class MidiOutput : public MidiDriver_BASE { +public: + MidiOutput(OSystem *system, MidiDriver *output, bool isMT32, bool defaultMT32); + ~MidiOutput(); + + void setSourceVolume(int source, int volume, bool apply=false); + + void initSource(int source); + void deinitSource(int source); + void stopNotesOnChannel(int channel); + + void setSoundSource(int source) { _curSource = source; } + + // MidiDriver_BASE interface + virtual void send(uint32 b); + virtual void sysEx(const byte *msg, uint16 length); + virtual void metaEvent(byte type, byte *data, uint16 length); + + // TODO: Get rid of the following two methods + void setTimerCallback(void *timerParam, void (*timerProc)(void *)) { _output->setTimerCallback(timerParam, timerProc); } + uint32 getBaseTempo() { return _output->getBaseTempo(); } + + +private: + void sendIntern(const byte event, const byte channel, byte param1, const byte param2); + void sendSysEx(const byte p1, const byte p2, const byte p3, const byte *buffer, const int size); + + OSystem *_system; + MidiDriver *_output; + + bool _isMT32; + bool _defaultMT32; + + struct Controller { + byte controller; + byte value; + }; + + enum { + kChannelLocked = 0x80, + kChannelProtected = 0x40 + }; + + struct Channel { + byte flags; + + byte program; + int16 pitchWheel; + + byte noteCount; + + Controller controllers[9]; + } _channels[16]; + + int lockChannel(); + void unlockChannel(int channel); + + int _curSource; + + struct SoundSource { + int volume; + + int8 channelMap[16]; + byte channelProgram[16]; + int16 channelPW[16]; + Controller controllers[16][9]; + + struct Note { + byte channel; + byte note; + }; + + Note notes[32]; + } _sources[4]; +}; + +MidiOutput::MidiOutput(OSystem *system, MidiDriver *output, bool isMT32, bool defaultMT32) : _system(system), _output(output) { + _isMT32 = isMT32; + _defaultMT32 = defaultMT32; + + int ret = _output->open(); + if (ret != MidiDriver::MERR_ALREADY_OPEN && ret != 0) + error("Couldn't open midi driver"); + + static const Controller defaultControllers[] = { + { 0x07, 0x7F }, { 0x01, 0x00 }, { 0x0A, 0x40 }, + { 0x0B, 0x7F }, { 0x40, 0x00 }, { 0x72, 0x00 }, + { 0x6E, 0x00 }, { 0x6F, 0x00 }, { 0x70, 0x00 } + }; + + static const byte defaultPrograms[] = { + 0x44, 0x30, 0x5F, 0x4E, 0x29, 0x03, 0x6E, 0x7A, 0xFF + }; + + static const byte sysEx1[] = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }; + static const byte sysEx2[] = { 3, 4, 3, 4, 3, 4, 3, 4, 4 }; + static const byte sysEx3[] = { 0, 3, 2 }; + + if (_isMT32) { + sendSysEx(0x7F, 0x00, 0x00, sysEx1, 1); + sendSysEx(0x10, 0x00, 0x0D, sysEx1, 9); + sendSysEx(0x10, 0x00, 0x04, sysEx2, 9); + sendSysEx(0x10, 0x00, 0x01, sysEx3, 3); + } else { + _output->sendGMReset(); + } + + memset(_channels, 0, sizeof(_channels)); + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 9; ++j) + _channels[i].controllers[j] = defaultControllers[j]; + _channels[i].pitchWheel = -1; + _channels[i].program = 0xFF; + } + + for (int i = 0; i < 9; ++i) { + for (int j = 1; j <= 9; ++j) + sendIntern(0xB0, j, defaultControllers[i].controller, defaultControllers[i].value); + } + + for (int i = 1; i <= 9; ++i) { + sendIntern(0xE0, i, 0x00, 0x40); + if (defaultPrograms[i - 1] != 0xFF) + sendIntern(0xC0, i, defaultPrograms[i - 1], 0x00); + } + + for (int i = 0; i < 4; ++i) { + _sources[i].volume = 256; + initSource(i); + } +} + + +MidiOutput::~MidiOutput() { + _output->close(); + delete _output; +} + +void MidiOutput::send(uint32 b) { + const byte event = b & 0xF0; + const byte channel = b & 0x0F; + byte param1 = (b >> 8) & 0xFF; + byte param2 = (b >> 16) & 0xFF; + + if (event == 0xE0) { // Pitch-Wheel + _channels[channel].pitchWheel = + _sources[_curSource].channelPW[channel] = (param2 << 8) | param1; + } else if (event == 0xC0) { // Program change + _channels[channel].program = + _sources[_curSource].channelProgram[channel] = param1; + } else if (event == 0xB0) { // Controller change + for (int i = 0; i < 9; ++i) { + Controller &cont = _sources[_curSource].controllers[channel][i]; + if (cont.controller == param1) { + cont.value = param2; + break; + } + } + + if (param1 == 0x07) { + param2 = (param2 * _sources[_curSource].volume) >> 8; + } else if (param1 == 0x6E) { // Lock Channel + if (param2 >= 0x40) { // Lock Channel + int chan = lockChannel(); + if (chan < 0) + chan = channel; + _sources[_curSource].channelMap[channel] = chan; + } else { // Unlock Channel + stopNotesOnChannel(channel); + unlockChannel(_sources[_curSource].channelMap[channel]); + _sources[_curSource].channelMap[channel] = channel; + } + } else if (param1 == 0x6F) { // Protect Channel + if (param2 >= 0x40) { // Protect Channel + _channels[channel].flags |= kChannelProtected; + } else { // Unprotect Channel + _channels[channel].flags &= ~kChannelProtected; + } + } else if (param1 == 0x7B) { // All notes off + // FIXME: Since the XMIDI parsers sends this + // on track change, we simply ignore it. + return; + } + } else if (event == 0x90 || event == 0x80) { // Note On/Off + if (!(_channels[channel].flags & kChannelLocked)) { + const bool remove = (event == 0x80) || (param2 == 0x00); + int note = -1; + + for (int i = 0; i < 32; ++i) { + if (remove) { + if (_sources[_curSource].notes[i].channel == channel && + _sources[_curSource].notes[i].note == param1) { + note = i; + break; + } + } else { + if (_sources[_curSource].notes[i].channel == 0xFF) { + note = i; + break; + } + } + } + + if (note != -1) { + if (remove) { + _sources[_curSource].notes[note].channel = 0xFF; + + --_channels[_sources[_curSource].channelMap[channel]].noteCount; + } else { + _sources[_curSource].notes[note].channel = channel; + _sources[_curSource].notes[note].note = param1; + + ++_channels[_sources[_curSource].channelMap[channel]].noteCount; + } + + sendIntern(event, _sources[_curSource].channelMap[channel], param1, param2); + } + } + return; + } + + if (!(_channels[channel].flags & kChannelLocked)) + sendIntern(event, _sources[_curSource].channelMap[channel], param1, param2); +} + +void MidiOutput::sendIntern(const byte event, const byte channel, byte param1, const byte param2) { + if (event == 0xC0) { + // MT32 -> GM conversion + if (!_isMT32 && _defaultMT32) + param1 = MidiDriver::_mt32ToGm[param1]; + } + + _output->send(event | channel, param1, param2); +} + +void MidiOutput::sysEx(const byte *msg, uint16 length) { + // Wait the time it takes to send the SysEx data + uint32 delay = (length + 2) * 1000 / 3125; + + // Plus an additional delay for the MT-32 rev00 + if (_isMT32) + delay += 40; + + _output->sysEx(msg, length); + _system->delayMillis(delay); +} + +void MidiOutput::sendSysEx(const byte p1, const byte p2, const byte p3, const byte *buffer, const int size) { + int bufferSize = 8 + size; + byte *outBuffer = new byte[bufferSize]; + assert(outBuffer); + + outBuffer[0] = 0x41; + outBuffer[1] = 0x10; + outBuffer[2] = 0x16; + outBuffer[3] = 0x12; + + outBuffer[4] = p1; + outBuffer[5] = p2; + outBuffer[6] = p3; + + memcpy(outBuffer + 7, buffer, size); + + uint16 checkSum = p1 + p2 + p3; + for (int i = 0; i < size; ++i) + checkSum += buffer[i]; + checkSum &= 0x7F; + checkSum -= 0x80; + checkSum = -checkSum; + checkSum &= 0x7F; + + outBuffer[7+size] = checkSum; + + sysEx(outBuffer, bufferSize); + + delete[] outBuffer; +} + +void MidiOutput::metaEvent(byte type, byte *data, uint16 length) { + if (type == 0x2F) // End of Track + deinitSource(_curSource); + + _output->metaEvent(type, data, length); +} + +void MidiOutput::setSourceVolume(int source, int volume, bool apply) { + _sources[source].volume = volume; + + if (apply) { + for (int i = 0; i < 16; ++i) { + // Controller 0 in the state table should always be '7' aka + // volume control + byte realVol = (_sources[source].controllers[i][0].value * volume) >> 8; + sendIntern(0xB0, i, 0x07, realVol); + } + } +} + +void MidiOutput::initSource(int source) { + memset(_sources[source].notes, -1, sizeof(_sources[source].notes)); + + for (int i = 0; i < 16; ++i) { + _sources[source].channelMap[i] = i; + _sources[source].channelProgram[i] = 0xFF; + _sources[source].channelPW[i] = -1; + + for (int j = 0; j < 9; ++j) + _sources[source].controllers[i][j] = _channels[i].controllers[j]; + } +} + +void MidiOutput::deinitSource(int source) { + for (int i = 0; i < 16; ++i) { + for (int j = 0; j < 9; ++j) { + const Controller &cont = _sources[source].controllers[i][j]; + + if (cont.controller == 0x40) { + if (cont.value >= 0x40) + sendIntern(0xB0, i, 0x40, 0); + } else if (cont.controller == 0x6E) { + if (cont.value >= 0x40) { + stopNotesOnChannel(i); + unlockChannel(_sources[source].channelMap[i]); + _sources[source].channelMap[i] = i; + } + } else if (cont.controller == 0x6F) { + if (cont.value >= 0x40) + _channels[i].flags &= ~kChannelProtected; + } else if (cont.controller == 0x70) { + if (cont.value >= 0x40) + sendIntern(0xB0, i, 0x70, 0); + } + } + } +} + +int MidiOutput::lockChannel() { + int channel = -1; + int notes = 0xFF; + byte flags = kChannelLocked | kChannelProtected; + + while (channel == -1) { + for (int i = _isMT32 ? 8 : 15; i >= 1; --i) { + if (_channels[i].flags & flags) + continue; + if (_channels[i].noteCount < notes) { + channel = i; + notes = _channels[i].noteCount; + } + } + + if (channel == -1) { + if (flags & kChannelProtected) + flags &= ~kChannelProtected; + else + break; + } + } + + if (channel == -1) + return -1; + + sendIntern(0xB0, channel, 0x40, 0); + stopNotesOnChannel(channel); + _channels[channel].noteCount = 0; + _channels[channel].flags |= kChannelLocked; + + return channel; +} + +void MidiOutput::unlockChannel(int channel) { + if (!(_channels[channel].flags & kChannelLocked)) + return; + + _channels[channel].flags &= ~kChannelLocked; + _channels[channel].noteCount = 0; + sendIntern(0xB0, channel, 0x40, 0); + sendIntern(0xB0, channel, 0x7B, 0); + + for (int i = 0; i < 9; ++i) { + if (_channels[channel].controllers[i].value != 0xFF) + sendIntern(0xB0, channel, _channels[channel].controllers[i].controller, _channels[channel].controllers[i].value); + } + + if (_channels[channel].program != 0xFF) + sendIntern(0xC0, channel, _channels[channel].program, 0); + + if (_channels[channel].pitchWheel != -1) + sendIntern(0xE0, channel, _channels[channel].pitchWheel & 0xFF, (_channels[channel].pitchWheel >> 8) & 0xFF); +} + +void MidiOutput::stopNotesOnChannel(int channel) { + for (int i = 0; i < 4; ++i) { + SoundSource &sound = _sources[i]; + for (int j = 0; j < 32; ++j) { + if (sound.notes[j].channel == channel) { + sound.notes[j].channel = 0xFF; + sendIntern(0x80, sound.channelMap[channel], sound.notes[j].note, 0); + --_channels[sound.channelMap[channel]].noteCount; + } + } + } +} + +#pragma mark - + +SoundMidiPC::SoundMidiPC(KyraEngine_v1 *vm, Audio::Mixer *mixer, MidiDriver *driver, kType type) : Sound(vm, mixer) { + _driver = driver; + _output = 0; + + _musicFile = _sfxFile = 0; + _currentResourceSet = 0; + memset(&_resInfo, 0, sizeof(_resInfo)); + + _music = MidiParser::createParser_XMIDI(); + assert(_music); + for (int i = 0; i < 3; ++i) { + _sfx[i] = MidiParser::createParser_XMIDI(); + assert(_sfx[i]); + } + + _musicVolume = _sfxVolume = 0; + _fadeMusicOut = false; + + _type = type; + assert(_type == kMidiMT32 || _type == kMidiGM || _type == kPCSpkr); + + // Only General MIDI isn't a Roland MT-32 MIDI implemenation, + // even the PC Speaker driver is a Roland MT-32 based MIDI implementation. + // Thus we set "_nativeMT32" for all types except Gerneral MIDI to true. + _nativeMT32 = (_type != kMidiGM); + + // KYRA1 does not include any General MIDI tracks, thus we have + // to overwrite the internal type with MT32 to get the correct + // file extension. + if (_vm->game() == GI_KYRA1 && _type == kMidiGM) + _type = kMidiMT32; + + // Display a warning about possibly wrong sound when the user only has + // a General MIDI device, but the game is setup to use Roland MT32 MIDI. + // (This will only happen in The Legend of Kyrandia 1 though, all other + // supported games include special General MIDI tracks). + if (_type == kMidiMT32 && !_nativeMT32) { + ::GUI::MessageDialog dialog(_("You appear to be using a General MIDI device,\n" + "but your game only supports Roland MT32 MIDI.\n" + "We try to map the Roland MT32 instruments to\n" + "General MIDI ones. It is still possible that\n" + "some tracks sound incorrect.")); + dialog.runModal(); + } +} + +SoundMidiPC::~SoundMidiPC() { + Common::StackLock lock(_mutex); + _output->setTimerCallback(0, 0); + + delete _music; + for (int i = 0; i < 3; ++i) + delete _sfx[i]; + + delete _output; // This automatically frees _driver (!) + + if (_musicFile != _sfxFile) + delete[] _sfxFile; + + delete[] _musicFile; + + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); +} + +bool SoundMidiPC::init() { + _output = new MidiOutput(_vm->_system, _driver, _nativeMT32, (_type != kMidiGM)); + assert(_output); + + updateVolumeSettings(); + + _music->setMidiDriver(_output); + _music->setTempo(_output->getBaseTempo()); + _music->setTimerRate(_output->getBaseTempo()); + + for (int i = 0; i < 3; ++i) { + _sfx[i]->setMidiDriver(_output); + _sfx[i]->setTempo(_output->getBaseTempo()); + _sfx[i]->setTimerRate(_output->getBaseTempo()); + } + + _output->setTimerCallback(this, SoundMidiPC::onTimer); + + if (_nativeMT32 && _type == kMidiMT32) { + const char *midiFile = 0; + const char *pakFile = 0; + if (_vm->game() == GI_KYRA1) { + midiFile = "INTRO"; + } else if (_vm->game() == GI_KYRA2) { + midiFile = "HOF_SYX"; + pakFile = "AUDIO.PAK"; + } else if (_vm->game() == GI_LOL) { + midiFile = "LOREINTR"; + + if (_vm->gameFlags().isDemo) { + if (_vm->gameFlags().useAltShapeHeader) { + // Intro demo + pakFile = "INTROVOC.PAK"; + } else { + // Kyra2 SEQ player based demo + pakFile = "GENERAL.PAK"; + midiFile = "LOLSYSEX"; + } + } else { + if (_vm->gameFlags().isTalkie) + pakFile = "ENG/STARTUP.PAK"; + else + pakFile = "INTROVOC.PAK"; + } + } + + if (!midiFile) + return true; + + if (pakFile) + _vm->resource()->loadPakFile(pakFile); + + loadSoundFile(midiFile); + playTrack(0); + + Common::Event event; + while (isPlaying() && !_vm->shouldQuit()) { + _vm->_system->updateScreen(); + _vm->_eventMan->pollEvent(event); + _vm->_system->delayMillis(10); + } + + if (pakFile) + _vm->resource()->unloadPakFile(pakFile); + } + + return true; +} + +void SoundMidiPC::updateVolumeSettings() { + Common::StackLock lock(_mutex); + + if (!_output) + return; + + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); + + const int newMusVol = (mute ? 0 : ConfMan.getInt("music_volume")); + _sfxVolume = (mute ? 0 : ConfMan.getInt("sfx_volume")); + + _output->setSourceVolume(0, newMusVol, newMusVol != _musicVolume); + _musicVolume = newMusVol; + + for (int i = 1; i < 4; ++i) + _output->setSourceVolume(i, _sfxVolume, false); +} + +void SoundMidiPC::initAudioResourceInfo(int set, void *info) { + if (set >= kMusicIntro && set <= kMusicFinale) { + delete _resInfo[set]; + _resInfo[set] = info ? new SoundResourceInfo_PC(*(SoundResourceInfo_PC*)info) : 0; + } +} + +void SoundMidiPC::selectAudioResourceSet(int set) { + if (set >= kMusicIntro && set <= kMusicFinale) { + if (_resInfo[set]) + _currentResourceSet = set; + } +} + +bool SoundMidiPC::hasSoundFile(uint file) const { + if (file < res()->fileListSize) + return (res()->fileList[file] != 0); + return false; +} + +void SoundMidiPC::loadSoundFile(uint file) { + if (file < res()->fileListSize) + loadSoundFile(res()->fileList[file]); +} + +void SoundMidiPC::loadSoundFile(Common::String file) { + Common::StackLock lock(_mutex); + file = getFileName(file); + + if (_mFileName == file) + return; + + if (!_vm->resource()->exists(file.c_str())) + return; + + // When loading a new file we stop all notes + // still running on our own, just to prevent + // glitches + for (int i = 0; i < 16; ++i) + _output->stopNotesOnChannel(i); + + delete[] _musicFile; + uint32 fileSize = 0; + _musicFile = _vm->resource()->fileData(file.c_str(), &fileSize); + _mFileName = file; + + _output->setSoundSource(0); + _music->loadMusic(_musicFile, fileSize); + _music->stopPlaying(); + + // Since KYRA1 uses the same file for SFX and Music + // we setup sfx to play from music file as well + if (_vm->game() == GI_KYRA1) { + for (int i = 0; i < 3; ++i) { + _output->setSoundSource(i+1); + _sfx[i]->loadMusic(_musicFile, fileSize); + _sfx[i]->stopPlaying(); + } + } +} + +void SoundMidiPC::loadSfxFile(Common::String file) { + Common::StackLock lock(_mutex); + + // Kyrandia 1 doesn't use a special sfx file + if (_vm->game() == GI_KYRA1) + return; + + file = getFileName(file); + + if (_sFileName == file) + return; + + if (!_vm->resource()->exists(file.c_str())) + return; + + delete[] _sfxFile; + + uint32 fileSize = 0; + _sfxFile = _vm->resource()->fileData(file.c_str(), &fileSize); + _sFileName = file; + + for (int i = 0; i < 3; ++i) { + _output->setSoundSource(i+1); + _sfx[i]->loadMusic(_sfxFile, fileSize); + _sfx[i]->stopPlaying(); + } +} + +void SoundMidiPC::playTrack(uint8 track) { + if (!_musicEnabled) + return; + + haltTrack(); + + Common::StackLock lock(_mutex); + + _fadeMusicOut = false; + _output->setSourceVolume(0, _musicVolume, true); + + _output->initSource(0); + _output->setSourceVolume(0, _musicVolume, true); + _music->setTrack(track); +} + +void SoundMidiPC::haltTrack() { + Common::StackLock lock(_mutex); + + _output->setSoundSource(0); + _music->stopPlaying(); + _output->deinitSource(0); +} + +bool SoundMidiPC::isPlaying() const { + Common::StackLock lock(_mutex); + + return _music->isPlaying(); +} + +void SoundMidiPC::playSoundEffect(uint8 track, uint8) { + if (!_sfxEnabled) + return; + + Common::StackLock lock(_mutex); + for (int i = 0; i < 3; ++i) { + if (!_sfx[i]->isPlaying()) { + _output->initSource(i+1); + _sfx[i]->setTrack(track); + return; + } + } +} + +void SoundMidiPC::stopAllSoundEffects() { + Common::StackLock lock(_mutex); + + for (int i = 0; i < 3; ++i) { + _output->setSoundSource(i+1); + _sfx[i]->stopPlaying(); + _output->deinitSource(i+1); + } +} + +void SoundMidiPC::beginFadeOut() { + Common::StackLock lock(_mutex); + + _fadeMusicOut = true; + _fadeStartTime = _vm->_system->getMillis(); +} + +void SoundMidiPC::pause(bool paused) { + Common::StackLock lock(_mutex); + + if (paused) { + _music->setMidiDriver(0); + for (int i = 0; i < 3; i++) + _sfx[i]->setMidiDriver(0); + for (int i = 0; i < 16; i++) + _output->stopNotesOnChannel(i); + } else { + _music->setMidiDriver(_output); + for (int i = 0; i < 3; ++i) + _sfx[i]->setMidiDriver(_output); + // Possible TODO (IMHO unnecessary): restore notes and/or update _fadeStartTime + } +} + +void SoundMidiPC::onTimer(void *data) { + SoundMidiPC *midi = (SoundMidiPC *)data; + + Common::StackLock lock(midi->_mutex); + + if (midi->_fadeMusicOut) { + static const uint32 musicFadeTime = 1 * 1000; + + if (midi->_fadeStartTime + musicFadeTime > midi->_vm->_system->getMillis()) { + int volume = (byte)((musicFadeTime - (midi->_vm->_system->getMillis() - midi->_fadeStartTime)) * midi->_musicVolume / musicFadeTime); + midi->_output->setSourceVolume(0, volume, true); + } else { + for (int i = 0; i < 16; ++i) + midi->_output->stopNotesOnChannel(i); + for (int i = 0; i < 4; ++i) + midi->_output->deinitSource(i); + + midi->_output->setSoundSource(0); + midi->_music->stopPlaying(); + + for (int i = 0; i < 3; ++i) { + midi->_output->setSoundSource(i+1); + midi->_sfx[i]->stopPlaying(); + } + + midi->_fadeMusicOut = false; + } + } + + midi->_output->setSoundSource(0); + midi->_music->onTimer(); + + for (int i = 0; i < 3; ++i) { + midi->_output->setSoundSource(i+1); + midi->_sfx[i]->onTimer(); + } +} + +Common::String SoundMidiPC::getFileName(const Common::String &str) { + Common::String file = str; + if (_type == kMidiMT32) + file += ".XMI"; + else if (_type == kMidiGM) + file += ".C55"; + else if (_type == kPCSpkr) + file += ".PCS"; + + if (_vm->resource()->exists(file.c_str())) + return file; + + return str + ".XMI"; +} + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_pcspk.cpp b/engines/kyra/sound/sound_pcspk.cpp new file mode 100644 index 0000000000..110addefd8 --- /dev/null +++ b/engines/kyra/sound/sound_pcspk.cpp @@ -0,0 +1,366 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_intern.h" + +#include "audio/mixer.h" +#include "audio/softsynth/pcspk.h" + +namespace Kyra { + +MidiDriver_PCSpeaker::MidiDriver_PCSpeaker(Audio::Mixer *mixer) + : MidiDriver_Emulated(mixer), _rate(mixer->getOutputRate()) { + _timerValue = 0; + memset(_channel, 0, sizeof(_channel)); + memset(_note, 0, sizeof(_note)); + + for (int i = 0; i < 2; ++i) + _note[i].hardwareChannel = 0xFF; + + _speaker = new Audio::PCSpeaker(_rate); + assert(_speaker); + _mixer->playStream(Audio::Mixer::kMusicSoundType, &_mixerSoundHandle, this, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO, true); + + _countdown = 0xFFFF; + _hardwareChannel[0] = 0xFF; + _modulationFlag = false; +} + +MidiDriver_PCSpeaker::~MidiDriver_PCSpeaker() { + _mixer->stopHandle(_mixerSoundHandle); + delete _speaker; + _speaker = 0; +} + +void MidiDriver_PCSpeaker::send(uint32 data) { + Common::StackLock lock(_mutex); + + uint8 channel = data & 0x0F; + uint8 param1 = (data >> 8) & 0xFF; + uint8 param2 = (data >> 16) & 0xFF; + + uint8 flags = 0x00; + + if (channel > 1) + return; + + switch (data & 0xF0) { + case 0x80: // note off + noteOff(channel, param1); + return; + + case 0x90: // note on + if (channel > 1) + return; + + if (param2) + noteOn(channel, param1); + else + noteOff(channel, param1); + return; + + case 0xB0: // controller + switch (param1) { + case 0x01: // modulation + _channel[channel].modulation = param2; + break; + + case 0x40: // hold + _channel[channel].hold = param2; + if (param2 < 0x40) + resetController(channel); + return; + + case 0x70: // voice protect + _channel[channel].voiceProtect = param2; + return; + + case 0x79: // all notes off + _channel[channel].hold = 0; + resetController(channel); + _channel[channel].modulation = 0; + _channel[channel].pitchBendLow = 0; + _channel[channel].pitchBendHigh = 0x40; + flags = 0x01; + break; + + default: + return; + } + break; + + case 0xE0: // pitch bend + flags = 0x01; + _channel[channel].pitchBendLow = param1; + _channel[channel].pitchBendHigh = param2; + break; + + default: + return; + } + + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled && _note[i].midiChannel == channel) { + _note[i].flags |= flags; + setupTone(i); + } + } +} + +void MidiDriver_PCSpeaker::resetController(int channel) { + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled && _note[i].midiChannel == channel && _note[i].processHold) + noteOff(channel, _note[i].note); + } +} + +void MidiDriver_PCSpeaker::noteOn(int channel, int note) { + int n = 0; + + while (n < 2 && _note[n].enabled) + ++n; + + if (n >= 2) + return; + + _note[n].midiChannel = channel; + _note[n].note = note; + _note[n].enabled = true; + _note[n].processHold = false; + _note[n].hardwareFlags = 0x20; + _note[n].priority = 0x7FFF; + _note[n].flags = 0x01; + + turnNoteOn(n); +} + +void MidiDriver_PCSpeaker::turnNoteOn(int note) { + if (_hardwareChannel[0] == 0xFF) { + _note[note].hardwareChannel = 0; + ++_channel[_note[note].midiChannel].noteCount; + _hardwareChannel[0] = _note[note].midiChannel; + _note[note].flags = 0x01; + + setupTone(note); + } else { + overwriteNote(note); + } +} + +void MidiDriver_PCSpeaker::overwriteNote(int note) { + int totalNotes = 0; + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled) { + ++totalNotes; + const int channel = _note[i].midiChannel; + + uint16 priority = 0xFFFF; + if (_channel[channel].voiceProtect < 0x40) + priority = _note[i].priority; + + if (_channel[channel].noteCount > priority) + priority = 0; + else + priority -= _channel[channel].noteCount; + + _note[i].precedence = priority; + } + } + + if (totalNotes <= 1) + return; + + do { + uint16 maxValue = 0; + uint16 minValue = 0xFFFF; + int newNote = 0; + + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled) { + if (_note[i].hardwareChannel == 0xFF) { + if (_note[i].precedence >= maxValue) { + maxValue = _note[i].precedence; + newNote = i; + } + } else { + if (_note[i].precedence <= minValue) { + minValue = _note[i].precedence; + note = i; + } + } + } + } + + if (maxValue < minValue) + return; + + turnNoteOff(_note[note].hardwareChannel); + _note[note].enabled = false; + + _note[newNote].hardwareChannel = _note[note].hardwareChannel; + ++_channel[_note[newNote].midiChannel].noteCount; + _hardwareChannel[_note[note].hardwareChannel] = _note[newNote].midiChannel; + _note[newNote].flags = 0x01; + + setupTone(newNote); + } while (--totalNotes); +} + +void MidiDriver_PCSpeaker::noteOff(int channel, int note) { + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled && _note[i].note == note && _note[i].midiChannel == channel) { + if (_channel[i].hold < 0x40) { + turnNoteOff(i); + _note[i].enabled = false; + } else { + _note[i].processHold = true; + } + } + } +} + +void MidiDriver_PCSpeaker::turnNoteOff(int note) { + if (_note[note].hardwareChannel != 0xFF) { + _note[note].hardwareFlags &= 0xDF; + _note[note].flags |= 1; + + setupTone(note); + + --_channel[_note[note].midiChannel].noteCount; + + _hardwareChannel[_note[note].hardwareChannel] = 0xFF; + _note[note].hardwareChannel = 0xFF; + } +} + +void MidiDriver_PCSpeaker::setupTone(int note) { + if (_note[note].hardwareChannel == 0xFF) + return; + + if (!(_note[note].flags & 0x01)) + return; + + if (!(_note[note].hardwareFlags & 0x20)) { + _speaker->stop(); + } else { + const int midiChannel = _note[note].midiChannel; + uint16 pitchBend = (_channel[midiChannel].pitchBendHigh << 7) | _channel[midiChannel].pitchBendLow; + + int noteValue = _note[note].note; + + noteValue -= 24; + do { + noteValue += 12; + } while (noteValue < 0); + + noteValue += 12; + do { + noteValue -= 12; + } while (noteValue > 95); + + int16 modulation = _note[note].modulation; + + int tableIndex = MAX(noteValue - 12, 0); + uint16 note1 = (_noteTable2[tableIndex] << 8) | _noteTable1[tableIndex]; + tableIndex = MIN(noteValue + 12, 95); + uint16 note2 = (_noteTable2[tableIndex] << 8) | _noteTable1[tableIndex]; + uint16 note3 = (_noteTable2[noteValue] << 8) | _noteTable1[noteValue]; + + int32 countdown = pitchBend - 0x2000; + countdown += modulation; + + if (countdown >= 0) + countdown *= (note2 - note3); + else + countdown *= (note3 - note1); + + countdown /= 0x2000; + countdown += note3; + + countdown = uint16(countdown & 0xFFFF); + if (countdown != _countdown) + _countdown = countdown; + + _speaker->play(Audio::PCSpeaker::kWaveFormSquare, 1193180 / _countdown, -1); + } + + _note[note].flags &= 0xFE; +} + +void MidiDriver_PCSpeaker::generateSamples(int16 *buffer, int numSamples) { + Common::StackLock lock(_mutex); + _speaker->readBuffer(buffer, numSamples); +} + +void MidiDriver_PCSpeaker::onTimer() { + /*Common::StackLock lock(_mutex); + + _timerValue += 20; + if (_timerValue < 120) + return; + _timerValue -= 120; + + _modulationFlag = !_modulationFlag; + for (int i = 0; i < 2; ++i) { + if (_note[i].enabled) { + uint16 modValue = 5 * _channel[_note[i].midiChannel].modulation; + if (_modulationFlag) + modValue = -modValue; + _note[i].modulation = modValue; + _note[i].flags |= 1; + + setupTone(i); + } + }*/ +} + +const uint8 MidiDriver_PCSpeaker::_noteTable1[] = { + 0x88, 0xB5, 0x4E, 0x40, 0x41, 0xCD, 0xC4, 0x3D, + 0x43, 0x7C, 0x2A, 0xD6, 0x88, 0xB5, 0xFF, 0xD1, + 0x20, 0xA7, 0xE2, 0x1E, 0xCE, 0xBE, 0xF2, 0x8A, + 0x44, 0x41, 0x7F, 0xE8, 0x90, 0x63, 0x63, 0x8F, + 0xE7, 0x5F, 0x01, 0xBD, 0xA2, 0xA0, 0xBF, 0xF4, + 0x48, 0xB1, 0x31, 0xC7, 0x70, 0x2F, 0xFE, 0xE0, + 0xD1, 0xD0, 0xDE, 0xFB, 0x24, 0x58, 0x98, 0xE3, + 0x39, 0x97, 0xFF, 0x6F, 0xE8, 0x68, 0xEF, 0x7D, + 0x11, 0xAC, 0x4C, 0xF1, 0x9C, 0x4B, 0xFF, 0xB7, + 0x74, 0x34, 0xF7, 0xBE, 0x88, 0x56, 0x26, 0xF8, + 0xCE, 0xA5, 0x7F, 0x5B, 0x3A, 0x1A, 0xFB, 0xDF, + 0xC4, 0xAB, 0x93, 0x7C, 0x67, 0x52, 0x3F, 0x2D +}; + +const uint8 MidiDriver_PCSpeaker::_noteTable2[] = { + 0x8E, 0x86, 0xFD, 0xF0, 0xE2, 0xD5, 0xC9, 0xBE, + 0xB3, 0xA9, 0xA0, 0x96, 0x8E, 0x86, 0x7E, 0x77, + 0x71, 0x6A, 0x64, 0x5F, 0x59, 0x54, 0x4F, 0x4B, + 0x47, 0x43, 0x3F, 0x3B, 0x38, 0x35, 0x32, 0x2F, + 0x2C, 0x2A, 0x28, 0x25, 0x23, 0x21, 0x1F, 0x1D, + 0x1C, 0x1A, 0x19, 0x17, 0x16, 0x15, 0x13, 0x12, + 0x11, 0x10, 0x0F, 0x0E, 0x0E, 0x0D, 0x0C, 0x0B, + 0x0B, 0x0A, 0x09, 0x09, 0x08, 0x08, 0x07, 0x07, + 0x07, 0x06, 0x06, 0x05, 0x05, 0x05, 0x04, 0x04, + 0x04, 0x04, 0x03, 0x03, 0x03, 0x03, 0x03, 0x02, + 0x02, 0x02, 0x02, 0x02, 0x02, 0x02, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01 +}; + +} // End of namespace Kyra diff --git a/engines/kyra/sound/sound_towns.cpp b/engines/kyra/sound/sound_towns.cpp new file mode 100644 index 0000000000..621c2f1419 --- /dev/null +++ b/engines/kyra/sound/sound_towns.cpp @@ -0,0 +1,749 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_intern.h" +#include "kyra/resource/resource.h" + +#include "common/config-manager.h" +#include "common/system.h" + +#include "backends/audiocd/audiocd.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +namespace Kyra { + +SoundTowns::SoundTowns(KyraEngine_v1 *vm, Audio::Mixer *mixer) + : Sound(vm, mixer), _lastTrack(-1), _musicTrackData(0), _sfxFileData(0), _cdaPlaying(0), + _sfxFileIndex((uint)-1), _musicFadeTable(0), _sfxWDTable(0), _sfxBTTable(0), _sfxChannel(0x46), _currentResourceSet(0) { + memset(&_resInfo, 0, sizeof(_resInfo)); + _player = new EuphonyPlayer(_mixer); +} + +SoundTowns::~SoundTowns() { + g_system->getAudioCDManager()->stop(); + haltTrack(); + delete _player; + delete[] _musicTrackData; + delete[] _sfxFileData; + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); +} + +bool SoundTowns::init() { + _vm->checkCD(); + int unused = 0; + _musicFadeTable = _vm->staticres()->loadRawData(k1TownsMusicFadeTable, unused); + _sfxWDTable = _vm->staticres()->loadRawData(k1TownsSFXwdTable, unused); + _sfxBTTable = _vm->staticres()->loadRawData(k1TownsSFXbtTable, unused); + _musicTrackData = new uint8[50570]; + + if (!_player->init()) + return false; + + if (!loadInstruments()) + return false; + + /*_player->driver()->intf()->callback(68); + _player->driver()->intf()->callback(70, 0x33);*/ + _player->driver()->setOutputVolume(1, 118, 118); + + // Initialize CD for audio + g_system->getAudioCDManager()->open(); + + return true; +} + +void SoundTowns::process() { + g_system->getAudioCDManager()->update(); +} + +void SoundTowns::playTrack(uint8 track) { + if (track < 2) + return; + track -= 2; + + uint tTableIndex = 3 * track; + + assert(tTableIndex + 2 < res()->cdaTableSize); + + int trackNum = (int)READ_LE_UINT32(&res()->cdaTable[tTableIndex + 2]); + int32 loop = (int32)READ_LE_UINT32(&res()->cdaTable[tTableIndex + 1]); + + if (track == _lastTrack && _musicEnabled) + return; + + beginFadeOut(); + + if (_musicEnabled == 2 && trackNum != -1) { + _player->driver()->setOutputVolume(1, 118, 118); + g_system->getAudioCDManager()->play(trackNum + 1, loop ? -1 : 1, 0, 0); + g_system->getAudioCDManager()->update(); + _cdaPlaying = true; + } else if (_musicEnabled) { + playEuphonyTrack(READ_LE_UINT32(&res()->cdaTable[tTableIndex]), loop); + _cdaPlaying = false; + } + + _lastTrack = track; +} + +void SoundTowns::haltTrack() { + _lastTrack = -1; + g_system->getAudioCDManager()->stop(); + g_system->getAudioCDManager()->update(); + _cdaPlaying = false; + + for (int i = 0; i < 6; i++) + _player->driver()->channelVolume(i, 0); + for (int i = 0x40; i < 0x46; i++) + _player->driver()->channelVolume(i, 0); + for (int i = 0; i < 32; i++) + _player->configPart_enable(i, 0); + _player->stop(); +} + +void SoundTowns::initAudioResourceInfo(int set, void *info) { + if (set >= kMusicIntro && set <= kMusicFinale) { + delete _resInfo[set]; + _resInfo[set] = info ? new SoundResourceInfo_Towns(*(SoundResourceInfo_Towns*)info) : 0; + } +} + +void SoundTowns::selectAudioResourceSet(int set) { + if (set >= kMusicIntro && set <= kMusicFinale) { + if (_resInfo[set]) + _currentResourceSet = set; + } +} + +bool SoundTowns::hasSoundFile(uint file) const { + if (file < res()->fileListSize) + return (res()->fileList[file] != 0); + return false; +} + +void SoundTowns::loadSoundFile(uint file) { + if (_sfxFileIndex == file || file >= res()->fileListSize) + return; + _sfxFileIndex = file; + delete[] _sfxFileData; + _sfxFileData = _vm->resource()->fileData(res()->fileList[file], 0); +} + +void SoundTowns::playSoundEffect(uint8 track, uint8) { + if (!_sfxEnabled || !_sfxFileData) + return; + + if (track == 0 || track == 10) { + stopAllSoundEffects(); + return; + } else if (track == 1) { + fadeOutSoundEffects(); + return; + } + + uint8 note = 60; + if (_sfxFileIndex == 5) { + if (track == 16) { + note = 62; + track = 15; + } else if (track == 17) { + note = 64; + track = 15; + } else if (track == 18) { + note = 65; + track = 15; + } + } + + uint8 *fileBody = _sfxFileData + 0x01B8; + int32 offset = (int32)READ_LE_UINT32(_sfxFileData + (track - 0x0B) * 4); + if (offset == -1) + return; + + if (!_player->driver()->soundEffectIsPlaying(_sfxChannel ^ 1)) { + _sfxChannel ^= 1; + } else if (_player->driver()->soundEffectIsPlaying(_sfxChannel)) { + _sfxChannel ^= 1; + _player->driver()->stopSoundEffect(_sfxChannel); + } + + uint32 *sfxHeader = (uint32 *)(fileBody + offset); + uint32 sfxHeaderID = READ_LE_UINT32(sfxHeader); + uint32 playbackBufferSize = sfxHeaderID == 1 ? 30704 : READ_LE_UINT32(&sfxHeader[3]); + + uint8 *sfxPlaybackBuffer = new uint8[playbackBufferSize + 32]; + memcpy(sfxPlaybackBuffer, fileBody + offset, 32); + + uint8 *dst = sfxPlaybackBuffer + 32; + memset(dst, 0x80, playbackBufferSize); + + uint8 *sfxBody = ((uint8 *)sfxHeader) + 0x20; + + if (!sfxHeaderID) { + memcpy(dst, sfxBody, playbackBufferSize); + } else if (sfxHeaderID == 1) { + Screen::decodeFrame4(sfxBody, dst, playbackBufferSize); + } else if (_sfxWDTable) { + uint8 *tgt = dst; + uint32 sfx_BtTable_Offset = 0; + uint32 sfx_WdTable_Offset = 0; + uint32 sfx_WdTable_Number = 5; + uint32 inSize = READ_LE_UINT32(&sfxHeader[1]); + + for (uint32 i = 0; i < inSize; i++) { + sfx_WdTable_Offset = (sfx_WdTable_Number * 3 << 9) + sfxBody[i] * 6; + sfx_WdTable_Number = READ_LE_UINT16(_sfxWDTable + sfx_WdTable_Offset); + + sfx_BtTable_Offset += (int16)READ_LE_UINT16(_sfxWDTable + sfx_WdTable_Offset + 2); + *tgt++ = _sfxBTTable[((sfx_BtTable_Offset >> 2) & 0xFF)]; + + sfx_BtTable_Offset += (int16)READ_LE_UINT16(_sfxWDTable + sfx_WdTable_Offset + 4); + *tgt++ = _sfxBTTable[((sfx_BtTable_Offset >> 2) & 0xFF)]; + } + } + + _player->driver()->channelVolume(_sfxChannel, 127); + _player->driver()->channelPan(_sfxChannel, 0x40); + _player->driver()->channelPitch(_sfxChannel, 0); + _player->driver()->playSoundEffect(_sfxChannel, note, 127, sfxPlaybackBuffer); + delete[] sfxPlaybackBuffer; +} + +void SoundTowns::updateVolumeSettings() { + if (!_player) + return; + + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); + + _player->driver()->setMusicVolume((mute ? 0 : ConfMan.getInt("music_volume"))); + _player->driver()->setSoundEffectVolume((mute ? 0 : ConfMan.getInt("sfx_volume"))); +} + +void SoundTowns::stopAllSoundEffects() { + _player->driver()->channelVolume(0x46, 0); + _player->driver()->channelVolume(0x47, 0); + _player->driver()->stopSoundEffect(0x46); + _player->driver()->stopSoundEffect(0x47); + _sfxChannel = 0x46; +} + +void SoundTowns::beginFadeOut() { + if (_cdaPlaying) { + for (int i = 118; i > 103; i--) { + _player->driver()->setOutputVolume(1, i, i); + _vm->delay(2 * _vm->tickLength()); + } + + for (int i = 103; i > 83; i -= 2) { + _player->driver()->setOutputVolume(1, i, i); + _vm->delay(2 * _vm->tickLength()); + } + + for (int i = 83; i > 58; i -= 2) { + _player->driver()->setOutputVolume(1, i, i); + _vm->delay(_vm->tickLength()); + } + + for (int i = 58; i > 0; i--) { + _player->driver()->setOutputVolume(1, i, i); + _vm->delay(1); + } + + _player->driver()->setOutputVolume(1, 0, 0); + + } else { + if (_lastTrack == -1) + return; + + uint32 ticks = 2; + int tickAdv = 0; + + uint16 fadeVolCur[12]; + uint16 fadeVolStep[12]; + + for (int i = 0; i < 6; i++) { + fadeVolCur[i] = READ_LE_UINT16(&_musicFadeTable[(_lastTrack * 12 + i) * 2]); + fadeVolStep[i] = fadeVolCur[i] / 50; + fadeVolCur[i + 6] = READ_LE_UINT16(&_musicFadeTable[(_lastTrack * 12 + 6 + i) * 2]); + fadeVolStep[i + 6] = fadeVolCur[i + 6] / 30; + } + + for (int i = 0; i < 12; i++) { + for (int ii = 0; ii < 6; ii++) + _player->driver()->channelVolume(ii, fadeVolCur[ii]); + for (int ii = 0x40; ii < 0x46; ii++) + _player->driver()->channelVolume(ii, fadeVolCur[ii - 0x3A]); + + for (int ii = 0; ii < 6; ii++) { + fadeVolCur[ii] -= fadeVolStep[ii]; + if (fadeVolCur[ii] < 10) + fadeVolCur[ii] = 0; + fadeVolCur[ii + 6] -= fadeVolStep[ii + 6]; + if (fadeVolCur[ii + 6] < 10) + fadeVolCur[ii + 6] = 0; + } + + if (++tickAdv == 3) { + tickAdv = 0; + ticks += 2; + } + _vm->delay(ticks * _vm->tickLength()); + } + } + + haltTrack(); +} + +bool SoundTowns::loadInstruments() { + uint8 *twm = _vm->resource()->fileData("twmusic.pak", 0); + if (!twm) + return false; + + Screen::decodeFrame4(twm, _musicTrackData, 50570); + for (int i = 0; i < 128; i++) + _player->driver()->loadInstrument(0, i, &_musicTrackData[i * 48 + 8]); + + Screen::decodeFrame4(twm + 3232, _musicTrackData, 50570); + for (int i = 0; i < 32; i++) + _player->driver()->loadInstrument(0x40, i, &_musicTrackData[i * 128 + 8]); + + _player->driver()->unloadWaveTable(-1); + uint8 *src = &_musicTrackData[32 * 128 + 8]; + for (int i = 0; i < 10; i++) { + _player->driver()->loadWaveTable(src); + src = src + READ_LE_UINT16(&src[12]) + 32; + } + + _player->driver()->reserveSoundEffectChannels(2); + + delete[] twm; + + return true; +} + +void SoundTowns::playEuphonyTrack(uint32 offset, int loop) { + uint8 *twm = _vm->resource()->fileData("twmusic.pak", 0); + Screen::decodeFrame4(twm + 19312 + offset, _musicTrackData, 50570); + delete[] twm; + + const uint8 *src = _musicTrackData + 852; + for (int i = 0; i < 32; i++) + _player->configPart_enable(i, *src++); + for (int i = 0; i < 32; i++) + _player->configPart_setType(i, *src++); + for (int i = 0; i < 32; i++) + _player->configPart_remap(i, *src++); + for (int i = 0; i < 32; i++) + _player->configPart_adjustVolume(i, *src++); + for (int i = 0; i < 32; i++) + _player->configPart_setTranspose(i, *src++); + + src = _musicTrackData + 1748; + for (int i = 0; i < 6; i++) + _player->driver()->assignPartToChannel(i, *src++); + for (int i = 0x40; i < 0x46; i++) + _player->driver()->assignPartToChannel(i, *src++); + + uint32 trackSize = READ_LE_UINT32(_musicTrackData + 2048); + uint8 startTick = _musicTrackData[2052]; + + _player->setTempo(_musicTrackData[2053]); + + src = _musicTrackData + 2054; + uint32 l = READ_LE_UINT32(src + trackSize); + trackSize += (l + 4); + l = READ_LE_UINT32(src + trackSize); + trackSize += (l + 4); + + _player->setLoopStatus(loop); + _player->startTrack(src, trackSize, startTick); +} + +void SoundTowns::fadeOutSoundEffects() { + for (int i = 127; i > 0; i-= 12) { + _player->driver()->channelVolume(0x46, i); + _player->driver()->channelVolume(0x47, i); + _vm->delay(_vm->tickLength()); + } + stopAllSoundEffects(); +} + +SoundPC98::SoundPC98(KyraEngine_v1 *vm, Audio::Mixer *mixer) : + Sound(vm, mixer), _musicTrackData(0), _sfxTrackData(0), _lastTrack(-1), _driver(0), _currentResourceSet(0) { + memset(&_resInfo, 0, sizeof(_resInfo)); +} + +SoundPC98::~SoundPC98() { + delete[] _musicTrackData; + delete[] _sfxTrackData; + delete _driver; + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); +} + +bool SoundPC98::init() { + _driver = new TownsPC98_AudioDriver(_mixer, TownsPC98_AudioDriver::kType26); + bool reslt = _driver->init(); + updateVolumeSettings(); + + // Initialize CD for audio + g_system->getAudioCDManager()->open(); + + return reslt; +} + +void SoundPC98::initAudioResourceInfo(int set, void *info) { + if (set >= kMusicIntro && set <= kMusicFinale) { + delete _resInfo[set]; + _resInfo[set] = info ? new Common::String(((SoundResourceInfo_PC98*)info)->pattern) : 0; + } +} + +void SoundPC98::selectAudioResourceSet(int set) { + if (set >= kMusicIntro && set <= kMusicFinale) { + if (_resInfo[set]) + _currentResourceSet = set; + } +} + +bool SoundPC98::hasSoundFile(uint file) const { + return true; +} + +void SoundPC98::loadSoundFile(uint) { + if (_currentResourceSet == kMusicIntro) { + delete[] _sfxTrackData; + _sfxTrackData = 0; + + int dataSize = 0; + const uint8 *tmp = _vm->staticres()->loadRawData(k1PC98IntroSfx, dataSize); + + if (!tmp) { + warning("Could not load static intro sound effects data\n"); + return; + } + + _sfxTrackData = new uint8[dataSize]; + memcpy(_sfxTrackData, tmp, dataSize); + } +} + +void SoundPC98::loadSoundFile(Common::String file) { + delete[] _sfxTrackData; + _sfxTrackData = _vm->resource()->fileData(file.c_str(), 0); +} + +void SoundPC98::playTrack(uint8 track) { + track -= 1; + + if (track == _lastTrack && _musicEnabled) + return; + + beginFadeOut(); + + Common::String musicFile = Common::String::format(resPattern(), track); + delete[] _musicTrackData; + _musicTrackData = _vm->resource()->fileData(musicFile.c_str(), 0); + if (_musicEnabled) + _driver->loadMusicData(_musicTrackData); + + _lastTrack = track; +} + +void SoundPC98::haltTrack() { + _lastTrack = -1; + g_system->getAudioCDManager()->stop(); + g_system->getAudioCDManager()->update(); + _driver->reset(); +} + +void SoundPC98::beginFadeOut() { + if (!_driver->musicPlaying()) + return; + + for (int i = 0; i < 20; i++) { + _driver->fadeStep(); + _vm->delay(32); + } + haltTrack(); +} + +void SoundPC98::playSoundEffect(uint8 track, uint8) { + if (!_sfxTrackData) + return; + + _driver->loadSoundEffectData(_sfxTrackData, track); +} + +void SoundPC98::updateVolumeSettings() { + if (!_driver) + return; + + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); + + _driver->setMusicVolume((mute ? 0 : ConfMan.getInt("music_volume"))); + _driver->setSoundEffectVolume((mute ? 0 : ConfMan.getInt("sfx_volume"))); +} + +// KYRA 2 + +SoundTownsPC98_v2::SoundTownsPC98_v2(KyraEngine_v1 *vm, Audio::Mixer *mixer) : + Sound(vm, mixer), _currentSFX(0), _musicTrackData(0), _sfxTrackData(0), _lastTrack(-1), _driver(0), _useFmSfx(false), _currentResourceSet(0) { + memset(&_resInfo, 0, sizeof(_resInfo)); +} + +SoundTownsPC98_v2::~SoundTownsPC98_v2() { + delete[] _musicTrackData; + delete[] _sfxTrackData; + delete _driver; + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); +} + +bool SoundTownsPC98_v2::init() { + _driver = new TownsPC98_AudioDriver(_mixer, _vm->gameFlags().platform == Common::kPlatformPC98 ? + TownsPC98_AudioDriver::kType86 : TownsPC98_AudioDriver::kTypeTowns); + + if (_vm->gameFlags().platform == Common::kPlatformFMTowns) { + if (_resInfo[_currentResourceSet]) + if (_resInfo[_currentResourceSet]->cdaTableSize) + _vm->checkCD(); + + // Initialize CD for audio + bool hasRealCD = g_system->getAudioCDManager()->open(); + + // FIXME: While checking for 'track1.XXX(X)' looks like + // a good idea, we should definitely not be doing this + // here. Basically our filenaming scheme could change + // or we could add support for other audio formats. Also + // this misses the possibility that we play the tracks + // right off CD. So we should find another way to + // check if we have access to CD audio. + Resource *r = _vm->resource(); + if (_musicEnabled && + (hasRealCD || r->exists("track1.mp3") || r->exists("track1.ogg") || r->exists("track1.flac") || r->exists("track1.fla") + || r->exists("track01.mp3") || r->exists("track01.ogg") || r->exists("track01.flac") || r->exists("track01.fla"))) + _musicEnabled = 2; + else + _musicEnabled = 1; + _useFmSfx = false; + + } else { + _useFmSfx = true; + } + + bool reslt = _driver->init(); + updateVolumeSettings(); + return reslt; +} + +void SoundTownsPC98_v2::initAudioResourceInfo(int set, void *info) { + if (set >= kMusicIntro && set <= kMusicFinale) { + delete _resInfo[set]; + _resInfo[set] = info ? new SoundResourceInfo_TownsPC98V2(*(SoundResourceInfo_TownsPC98V2*)info) : 0; + } +} + +void SoundTownsPC98_v2::selectAudioResourceSet(int set) { + if (set >= kMusicIntro && set <= kMusicFinale) { + if (_resInfo[set]) + _currentResourceSet = set; + } +} + +bool SoundTownsPC98_v2::hasSoundFile(uint file) const { + if (file < res()->fileListSize) + return (res()->fileList[file] != 0); + return false; +} + +void SoundTownsPC98_v2::loadSoundFile(Common::String file) { + delete[] _sfxTrackData; + _sfxTrackData = _vm->resource()->fileData(file.c_str(), 0); +} + +void SoundTownsPC98_v2::process() { + g_system->getAudioCDManager()->update(); +} + +void SoundTownsPC98_v2::playTrack(uint8 track) { + if (track == _lastTrack && _musicEnabled) + return; + + int trackNum = -1; + if (_vm->gameFlags().platform == Common::kPlatformFMTowns) { + for (uint i = 0; i < res()->cdaTableSize; i++) { + if (track == (uint8)READ_LE_UINT16(&res()->cdaTable[i * 2])) { + trackNum = (int)READ_LE_UINT16(&res()->cdaTable[i * 2 + 1]) - 1; + break; + } + } + } + + beginFadeOut(); + + Common::String musicFile = res()->pattern ? Common::String::format(res()->pattern, track) : (res()->fileList ? res()->fileList[track] : 0); + if (musicFile.empty()) + return; + + delete[] _musicTrackData; + + _musicTrackData = _vm->resource()->fileData(musicFile.c_str(), 0); + _driver->loadMusicData(_musicTrackData, true); + + if (_musicEnabled == 2 && trackNum != -1) { + g_system->getAudioCDManager()->play(trackNum+1, _driver->looping() ? -1 : 1, 0, 0); + g_system->getAudioCDManager()->update(); + } else if (_musicEnabled) { + _driver->cont(); + } + + _lastTrack = track; +} + +void SoundTownsPC98_v2::haltTrack() { + _lastTrack = -1; + g_system->getAudioCDManager()->stop(); + g_system->getAudioCDManager()->update(); + _driver->reset(); +} + +void SoundTownsPC98_v2::beginFadeOut() { + if (!_driver->musicPlaying()) + return; + + for (int i = 0; i < 20; i++) { + _driver->fadeStep(); + _vm->delay(32); + } + + haltTrack(); +} + +int32 SoundTownsPC98_v2::voicePlay(const char *file, Audio::SoundHandle *handle, uint8 volume, uint8 priority, bool) { + static const uint16 rates[] = { 0x10E1, 0x0CA9, 0x0870, 0x0654, 0x0438, 0x032A, 0x021C, 0x0194 }; + static const char patternHOF[] = "%s.PCM"; + static const char patternLOL[] = "%s.VOC"; + + int h = 0; + if (_currentSFX) { + while (h < kNumChannelHandles && _mixer->isSoundHandleActive(_soundChannels[h].handle)) + h++; + + if (h >= kNumChannelHandles) { + h = 0; + while (h < kNumChannelHandles && _soundChannels[h].priority > priority) + ++h; + if (h < kNumChannelHandles) + voiceStop(&_soundChannels[h].handle); + } + + if (h >= kNumChannelHandles) + return 0; + } + + Common::String fileName = Common::String::format( _vm->game() == GI_LOL ? patternLOL : patternHOF, file); + + uint8 *data = _vm->resource()->fileData(fileName.c_str(), 0); + uint8 *src = data; + if (!src) + return 0; + + uint16 sfxRate = rates[READ_LE_UINT16(src)]; + src += 2; + bool compressed = (READ_LE_UINT16(src) & 1) ? true : false; + src += 2; + uint32 outsize = READ_LE_UINT32(src); + uint8 *sfx = (uint8 *)malloc(outsize); + uint8 *dst = sfx; + src += 4; + + if (compressed) { + for (uint32 i = outsize; i;) { + uint8 cnt = *src++; + if (cnt & 0x80) { + cnt &= 0x7F; + memset(dst, *src++, cnt); + } else { + memcpy(dst, src, cnt); + src += cnt; + } + dst += cnt; + i -= cnt; + } + } else { + memcpy(dst, src, outsize); + } + + for (uint32 i = 0; i < outsize; i++) { + uint8 cmd = sfx[i]; + if (cmd & 0x80) { + cmd = ~cmd; + } else { + cmd |= 0x80; + if (cmd == 0xFF) + cmd--; + } + if (cmd < 0x80) + cmd = 0x80 - cmd; + sfx[i] = cmd; + } + + _currentSFX = Audio::makeRawStream(sfx, outsize, sfxRate * 10, Audio::FLAG_UNSIGNED | Audio::FLAG_LITTLE_ENDIAN); + _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundChannels[h].handle, _currentSFX, -1, volume); + _soundChannels[h].priority = priority; + if (handle) + *handle = _soundChannels[h].handle; + + delete[] data; + return 1; +} + +void SoundTownsPC98_v2::playSoundEffect(uint8 track, uint8) { + if (!_useFmSfx || !_sfxTrackData) + return; + + _driver->loadSoundEffectData(_sfxTrackData, track); +} + +void SoundTownsPC98_v2::updateVolumeSettings() { + if (!_driver) + return; + + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); + + _driver->setMusicVolume((mute ? 0 : ConfMan.getInt("music_volume"))); + _driver->setSoundEffectVolume((mute ? 0 : ConfMan.getInt("sfx_volume"))); +} + +} // End of namespace Kyra + +#undef EUPHONY_FADEOUT_TICKS diff --git a/engines/kyra/sound/sound_towns_darkmoon.cpp b/engines/kyra/sound/sound_towns_darkmoon.cpp new file mode 100644 index 0000000000..25fd4142b9 --- /dev/null +++ b/engines/kyra/sound/sound_towns_darkmoon.cpp @@ -0,0 +1,278 @@ +/* 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 2 + * 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, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + */ + +#include "kyra/sound/sound_intern.h" +#include "kyra/resource/resource.h" + +#include "common/config-manager.h" +#include "common/system.h" + +#include "backends/audiocd/audiocd.h" + +#include "audio/audiostream.h" +#include "audio/decoders/raw.h" + +namespace Kyra { + +SoundTowns_Darkmoon::SoundTowns_Darkmoon(KyraEngine_v1 *vm, Audio::Mixer *mixer) : Sound(vm, mixer) { + _intf = new TownsAudioInterface(mixer, this, false); + _pcmData = 0; + _pcmVol = 0; + _timer = 0; + _timerSwitch = 0; + memset(_pcmResource, 0, sizeof(_pcmResource)); +} + +SoundTowns_Darkmoon::~SoundTowns_Darkmoon() { + for (int i = 0; i < 3; i++) + initAudioResourceInfo(i, 0); + delete _intf; + delete[] _pcmData; +} + +bool SoundTowns_Darkmoon::init() { + if (!_intf->init()) + return false; + + _intf->callback(21, 255, 1); + _intf->callback(21, 0, 1); + _intf->callback(22, 255, 221); + + _intf->callback(70, 0x31); + _intf->callback(33, 1); + _intf->callback(8, 0x47, 127); + _intf->callback(67, 1, 127, 127); + + _intf->setSoundEffectChanMask(-1); + + _lastSfxChan = 0x46; + _lastEnvChan = 0x40; + + updateVolumeSettings(); + + return true; +} + +void SoundTowns_Darkmoon::timerCallback(int timerId) { + switch (timerId) { + case 1: + _timerSwitch = (_timerSwitch + 1) % 4; + if (!_timerSwitch) + _timer++; + break; + default: + break; + } +} + +void SoundTowns_Darkmoon::initAudioResourceInfo(int set, void *info) { + delete _pcmResource[set]; + _pcmResource[set] = info ? new SoundResourceInfo_TownsEoB(*(SoundResourceInfo_TownsEoB*)info) : 0; +} + +void SoundTowns_Darkmoon::selectAudioResourceSet(int set) { + delete[] _pcmData; + + if (!_pcmResource[set] || !_pcmResource[kMusicIngame]) + return; + + _pcmDataSize = _pcmResource[kMusicIngame]->pcmDataSize; + _pcmData = new uint8[_pcmDataSize]; + _pcmVol = _pcmResource[set]->pcmVolume; + memcpy(_pcmData, _pcmResource[kMusicIngame]->pcmData, _pcmDataSize); + + if (set == kMusicIngame) + return; + + memcpy(_pcmData, _pcmResource[set]->pcmData, _pcmResource[set]->pcmDataSize); +} + +bool SoundTowns_Darkmoon::hasSoundFile(uint file) const { + return true; +} + +void SoundTowns_Darkmoon::loadSoundFile(Common::String name) { + Common::SeekableReadStream *s = _vm->resource()->createReadStream(Common::String::format("%s.SDT", name.c_str())); + if (!s) + error("Failed to load sound file '%s.SDT'", name.c_str()); + + for (int i = 0; i < 120; i++) { + _soundTable[i].type = s->readSByte(); + _soundTable[i].para1 = s->readSint32LE(); + _soundTable[i].para2 = s->readSint16LE(); + } + + delete s; + + uint32 bytesLeft; + uint8 *pmb = _vm->resource()->fileData(Common::String::format("%s.PMB", name.c_str()).c_str(), &bytesLeft); + + _vm->delay(300); + + if (pmb) { + uint8 *src = pmb + 8; + for (int i = 0; i < 32; i++) + _intf->callback(5, 0x40, i, &src[i << 7]); + + _intf->callback(35, -1); + src += 0x1000; + bytesLeft -= 0x1008; + + while (bytesLeft) { + _intf->callback(34, src); + uint32 len = READ_LE_UINT16(&src[12]) + 32; + src = src + len; + bytesLeft -= len; + } + + delete[] pmb; + } else { + warning("Sound file '%s.PMB' not found.", name.c_str()); + // TODO + } +} + +void SoundTowns_Darkmoon::playTrack(uint8 track) { + if (track >= 120 || !_sfxEnabled) + return; + + uint8 *pcm = 0; + + switch (_soundTable[track].type) { + case -1: + if (track == 0) + haltTrack(); + else if (track == 2) + beginFadeOut(); + break; + + case 0: + if (_soundTable[track].para1 == -1 || (uint32)_soundTable[track].para1 > _pcmDataSize) + return; + + pcm = _pcmData + _soundTable[track].para1; + WRITE_LE_UINT16(&pcm[24], _soundTable[track].para2 * 98 / 1000); + + _intf->callback(39, 0x47); + _intf->callback(37, 0x47, 60, track == 11 ? 127 : _pcmVol, pcm); + break; + + case 2: + resetTrigger(); + g_system->getAudioCDManager()->play(_soundTable[track].para1 - 1, 1, 0, 0); + break; + + case 3: + _lastSfxChan ^= 3; + _intf->callback(39, _lastSfxChan); + _intf->callback(4, _lastSfxChan, _soundTable[track].para1); + _intf->callback(1, _lastSfxChan, _soundTable[track].para2, 127); + break; + + default: + break; + } +} + +void SoundTowns_Darkmoon::haltTrack() { + _intf->callback(39, 0x47); + _intf->callback(39, 0x46); + _intf->callback(39, 0x45); + + g_system->getAudioCDManager()->stop(); +} + +bool SoundTowns_Darkmoon::isPlaying() const { + return g_system->getAudioCDManager()->isPlaying(); +} + +void SoundTowns_Darkmoon::playSoundEffect(uint8 track, uint8 volume) { + if (!_sfxEnabled) + return; + + if (volume == 255) + return playTrack(track); + + uint8 *pcm = 0; + + switch (_soundTable[track].type) { + case 0: + if (_soundTable[track].para1 == -1 || (uint32)_soundTable[track].para1 > _pcmDataSize) + return; + + pcm = _pcmData + _soundTable[track].para1; + WRITE_LE_UINT16(&pcm[24], _soundTable[track].para2 * 98 / 1000); + + _intf->callback(39, 0x47); + _intf->callback(37, 0x47, 60, volume, pcm); + break; + + case 3: + _intf->callback(2, _lastEnvChan); + _intf->callback(4, _lastEnvChan, _soundTable[track].para1); + _intf->callback(1, _lastEnvChan, _soundTable[track].para2, volume); + break; + + default: + break; + } + + if (++_lastEnvChan == 0x43) + _lastEnvChan = 0x40; +} + +void SoundTowns_Darkmoon::stopAllSoundEffects() { + _intf->callback(39, 0x42); + _intf->callback(39, 0x41); + _intf->callback(39, 0x40); +} + +void SoundTowns_Darkmoon::beginFadeOut() { + for (int vol = 127; vol >= 0; vol -= 2) { + _intf->callback(67, 1, vol, vol); + _vm->delay(16); + } + + _intf->callback(67, 1, 0, 0); + _intf->callback(70, 1); + + g_system->getAudioCDManager()->stop(); + + _intf->callback(70, 0x31); + _intf->callback(67, 1, 127, 127); +} + +void SoundTowns_Darkmoon::updateVolumeSettings() { + bool mute = (ConfMan.hasKey("mute")) ? ConfMan.getBool("mute") : false; + _intf->setSoundEffectVolume((mute ? 0 : ConfMan.getInt("sfx_volume"))); +} + +int SoundTowns_Darkmoon::checkTrigger() { + return _timer; +} + +void SoundTowns_Darkmoon::resetTrigger() { + _timer = 0; + _timerSwitch = 0; +} + +} // End of namespace Kyra |