From 66bb075f1c107e1c42d1ac6601e20a9388d15d6a Mon Sep 17 00:00:00 2001 From: Coen Rampen Date: Sat, 23 Apr 2022 20:39:25 +0200 Subject: [PATCH] AGOS: Add E2/WW AdLib and MT-32 SFX and enhancements This adds support for the AdLib and MT-32 sound effects in Elvira 2 and Waxworks. It adds an option to the UI to toggle between sampled and synthesized SFX. It also adds the following enhancements: - AdLib OPL3 mode for Elvira 2, Waxworks and Simon 1 floppy demo. This can be selected using a new UI option. - Mixed AdLib/MIDI mode for Elvira 2 and Waxworks. - Implemented "monophonic chords", a feature of the original MIDI code which would play only the highest note of a chord on AdLib. Most noticable in the Waxworks music. - Added UI option to select Simon 1 DOS music tempos. - Rewrite of the AdLib and MT-32 drivers to remove duplication and make use of features of the standard multisource drivers. - Refactored MidiPlayer to standardize interface and remove code moved to the drivers and parsers. --- engines/agos/POTFILES | 1 + engines/agos/agos.cpp | 20 +- engines/agos/agos.h | 8 +- engines/agos/detection.cpp | 56 + engines/agos/drivers/accolade/adlib.cpp | 1006 ++++++---------- engines/agos/drivers/accolade/adlib.h | 132 +-- engines/agos/drivers/accolade/mididriver.h | 8 +- engines/agos/drivers/accolade/mt32.cpp | 273 ++--- engines/agos/drivers/accolade/mt32.h | 71 +- engines/agos/drivers/simon1/adlib.cpp | 4 +- engines/agos/drivers/simon1/adlib.h | 2 +- engines/agos/drivers/simon1/adlib_win.cpp | 38 - engines/agos/drivers/simon1/adlib_win.h | 44 - engines/agos/midi.cpp | 1242 +++++--------------- engines/agos/midi.h | 106 +- engines/agos/midiparser_s1d.cpp | 95 +- engines/agos/module.mk | 2 +- engines/agos/res_snd.cpp | 134 ++- engines/agos/script_e2.cpp | 2 +- engines/agos/script_s1.cpp | 2 +- engines/agos/sfxparser_accolade.cpp | 508 ++++++++ engines/agos/sfxparser_accolade.h | 220 ++++ engines/agos/vga.cpp | 7 +- engines/agos/vga_e2.cpp | 6 +- 24 files changed, 1892 insertions(+), 2095 deletions(-) delete mode 100644 engines/agos/drivers/simon1/adlib_win.cpp delete mode 100644 engines/agos/drivers/simon1/adlib_win.h create mode 100644 engines/agos/sfxparser_accolade.cpp create mode 100644 engines/agos/sfxparser_accolade.h diff --git a/engines/agos/POTFILES b/engines/agos/POTFILES index 95027f8fe42..ac001c84591 100644 --- a/engines/agos/POTFILES +++ b/engines/agos/POTFILES @@ -2,3 +2,4 @@ engines/agos/saveload.cpp engines/agos/animation.cpp engines/agos/metaengine.cpp engines/agos/midi.cpp +engines/agos/detection.cpp diff --git a/engines/agos/agos.cpp b/engines/agos/agos.cpp index 91060bfcd60..9e5e6a3d917 100644 --- a/engines/agos/agos.cpp +++ b/engines/agos/agos.cpp @@ -525,6 +525,7 @@ AGOSEngine::AGOSEngine(OSystem *system, const AGOSGameDescription *gd) // syncSoundSettings. _musicVolume = 192; _effectsVolume = 192; + _useDigitalSfx = true; _saveLoadType = 0; _saveLoadSlot = 0; @@ -611,12 +612,29 @@ Common::Error AGOSEngine::init() { ((getFeatures() & GF_TALKIE) && getPlatform() == Common::kPlatformAcorn) || (getPlatform() == Common::kPlatformDOS || getPlatform() == Common::kPlatformPC98)) { - int ret = _midi->open(getGameType(), getPlatform(), (getFeatures() & GF_DEMO)); + int ret = _midi->open(); if (ret) warning("MIDI Player init failed: \"%s\"", MidiDriver::getErrorName(ret)); _midiEnabled = true; } + // Digital SFX are used if MIDI SFX are not available or if the "prefer + // digital SFX" setting is set to true or is not present at all. + // Two exceptions to this are: + // - Elvira 2 DOS needs an optional file to enable digital SFX. If it is + // not present, MIDI SFX are used. + // - Simon 1 DOS floppy has only MIDI SFX. + // Note that MIDI SFX can be safely used if the MidiPlayer failed to + // initialize; they just will not play. + _useDigitalSfx = !_midiEnabled || !_midi->hasMidiSfx() || !ConfMan.hasKey("prefer_digitalsfx") || ConfMan.getBool("prefer_digitalsfx"); + if ((getGameType() == GType_ELVIRA2 && getPlatform() == Common::kPlatformDOS && !SearchMan.hasFile("013.VGA")) || + (getGameType() == GType_SIMON1 && getPlatform() == Common::kPlatformDOS && !(getFeatures() & GF_TALKIE))) { + _useDigitalSfx = false; + } + if (!_useDigitalSfx && (getGameType() == GType_ELVIRA2 || getGameType() == GType_WW) && getPlatform() == Common::kPlatformDOS) { + // Load the MIDI SFX data file for Elvira 2 and Waxworks DOS. + loadMidiSfx(); + } // allocate buffers _backGroundBuf = new Graphics::Surface(); diff --git a/engines/agos/agos.h b/engines/agos/agos.h index 94beead2735..0a2e16f6ccc 100644 --- a/engines/agos/agos.h +++ b/engines/agos/agos.h @@ -253,8 +253,6 @@ public: const char *getFileName(int type) const; protected: - void playSting(uint16 a); - const byte *_vcPtr; /* video code ptr */ uint16 _vcGetOutOfCode; @@ -599,6 +597,7 @@ protected: // The current SFX and ambient volume, or the last used volume if SFX // and/or ambient sounds are currently muted. uint16 _effectsVolume; + bool _useDigitalSfx; uint8 _saveGameNameLen; uint16 _saveLoadRowCurPos; @@ -653,8 +652,12 @@ protected: void decompressPN(Common::Stack &dataList, uint8 *&dataOut, int &dataOutSize); void loadOffsets(const char *filename, int number, uint32 &file, uint32 &offset, uint32 &compressedSize, uint32 &size); void loadSound(uint16 sound, int16 pan, int16 vol, uint16 type); + void playSfx(uint16 sound, uint16 freq, uint16 flags, bool canUseMidiSfx); void loadSound(uint16 sound, uint16 freq, uint16 flags); + void loadMidiSfx(); + virtual void playMidiSfx(uint16 sound); void loadVoice(uint speechId); + void stopAllSfx(); void loadSoundFile(const char *filename); @@ -1904,6 +1907,7 @@ protected: int userGameGetKey(bool *b, uint maxChar) override; void playMusic(uint16 music, uint16 track) override; + void playMidiSfx(uint16 sound) override; void vcStopAnimation(uint16 zone, uint16 sprite) override; diff --git a/engines/agos/detection.cpp b/engines/agos/detection.cpp index ce14c9410fb..b279d7e8593 100644 --- a/engines/agos/detection.cpp +++ b/engines/agos/detection.cpp @@ -24,9 +24,11 @@ #include "engines/advancedDetector.h" #include "engines/obsolete.h" +#include "common/config-manager.h" #include "common/system.h" #include "common/textconsole.h" #include "common/installshield_cab.h" +#include "common/translation.h" #include "agos/detection.h" #include "agos/intern_detection.h" @@ -71,6 +73,31 @@ static const char *const directoryGlobs[] = { using namespace AGOS; +static const ExtraGuiOption opl3Mode = { + _s("AdLib OPL3 mode"), + _s("When AdLib is selected, OPL3 features will be used. Depending on the \ + game, this will prevent cut-off notes, add extra notes or instruments \ + and/or add stereo."), + "opl3_mode", + false +}; + +static const ExtraGuiOption useDosTempos = { + _s("Use DOS version music tempos"), + _s("Selecting this option will play the music using the tempos used by \ + the DOS version of the game. Otherwise, the faster tempos of the Windows \ + version will be used."), + "dos_music_tempos", + false +}; + +static const ExtraGuiOption preferDigitalSfx = { + _s("Prefer digital sound effects"), + _s("Prefer digital sound effects instead of synthesized ones"), + "prefer_digitalsfx", + true +}; + class AgosMetaEngineDetection : public AdvancedMetaEngineDetection { public: AgosMetaEngineDetection() : AdvancedMetaEngineDetection(AGOS::gameDescriptions, sizeof(AGOS::AGOSGameDescription), agosGames) { @@ -98,6 +125,35 @@ public: const DebugChannelDef *getDebugChannels() const override { return debugFlagList; } + + const ExtraGuiOptions getExtraGuiOptions(const Common::String &target) const override { + const Common::String gameid = ConfMan.get("gameid", target); + const Common::String platform = ConfMan.get("platform", target); + const Common::String extra = ConfMan.get("extra", target); + + ExtraGuiOptions options; + if (target.empty() || ((gameid == "elvira2" || gameid == "waxworks" || gameid == "simon1") && platform == "pc")) { + // DOS versions of Elvira 2, Waxworks and Simon 1 can optionally + // make use of AdLib OPL3 features. + options.push_back(opl3Mode); + } + if (target.empty() || (gameid == "simon1" && ((platform == "pc" && extra != "Floppy Demo") || platform == "windows" || + (platform == "acorn" && extra.contains("CD"))))) { + // Simon 1 DOS (except the floppy demo), Windows and Acorn CD can + // choose between the DOS or Windows music tempos. + ExtraGuiOption dosTemposOption = useDosTempos; + // DOS tempos are default for the DOS versions; other versions use + // the Windows tempos by default. + dosTemposOption.defaultState = platform == "pc"; + options.push_back(dosTemposOption); + } + if (target.empty() || ((gameid == "elvira2" || gameid == "waxworks") && platform == "pc")) { + // DOS versions of Elvira 2 and Waxworks can use either Adlib or + // digital SFX. + options.push_back(preferDigitalSfx); + } + return options; + } }; REGISTER_PLUGIN_STATIC(AGOS_DETECTION, PLUGIN_TYPE_ENGINE_DETECTION, AgosMetaEngineDetection); diff --git a/engines/agos/drivers/accolade/adlib.cpp b/engines/agos/drivers/accolade/adlib.cpp index f14e544effc..7b16af317c5 100644 --- a/engines/agos/drivers/accolade/adlib.cpp +++ b/engines/agos/drivers/accolade/adlib.cpp @@ -19,44 +19,19 @@ * */ -#include "agos/drivers/accolade/mididriver.h" #include "agos/drivers/accolade/adlib.h" -#include "audio/fmopl.h" -#include "audio/mididrv.h" +#include "agos/drivers/accolade/mididriver.h" namespace AGOS { -#define AGOS_ADLIB_VOICES_MELODIC_COUNT 6 -#define AGOS_ADLIB_VOICES_PERCUSSION_START 6 -#define AGOS_ADLIB_VOICES_PERCUSSION_COUNT 5 -#define AGOS_ADLIB_VOICES_PERCUSSION_CYMBAL 9 - -// 5 instruments on top of the regular MIDI ones -// used by the MUSIC.DRV variant for percussion instruments -#define AGOS_ADLIB_EXTRA_INSTRUMENT_COUNT 5 - -const byte operator1Register[AGOS_ADLIB_VOICES_COUNT] = { - 0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x14, 0x12, 0x15, 0x11 -}; - -const byte operator2Register[AGOS_ADLIB_VOICES_COUNT] = { - 0x03, 0x04, 0x05, 0x0B, 0x0C, 0x0D, 0x13, 0xFF, 0xFF, 0xFF, 0xFF -}; - -// percussion: -// voice 6 - base drum - also uses operator 13h -// voice 7 - snare drum -// voice 8 - tom tom -// voice 9 - cymbal -// voice 10 - hi hat -const byte percussionBits[AGOS_ADLIB_VOICES_PERCUSSION_COUNT] = { - 0x10, 0x08, 0x04, 0x02, 0x01 -}; - // hardcoded, dumped from Accolade music system // same for INSTR.DAT + MUSIC.DRV, except that MUSIC.DRV does the lookup differently -const byte percussionKeyNoteChannelTable[] = { +// Numbers 6-A correspond to MIDI channels in the original driver, but here they +// are directly mapped to rhythm instrument types (bass drum, snare drum, +// tom tom, cymbal and hi-hat respectively). F means there is no instrument +// defined for the rhythm note. +const byte MidiDriver_Accolade_AdLib::RHYTHM_NOTE_INSTRUMENT_TYPES[] = { 0x06, 0x07, 0x07, 0x07, 0x07, 0x08, 0x0A, 0x08, 0x0A, 0x08, 0x0A, 0x08, 0x08, 0x09, 0x08, 0x09, 0x0F, 0x0F, 0x0A, 0x0F, 0x0A, 0x0F, 0x0F, 0x0F, 0x08, 0x08, 0x08, 0x08, 0x08, 0x08, @@ -64,26 +39,25 @@ const byte percussionKeyNoteChannelTable[] = { }; // hardcoded, dumped from Accolade music system (INSTR.DAT variant) -const uint16 frequencyLookUpTable[12] = { - 0x02B2, 0x02DB, 0x0306, 0x0334, 0x0365, 0x0399, 0x03CF, - 0xFE05, 0xFE23, 0xFE44, 0xFE67, 0xFE8B +const uint16 MidiDriver_Accolade_AdLib::OPL_NOTE_FREQUENCIES_INSTR_DAT[] = { + 0x02B2, 0x02DB, 0x0306, 0x0334, 0x0365, 0x0399, + 0x03CF, 0xFE05, 0xFE23, 0xFE44, 0xFE67, 0xFE8B }; // hardcoded, dumped from Accolade music system (MUSIC.DRV variant) -const uint16 frequencyLookUpTableMusicDrv[12] = { - 0x0205, 0x0223, 0x0244, 0x0267, 0x028B, 0x02B2, 0x02DB, - 0x0306, 0x0334, 0x0365, 0x0399, 0x03CF +const uint16 MidiDriver_Accolade_AdLib::OPL_NOTE_FREQUENCIES_MUSIC_DRV[] = { + 0x0205, 0x0223, 0x0244, 0x0267, 0x028B, 0x02B2, + 0x02DB, 0x0306, 0x0334, 0x0365, 0x0399, 0x03CF }; -// // Accolade adlib music driver // // Remarks: // // There are at least 2 variants of this sound system. -// One for the games Elvira 1 + Elvira 2 +// One for the game Elvira 1 // It seems it was also used for the game "Altered Destiny" -// Another one for the games Waxworks + Simon, the Sorcerer 1 Demo +// Another one for the games Elvira 2 + Waxworks + Simon, the Sorcerer 1 Demo // // First one uses the file INSTR.DAT for instrument data, channel mapping etc. // Second one uses the file MUSIC.DRV, which actually contains driver code + instrument data + channel mapping, etc. @@ -92,697 +66,437 @@ const uint16 frequencyLookUpTableMusicDrv[12] = { // feature was at least definitely disabled for Simon, the Sorcerer 1 demo and for the Waxworks demo too. // // I have currently not implemented dynamic channel allocation. +MidiDriver_Accolade_AdLib::MidiDriver_Accolade_AdLib(OPL::Config::OplType oplType, bool newVersion) : MidiDriver_ADLIB_Multisource(oplType) { + _instrumentBank = nullptr; + _rhythmBank = nullptr; + _newVersion = newVersion; + _oplNoteFrequencies = _newVersion ? OPL_NOTE_FREQUENCIES_MUSIC_DRV : OPL_NOTE_FREQUENCIES_INSTR_DAT; -MidiDriver_Accolade_AdLib::MidiDriver_Accolade_AdLib() - : _masterVolume(143), _opl(nullptr), - _adlibTimerProc(nullptr), _adlibTimerParam(nullptr), _isOpen(false) { - memset(_channelMapping, 0, sizeof(_channelMapping)); - memset(_instrumentMapping, 0, sizeof(_instrumentMapping)); - memset(_instrumentVolumeAdjust, 0, sizeof(_instrumentVolumeAdjust)); - memset(_percussionKeyNoteMapping, 0, sizeof(_percussionKeyNoteMapping)); - - _instrumentTable = nullptr; - _instrumentCount = 0; - _musicDrvMode = false; - _percussionReg = 0x20; + Common::fill(_channelRemapping, _channelRemapping + ARRAYSIZE(_channelRemapping), 0); + Common::fill(_instrumentRemapping, _instrumentRemapping + ARRAYSIZE(_instrumentRemapping), 0); + Common::fill(_volumeAdjustments, _volumeAdjustments + ARRAYSIZE(_volumeAdjustments), 0); + Common::fill(_sfxNoteFractions, _sfxNoteFractions + ARRAYSIZE(_sfxNoteFractions), 0); + memset(_sfxInstruments, 0, sizeof(_sfxInstruments)); } MidiDriver_Accolade_AdLib::~MidiDriver_Accolade_AdLib() { - if (_instrumentTable) { - delete[] _instrumentTable; - _instrumentCount = 0; - } + if (_instrumentBank) + delete[] _instrumentBank; + if (_rhythmBank) + delete[] _rhythmBank; } int MidiDriver_Accolade_AdLib::open() { -// debugC(kDebugLevelAdLibDriver, "AdLib: starting driver"); + _modulationDepth = MODULATION_DEPTH_LOW; + _vibratoDepth = VIBRATO_DEPTH_LOW; - _opl = OPL::Config::create(OPL::Config::kOpl2); + int result = MidiDriver_ADLIB_Multisource::open(); - if (!_opl) - return -1; + if (result == 0) { + // Rhythm mode is always on. + setRhythmMode(true); - _opl->init(); + // The original driver writes out default instruments to all channels + // here. This implementation writes instruments before note on, so this + // is not necessary. - _isOpen = true; + // driver initialization does this here: + // INSTR.DAT + // noteOn(9, 0x29, 0); + // noteOff(9, 0x26, false); + // MUSIC.DRV + // noteOn(9, 0x26, 0); + // noteOff(9, 0x26, false); + } - _opl->start(new Common::Functor0Mem(this, &MidiDriver_Accolade_AdLib::onTimer)); + return result; +} - resetAdLib(); +void MidiDriver_Accolade_AdLib::send(int8 source, uint32 b) { + // Remap the MIDI channel according to the channel map. + // (Seems to be 1 on 1 for AdLib...) + byte channel = b & 0xF; + channel = _channelRemapping[channel]; + b &= 0xFFFFFFF0; + b |= channel; + byte command = b & 0xF0; - // Finally set up default instruments - for (byte FMvoiceNr = 0; FMvoiceNr < AGOS_ADLIB_VOICES_COUNT; FMvoiceNr++) { - if (FMvoiceNr < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // Regular FM voices with instrument 0 - programChangeSetInstrument(FMvoiceNr, 0, 0); - } else { - byte percussionInstrument; - if (!_musicDrvMode) { - // INSTR.DAT: percussion voices with instrument 1, 2, 3, 4 and 5 - percussionInstrument = FMvoiceNr - AGOS_ADLIB_VOICES_PERCUSSION_START + 1; + if (_oplType != OPL::Config::kOpl3 && _sources[source].type != SOURCE_TYPE_SFX && command != MIDI_COMMAND_PROGRAM_CHANGE) { + // Filter out events for channels used by SFX. + // Program change events are always accepted; they just set the program + // for the music source and do not affect the SFX notes. + if (_activeNotes[channel].channelAllocated) + return; + } + + MidiDriver_ADLIB_Multisource::send(source, b); +} + +void MidiDriver_Accolade_AdLib::deinitSource(uint8 source) { + if (_sources[source].type == SOURCE_TYPE_SFX) { + // When a sound effect ends, the original driver will immediately + // rewrite the music instrument for the channel used by this sound + // effect. This has the effect of stopping the release phase of the + // sound effect. This is reproduced here to make sure the sound effects + // sound the same. + byte channel = _channelAllocations[source][0]; + // OPL3 mode has no fixed instrument assignment to the OPL channel, so + // just use instrument 0. + byte program = 0; + if (_oplType != OPL::Config::kOpl3) { + // For OPL2, get the current music instrument for this OPL channel. + program = _controlData[0][channel].program; + if (_instrumentRemapping) + // Apply instrument remapping (if specified) to instrument channels. + program = _instrumentRemapping[program]; + } + + InstrumentInfo instrument { }; + instrument.instrumentId = program; + instrument.instrumentDef = &_instrumentBank[program]; + instrument.oplNote = 0; + + writeInstrument(channel, instrument); + + // Clear other SFX data. + _sfxNoteFractions[source - 1] = 0; + } + + MidiDriver_ADLIB_Multisource::deinitSource(source); +} + +uint8 MidiDriver_Accolade_AdLib::allocateOplChannel(uint8 channel, uint8 source, uint8 instrumentId) { + Common::StackLock lock(_allocationMutex); + + if (_sources[source].type == SOURCE_TYPE_SFX) { + if (_channelAllocations[source][0] == 0xFF) { + // Allocate a channel for this SFX source. + byte allocatedChannel; + if (_oplType != OPL::Config::kOpl3) { + // For OPL2, use channels 5 and 4. + allocatedChannel = 6 - source; } else { - // MUSIC.DRV: percussion voices with instrument 0x80, 0x81, 0x82, 0x83 and 0x84 - percussionInstrument = FMvoiceNr - AGOS_ADLIB_VOICES_PERCUSSION_START + 0x80; + // For OPL3, use the dynamic allocation algorithm. + allocatedChannel = MidiDriver_ADLIB_Multisource::allocateOplChannel(channel, source, instrumentId); } - programChangeSetInstrument(FMvoiceNr, percussionInstrument, percussionInstrument); + + _activeNotesMutex.lock(); + + ActiveNote *activeNote = &_activeNotes[allocatedChannel]; + if (activeNote->noteActive) { + // Turn off the note currently playing on this OPL channel. + writeKeyOff(allocatedChannel, activeNote->instrumentDef->rhythmType); + } + _channelAllocations[source][0] = allocatedChannel; + activeNote->channelAllocated = true; + activeNote->source = source; + activeNote->channel = channel; + activeNote->oplNote = 0; + + _activeNotesMutex.unlock(); } + + // Return the allocated channel. + return _channelAllocations[source][0]; } - // driver initialization does this here: - // INSTR.DAT - // noteOn(9, 0x29, 0); - // noteOff(9, 0x26, false); - // MUSIC.DRV - // noteOn(9, 0x26, 0); - // noteOff(9, 0x26, false); + // Channel allocation for music sources. + if (_oplType != OPL::Config::kOpl3) { + // For OPL2, discard events for channels 6 and 7 and channels allocated + // for SFX. + if (channel >= 6 || _activeNotes[channel].channelAllocated) + return 0xFF; - return 0; -} - -void MidiDriver_Accolade_AdLib::close() { - delete _opl; - _isOpen = false; -} - -void MidiDriver_Accolade_AdLib::setVolume(byte volume) { - _masterVolume = volume; - for (int i = 0; i < AGOS_ADLIB_VOICES_COUNT; i++) { - // Re-set registers - noteOnSetVolume(i, 1, _channels[i].velocity); - if (i <= AGOS_ADLIB_VOICES_PERCUSSION_START) { - // Set second operator for FM voices + first percussion - noteOnSetVolume(i, 2, _channels[i].velocity); - } + // Then just map MIDI channels 0-5 to OPL channels 0-5. + return channel; + } else { + // For OPL3, use the dynamic allocation algorithm. + return MidiDriver_ADLIB_Multisource::allocateOplChannel(channel, source, instrumentId); } } -void MidiDriver_Accolade_AdLib::onTimer() { - if (_adlibTimerProc) - (*_adlibTimerProc)(_adlibTimerParam); +byte MidiDriver_Accolade_AdLib::getNumberOfSfxSources() { + // With OPL3 more channels are available for SFX. + return _oplType == OPL::Config::kOpl3 ? 4 : 2; } -void MidiDriver_Accolade_AdLib::resetAdLib() { - // The original driver sent 0x00 to register 0x00 up to 0xF5 - setRegister(0xBD, 0x00); // Disable rhythm - - // reset FM voice instrument data - resetAdLibOperatorRegisters(0x20, 0); - resetAdLibOperatorRegisters(0x60, 0); - resetAdLibOperatorRegisters(0x80, 0); - resetAdLibFMVoiceChannelRegisters(0xA0, 0); - resetAdLibFMVoiceChannelRegisters(0xB0, 0); - resetAdLibFMVoiceChannelRegisters(0xC0, 0); - resetAdLibOperatorRegisters(0xE0, 0); - resetAdLibOperatorRegisters(0x40, 0x3F); // original driver sent 0x00 - - setRegister(0x01, 0x20); // enable waveform control on both operators - setRegister(0x04, 0x60); // Timer control - - setRegister(0x08, 0); // select FM music mode - setRegister(0xBD, 0x20); // Enable rhythm - - // reset our percussion register - _percussionReg = 0x20; -} - -void MidiDriver_Accolade_AdLib::resetAdLibOperatorRegisters(byte baseRegister, byte value) { - byte operatorIndex; - - for (operatorIndex = 0; operatorIndex < 0x16; operatorIndex++) { - switch (operatorIndex) { - case 0x06: - case 0x07: - case 0x0E: - case 0x0F: - break; - default: - setRegister(baseRegister + operatorIndex, value); - } - } -} - -void MidiDriver_Accolade_AdLib::resetAdLibFMVoiceChannelRegisters(byte baseRegister, byte value) { - byte FMvoiceChannel; - - for (FMvoiceChannel = 0; FMvoiceChannel < AGOS_ADLIB_VOICES_COUNT; FMvoiceChannel++) { - setRegister(baseRegister + FMvoiceChannel, value); - } -} - -// MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php -void MidiDriver_Accolade_AdLib::send(uint32 b) { - byte command = b & 0xf0; - byte channel = b & 0xf; - byte op1 = (b >> 8) & 0xff; - byte op2 = (b >> 16) & 0xff; - - byte mappedChannel = _channelMapping[channel]; - byte mappedInstrument = 0; - - // Ignore everything that is outside of our channel range - if (mappedChannel >= AGOS_ADLIB_VOICES_COUNT) +void MidiDriver_Accolade_AdLib::loadSfxInstrument(uint8 source, byte *instrumentData) { + if (source > (_oplType == OPL::Config::kOpl3 ? 4 : 2)) return; - switch (command) { - case 0x80: - noteOff(mappedChannel, op1, false); - break; - case 0x90: - // Convert noteOn with velocity 0 to a noteOff - if (op2 == 0) - return noteOff(mappedChannel, op1, false); + // Copy instrument data into SFX instruments bank. + loadInstrumentData(_sfxInstruments[source - 1], instrumentData, RHYTHM_TYPE_UNDEFINED, 0, _newVersion); - noteOn(mappedChannel, op1, op2); - break; - case 0xb0: // Control change - // Doesn't seem to be implemented - break; - case 0xc0: // Program Change - mappedInstrument = _instrumentMapping[op1]; - programChange(mappedChannel, mappedInstrument, op1); - break; - case 0xa0: // Polyphonic key pressure (aftertouch) - case 0xd0: // Channel pressure (aftertouch) - // Aftertouch doesn't seem to be implemented - break; - case 0xe0: - // No pitch bend change - break; - case 0xf0: // SysEx - warning("ADLIB: SysEx: %x", b); - break; - default: - warning("ADLIB: Unknown event %02x", command); - } + _activeNotesMutex.lock(); + + // Allocate a channel + programChange(0, 0, source); + InstrumentInfo instrument = determineInstrument(0, source, 0); + uint8 oplChannel = allocateOplChannel(0, source, instrument.instrumentId); + + // Update the active note data. + ActiveNote *activeNote = &_activeNotes[oplChannel]; + activeNote->instrumentId = instrument.instrumentId; + activeNote->instrumentDef = instrument.instrumentDef; + + _activeNotesMutex.unlock(); } -void MidiDriver_Accolade_AdLib::setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) { - _adlibTimerProc = timerProc; - _adlibTimerParam = timerParam; +void MidiDriver_Accolade_AdLib::setSfxNoteFraction(uint8 source, uint16 noteFraction) { + // Note is in the upper byte. + _activeNotes[_channelAllocations[source][0]].oplNote = noteFraction >> 8; + // Note fraction is in the lower byte. + _sfxNoteFractions[source - 1] = noteFraction & 0xFF; } -void MidiDriver_Accolade_AdLib::noteOn(byte FMvoiceChannel, byte note, byte velocity) { - byte adjustedNote = note; - byte regValueA0h = 0; - byte regValueB0h = 0; - - // adjust velocity - byte adjustedVelocity = velocity + _channels[FMvoiceChannel].volumeAdjust; - - if (!_musicDrvMode) { - // INSTR.DAT - // force note-off - noteOff(FMvoiceChannel, note, true); +void MidiDriver_Accolade_AdLib::updateSfxNote(uint8 source) { + writeFrequency(_channelAllocations[source][0]); +} +MidiDriver_Accolade_AdLib::InstrumentInfo MidiDriver_Accolade_AdLib::determineInstrument(uint8 channel, uint8 source, uint8 note) { + if (_sources[source].type == SOURCE_TYPE_SFX) { + // For SFX sources, return an instrument from the SFX bank. + InstrumentInfo instrument { }; + instrument.instrumentId = 0xFFFF - source; + instrument.instrumentDef = &_sfxInstruments[source - 1]; + instrument.oplNote = note; + return instrument; } else { - // MUSIC.DRV - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // force note-off, but only for actual FM voice channels - noteOff(FMvoiceChannel, note, true); - } + return MidiDriver_ADLIB_Multisource::determineInstrument(channel, source, note); } +} - if (FMvoiceChannel != 9) { - // regular FM voice - - if (!_musicDrvMode) { - // INSTR.DAT: adjust key note - while (adjustedNote < 24) - adjustedNote += 12; - adjustedNote -= 12; +uint16 MidiDriver_Accolade_AdLib::calculateFrequency(uint8 channel, uint8 source, uint8 note) { + if (!_newVersion) { + // Elvira 1 version. + if (channel != MIDI_RHYTHM_CHANNEL) { + // All melodic notes are lowered by 1 octave, except the lowest notes. + while (note < 0x18) + note += 0xC; + note -= 0xC; } - + // Highest 32 notes are clipped. + if (note > 0x5F) + note = 0x5F; } else { - // percussion channel - // MUSIC.DRV variant didn't do this adjustment, it directly used a pointer - adjustedNote -= 36; - if (adjustedNote > 40) { // Security check - warning("ADLIB: bad percussion channel note"); - return; - } - - byte percussionChannel = percussionKeyNoteChannelTable[adjustedNote]; - if (percussionChannel >= AGOS_ADLIB_VOICES_COUNT) - return; // INSTR.DAT variant checked for ">" instead of ">=", which seems to have been a bug - - // Map the keynote accordingly - adjustedNote = _percussionKeyNoteMapping[adjustedNote]; - // Now overwrite the FM voice channel - FMvoiceChannel = percussionChannel; + // Elvira 2 / Waxworks version. + // Notes 19 and higher are transposed down 19 semitones. + // Note that this is about 1.5 octave, which implies that notes 0-18 are + // not played accurately by this driver. + if (note >= 0x13) + note -= 0x13; } - if (!_musicDrvMode) { - // INSTR.DAT - - // Save this key note - _channels[FMvoiceChannel].currentNote = adjustedNote; - - adjustedVelocity += 24; - if (adjustedVelocity > 120) - adjustedVelocity = 120; - adjustedVelocity = adjustedVelocity >> 1; // divide by 2 + // Determine octave and note within octave, and look up the matching OPL + // frequency. + int8 block = note / 12; + if (!_newVersion) + // Elvira 1 version lowers the octave by 1 (note that melodic notes + // were lowered 1 octave earlier). + block--; + uint8 octaveNote = note % 12; + // Look up the note frequency. + uint16 baseFrequency = _oplNoteFrequencies[octaveNote]; + uint16 frequency; + if (!_newVersion) { + // Elvira 1 version has a negative frequency lookup value for notes + // which are in a higher octave than the others. + if (baseFrequency & 0x8000) + block++; + // Clear the high bits of the negative lookup values. + frequency = baseFrequency & 0x3FF; + if (block < 0) { + // If octave is now negative, halve the frequency and increase + // octave. + frequency >>= 1; + block++; + } } else { - // MUSIC.DRV - adjustedVelocity = adjustedVelocity >> 1; // divide by 2 - } - - // Save velocity in the case volume will need to be changed - _channels[FMvoiceChannel].velocity = adjustedVelocity; - // Set volume of voice channel - noteOnSetVolume(FMvoiceChannel, 1, adjustedVelocity); - if (FMvoiceChannel <= AGOS_ADLIB_VOICES_PERCUSSION_START) { - // Set second operator for FM voices + first percussion - noteOnSetVolume(FMvoiceChannel, 2, adjustedVelocity); - } - - if (FMvoiceChannel >= AGOS_ADLIB_VOICES_PERCUSSION_START) { - // Percussion - byte percussionIdx = FMvoiceChannel - AGOS_ADLIB_VOICES_PERCUSSION_START; - - // Enable bit of the requested percussion type - assert(percussionIdx < AGOS_ADLIB_VOICES_PERCUSSION_COUNT); - _percussionReg |= percussionBits[percussionIdx]; - setRegister(0xBD, _percussionReg); - } - - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_CYMBAL) { - // FM voice, Base Drum, Snare Drum + Tom Tom - byte adlibNote = adjustedNote; - byte adlibOctave = 0; - byte adlibFrequencyIdx = 0; - uint16 adlibFrequency = 0; - - if (!_musicDrvMode) { - // INSTR.DAT - if (adlibNote >= 0x60) - adlibNote = 0x5F; - - adlibOctave = (adlibNote / 12) - 1; - adlibFrequencyIdx = adlibNote % 12; - adlibFrequency = frequencyLookUpTable[adlibFrequencyIdx]; - - if (adlibFrequency & 0x8000) - adlibOctave++; - if (adlibOctave & 0x80) { - adlibOctave++; - adlibFrequency = adlibFrequency >> 1; - } - - } else { - // MUSIC.DRV variant - if (adlibNote >= 19) - adlibNote -= 19; - - adlibOctave = (adlibNote / 12); - adlibFrequencyIdx = adlibNote % 12; - // additional code, that will lookup octave and do a multiplication with it - // noteOn however calls the frequency calculation in a way that it multiplies with 0 - adlibFrequency = frequencyLookUpTableMusicDrv[adlibFrequencyIdx]; - } - - regValueA0h = adlibFrequency & 0xFF; - regValueB0h = ((adlibFrequency & 0x300) >> 8) | (adlibOctave << 2); - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // set Key-On flag for regular FM voices, but not for percussion - regValueB0h |= 0x20; - } - - setRegister(0xA0 + FMvoiceChannel, regValueA0h); - setRegister(0xB0 + FMvoiceChannel, regValueB0h); - _channels[FMvoiceChannel].currentA0hReg = regValueA0h; - _channels[FMvoiceChannel].currentB0hReg = regValueB0h; - - if (_musicDrvMode) { - // MUSIC.DRV - if (FMvoiceChannel < AGOS_ADLIB_VOICES_MELODIC_COUNT) { - _channels[FMvoiceChannel].currentNote = adjustedNote; - } + // Elvira 2 / Waxworks version adds the note fraction for SFX. + uint16 fractionFrequency = 0; + if (_sources[source].type == SOURCE_TYPE_SFX) { + // Because the frequency differences between notes are not constant + // the fraction is multiplied by a factor depending on the note. + fractionFrequency = (((octaveNote + 1) / 6) + 2) * (_sfxNoteFractions[source - 1] >> 4); } + frequency = baseFrequency + fractionFrequency; } + // Note that when processing sound effects, the note can be higher than the + // MIDI maximum value of 0x7F. The original interpreter depends on this for + // correct playback of the sound effect. However, this can cause block to + // overflow the 3 bit range available to it in the OPL registers. + block &= 0x7; + + return block << 10 | frequency; } -// 100% the same for INSTR.DAT and MUSIC.DRV variants -// except for a bug, that was introduced for MUSIC.DRV -void MidiDriver_Accolade_AdLib::noteOnSetVolume(byte FMvoiceChannel, byte operatorNr, byte velocity) { - byte operatorReg = 0; - byte regValue40h = 0; - const InstrumentEntry *curInstrument = nullptr; - - // Adjust velocity with the master volume - uint16 adjustedVelocity = CLIP((velocity * _masterVolume) / 255, 0, 0x3F); - - regValue40h = (63 - adjustedVelocity) & 0x3F; - - if ((operatorNr == 1) && (FMvoiceChannel <= AGOS_ADLIB_VOICES_PERCUSSION_START)) { - // first operator of FM voice channels or first percussion channel - curInstrument = _channels[FMvoiceChannel].currentInstrumentPtr; - if (!(curInstrument->regC0 & 0x01)) { // check, if both operators produce sound - // only one does, instrument wants fixed volume - if (operatorNr == 1) { - regValue40h = curInstrument->reg40op1; - } else { - regValue40h = curInstrument->reg40op2; - } - - // not sure, if we are supposed to implement these bugs, or not -#if 0 - if (!_musicDrvMode) { - // Table is 16 bytes instead of 18 bytes - if ((FMvoiceChannel == 7) || (FMvoiceChannel == 9)) { - regValue40h = 0; - warning("volume set bug (original)"); - } - } - if (_musicDrvMode) { - // MUSIC.DRV variant has a bug, which will overwrite these registers - // for all operators above 11 / 0Bh, which means percussion will always - // get a value of 0 (the table holding those bytes was 12 bytes instead of 18 - if (FMvoiceChannel >= AGOS_ADLIB_VOICES_PERCUSSION_START) { - regValue40h = 0; - warning("volume set bug (original)"); - } - } -#endif +uint8 MidiDriver_Accolade_AdLib::calculateUnscaledVolume(uint8 channel, uint8 source, uint8 velocity, OplInstrumentDefinition &instrumentDef, uint8 operatorNum) { + // A volume adjustment is applied to the velocity of melodic notes. + int8 volumeAdjustment = 0; + if (_sources[source].type != SOURCE_TYPE_SFX) { + if (instrumentDef.rhythmType == RHYTHM_TYPE_UNDEFINED) { + byte program = _controlData[source][channel].program; + volumeAdjustment = _volumeAdjustments[program]; + } else if (!_newVersion) { + // For rhythm notes, the Elvira 1 version of the driver checks the + // current "instrument" of channel 9. In this driver channel 9 + // corresponds to the cymbal rhythm instrument, which is set to + // instrument definition 4. It then reads the volume adjustment + // for instrument 4 and applies this to all rhythm notes. This + // seems quite dubious and might be a bug, but it is reproduced + // here so rhythm volume is the same as the original interpreter. + // The Elvira 2 / Waxworks driver skips volume adjustment + // completely for rhythm notes. + volumeAdjustment = _volumeAdjustments[4]; } } + // Note velocity and the volume adjustment are added, clipped to normal + // velocity range and divided by 2 to get an OPL volume value. + uint8 volume = CLIP(velocity + volumeAdjustment, 0, 0x7F); - if (operatorNr == 1) { - operatorReg = operator1Register[FMvoiceChannel]; - } else { - operatorReg = operator2Register[FMvoiceChannel]; + if (!_newVersion) { + // The Elvira 1 version raises the volume a bit and clips the highest + // values. + volume += 0x18; + if (volume > 0x78) + volume = 0x78; } - assert(operatorReg != 0xFF); // Security check - setRegister(0x40 + operatorReg, regValue40h); + + // Invert the volume. + return 0x3F - (volume >> 1); } -void MidiDriver_Accolade_AdLib::noteOff(byte FMvoiceChannel, byte note, bool dontCheckNote) { - byte adjustedNote = note; - byte regValueB0h = 0; - - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // regular FM voice - - if (!_musicDrvMode) { - // INSTR.DAT: adjust key note - while (adjustedNote < 24) - adjustedNote += 12; - adjustedNote -= 12; - } - - if (!dontCheckNote) { - // check, if current note is also the current actually playing channel note - if (_channels[FMvoiceChannel].currentNote != adjustedNote) - return; // not the same -> ignore this note off command - } - - regValueB0h = _channels[FMvoiceChannel].currentB0hReg & 0xDF; // Remove "key on" bit - setRegister(0xB0 + FMvoiceChannel, regValueB0h); - - } else { - // percussion - adjustedNote -= 36; - if (adjustedNote > 40) { // Security check - warning("ADLIB: bad percussion channel note"); - return; - } - - byte percussionChannel = percussionKeyNoteChannelTable[adjustedNote]; - if (percussionChannel > AGOS_ADLIB_VOICES_COUNT) - return; - - byte percussionIdx = percussionChannel - AGOS_ADLIB_VOICES_PERCUSSION_START; - - // Disable bit of the requested percussion type - assert(percussionIdx < AGOS_ADLIB_VOICES_PERCUSSION_COUNT); - _percussionReg &= ~percussionBits[percussionIdx]; - setRegister(0xBD, _percussionReg); - } +void MidiDriver_Accolade_AdLib::writePanning(uint8 oplChannel, OplInstrumentRhythmType rhythmType) { + // The Elvira 1 driver does not write the Cx register for rhythm + // instruments except the bass drum; the Elvira 2 / Waxworks driver does + // not write it for the bass drum either. + if (rhythmType == RHYTHM_TYPE_UNDEFINED || (rhythmType == RHYTHM_TYPE_BASS_DRUM && !_newVersion)) + MidiDriver_ADLIB_Multisource::writePanning(oplChannel, rhythmType); } -void MidiDriver_Accolade_AdLib::programChange(byte FMvoiceChannel, byte mappedInstrumentNr, byte MIDIinstrumentNr) { - if (mappedInstrumentNr >= _instrumentCount) { - warning("ADLIB: tried to set non-existent instrument"); - return; // out of range - } - - // setup instrument - //warning("ADLIB: program change for FM voice channel %d, instrument id %d", FMvoiceChannel, mappedInstrumentNr); - - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // Regular FM voice - programChangeSetInstrument(FMvoiceChannel, mappedInstrumentNr, MIDIinstrumentNr); - - } else { - // Percussion - // set default instrument (again) - byte percussionInstrumentNr = 0; - const InstrumentEntry *instrumentPtr; - - if (!_musicDrvMode) { - // INSTR.DAT: percussion default instruments start at instrument 1 - percussionInstrumentNr = FMvoiceChannel - AGOS_ADLIB_VOICES_PERCUSSION_START + 1; - } else { - // MUSIC.DRV: percussion default instruments start at instrument 0x80 - percussionInstrumentNr = FMvoiceChannel - AGOS_ADLIB_VOICES_PERCUSSION_START + 0x80; - } - if (percussionInstrumentNr >= _instrumentCount) { - warning("ADLIB: tried to set non-existent instrument"); - return; - } - instrumentPtr = &_instrumentTable[percussionInstrumentNr]; - _channels[FMvoiceChannel].currentInstrumentPtr = instrumentPtr; - _channels[FMvoiceChannel].volumeAdjust = _instrumentVolumeAdjust[percussionInstrumentNr]; - } +void MidiDriver_Accolade_AdLib::writeFrequency(uint8 oplChannel, OplInstrumentRhythmType rhythmType) { + // The original driver does not write the frequency for the cymbal and + // hi-hat instruments. + if (rhythmType != RHYTHM_TYPE_HI_HAT && rhythmType != RHYTHM_TYPE_CYMBAL) + MidiDriver_ADLIB_Multisource::writeFrequency(oplChannel, rhythmType); } -void MidiDriver_Accolade_AdLib::programChangeSetInstrument(byte FMvoiceChannel, byte mappedInstrumentNr, byte MIDIinstrumentNr) { - const InstrumentEntry *instrumentPtr; - byte op1Reg = 0; - byte op2Reg = 0; +void MidiDriver_Accolade_AdLib::loadInstrumentData(OplInstrumentDefinition &definition, byte *instrumentData, + OplInstrumentRhythmType rhythmType, byte rhythmNote, bool newVersion) { + definition.fourOperator = false; - if (mappedInstrumentNr >= _instrumentCount) { - warning("ADLIB: tried to set non-existent instrument"); - return; // out of range + definition.connectionFeedback0 = instrumentData[8]; + definition.operator0.freqMultMisc = instrumentData[0]; + // The original driver does not add the KSL bits to the calculated + // volume when writing the level registers. To replicate this, the KSL + // bits are set to 0 for operators affected by volume. + // Note that the Elvira 2 / Waxworks driver has a bug which will cause + // the operator 0 level register of the bass drum instrument to be + // overwritten by the connection bit (usually 0) of another instrument + // if the bass drum connection is FM (and it is). This is fixed here by + // setting the correct value. The Elvira 1 version does not have this + // bug. + definition.operator0.level = (definition.connectionFeedback0 & 1) ? 0 : instrumentData[1]; + definition.operator0.decayAttack = instrumentData[2]; + definition.operator0.releaseSustain = instrumentData[3]; + // The original driver only writes 0 to the waveform select registers + // during initialization, so only sine waveform is used. + definition.operator0.waveformSelect = 0; + definition.operator1.freqMultMisc = instrumentData[4]; + definition.operator1.level = 0; + definition.operator1.decayAttack = instrumentData[6]; + definition.operator1.releaseSustain = instrumentData[7]; + definition.operator1.waveformSelect = 0; + if (newVersion) { + // The Elvira 2 / Waxworks driver always sets the last two bits of + // the sustain value. + // This was done during "programChange" in the original driver + definition.operator0.releaseSustain |= 3; + definition.operator1.releaseSustain |= 3; } - // setup instrument - instrumentPtr = &_instrumentTable[mappedInstrumentNr]; - //warning("set instrument for FM voice channel %d, instrument id %d", FMvoiceChannel, mappedInstrumentNr); - - op1Reg = operator1Register[FMvoiceChannel]; - op2Reg = operator2Register[FMvoiceChannel]; - - setRegister(0x20 + op1Reg, instrumentPtr->reg20op1); - setRegister(0x40 + op1Reg, instrumentPtr->reg40op1); - setRegister(0x60 + op1Reg, instrumentPtr->reg60op1); - setRegister(0x80 + op1Reg, instrumentPtr->reg80op1); - - if (FMvoiceChannel <= AGOS_ADLIB_VOICES_PERCUSSION_START) { - // set 2nd operator as well for FM voices and first percussion voice - setRegister(0x20 + op2Reg, instrumentPtr->reg20op2); - setRegister(0x40 + op2Reg, instrumentPtr->reg40op2); - setRegister(0x60 + op2Reg, instrumentPtr->reg60op2); - setRegister(0x80 + op2Reg, instrumentPtr->reg80op2); - - if (!_musicDrvMode) { - // set Feedback / Algorithm as well - setRegister(0xC0 + FMvoiceChannel, instrumentPtr->regC0); - } else { - if (FMvoiceChannel < AGOS_ADLIB_VOICES_PERCUSSION_START) { - // set Feedback / Algorithm as well for regular FM voices only - setRegister(0xC0 + FMvoiceChannel, instrumentPtr->regC0); - } - } - } - - // Remember instrument - _channels[FMvoiceChannel].currentInstrumentPtr = instrumentPtr; - _channels[FMvoiceChannel].volumeAdjust = _instrumentVolumeAdjust[MIDIinstrumentNr]; + definition.rhythmType = rhythmType; + definition.rhythmNote = rhythmNote; } -void MidiDriver_Accolade_AdLib::setRegister(int reg, int value) { - _opl->writeReg(reg, value); - //warning("OPL %x %x (%d)", reg, value, value); -} +void MidiDriver_Accolade_AdLib::readDriverData(byte *driverData, uint16 driverDataSize, bool newVersion) { + uint16 minDataSize = newVersion ? 468 : 354; + if (driverDataSize < minDataSize) + error("ACCOLADE-ADLIB: Expected minimum driver data size of %d - got %d", minDataSize, driverDataSize); -uint32 MidiDriver_Accolade_AdLib::property(int prop, uint32 param) { - return 0; -} + // INSTR.DAT Data is like this: + // 128 bytes instrument mapping + // 128 bytes instrument volume adjust (signed!) + // 16 bytes unknown + // 16 bytes channel mapping + // 64 bytes key note mapping (not used for MT32) + // 1 byte instrument count + // 1 byte bytes per instrument + // x bytes no instruments used for MT32 -// Called right at the start, we get an INSTR.DAT entry -bool MidiDriver_Accolade_AdLib::setupInstruments(byte *driverData, uint16 driverDataSize, bool useMusicDrvFile) { - uint16 channelMappingOffset = 0; - uint16 channelMappingSize = 0; - uint16 instrumentMappingOffset = 0; - uint16 instrumentMappingSize = 0; - uint16 instrumentVolumeAdjustOffset = 0; - uint16 instrumentVolumeAdjustSize = 0; - uint16 keyNoteMappingOffset = 0; - uint16 keyNoteMappingSize = 0; - uint16 instrumentCount = 0; - uint16 instrumentDataOffset = 0; - uint16 instrumentDataSize = 0; - uint16 instrumentEntrySize = 0; + // music.drv is basically a driver, but with a few fixed locations for certain data - if (!useMusicDrvFile) { - // INSTR.DAT: we expect at least 354 bytes - if (driverDataSize < 354) - return false; + uint16 channelMappingOffset = newVersion ? 396 : 256 + 16; + Common::copy(driverData + channelMappingOffset, driverData + channelMappingOffset + ARRAYSIZE(_channelRemapping), _channelRemapping); - // Data is like this: - // 128 bytes instrument mapping - // 128 bytes instrument volume adjust (signed!) - // 16 bytes unknown - // 16 bytes channel mapping - // 64 bytes key note mapping (not used for MT32) - // 1 byte instrument count - // 1 byte bytes per instrument - // x bytes no instruments used for MT32 + uint16 instrumentMappingOffset = newVersion ? 140 : 0; + Common::copy(driverData + instrumentMappingOffset, driverData + instrumentMappingOffset + ARRAYSIZE(_instrumentRemapping), _instrumentRemapping); + setInstrumentRemapping(_instrumentRemapping); - channelMappingOffset = 256 + 16; - channelMappingSize = 16; - instrumentMappingOffset = 0; - instrumentMappingSize = 128; - instrumentVolumeAdjustOffset = 128; - instrumentVolumeAdjustSize = 128; - keyNoteMappingOffset = 256 + 16 + 16; - keyNoteMappingSize = 64; + uint16 volumeAdjustmentsOffset = newVersion ? 140 + 128 : 128; + int8 *volumeAdjustmentsData = (int8 *)driverData + volumeAdjustmentsOffset; + Common::copy(volumeAdjustmentsData, volumeAdjustmentsData + ARRAYSIZE(_volumeAdjustments), _volumeAdjustments); - byte instrDatInstrumentCount = driverData[256 + 16 + 16 + 64]; + if (!newVersion) { byte instrDatBytesPerInstrument = driverData[256 + 16 + 16 + 64 + 1]; // We expect 9 bytes per instrument if (instrDatBytesPerInstrument != 9) - return false; - // And we also expect at least one adlib instrument - if (!instrDatInstrumentCount) - return false; - - instrumentCount = instrDatInstrumentCount; - instrumentDataOffset = 256 + 16 + 16 + 64 + 2; - instrumentDataSize = instrDatBytesPerInstrument * instrDatInstrumentCount; - instrumentEntrySize = instrDatBytesPerInstrument; - - } else { - // MUSIC.DRV: we expect at least 468 bytes - if (driverDataSize < 468) - return false; - - // music.drv is basically a driver, but with a few fixed locations for certain data - - channelMappingOffset = 396; - channelMappingSize = 16; - instrumentMappingOffset = 140; - instrumentMappingSize = 128; - instrumentVolumeAdjustOffset = 140 + 128; - instrumentVolumeAdjustSize = 128; - keyNoteMappingOffset = 376 + 36; // adjust by 36, because we adjust keyNote before mapping (see noteOn) - keyNoteMappingSize = 64; - - // seems to have used 128 + 5 instruments - // 128 regular ones and an additional 5 for percussion - instrumentCount = 128 + AGOS_ADLIB_EXTRA_INSTRUMENT_COUNT; - instrumentDataOffset = 722; - instrumentEntrySize = 9; - instrumentDataSize = instrumentCount * instrumentEntrySize; + error("ACCOLADE-ADLIB: Expected instrument definitions of length 9 - got length %d", instrDatBytesPerInstrument); } - // Channel mapping - if (channelMappingSize) { - // Get these 16 bytes for MIDI channel mapping - if (channelMappingSize != sizeof(_channelMapping)) - return false; + byte instrumentDefinitionCount = newVersion ? 128 : driverData[256 + 16 + 16 + 64]; + uint16 rhythmNoteOffset = newVersion ? 376 + 36 : 256 + 16 + 16; + uint16 instrumentDataOffset = newVersion ? 722 : 256 + 16 + 16 + 64 + 2; - memcpy(_channelMapping, driverData + channelMappingOffset, sizeof(_channelMapping)); - } else { - // Set up straight mapping - for (uint16 channelNr = 0; channelNr < sizeof(_channelMapping); channelNr++) { - _channelMapping[channelNr] = channelNr; - } + _instrumentBank = new OplInstrumentDefinition[instrumentDefinitionCount]; + for (int i = 0; i < instrumentDefinitionCount; i++) { + byte *instrumentData = driverData + instrumentDataOffset + (i * 9); + loadInstrumentData(_instrumentBank[i], instrumentData, RHYTHM_TYPE_UNDEFINED, 0, newVersion); } - if (instrumentMappingSize) { - // And these for instrument mapping - if (instrumentMappingSize > sizeof(_instrumentMapping)) - return false; + _rhythmBank = new OplInstrumentDefinition[40]; + _rhythmBankFirstNote = 36; + _rhythmBankLastNote = 75; + // Elvira 1 version uses instruments 1-5 for rhythm, Elvira 2 / Waxworks + // version uses 0x80-0x84. + byte *rhythmInstrumentDefinitions = driverData + instrumentDataOffset + ((newVersion ? 0x80 : 1) * 9); + byte *rhythmNotes = driverData + rhythmNoteOffset; + for (int i = 0; i < 40; i++) { + byte instrumentDefNumber = RHYTHM_NOTE_INSTRUMENT_TYPES[i] > 0xA ? 0 : RHYTHM_NOTE_INSTRUMENT_TYPES[i] - 6; + OplInstrumentRhythmType rhythmType = RHYTHM_NOTE_INSTRUMENT_TYPES[i] > 0xA ? RHYTHM_TYPE_UNDEFINED : + static_cast(11 - RHYTHM_NOTE_INSTRUMENT_TYPES[i]); + byte *instrumentData = rhythmInstrumentDefinitions + (instrumentDefNumber * 9); - memcpy(_instrumentMapping, driverData + instrumentMappingOffset, instrumentMappingSize); + loadInstrumentData(_rhythmBank[i], instrumentData, rhythmType, rhythmNotes[i], newVersion); } - // Set up straight mapping for the remaining data - for (uint16 instrumentNr = instrumentMappingSize; instrumentNr < sizeof(_instrumentMapping); instrumentNr++) { - _instrumentMapping[instrumentNr] = instrumentNr; - } - - if (instrumentVolumeAdjustSize) { - if (instrumentVolumeAdjustSize != sizeof(_instrumentVolumeAdjust)) - return false; - - memcpy(_instrumentVolumeAdjust, driverData + instrumentVolumeAdjustOffset, instrumentVolumeAdjustSize); - } - - // Get key note mapping, if available - if (keyNoteMappingSize) { - if (keyNoteMappingSize != sizeof(_percussionKeyNoteMapping)) - return false; - - memcpy(_percussionKeyNoteMapping, driverData + keyNoteMappingOffset, keyNoteMappingSize); - } - - // Check, if there are enough bytes left to hold all instrument data - if (driverDataSize < (instrumentDataOffset + instrumentDataSize)) - return false; - - // We release previous instrument data, just in case - if (_instrumentTable) - delete[] _instrumentTable; - - _instrumentTable = new InstrumentEntry[instrumentCount]; - _instrumentCount = instrumentCount; - - byte *instrDATReadPtr = driverData + instrumentDataOffset; - InstrumentEntry *instrumentWritePtr = _instrumentTable; - - for (uint16 instrumentNr = 0; instrumentNr < _instrumentCount; instrumentNr++) { - memcpy(instrumentWritePtr, instrDATReadPtr, sizeof(InstrumentEntry)); - instrDATReadPtr += instrumentEntrySize; - instrumentWritePtr++; - } - - // Enable MUSIC.DRV-Mode (slightly different behaviour) - if (useMusicDrvFile) - _musicDrvMode = true; - - if (_musicDrvMode) { - // Extra code for MUSIC.DRV - - // This was done during "programChange" in the original driver - instrumentWritePtr = _instrumentTable; - for (uint16 instrumentNr = 0; instrumentNr < _instrumentCount; instrumentNr++) { - instrumentWritePtr->reg80op1 |= 0x03; // set release rate - instrumentWritePtr->reg80op2 |= 0x03; - instrumentWritePtr++; - } - } - return true; } -MidiDriver *MidiDriver_Accolade_AdLib_create(Common::String driverFilename) { - byte *driverData = nullptr; +MidiDriver_Multisource *MidiDriver_Accolade_AdLib_create(Common::String driverFilename, OPL::Config::OplType oplType) { + byte *driverData = nullptr; uint16 driverDataSize = 0; - bool isMusicDrvFile = false; + bool newVersion = false; - MidiDriver_Accolade_readDriver(driverFilename, MT_ADLIB, driverData, driverDataSize, isMusicDrvFile); + MidiDriver_Accolade_readDriver(driverFilename, MT_ADLIB, driverData, driverDataSize, newVersion); if (!driverData) error("ACCOLADE-ADLIB: error during readDriver()"); - MidiDriver_Accolade_AdLib *driver = new MidiDriver_Accolade_AdLib(); - if (driver) { - if (!driver->setupInstruments(driverData, driverDataSize, isMusicDrvFile)) { - delete driver; - driver = nullptr; - } - } + MidiDriver_Accolade_AdLib *driver = new MidiDriver_Accolade_AdLib(oplType, newVersion); + if (!driver) + error("ACCOLADE-ADLIB: could not create driver"); + + driver->readDriverData(driverData, driverDataSize, newVersion); delete[] driverData; return driver; diff --git a/engines/agos/drivers/accolade/adlib.h b/engines/agos/drivers/accolade/adlib.h index fa2741ec75c..595b2d751e3 100644 --- a/engines/agos/drivers/accolade/adlib.h +++ b/engines/agos/drivers/accolade/adlib.h @@ -19,102 +19,78 @@ * */ -#include "agos/drivers/accolade/mididriver.h" +#ifndef AGOS_DRIVERS_ACCOLADE_ADLIB_H +#define AGOS_DRIVERS_ACCOLADE_ADLIB_H -#include "audio/fmopl.h" -#include "audio/mididrv.h" +#include "audio/adlib_ms.h" namespace AGOS { -#define AGOS_ADLIB_VOICES_COUNT 11 +class MidiDriver_Accolade_AdLib : public MidiDriver_ADLIB_Multisource { +protected: + static const byte RHYTHM_NOTE_INSTRUMENT_TYPES[40]; + static const uint16 OPL_NOTE_FREQUENCIES_INSTR_DAT[12]; + static const uint16 OPL_NOTE_FREQUENCIES_MUSIC_DRV[12]; -struct InstrumentEntry { - byte reg20op1; // Amplitude Modulation / Vibrato / Envelope Generator Type / Keyboard Scaling Rate / Modulator Frequency Multiple - byte reg40op1; // Level Key Scaling / Total Level - byte reg60op1; // Attack Rate / Decay Rate - byte reg80op1; // Sustain Level / Release Rate - byte reg20op2; // Amplitude Modulation / Vibrato / Envelope Generator Type / Keyboard Scaling Rate / Modulator Frequency Multiple - byte reg40op2; // Level Key Scaling / Total Level - byte reg60op2; // Attack Rate / Decay Rate - byte reg80op2; // Sustain Level / Release Rate - byte regC0; // Feedback / Algorithm, bit 0 - set -> both operators in use -}; - -class MidiDriver_Accolade_AdLib : public MidiDriver { public: - MidiDriver_Accolade_AdLib(); + MidiDriver_Accolade_AdLib(OPL::Config::OplType oplType, bool newVersion = false); ~MidiDriver_Accolade_AdLib() override; - // MidiDriver int open() override; - void close() override; - void send(uint32 b) override; - MidiChannel *allocateChannel() override { return NULL; } - MidiChannel *getPercussionChannel() override { return NULL; } + using MidiDriver_ADLIB_Multisource::send; + void send(int8 source, uint32 b) override; + void deinitSource(uint8 source) override; - bool isOpen() const override { return _isOpen; } - uint32 getBaseTempo() override { return 1000000 / OPL::OPL::kDefaultCallbackFrequency; } + // Read the specified data from INSTR.DAT or MUSIC.DRV. + void readDriverData(byte *driverData, uint16 driverDataSize, bool isMusicDrv); - void setVolume(byte volume); - uint32 property(int prop, uint32 param) override; + // Returns the number of simultaneous SFX sources supported by the current + // driver configuration. + byte getNumberOfSfxSources(); + // Loads the specified instrument for the specified instrument source. + void loadSfxInstrument(uint8 source, byte *instrumentData); + // Sets the note (upper byte) and note fraction (lower byte; 1/256th notes) + // for the specified SFX source. + void setSfxNoteFraction(uint8 source, uint16 noteFraction); + // Writes out the current frequency for the specified SFX source. + void updateSfxNote(uint8 source); - bool setupInstruments(byte *instrumentData, uint16 instrumentDataSize, bool useMusicDrvFile); +protected: + InstrumentInfo determineInstrument(uint8 channel, uint8 source, uint8 note) override; - void setTimerCallback(void *timerParam, Common::TimerManager::TimerProc timerProc) override; + uint8 allocateOplChannel(uint8 channel, uint8 source, uint8 instrumentId) override; + uint16 calculateFrequency(uint8 channel, uint8 source, uint8 note) override; + uint8 calculateUnscaledVolume(uint8 channel, uint8 source, uint8 velocity, OplInstrumentDefinition &instrumentDef, uint8 operatorNum) override; -private: - bool _musicDrvMode; + void writePanning(uint8 oplChannel, OplInstrumentRhythmType rhythmType = RHYTHM_TYPE_UNDEFINED) override; + void writeFrequency(uint8 oplChannel, OplInstrumentRhythmType rhythmType = RHYTHM_TYPE_UNDEFINED) override; + + // Copies the specified instrument data (in INSTR.DAT/MUSIC.DRV format) + // into the specified instrument definition. + void loadInstrumentData(OplInstrumentDefinition &definition, byte *instrumentData, + OplInstrumentRhythmType rhythmType, byte rhythmNote, bool newVersion); + + // False if the driver should have the behavior of the Elvira 1 driver; + // true if it should have the behavior of the Elvira 2 / Waxworks version. + bool _newVersion; - // from INSTR.DAT/MUSIC.DRV - simple mapping between MIDI channel and MT32 channel - byte _channelMapping[AGOS_MIDI_CHANNEL_COUNT]; - // from INSTR.DAT/MUSIC.DRV - simple mapping between MIDI instruments and MT32 instruments - byte _instrumentMapping[AGOS_MIDI_INSTRUMENT_COUNT]; // from INSTR.DAT/MUSIC.DRV - volume adjustment per instrument - signed char _instrumentVolumeAdjust[AGOS_MIDI_INSTRUMENT_COUNT]; - // simple mapping between MIDI key notes and MT32 key notes - byte _percussionKeyNoteMapping[AGOS_MIDI_KEYNOTE_COUNT]; + int8 _volumeAdjustments[128]; + // from INSTR.DAT/MUSIC.DRV - simple mapping between MIDI channel and AdLib channel + byte _channelRemapping[16]; + // from INSTR.DAT/MUSIC.DRV - simple mapping between MIDI instruments and AdLib instruments + byte _instrumentRemapping[128]; + // Points to one of the OPL_NOTE_FREQUENCIES arrays, depending on the driver version + const uint16 *_oplNoteFrequencies; - // from INSTR.DAT/MUSIC.DRV - adlib instrument data - InstrumentEntry *_instrumentTable; - byte _instrumentCount; + // Data used by AdLib SFX (Elvira 2 / Waxworks) - struct ChannelEntry { - const InstrumentEntry *currentInstrumentPtr; - byte currentNote; - byte currentA0hReg; - byte currentB0hReg; - int16 volumeAdjust; - byte velocity; - - ChannelEntry() : currentInstrumentPtr(NULL), currentNote(0), - currentA0hReg(0), currentB0hReg(0), volumeAdjust(0), velocity(0) { } - }; - - byte _percussionReg; - - OPL::OPL *_opl; - int _masterVolume; - - Common::TimerManager::TimerProc _adlibTimerProc; - void *_adlibTimerParam; - - bool _isOpen; - - // stores information about all FM voice channels - ChannelEntry _channels[AGOS_ADLIB_VOICES_COUNT]; - - void onTimer(); - - void resetAdLib(); - void resetAdLibOperatorRegisters(byte baseRegister, byte value); - void resetAdLibFMVoiceChannelRegisters(byte baseRegister, byte value); - - void programChange(byte FMvoiceChannel, byte mappedInstrumentNr, byte MIDIinstrumentNr); - void programChangeSetInstrument(byte FMvoiceChannel, byte mappedInstrumentNr, byte MIDIinstrumentNr); - void setRegister(int reg, int value); - void noteOn(byte FMvoiceChannel, byte note, byte velocity); - void noteOnSetVolume(byte FMvoiceChannel, byte operatorReg, byte velocity); - void noteOff(byte FMvoiceChannel, byte note, bool dontCheckNote); + // Instrument definition for each SFX source + OplInstrumentDefinition _sfxInstruments[4]; + // Current MIDI note fraction (1/256th notes) for each SFX source + byte _sfxNoteFractions[4]; }; } // End of namespace AGOS + +#endif diff --git a/engines/agos/drivers/accolade/mididriver.h b/engines/agos/drivers/accolade/mididriver.h index b78fb029c39..3b6df3f0702 100644 --- a/engines/agos/drivers/accolade/mididriver.h +++ b/engines/agos/drivers/accolade/mididriver.h @@ -23,7 +23,11 @@ #define AGOS_DRIVERS_ACCOLADE_MIDIDRIVER_H #include "agos/agos.h" + +#include "audio/fmopl.h" #include "audio/mididrv.h" +#include "audio/mididrv_ms.h" + #include "common/error.h" namespace AGOS { @@ -35,8 +39,8 @@ namespace AGOS { extern void MidiDriver_Accolade_readDriver(Common::String filename, MusicType requestedDriverType, byte *&driverData, uint16 &driverDataSize, bool &isMusicDrvFile); -extern MidiDriver *MidiDriver_Accolade_AdLib_create(Common::String driverFilename); -extern MidiDriver *MidiDriver_Accolade_MT32_create(Common::String driverFilename); +extern MidiDriver_Multisource *MidiDriver_Accolade_AdLib_create(Common::String driverFilename, OPL::Config::OplType oplType); +extern MidiDriver_Multisource *MidiDriver_Accolade_MT32_create(Common::String driverFilename); extern MidiDriver *MidiDriverPC98_create(MidiDriver::DeviceHandle dev); } // End of namespace AGOS diff --git a/engines/agos/drivers/accolade/mt32.cpp b/engines/agos/drivers/accolade/mt32.cpp index b95d1f83321..c84448f3214 100644 --- a/engines/agos/drivers/accolade/mt32.cpp +++ b/engines/agos/drivers/accolade/mt32.cpp @@ -19,199 +19,154 @@ * */ -#include "agos/drivers/accolade/mididriver.h" #include "agos/drivers/accolade/mt32.h" -#include "audio/mididrv.h" - -#include "common/config-manager.h" +#include "agos/drivers/accolade/mididriver.h" +#include "agos/sfxparser_accolade.h" namespace AGOS { -MidiDriver_Accolade_MT32::MidiDriver_Accolade_MT32() { - _driver = nullptr; - _isOpen = false; - _nativeMT32 = false; - _baseFreq = 250; +const uint8 MidiDriver_Accolade_MT32::SYSEX_INSTRUMENT_ASSIGNMENT[7] = { 0x02, 0x00, 0x18, 0x32, 0x01, 0x00, 0x01 }; - memset(_channelMapping, 0, sizeof(_channelMapping)); - memset(_instrumentMapping, 0, sizeof(_instrumentMapping)); +MidiDriver_Accolade_MT32::MidiDriver_Accolade_MT32() : MidiDriver_MT32GM(MT_MT32) { + Common::fill(_channelRemapping, _channelRemapping + ARRAYSIZE(_channelRemapping), 0); + Common::fill(_instrumentRemapping, _instrumentRemapping + ARRAYSIZE(_instrumentRemapping), 0); + Common::fill(_channelLocks, _channelLocks + ARRAYSIZE(_channelLocks), false); } -MidiDriver_Accolade_MT32::~MidiDriver_Accolade_MT32() { - Common::StackLock lock(_mutex); - if (_driver) { - _driver->setTimerCallback(nullptr, nullptr); - _driver->close(); - delete _driver; - } - _driver = nullptr; +int MidiDriver_Accolade_MT32::open(MidiDriver *driver, bool nativeMT32) { + int result = MidiDriver_MT32GM::open(driver, nativeMT32); + + setInstrumentRemapping(_instrumentRemapping); + + return result; } -int MidiDriver_Accolade_MT32::open() { - assert(!_driver); +void MidiDriver_Accolade_MT32::send(int8 source, uint32 b) { + byte dataChannel = b & 0xf; + int8 outputChannel = mapSourceChannel(source, dataChannel); -// debugC(kDebugLevelMT32Driver, "MT32: starting driver"); + MidiChannelControlData &controlData = *_controlData[outputChannel]; - // Setup midi driver - MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_MIDI | MDT_PREFER_MT32); - MusicType musicType = MidiDriver::getMusicType(dev); + // Check if this event is sent by a music source and the channel is locked + // by an SFX source. + bool channelLockedByOtherSource = _sources[source].type != SOURCE_TYPE_SFX && _channelLocks[outputChannel]; - // check, if we got a real MT32 (or MUNT, or MUNT over MIDI) - switch (musicType) { - case MT_MT32: - _nativeMT32 = true; - break; - case MT_GM: - if (ConfMan.getBool("native_mt32")) { - _nativeMT32 = true; + processEvent(source, b, outputChannel, controlData, channelLockedByOtherSource); +} + +int8 MidiDriver_Accolade_MT32::mapSourceChannel(uint8 source, uint8 dataChannel) { + if (!_isOpen) + // Use 1 on 1 mapping during device initialization. + return dataChannel; + + if (_sources[source].type == SOURCE_TYPE_SFX) { + // Use channels 7 and 8 for SFX (sources 1 and 2). + uint8 sfxChannel = 9 - source; + + _allocationMutex.lock(); + + if (!_channelLocks[sfxChannel]) { + // Lock channel + stopAllNotes(0xFF, sfxChannel); + _channelLocks[sfxChannel] = true; } - break; - default: - break; - } - _driver = MidiDriver::createMidi(dev); - if (!_driver) - return 255; + _allocationMutex.unlock(); - int ret = _driver->open(); - if (ret) - return ret; - - if (_nativeMT32) - _driver->sendMT32Reset(); - else - _driver->sendGMReset(); - - return 0; -} - -void MidiDriver_Accolade_MT32::close() { - if (_driver) { - _driver->close(); + return sfxChannel; + } else { + return _channelRemapping[dataChannel]; } } -// MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php -void MidiDriver_Accolade_MT32::send(uint32 b) { - byte command = b & 0xf0; - byte channel = b & 0xf; +void MidiDriver_Accolade_MT32::deinitSource(uint8 source) { + _allocationMutex.lock(); - if (command == 0xF0) { - if (_driver) { - _driver->send(b); + if (_sources[source].type == SOURCE_TYPE_SFX) { + for (int i = 0; i < MIDI_CHANNEL_COUNT; i++) { + if (_controlData[i]->source == source) { + // Restore the music instrument. + programChange(i, _controlData[i]->program, 0, *_controlData[i], false); + // Unlock the channel. + _channelLocks[i] = false; + } } + } + + _allocationMutex.unlock(); + + MidiDriver_MT32GM::deinitSource(source); +} + +void MidiDriver_Accolade_MT32::loadSfxInstrument(uint8 source, byte *instrumentData) { + if (!(source == 1 || source == 2)) { + warning("MidiDriver_Accolade_MT32::loadSfxInstrument - unexpected source %d", source); return; } - byte mappedChannel = _channelMapping[channel]; + // Send the instrument data to the timbre memory (patch 1 or 2). + uint32 address = (0x08 << 14) | (((source - 1) * 2) << 7); + sysExMT32(instrumentData + 3, SfxParser_Accolade::INSTRUMENT_SIZE_MT32 - 3, address, true, true, source); - if (mappedChannel < AGOS_MIDI_CHANNEL_COUNT) { - // channel mapped to an actual MIDI channel, so use that one - b = (b & 0xFFFFFFF0) | mappedChannel; - if (command == 0xC0) { - // Program change - // Figure out the requested instrument - byte midiInstrument = (b >> 8) & 0xFF; - byte mappedInstrument = _instrumentMapping[midiInstrument]; - - // If there is no actual MT32 (or MUNT), we make a second mapping to General MIDI instruments - if (!_nativeMT32) { - mappedInstrument = (MidiDriver::_mt32ToGm[mappedInstrument]); - } - // And replace it - b = (b & 0xFFFF00FF) | (mappedInstrument << 8); - } - - if (_driver) { - _driver->send(b); - } - } + // Allocate the new patch to instrument number 0x75 or 0x76. + byte instrNum = SFX_PROGRAM_BASE + source - 1; + address = (0x05 << 14) | instrNum << 3; + byte instrAssignData[7]; + Common::copy(SYSEX_INSTRUMENT_ASSIGNMENT, SYSEX_INSTRUMENT_ASSIGNMENT + ARRAYSIZE(instrAssignData), instrAssignData); + instrAssignData[1] = source - 1; + sysExMT32(instrAssignData, 7, address, true, true, source); } -// Called right at the start, we get an INSTR.DAT entry -bool MidiDriver_Accolade_MT32::setupInstruments(byte *driverData, uint16 driverDataSize, bool useMusicDrvFile) { - uint16 channelMappingOffset = 0; - uint16 channelMappingSize = 0; - uint16 instrumentMappingOffset = 0; - uint16 instrumentMappingSize = 0; - - if (!useMusicDrvFile) { - // INSTR.DAT: we expect at least 354 bytes - if (driverDataSize < 354) - return false; - - // Data is like this: - // 128 bytes instrument mapping - // 128 bytes instrument volume adjust (signed!) (not used for MT32) - // 16 bytes unknown - // 16 bytes channel mapping - // 64 bytes key note mapping (not really used for MT32) - // 1 byte instrument count - // 1 byte bytes per instrument - // x bytes no instruments used for MT32 - - channelMappingOffset = 256 + 16; - channelMappingSize = 16; - instrumentMappingOffset = 0; - instrumentMappingSize = 128; - - } else { - // MUSIC.DRV: we expect at least 468 bytes - if (driverDataSize < 468) - return false; - - channelMappingOffset = 396; - channelMappingSize = 16; - instrumentMappingOffset = 140; - instrumentMappingSize = 128; - } - - // Channel mapping - if (channelMappingSize) { - // Get these 16 bytes for MIDI channel mapping - if (channelMappingSize != sizeof(_channelMapping)) - return false; - - memcpy(_channelMapping, driverData + channelMappingOffset, sizeof(_channelMapping)); - } else { - // Set up straight mapping - for (uint16 channelNr = 0; channelNr < sizeof(_channelMapping); channelNr++) { - _channelMapping[channelNr] = channelNr; - } - } - - if (instrumentMappingSize) { - // And these for instrument mapping - if (instrumentMappingSize > sizeof(_instrumentMapping)) - return false; - - memcpy(_instrumentMapping, driverData + instrumentMappingOffset, instrumentMappingSize); - } - // Set up straight mapping for the remaining data - for (uint16 instrumentNr = instrumentMappingSize; instrumentNr < sizeof(_instrumentMapping); instrumentNr++) { - _instrumentMapping[instrumentNr] = instrumentNr; - } - return true; +void MidiDriver_Accolade_MT32::changeSfxInstrument(uint8 source) { + // Change to the newly loaded instrument. + byte channel = mapSourceChannel(source, 0); + MidiChannelControlData &controlData = *_controlData[channel]; + byte originalInstrument = controlData.program; + programChange(channel, SFX_PROGRAM_BASE + source - 1, source, controlData); + // Store the original instrument so it can be used when deinitializing + // the source. + controlData.program = originalInstrument; } -MidiDriver *MidiDriver_Accolade_MT32_create(Common::String driverFilename) { - byte *driverData = nullptr; +void MidiDriver_Accolade_MT32::readDriverData(byte *driverData, uint16 driverDataSize, bool newVersion) { + uint16 minDataSize = newVersion ? 468 : 354; + if (driverDataSize < minDataSize) + error("ACCOLADE-ADLIB: Expected minimum driver data size of %d - got %d", minDataSize, driverDataSize); + + // INSTR.DAT Data is like this: + // 128 bytes instrument mapping + // 128 bytes instrument volume adjust (signed!) (not used for MT32) + // 16 bytes unknown + // 16 bytes channel mapping + // 64 bytes key note mapping (not really used for MT32) + // 1 byte instrument count + // 1 byte bytes per instrument + // x bytes no instruments used for MT32 + + // music.drv is basically a driver, but with a few fixed locations for certain data + + uint16 channelMappingOffset = newVersion ? 396 : 256 + 16; + Common::copy(driverData + channelMappingOffset, driverData + channelMappingOffset + ARRAYSIZE(_channelRemapping), _channelRemapping); + + uint16 instrumentMappingOffset = newVersion ? 140 : 0; + Common::copy(driverData + instrumentMappingOffset, driverData + instrumentMappingOffset + ARRAYSIZE(_instrumentRemapping), _instrumentRemapping); +} + +MidiDriver_Multisource *MidiDriver_Accolade_MT32_create(Common::String driverFilename) { + byte *driverData = nullptr; uint16 driverDataSize = 0; - bool isMusicDrvFile = false; + bool newVersion = false; - MidiDriver_Accolade_readDriver(driverFilename, MT_MT32, driverData, driverDataSize, isMusicDrvFile); + MidiDriver_Accolade_readDriver(driverFilename, MT_MT32, driverData, driverDataSize, newVersion); if (!driverData) - error("ACCOLADE-ADLIB: error during readDriver()"); + error("ACCOLADE-MT32: error during readDriver()"); MidiDriver_Accolade_MT32 *driver = new MidiDriver_Accolade_MT32(); - if (driver) { - if (!driver->setupInstruments(driverData, driverDataSize, isMusicDrvFile)) { - delete driver; - driver = nullptr; - } - } + if (!driver) + error("ACCOLADE-MT32: could not create driver"); + + driver->readDriverData(driverData, driverDataSize, newVersion); delete[] driverData; return driver; diff --git a/engines/agos/drivers/accolade/mt32.h b/engines/agos/drivers/accolade/mt32.h index 3ab34168b1c..9fdb85bfca0 100644 --- a/engines/agos/drivers/accolade/mt32.h +++ b/engines/agos/drivers/accolade/mt32.h @@ -19,65 +19,48 @@ * */ -#include "agos/drivers/accolade/mididriver.h" +#ifndef AGOS_DRIVERS_ACCOLADE_MT32_H +#define AGOS_DRIVERS_ACCOLADE_MT32_H -#include "audio/mididrv.h" - -#include "common/mutex.h" +#include "audio/mt32gm.h" namespace AGOS { -class MidiDriver_Accolade_MT32 : public MidiDriver { +class MidiDriver_Accolade_MT32 : public MidiDriver_MT32GM { +protected: + static const uint8 SFX_PROGRAM_BASE = 0x75; + static const uint8 SYSEX_INSTRUMENT_ASSIGNMENT[7]; + public: MidiDriver_Accolade_MT32(); - ~MidiDriver_Accolade_MT32() override; - // MidiDriver - int open() override; - void close() override; - bool isOpen() const override { return _isOpen; } + int open(MidiDriver *driver, bool nativeMT32) override; + using MidiDriver_MT32GM::send; + void send(int8 source, uint32 b) override; - void send(uint32 b) override; + int8 mapSourceChannel(uint8 source, uint8 dataChannel) override; + void deinitSource(uint8 source) override; - MidiChannel *allocateChannel() override { - if (_driver) - return _driver->allocateChannel(); - return NULL; - } - MidiChannel *getPercussionChannel() override { - if (_driver) - return _driver->getPercussionChannel(); - return NULL; - } + // Read the specified data from INSTR.DAT or MUSIC.DRV. + void readDriverData(byte *driverData, uint16 driverDataSize, bool isMusicDrv); - void setTimerCallback(void *timer_param, Common::TimerManager::TimerProc timer_proc) override { - if (_driver) - _driver->setTimerCallback(timer_param, timer_proc); - } - - uint32 getBaseTempo() override { - if (_driver) { - return _driver->getBaseTempo(); - } - return 1000000 / _baseFreq; - } + // Loads the specified instrument for the specified instrument source. + void loadSfxInstrument(uint8 source, byte *instrumentData); + // Changes the channel assigned to the specified SFX source to the SFX + // program number. + void changeSfxInstrument(uint8 source); protected: - Common::Mutex _mutex; - MidiDriver *_driver; - bool _nativeMT32; // native MT32, may also be our MUNT, or MUNT over MIDI - - bool _isOpen; - int _baseFreq; - -private: // simple mapping between MIDI channel and MT32 channel - byte _channelMapping[AGOS_MIDI_CHANNEL_COUNT]; + byte _channelRemapping[16]; // simple mapping between MIDI instruments and MT32 instruments - byte _instrumentMapping[AGOS_MIDI_INSTRUMENT_COUNT]; + byte _instrumentRemapping[128]; -public: - bool setupInstruments(byte *instrumentData, uint16 instrumentDataSize, bool useMusicDrvFile); + // Indicates if a MIDI channel is locked by an SFX source and unavailable + // for music. + bool _channelLocks[MIDI_CHANNEL_COUNT]; }; } // End of namespace AGOS + +#endif diff --git a/engines/agos/drivers/simon1/adlib.cpp b/engines/agos/drivers/simon1/adlib.cpp index cb035906ea4..1f88cd4be8a 100644 --- a/engines/agos/drivers/simon1/adlib.cpp +++ b/engines/agos/drivers/simon1/adlib.cpp @@ -299,7 +299,7 @@ uint8 MidiDriver_Simon1_AdLib::calculateUnscaledVolume(uint8 channel, uint8 sour return 0x3F - calculatedVolume; } -MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilename) { +MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilename, OPL::Config::OplType oplType) { // Load instrument data. Common::File ibk; @@ -319,7 +319,7 @@ MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilena return nullptr; } - MidiDriver_Simon1_AdLib *driver = new MidiDriver_Simon1_AdLib(OPL::Config::kOpl3, instrumentData); + MidiDriver_Simon1_AdLib *driver = new MidiDriver_Simon1_AdLib(oplType, instrumentData); delete[] instrumentData; return driver; diff --git a/engines/agos/drivers/simon1/adlib.h b/engines/agos/drivers/simon1/adlib.h index f84d7a9aeb3..7d4a5fee3c8 100644 --- a/engines/agos/drivers/simon1/adlib.h +++ b/engines/agos/drivers/simon1/adlib.h @@ -71,7 +71,7 @@ private: bool _musicRhythmNotesDisabled; }; -MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilename); +MidiDriver_Multisource *createMidiDriverSimon1AdLib(const char *instrumentFilename, OPL::Config::OplType); } // End of namespace AGOS diff --git a/engines/agos/drivers/simon1/adlib_win.cpp b/engines/agos/drivers/simon1/adlib_win.cpp deleted file mode 100644 index 739426e0bc1..00000000000 --- a/engines/agos/drivers/simon1/adlib_win.cpp +++ /dev/null @@ -1,38 +0,0 @@ -/* ScummVM - Graphic Adventure Engine - * - * ScummVM is the legal property of its developers, whose names - * are too numerous to list here. Please refer to the COPYRIGHT - * file distributed with this source distribution. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#include "agos/drivers/simon1/adlib_win.h" - -namespace AGOS { - -MidiDriver_Simon1_AdLib_Windows::MidiDriver_Simon1_AdLib_Windows() : MidiDriver_ADLIB_Multisource(OPL::Config::kOpl3) { } - -void MidiDriver_Simon1_AdLib_Windows::programChange(uint8 channel, uint8 program, uint8 source) { - // WORKAROUND The Windows version of Simon The Sorcerer uses the MT-32 MIDI - // data of the DOS versions, but plays this using Windows' General MIDI - // system. As a result, the music is played using different instruments - // than intended. This is fixed here by mapping the MT-32 instruments to - // GM instruments using MidiDriver's standard mapping. - uint8 gmInstrument = _mt32ToGm[program]; - MidiDriver_ADLIB_Multisource::programChange(channel, gmInstrument, source); -} - -} // End of namespace AGOS diff --git a/engines/agos/drivers/simon1/adlib_win.h b/engines/agos/drivers/simon1/adlib_win.h deleted file mode 100644 index 9ca679d411e..00000000000 --- a/engines/agos/drivers/simon1/adlib_win.h +++ /dev/null @@ -1,44 +0,0 @@ -/* ScummVM - Graphic Adventure Engine - * - * ScummVM is the legal property of its developers, whose names - * are too numerous to list here. Please refer to the COPYRIGHT - * file distributed with this source distribution. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - * - */ - -#ifndef AGOS_SIMON1_ADLIB_WIN_H -#define AGOS_SIMON1_ADLIB_WIN_H - -#include "audio/adlib_ms.h" - -namespace AGOS { - -/** - * AdLib MIDI driver for the Windows version of Simon The Sorcerer. - * This driver contains a workaround for converting the MT-32 instruments to - * the General MIDI instruments used by this driver. - */ -class MidiDriver_Simon1_AdLib_Windows : public MidiDriver_ADLIB_Multisource { -public: - MidiDriver_Simon1_AdLib_Windows(); - -protected: - void programChange(uint8 channel, uint8 program, uint8 source) override; -}; - -} // End of namespace AGOS - -#endif diff --git a/engines/agos/midi.cpp b/engines/agos/midi.cpp index 9d0f1f19289..65069f865bf 100644 --- a/engines/agos/midi.cpp +++ b/engines/agos/midi.cpp @@ -34,7 +34,6 @@ #include "agos/drivers/accolade/adlib.h" #include "agos/drivers/accolade/mt32.h" #include "agos/drivers/simon1/adlib.h" -#include "agos/drivers/simon1/adlib_win.h" // Miles Audio for Simon 2 #include "audio/miles.h" #include "audio/midiparser.h" @@ -48,16 +47,15 @@ namespace AGOS { - // MidiParser_S1D is not considered part of the standard // MidiParser suite, but we still try to mask its details // and just provide a factory function. -extern MidiParser *MidiParser_createS1D(); +extern MidiParser *MidiParser_createS1D(uint8 source = 0, bool monophonicChords = false); // This instrument remapping has been constructed by checking how the GM // instruments correspond to MT-32 instruments in other tracks (f.e. track 10-3 // is similar to track 11). -byte MidiPlayer::SIMON2_TRACK10_GM_MT32_INSTRUMENT_REMAPPING[] { +const byte MidiPlayer::SIMON2_TRACK10_GM_MT32_INSTRUMENT_REMAPPING[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x33, 0x00, 0x00, @@ -69,29 +67,17 @@ byte MidiPlayer::SIMON2_TRACK10_GM_MT32_INSTRUMENT_REMAPPING[] { }; MidiPlayer::MidiPlayer(AGOSEngine *vm) { - // Since initialize() is called every time the music changes, - // this is where we'll initialize stuff that must persist - // between songs. _vm = vm; _driver = nullptr; _driverMsMusic = nullptr; _driverMsSfx = nullptr; - _map_mt32_to_gm = false; - _current = nullptr; - - _musicVolume = 255; - _sfxVolume = 255; - - resetVolumeTable(); _paused = false; - _currentTrack = 255; - _loopTrack = 0; _queuedTrack = 255; _loopQueuedTrack = 0; - _musicMode = kMusicModeDisabled; + _pc98 = false; _deviceType = MT_NULL; _dataType = MT_NULL; @@ -99,226 +85,272 @@ MidiPlayer::MidiPlayer(AGOSEngine *vm) { _parserSfx = nullptr; _musicData = nullptr; _sfxData = nullptr; + _parserSfxAccolade = nullptr; } MidiPlayer::~MidiPlayer() { stop(); - stopSfx(); - - Common::StackLock lock(_mutex); + stop(true); + // Close the drivers first before locking the mutex. Otherwise a deadlock + // can occur where the timer thread has locked the timer mutex while + // waiting for the MidiPlayer mutex in the callback, while the main thread + // has locked the MidiPlayer mutex and waits for the timer mutex to remove + // a driver callback. if (_driverMsSfx && _driverMsSfx != _driverMsMusic) { _driverMsSfx->setTimerCallback(nullptr, nullptr); _driverMsSfx->close(); - delete _driverMsSfx; - _driverMsSfx = nullptr; } if (_driverMsMusic) { _driverMsMusic->setTimerCallback(nullptr, nullptr); _driverMsMusic->close(); - delete _driverMsMusic; - _driverMsMusic = nullptr; } else if (_driver) { _driver->setTimerCallback(nullptr, nullptr); _driver->close(); - delete _driver; - _driver = nullptr; } + Common::StackLock lock(_mutex); + if (_parserMusic) delete _parserMusic; if (_parserSfx) delete _parserSfx; + if (_parserSfxAccolade) + delete _parserSfxAccolade; if (_musicData) delete[] _musicData; if (_sfxData) delete[] _sfxData; - clearConstructs(); + if (_driverMsSfx && _driverMsSfx != _driverMsMusic) { + delete _driverMsSfx; + _driverMsSfx = nullptr; + } + if (_driverMsMusic) { + delete _driverMsMusic; + _driverMsMusic = nullptr; + } else if (_driver) { + delete _driver; + _driver = nullptr; + } } -int MidiPlayer::open(int gameType, Common::Platform platform, bool isDemo) { +int MidiPlayer::open() { // Don't call open() twice! assert(!_driver); - Common::String accoladeDriverFilename; + _pc98 = _vm->getGameType() == GType_ELVIRA1 && _vm->getPlatform() == Common::kPlatformPC98; // All games have MT-32 data, except Simon 2, which has both GM and MT-32 // data for DOS, or only GM for Windows. Some of the GM tracks have extra - // instruments compared to the MT-32 tracks, so we prefer GM. - int devFlags = MDT_MIDI | MDT_ADLIB | (_vm->getGameType() == GType_SIMON2 ? MDT_PREFER_GM : MDT_PREFER_MT32); + // instruments compared to the MT-32 tracks, so GM is preferred. + int devFlags = MDT_MIDI | (_pc98 ? MDT_PC98 : MDT_ADLIB) | + (_vm->getGameType() == GType_SIMON2 ? MDT_PREFER_GM : MDT_PREFER_MT32); - if (_vm->getGameType() == GType_SIMON1) { - // Check the type of device that the user has configured. - MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(devFlags); - _deviceType = MidiDriver::getMusicType(dev); - if (_deviceType == MT_GM && ConfMan.getBool("native_mt32")) - _deviceType = MT_MT32; + // Check the type of device that the user has configured. + MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(devFlags); + _deviceType = MidiDriver::getMusicType(dev); + if (_deviceType == MT_GM && ConfMan.getBool("native_mt32")) + _deviceType = MT_MT32; - // All versions of Simon 1 that use MIDI have data that targets the - // MT-32 (even the Windows and Acorn versions). - _dataType = MT_MT32; - - if (_dataType == MT_MT32 && _deviceType == MT_GM) { - // Not a real MT32 / no MUNT - ::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(); - } - - if (_vm->getPlatform() == Common::kPlatformDOS && - (_vm->getFeatures() & GF_DEMO) && !(_vm->getFeatures() & GF_TALKIE)) { - // DOS floppy demo uses the older Accolade music drivers. - _musicMode = kMusicModeAccolade; - accoladeDriverFilename = "MUSIC.DRV"; - - switch (_deviceType) { - case MT_ADLIB: - _driver = MidiDriver_Accolade_AdLib_create(accoladeDriverFilename); - break; - case MT_MT32: - _driver = MidiDriver_Accolade_MT32_create(accoladeDriverFilename); - break; - default: - _driver = new MidiDriver_NULL_Multisource(); - break; - } - - // Create the MIDI parser for the MUS format used by this version. - _parserMusic = MidiParser_createS1D(); - _parserMusic->property(MidiParser::mpDisableAutoStartPlayback, true); - - // Open the MIDI driver. - int returnCode = _driver->open(); - if (returnCode != 0) - error("MidiPlayer::open - Failed to open MIDI music driver - error code %d.", returnCode); - - // Connect the driver and the parser. - _parserMusic->setMidiDriver(_driver); - _parserMusic->setTimerRate(_driver->getBaseTempo()); - - _driver->setTimerCallback(_parserMusic, &_parserMusic->timerCallback); - - return 0; - } else { - // Create the correct driver(s) for the device type and platform. - switch (_deviceType) { - case MT_ADLIB: - if (_vm->getPlatform() == Common::kPlatformDOS) { - // DOS versions use a specific AdLib driver implementation - // that needs an instrument bank file. - _driverMsMusic = createMidiDriverSimon1AdLib("MT_FM.IBK"); - if (!(_vm->getFeatures() & GF_TALKIE)) { - // DOS floppy version has MIDI SFX for AdLib. - _driverMsSfx = _driverMsMusic; - } - } else { - // Windows and Acorn CD - // TODO Acorn does not use an OPL chip, but we don't have - // an implementation for the Acorn audio. It has the same - // music data has the DOS CD version, but it does not have - // the MT_FM.IBK instrument bank, so we use the standard - // OPL MIDI driver. - _driverMsMusic = new MidiDriver_Simon1_AdLib_Windows(); - } - break; - case MT_MT32: - case MT_GM: - _driverMsMusic = new MidiDriver_MT32GM(_dataType); - if (_vm->getPlatform() == Common::kPlatformDOS && !(_vm->getFeatures() & GF_TALKIE) && - ConfMan.getBool("multi_midi")) { - // DOS floppy version uses AdLib MIDI SFX if mixed MIDI - // mode is active. - _driverMsSfx = createMidiDriverSimon1AdLib("MT_FM.IBK"); - } - break; - default: - _driverMsMusic = new MidiDriver_NULL_Multisource(); - break; - } - _driver = _driverMsMusic; - - // WORKAROUND The Simon 1 MIDI data does not always set a value for - // program, volume or panning before it starts playing notes on a - // MIDI channel. This can cause settings for these parameters from - // a previous track to be used unintentionally. To correct this, - // default values for these parameters are set when a new track is - // started. - _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_PROGRAM); - _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_VOLUME); - _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_PANNING); - _driverMsMusic->property(MidiDriver::PROP_USER_VOLUME_SCALING, true); - - // Create the MIDI parser(s) for the format used by the platform. - if (_vm->getPlatform() == Common::kPlatformWindows) { - // Windows version uses a specific SMF variant. - _parserMusic = new MidiParser_SimonWin(0, true); - } else { - // DOS floppy & CD and Acorn CD use GMF (also an SMF variant). - _parserMusic = new MidiParser_GMF(0); - - if (_driverMsSfx) { - // DOS floppy needs a second parser for AdLib SFX. - _parserSfx = new MidiParser_GMF(1); - _parserMusic->property(MidiParser::mpDisableAllNotesOffMidiEvents, true); - _parserSfx->property(MidiParser::mpDisableAllNotesOffMidiEvents, true); - _parserSfx->property(MidiParser::mpDisableAutoStartPlayback, true); - } - } - _parserMusic->property(MidiParser::mpDisableAutoStartPlayback, true); - - // Open the MIDI driver(s). - int returnCode = _driverMsMusic->open(); - if (returnCode != 0) - error("MidiPlayer::open - Failed to open MIDI music driver - error code %d.", returnCode); - _driverMsMusic->syncSoundSettings(); - if (_driverMsSfx && _driverMsMusic != _driverMsSfx) { - _driverMsSfx->property(MidiDriver::PROP_USER_VOLUME_SCALING, true); - returnCode = _driverMsSfx->open(); - if (returnCode != 0) - error("MidiPlayer::open - Failed to open MIDI SFX driver - error code %d.", returnCode); - _driverMsSfx->syncSoundSettings(); - } - - // Connect the driver(s) and parser(s). - _parserMusic->setMidiDriver(_driverMsMusic); - _parserMusic->setTimerRate(_driverMsMusic->getBaseTempo()); - if (_parserSfx) { - _parserSfx->setMidiDriver(_driverMsSfx); - _parserSfx->setTimerRate(_driverMsSfx->getBaseTempo()); - } - - if (_driverMsSfx == _driverMsMusic) { - // Use MidiPlayer::onTimer to trigger both parsers from the - // single driver (it can only have one timer callback). - _driverMsMusic->setTimerCallback(this, &onTimer); - } else { - // Trigger each parser callback from the corresponding driver. - _driverMsMusic->setTimerCallback(_parserMusic, &_parserMusic->timerCallback); - if (_driverMsSfx) { - _driverMsSfx->setTimerCallback(_parserSfx, &_parserSfx->timerCallback); - } - } - - return 0; - } - } else if (_vm->getGameType() == GType_SIMON2) { - // Check the type of device that the user has configured. - MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(devFlags); - _deviceType = MidiDriver::getMusicType(dev); - if (_deviceType == MT_GM && ConfMan.getBool("native_mt32")) - _deviceType = MT_MT32; - - // DOS version has both MT-32 and GM tracks; Windows is GM only. + if (_vm->getGameType() == GType_SIMON2) { + // Simon 2 DOS version has both MT-32 and GM tracks; Windows is GM only. _dataType = (_vm->getPlatform() == Common::kPlatformDOS && _deviceType == MT_MT32) ? MT_MT32 : MT_GM; + } else { + // All the other games' MIDI data targets MT-32 (even Simon 1 Windows + // and Acorn). + _dataType = MT_MT32; + } + + if (_dataType == MT_MT32 && _deviceType == MT_GM) { + // Not a real MT32 / no MUNT + ::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(); + } + + // Elvira 2, Waxworks and Simon 1 DOS floppy have MIDI SFX. + bool usesMidiSfx = _vm->getGameType() == GType_WW || _vm->getGameType() == GType_ELVIRA2 || + (_vm->getGameType() == GType_SIMON1 && _vm->getPlatform() == Common::kPlatformDOS && + !(_vm->getFeatures() & GF_DEMO) && !(_vm->getFeatures() & GF_TALKIE)); + + // OPL3 is used for Windows and Acorn versions, all Simon 2 versions and + // if the user has set the OPL3 mode option. Otherwise OPL2 is used. + OPL::Config::OplType oplType = (_vm->getPlatform() != Common::kPlatformDOS || + _vm->getGameType() == GType_SIMON2 || ConfMan.getBool("opl3_mode")) ? OPL::Config::kOpl3 : OPL::Config::kOpl2; + + // Create drivers and parsers for the different versions of the games. + if ((_vm->getGameType() == GType_ELVIRA1 && _vm->getPlatform() == Common::kPlatformDOS) || + _vm->getGameType() == GType_ELVIRA2 || _vm->getGameType() == GType_WW || + (_vm->getGameType() == GType_SIMON1 && _vm->getPlatform() == Common::kPlatformDOS && + (_vm->getFeatures() & GF_DEMO) && !(_vm->getFeatures() & GF_TALKIE))) { + // Elvira 1 DOS, Elvira 2, Waxworks and Simon 1 DOS floppy demo + + // These games use drivers used by several Accolade games. + // Elvira 1 DOS uses an older version of the Accolade drivers which + // uses different files. + Common::String accoladeDriverFilename = _vm->getGameType() == GType_ELVIRA1 ? "INSTR.DAT" : "MUSIC.DRV"; + switch (_deviceType) { + case MT_ADLIB: + _driverMsMusic = _driverMsSfx = MidiDriver_Accolade_AdLib_create(accoladeDriverFilename, oplType); + if (_vm->getGameType() == GType_WW) + // WORKAROUND Some Waxworks tracks do not set an instrument on + // a MIDI channel. This will cause that channel to play with + // whatever instrument was set there by a previous track. + // This is fixed by setting default instruments on all channels + // before starting a track. + _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_PROGRAM); + if (usesMidiSfx) + // Elvira 2 and Waxworks have AdLib SFX. + _parserSfxAccolade = new SfxParser_Accolade_AdLib(); + break; + case MT_MT32: + case MT_GM: + _driverMsMusic = MidiDriver_Accolade_MT32_create(accoladeDriverFilename); + if (_vm->getGameType() == GType_WW) { + // WORKAROUND See above. + int16 defaultInstruments[16]; + Common::fill(defaultInstruments, defaultInstruments + ARRAYSIZE(defaultInstruments), -1); + Common::copy(MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS, MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS + ARRAYSIZE(MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS), defaultInstruments + 1); + _driverMsMusic->setControllerDefaults(MidiDriver_Multisource::CONTROLLER_DEFAULT_PROGRAM, defaultInstruments); + } + if (usesMidiSfx) { + if (ConfMan.getBool("multi_midi")) { + // Use AdLib SFX with MT-32 music. + _driverMsSfx = MidiDriver_Accolade_AdLib_create(accoladeDriverFilename, oplType); + _parserSfxAccolade = new SfxParser_Accolade_AdLib(); + } else { + // Use MT-32 SFX. + _driverMsSfx = _driverMsMusic; + _parserSfxAccolade = new SfxParser_Accolade_MT32(); + } + } + break; + default: + _driverMsMusic = new MidiDriver_NULL_Multisource(); + break; + } + _driver = _driverMsMusic; + + // These games use the MUS MIDI format used by several Accolade games. + // The Elvira 2 / Waxworks version of the AdLib driver has a mechanism + // that will only play the highest note of a chord; the Elvira 1 + // version does not have this. + _parserMusic = MidiParser_createS1D(0, _vm->getGameType() != GType_ELVIRA1 && _deviceType == MT_ADLIB); + } else if (_vm->getGameType() == GType_ELVIRA1 && _vm->getPlatform() == Common::kPlatformPC98) { + // Elvira 1 PC-98 + + // This version has its own drivers. It uses the same MUS format as the + // DOS version. + _driver = MidiDriverPC98_create(dev); + _parserMusic = MidiParser_createS1D(0); + } else if (_vm->getGameType() == GType_SIMON1) { + // Simon 1 (except the DOS floppy demo) + + // The DOS versions have their own drivers. The Windows version uses + // the standard Windows drivers. switch (_deviceType) { case MT_ADLIB: if (_vm->getPlatform() == Common::kPlatformDOS) { + // The DOS version AdLib driver uses an instrument bank file. + _driverMsMusic = createMidiDriverSimon1AdLib("MT_FM.IBK", oplType); + if (!(_vm->getFeatures() & GF_TALKIE)) { + // The DOS floppy version has AdLib MIDI SFX. + _driverMsSfx = _driverMsMusic; + } + } else { + // Windows and Acorn CD + // TODO Acorn does not use an OPL chip, but ScummVM doesn't + // have an implementation of the Acorn audio. It has the same + // music data as the DOS CD version, but it does not have + // the MT_FM.IBK instrument bank, so the standard AdLib MIDI + // driver is used. + _driverMsMusic = new MidiDriver_ADLIB_Multisource(oplType); + // WORKAROUND These versions have the same music data as the + // DOS versions, which target MT-32, despite Windows using GM + // as its MIDI format. To fix this, the MT-32 instruments are + // remapped to corresponding GM instruments. + _driverMsMusic->setInstrumentRemapping(MidiDriver::_mt32ToGm); + } + + // WORKAROUND The Simon 1 MIDI data does not always set a value for + // program before it starts playing notes on a MIDI channel. This + // can cause instruments from a previous track to be used + // unintentionally. + // To correct this, default instruments are set when a new track is + // started. + _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_PROGRAM); + + break; + case MT_MT32: + case MT_GM: + _driverMsMusic = new MidiDriver_MT32GM(_dataType); + + // WORKAROUND See above. + int16 defaultInstruments[16]; + Common::fill(defaultInstruments, defaultInstruments + ARRAYSIZE(defaultInstruments), -1); + Common::copy(MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS, MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS + ARRAYSIZE(MidiDriver_MT32GM::MT32_DEFAULT_INSTRUMENTS), defaultInstruments + 1); + _driverMsMusic->setControllerDefaults(MidiDriver_Multisource::CONTROLLER_DEFAULT_PROGRAM, defaultInstruments); + + if (_vm->getPlatform() == Common::kPlatformDOS && !(_vm->getFeatures() & GF_TALKIE) && + ConfMan.getBool("multi_midi")) { + // The DOS floppy version can use AdLib MIDI SFX with MT-32 + // music. + _driverMsSfx = createMidiDriverSimon1AdLib("MT_FM.IBK", oplType); + } + + break; + default: + _driverMsMusic = new MidiDriver_NULL_Multisource(); + break; + } + _driver = _driverMsMusic; + + // WORKAROUND The Simon 1 MIDI data does not always set a value for + // volume or panning before it starts playing notes on a MIDI channel. + // This can cause settings for these parameters from a previous track + // to be used unintentionally. To correct this, default values for + // these parameters are set when a new track is started. + _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_VOLUME); + _driverMsMusic->setControllerDefault(MidiDriver_Multisource::CONTROLLER_DEFAULT_PANNING); + + // The Windows version uses music tempos which are noticably faster + // than those used by the DOS versions. The MIDI parsers can be + // configured to use one of both tempos. The DOS tempos will be used + // for the DOS versions or if the user has selected the "Use DOS music + // tempos" option. Otherwise the Windows tempos will be used. + bool useDosTempos = ConfMan.hasKey("dos_music_tempos") ? ConfMan.getBool("dos_music_tempos") : _vm->getPlatform() == Common::kPlatformDOS; + + // Create the MIDI parser(s) for the format used by the platform. + if (_vm->getPlatform() == Common::kPlatformWindows) { + // The Windows version uses a slightly different SMF variant. + _parserMusic = new MidiParser_SimonWin(0, useDosTempos); + } else { + // DOS floppy & CD and Acorn CD use GMF (also an SMF variant). + _parserMusic = new MidiParser_GMF(0, useDosTempos); + + if (_driverMsSfx) { + // DOS floppy needs a second GMF parser for AdLib SFX. + _parserSfx = new MidiParser_GMF(1, true); + _parserMusic->property(MidiParser::mpDisableAllNotesOffMidiEvents, true); + _parserSfx->property(MidiParser::mpDisableAllNotesOffMidiEvents, true); + _parserSfx->property(MidiParser::mpDisableAutoStartPlayback, true); + } + } + } else if (_vm->getGameType() == GType_SIMON2) { + // Simon 2 + + // The DOS version uses MIDPAK, which is similar to Miles AIL v2. + // The Windows version uses the standard Windows drivers. + switch (_deviceType) { + case MT_ADLIB: + if (_vm->getPlatform() == Common::kPlatformDOS) { + // DOS if (Common::File::exists("MIDPAK.AD")) { // if there is a file called MIDPAK.AD, use it directly warning("MidiPlayer::open - SIMON 2: using MIDPAK.AD"); @@ -344,6 +376,7 @@ int MidiPlayer::open(int gameType, Common::Platform platform, bool isDemo) { case MT_MT32: case MT_GM: if (_vm->getPlatform() == Common::kPlatformDOS) { + // DOS _driverMsMusic = Audio::MidiDriver_Miles_MIDI_create(_dataType, ""); } else { // Windows @@ -355,353 +388,70 @@ int MidiPlayer::open(int gameType, Common::Platform platform, bool isDemo) { break; } _driver = _driverMsMusic; - _driverMsMusic->property(MidiDriver::PROP_USER_VOLUME_SCALING, true); // Create the MIDI parser(s) for the format used by the platform. if (_vm->getPlatform() == Common::kPlatformDOS) { + // DOS uses Miles XMIDI. _parserMusic = MidiParser::createParser_XMIDI(MidiParser::defaultXMidiCallback, 0, 0); } else { - // Windows version uses a specific SMF variant. + // Windows version uses an SMF variant (same as Simon 1 Windows). _parserMusic = new MidiParser_SimonWin(0); } - _parserMusic->property(MidiParser::mpDisableAutoStartPlayback, true); + } - // Open the MIDI driver. - int returnCode = _driverMsMusic->open(); + // Set common properties for the drivers and music parser. + if (_driverMsMusic) + _driverMsMusic->property(MidiDriver::PROP_USER_VOLUME_SCALING, true); + if (_driverMsSfx && _driverMsSfx != _driverMsMusic) + _driverMsSfx->property(MidiDriver::PROP_USER_VOLUME_SCALING, true); + + _parserMusic->property(MidiParser::mpDisableAutoStartPlayback, true); + + // Open the MIDI driver(s). + int returnCode = _driver->open(); + if (returnCode != 0) + error("MidiPlayer::open - Failed to open MIDI music driver - error code %d.", returnCode); + if (_driverMsSfx && _driverMsSfx != _driverMsMusic) { + returnCode = _driverMsSfx->open(); if (returnCode != 0) - error("MidiPlayer::open - Failed to open MIDI music driver - error code %d.", returnCode); - _driverMsMusic->syncSoundSettings(); - - // Connect the driver and parser. - _parserMusic->setMidiDriver(_driverMsMusic); - _parserMusic->setTimerRate(_driverMsMusic->getBaseTempo()); - _driverMsMusic->setTimerCallback(this, &onTimer); - - return 0; + error("MidiPlayer::open - Failed to open MIDI SFX driver - error code %d.", returnCode); } - // TODO Old code below is no longer used for Simon 1 and 2. - _deviceType = MT_INVALID; + syncSoundSettings(); - switch (gameType) { - case GType_ELVIRA1: - if (platform == Common::kPlatformPC98) { - _musicMode = kMusicModePC98; - devFlags = (devFlags & ~MDT_ADLIB) | MDT_PC98; - } else { - _musicMode = kMusicModeAccolade; - accoladeDriverFilename = "INSTR.DAT"; - } - break; - case GType_ELVIRA2: - case GType_WW: - // Attention: Elvira 2 shipped with INSTR.DAT and MUSIC.DRV - // MUSIC.DRV is the correct one. INSTR.DAT seems to be a left-over - _musicMode = kMusicModeAccolade; - accoladeDriverFilename = "MUSIC.DRV"; - break; - case GType_SIMON1: - if (isDemo) { - _musicMode = kMusicModeAccolade; - accoladeDriverFilename = "MUSIC.DRV"; - } else if (Common::File::exists("MT_FM.IBK")) { - _musicMode = kMusicModeSimon1; - } - break; - case GType_SIMON2: - //_musicMode = kMusicModeMilesAudio; - // currently disabled, because there are a few issues - // MT32 seems to work fine now, AdLib seems to use bad instruments and is also outputting music on - // the right speaker only. The original driver did initialize the panning to 0 and the Simon2 XMIDI - // tracks don't set panning at all. We can reset panning to be centered, which would solve this - // issue, but we still don't know who's setting it in the original interpreter. - break; - default: - break; + // Connect the driver(s) and the parser(s). + _parserMusic->setMidiDriver(_driver); + _parserMusic->setTimerRate(_driver->getBaseTempo()); + if (_parserSfx) { + _parserSfx->setMidiDriver(_driverMsSfx); + } else if (_parserSfxAccolade) { + _parserSfxAccolade->setMidiDriver(_driverMsSfx); } - MidiDriver::DeviceHandle dev; - int ret = 0; - - if (_musicMode == kMusicModePC98) { - dev = MidiDriver::detectDevice(devFlags); - _driver = MidiDriverPC98_create(dev); - if (_driver && !_driver->open()) { - _driver->setTimerCallback(this, &onTimer); - return 0; + if ((_parserSfx || _parserSfxAccolade) && _driverMsSfx == _driver) { + // Use MidiPlayer::onTimer to trigger both parsers from the + // single driver (it can only have one timer callback). + _driver->setTimerCallback(this, &onTimer); + if (_parserSfx) { + _parserSfx->setTimerRate(_driver->getBaseTempo()); + } else if (_parserSfxAccolade) { + _parserSfxAccolade->setTimerRate(_driver->getBaseTempo()); } - } else if (_musicMode != kMusicModeDisabled) { - dev = MidiDriver::detectDevice(devFlags); - _deviceType = MidiDriver::getMusicType(dev); - - switch (_deviceType) { - case MT_ADLIB: - case MT_MT32: - break; - case MT_GM: - if (!ConfMan.getBool("native_mt32")) { - // Not a real MT32 / no MUNT - ::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(); - } - // Switch to MT32 driver in any case - _deviceType = MT_MT32; - break; - default: - _musicMode = kMusicModeDisabled; - break; + } else { + // Connect each parser to its own driver. + _driver->setTimerCallback(_parserMusic, &_parserMusic->timerCallback); + if (_parserSfx) { + _driverMsSfx->setTimerCallback(_parserSfx, &_parserSfx->timerCallback); + _parserSfx->setTimerRate(_driverMsSfx->getBaseTempo()); + } else if (_parserSfxAccolade) { + _driverMsSfx->setTimerCallback(_parserSfxAccolade, &_parserSfxAccolade->timerCallback); + _parserSfxAccolade->setTimerRate(_driverMsSfx->getBaseTempo()); } } - switch (_musicMode) { - case kMusicModeAccolade: { - // Setup midi driver - switch (_deviceType) { - case MT_ADLIB: - _driver = MidiDriver_Accolade_AdLib_create(accoladeDriverFilename); - break; - case MT_MT32: - _driver = MidiDriver_Accolade_MT32_create(accoladeDriverFilename); - break; - default: - assert(0); - break; - } - if (!_driver) - return 255; - - ret = _driver->open(); - if (ret == 0) { - // Reset is done inside our MIDI driver - _driver->setTimerCallback(this, &onTimer); - } - - //setTimerRate(_driver->getBaseTempo()); - return 0; - } - - case kMusicModeMilesAudio: { - switch (_deviceType) { - case MT_ADLIB: { - Common::File instrumentDataFile; - if (instrumentDataFile.exists("MIDPAK.AD")) { - // if there is a file called MIDPAK.AD, use it directly - warning("SIMON 2: using MIDPAK.AD"); - _driver = Audio::MidiDriver_Miles_AdLib_create("MIDPAK.AD", "MIDPAK.AD"); - } else { - // if there is no file called MIDPAK.AD, try to extract it from the file SETUP.SHR - // if we didn't do this, the user would be forced to "install" the game instead of simply - // copying all files from CD-ROM. - Common::SeekableReadStream *midpakAdLibStream; - midpakAdLibStream = simon2SetupExtractFile("MIDPAK.AD"); - if (!midpakAdLibStream) - error("MidiPlayer: could not extract MIDPAK.AD from SETUP.SHR"); - - // Pass this extracted data to the driver - warning("SIMON 2: using MIDPAK.AD extracted from SETUP.SHR"); - _driver = Audio::MidiDriver_Miles_AdLib_create("", "", midpakAdLibStream); - delete midpakAdLibStream; - } - // TODO: not sure what's going wrong with AdLib - // it doesn't seem to matter if we use the regular XMIDI tracks or the 2nd set meant for MT32 - break; - } - case MT_MT32: - _driver = Audio::MidiDriver_Miles_MT32_create(""); - _nativeMT32 = true; // use 2nd set of XMIDI tracks - break; - case MT_GM: - if (ConfMan.getBool("native_mt32")) { - _driver = Audio::MidiDriver_Miles_MT32_create(""); - _nativeMT32 = true; // use 2nd set of XMIDI tracks - } - break; - - default: - break; - } - if (!_driver) - return 255; - - ret = _driver->open(); - if (ret == 0) { - // Reset is done inside our MIDI driver - _driver->setTimerCallback(this, &onTimer); - } - return 0; - } - - case kMusicModeSimon1: { - // This only handles the original AdLib driver of Simon1. - if (_deviceType == MT_ADLIB) { - _map_mt32_to_gm = false; - _nativeMT32 = false; - - _driver = createMidiDriverSimon1AdLib("MT_FM.IBK"); - if (_driver && _driver->open() == 0) { - _driver->setTimerCallback(this, &onTimer); - // Like the original, we enable the rhythm support by default. - _driver->send(0xB0, 0x67, 0x01); - return 0; - } - - delete _driver; - _driver = nullptr; - } - - _musicMode = kMusicModeDisabled; - } - - default: - break; - } - - dev = MidiDriver::detectDevice(MDT_ADLIB | MDT_MIDI | (gameType == GType_SIMON1 ? MDT_PREFER_MT32 : MDT_PREFER_GM)); - _nativeMT32 = ((MidiDriver::getMusicType(dev) == MT_MT32) || ConfMan.getBool("native_mt32")); - - _driver = MidiDriver::createMidi(dev); - if (!_driver) - return 255; - - if (_nativeMT32) - _driver->property(MidiDriver::PROP_CHANNEL_MASK, 0x03FE); - - _map_mt32_to_gm = (gameType != GType_SIMON2 && !_nativeMT32); - - ret = _driver->open(); - if (ret) - return ret; - _driver->setTimerCallback(this, &onTimer); - - if (_nativeMT32) - _driver->sendMT32Reset(); - else - _driver->sendGMReset(); - return 0; } -void MidiPlayer::send(uint32 b) { - if (!_current) - return; - - if (_musicMode != kMusicModeDisabled) { - // Handle volume control for Simon1 output. - if (_musicMode == kMusicModeSimon1) { - // The driver does not support any volume control, thus we simply - // scale the velocities on note on for now. - // TODO: We should probably handle this at output level at some - // point. Then we can allow volume changes to affect already - // playing notes too. For now this simple change allows us to - // have some simple volume control though. - if ((b & 0xF0) == 0x90) { - byte volume = (b >> 16) & 0x7F; - - if (_current == &_sfx) { - volume = volume * _sfxVolume / 255; - } else if (_current == &_music) { - volume = volume * _musicVolume / 255; - } - - b = (b & 0xFF00FFFF) | (volume << 16); - } - } - - // Send directly to Accolade/Miles/Simon1 Audio driver - _driver->send(b); - return; - } - - byte channel = (byte)(b & 0x0F); - if ((b & 0xFFF0) == 0x07B0) { - // Adjust volume changes by master music and master sfx volume. - byte volume = (byte)((b >> 16) & 0x7F); - _current->volume[channel] = volume; - if (_current == &_sfx) - volume = volume * _sfxVolume / 255; - else if (_current == &_music) - volume = volume * _musicVolume / 255; - b = (b & 0xFF00FFFF) | (volume << 16); - } else if ((b & 0xF0) == 0xC0 && _map_mt32_to_gm) { - b = (b & 0xFFFF00FF) | (MidiDriver::_mt32ToGm[(b >> 8) & 0xFF] << 8); - } else if ((b & 0xFFF0) == 0x007BB0) { - // Only respond to an All Notes Off if this channel - // has already been allocated. - if (!_current->channel[b & 0x0F]) - return; - } else if ((b & 0xFFF0) == 0x79B0) { - // "Reset All Controllers". There seems to be some confusion - // about what this message should do to the volume controller. - // See http://www.midi.org/about-midi/rp15.shtml for more - // information. - // - // If I understand it correctly, the current standard indicates - // that the volume should be reset, but the next revision will - // exclude it. On my system, both ALSA and FluidSynth seem to - // reset it, while AdLib does not. Let's follow the majority. - - _current->volume[channel] = 127; - } - - // Allocate channels if needed - if (!_current->channel[channel]) - _current->channel[channel] = (channel == 9) ? _driver->getPercussionChannel() : _driver->allocateChannel(); - - if (_current->channel[channel]) { - if (channel == 9) { - if (_current == &_sfx) - _current->channel[9]->volume(_current->volume[9] * _sfxVolume / 255); - else if (_current == &_music) - _current->channel[9]->volume(_current->volume[9] * _musicVolume / 255); - } - _current->channel[channel]->send(b); - if ((b & 0xFFF0) == 0x79B0) { - // We have received a "Reset All Controllers" message - // and passed it on to the MIDI driver. This may or may - // not have affected the volume controller. To ensure - // consistent behavior, explicitly set the volume to - // what we think it should be. - - if (_current == &_sfx) - _current->channel[channel]->volume(_current->volume[channel] * _sfxVolume / 255); - else if (_current == &_music) - _current->channel[channel]->volume(_current->volume[channel] * _musicVolume / 255); - } - } -} - -void MidiPlayer::metaEvent(byte type, byte *data, uint16 length) { - // Only thing we care about is End of Track. - if (!_current || type != 0x2F) { - return; - } else if (_current == &_sfx) { - clearConstructs(_sfx); - } else if (_loopTrack) { - _current->parser->jumpToTick(0); - } else if (_queuedTrack != 255) { - _currentTrack = 255; - byte destination = _queuedTrack; - _queuedTrack = 255; - _loopTrack = _loopQueuedTrack; - _loopQueuedTrack = false; - - // Remember, we're still inside the locked mutex. - // Have to unlock it before calling jump() - // (which locks it itself), and then relock it - // upon returning. - _mutex.unlock(); - startTrack(destination); - _mutex.lock(); - } else { - stop(); - } -} - void MidiPlayer::onTimer(void *data) { MidiPlayer *p = (MidiPlayer *)data; Common::StackLock lock(p->_mutex); @@ -720,25 +470,16 @@ void MidiPlayer::onTimer(void *data) { if (p->_parserSfx) p->_parserSfx->onTimer(); - if (!p->_paused) { - if (p->_music.parser && p->_currentTrack != 255) { - p->_current = &p->_music; - p->_music.parser->onTimer(); - } - } - if (p->_sfx.parser) { - p->_current = &p->_sfx; - p->_sfx.parser->onTimer(); - } - p->_current = nullptr; + if (p->_parserSfxAccolade) + p->_parserSfxAccolade->onTimer(); } bool MidiPlayer::usesMT32Data() const { return _dataType == MT_MT32; } -bool MidiPlayer::hasAdLibSfx() const { - return _parserSfx != nullptr; +bool MidiPlayer::hasMidiSfx() const { + return _parserSfx != nullptr || _parserSfxAccolade != nullptr; } bool MidiPlayer::isPlaying(bool checkQueued) { @@ -747,80 +488,35 @@ bool MidiPlayer::isPlaying(bool checkQueued) { return _parserMusic->isPlaying() && (!checkQueued || _queuedTrack != 255); } -void MidiPlayer::startTrack(int track) { +void MidiPlayer::stop(bool sfx) { Common::StackLock lock(_mutex); - if (track == _currentTrack) - return; +if (!sfx) { + // Clear the queued track to prevent it from starting when the current + // track is stopped. + _queuedTrack = 255; - if (_music.num_songs > 0) { - if (track >= _music.num_songs) - return; - - if (_music.parser) { - _current = &_music; - delete _music.parser; - _current = nullptr; - _music.parser = nullptr; + if (_parserMusic) { + _parserMusic->stopPlaying(); + if (_driverMsMusic) + _driverMsMusic->deinitSource(0); } - - MidiParser *parser = MidiParser::createParser_SMF(); - parser->property (MidiParser::mpMalformedPitchBends, 1); - parser->setMidiDriver(this); - parser->setTimerRate(_driver->getBaseTempo()); - if (!parser->loadMusic(_music.songs[track], _music.song_sizes[track])) { - warning("Error reading track %d", track); - delete parser; - parser = nullptr; + } else { + if (_parserSfx) { + _parserSfx->stopPlaying(); + if (_driverMsSfx) + _driverMsSfx->deinitSource(1); } - - _currentTrack = (byte)track; - _music.parser = parser; // That plugs the power cord into the wall - } else if (_music.parser) { - if (!_music.parser->setTrack(track)) { - return; + if (_parserSfxAccolade) { + _parserSfxAccolade->stopAll(); } - _currentTrack = (byte)track; - _current = &_music; - _music.parser->jumpToTick(0); - _current = nullptr; - } -} - -void MidiPlayer::stop() { - Common::StackLock lock(_mutex); - - // Clear the queued track to prevent it from starting when the current - // track is stopped. - _queuedTrack = 255; - - if (_parserMusic) { - _parserMusic->stopPlaying(); - if (_driverMsMusic) - _driverMsMusic->deinitSource(0); - } - - if (_music.parser) { - _current = &_music; - _music.parser->jumpToTick(0); - } - _current = nullptr; - _currentTrack = 255; -} - -void MidiPlayer::stopSfx() { - Common::StackLock lock(_mutex); - - if (_parserSfx) { - _parserSfx->stopPlaying(); - if (_driverMsSfx) - _driverMsSfx->deinitSource(1); } } void MidiPlayer::pause(bool b) { if (_paused == b || !_driver) return; + _paused = b; Common::StackLock lock(_mutex); @@ -836,20 +532,8 @@ void MidiPlayer::pause(bool b) { if (_parserSfx) _parserSfx->resumePlaying(); } - - // if using the driver Accolade_AdLib call setVolume() to turn off\on the volume on all channels - if (_deviceType == MT_ADLIB && _musicMode == kMusicModeAccolade) { - static_cast (_driver)->setVolume(_paused ? 0 : ConfMan.getInt("music_volume")); - } else if (_musicMode == kMusicModePC98) { - _driver->property(0x30, _paused ? 1 : 0); - } - - for (int i = 0; i < 16; ++i) { - if (_music.channel[i]) - _music.channel[i]->volume(_paused ? 0 : (_music.volume[i] * _musicVolume / 255)); - if (_sfx.channel[i]) - _sfx.channel[i]->volume(_paused ? 0 : (_sfx.volume[i] * _sfxVolume / 255)); - } + if (_parserSfxAccolade) + _parserSfxAccolade->pauseAll(_paused); } void MidiPlayer::fadeOut() { @@ -862,59 +546,30 @@ void MidiPlayer::fadeOut() { _driverMsMusic->startFade(0, 1000, 0); } -void MidiPlayer::setVolume(int musicVol, int sfxVol) { - musicVol = CLIP(musicVol, 0, 255); - sfxVol = CLIP(sfxVol, 0, 255); - - if (_musicVolume == musicVol && _sfxVolume == sfxVol) - return; - - _musicVolume = musicVol; - _sfxVolume = sfxVol; - - if (_musicMode == kMusicModePC98) { - _driver->property(0x10, _musicVolume); - _driver->property(0x20, _sfxVolume); - } else if (_musicMode == kMusicModeAccolade && _deviceType == MT_ADLIB) { - static_cast (_driver)->setVolume(_musicVolume); - } - - // Now tell all the channels this. - Common::StackLock lock(_mutex); - if (_driver && !_paused) { - for (int i = 0; i < 16; ++i) { - if (_music.channel[i]) - _music.channel[i]->volume(_music.volume[i] * _musicVolume / 255); - if (_sfx.channel[i]) - _sfx.channel[i]->volume(_sfx.volume[i] * _sfxVolume / 255); - } - } -} - void MidiPlayer::syncSoundSettings() { if (_driverMsMusic) _driverMsMusic->syncSoundSettings(); if (_driverMsSfx) _driverMsSfx->syncSoundSettings(); - // Sync code for non-multisource drivers. - // TODO Remove when no longer necessary - bool mute = false; - if (ConfMan.hasKey("mute")) - mute = ConfMan.getBool("mute"); + if (_pc98) { + // Sync code for non-multisource drivers. + bool mute = false; + if (ConfMan.hasKey("mute")) + mute = ConfMan.getBool("mute"); - // Sync the engine with the config manager - int soundVolumeMusic = ConfMan.getInt("music_volume"); - int soundVolumeSFX = ConfMan.getInt("sfx_volume"); + // Sync the engine with the config manager + int soundVolumeMusic = ConfMan.getInt("music_volume"); + int soundVolumeSFX = ConfMan.getInt("sfx_volume"); - setVolume(mute ? 0 : soundVolumeMusic, mute ? 0 : soundVolumeSFX); + _driver->property(0x10, mute ? 0 : soundVolumeMusic); + _driver->property(0x20, mute ? 0 : soundVolumeSFX); + } } void MidiPlayer::setLoop(bool loop) { Common::StackLock lock(_mutex); - _loopTrack = loop; - if (_parserMusic) _parserMusic->property(MidiParser::mpAutoLoop, loop); } @@ -938,61 +593,29 @@ void MidiPlayer::queueTrack(int track, bool loop) { } } -void MidiPlayer::clearConstructs() { - clearConstructs(_music); - clearConstructs(_sfx); -} - -void MidiPlayer::clearConstructs(MusicInfo &info) { - int i; - if (info.num_songs > 0) { - for (i = 0; i < info.num_songs; ++i) - free(info.songs[i]); - info.num_songs = 0; - } - - free(info.data); - info.data = nullptr; - - delete info.parser; - info.parser = nullptr; - - if (_driver) { - for (i = 0; i < 16; ++i) { - if (info.channel[i]) { - info.channel[i]->allNotesOff(); - info.channel[i]->release(); - } - } - } - info.clear(); -} - -void MidiPlayer::resetVolumeTable() { - int i; - for (i = 0; i < 16; ++i) { - _music.volume[i] = _sfx.volume[i] = 127; - if (_driver) - _driver->send(((_musicVolume >> 1) << 16) | 0x7B0 | i); - } -} - -void MidiPlayer::loadMusic(Common::SeekableReadStream *in, int32 size, bool sfx) { +void MidiPlayer::load(Common::SeekableReadStream *in, int32 size, bool sfx) { Common::StackLock lock(_mutex); + if (sfx && _parserSfxAccolade) { + _parserSfxAccolade->load(in, size); + return; + } + MidiParser *parser = sfx ? _parserSfx : _parserMusic; + if (!parser) + return; if (size < 0) { // Use the parser to determine the size of the MIDI data. int64 startPos = in->pos(); size = parser->determineDataSize(in); if (size < 0) { - warning("MidiPlayer::loadMusic - Could not determine size of music data"); + warning("MidiPlayer::load - Could not determine size of music data"); return; } // determineDataSize might move the stream position, so return it to // the original position. - in->seek(startPos, SEEK_SET); + in->seek(startPos); } parser->unloadMusic(); @@ -1013,7 +636,14 @@ void MidiPlayer::loadMusic(Common::SeekableReadStream *in, int32 size, bool sfx) void MidiPlayer::play(int track, bool sfx, bool sfxUsesRhythm, bool queued) { Common::StackLock lock(_mutex); + if (sfx && _parserSfxAccolade) { + _parserSfxAccolade->play(track); + return; + } + MidiParser *parser = sfx ? _parserSfx : _parserMusic; + if (!parser) + return; if (parser->setTrack(track)) { if (sfx && sfxUsesRhythm) { @@ -1041,232 +671,6 @@ void MidiPlayer::play(int track, bool sfx, bool sfxUsesRhythm, bool queued) { } } -// TODO Remove dead code - -static const int simon1_gmf_size[] = { - 8900, 12166, 2848, 3442, 4034, 4508, 7064, 9730, 6014, 4742, 3138, - 6570, 5384, 8909, 6457, 16321, 2742, 8968, 4804, 8442, 7717, - 9444, 5800, 1381, 5660, 6684, 2456, 4744, 2455, 1177, 1232, - 17256, 5103, 8794, 4884, 16 -}; - -void MidiPlayer::loadSMF(Common::SeekableReadStream *in, int song, bool sfx) { - Common::StackLock lock(_mutex); - - MusicInfo *p = sfx ? &_sfx : &_music; - clearConstructs(*p); - - uint32 startpos = in->pos(); - byte header[4]; - in->read(header, 4); - bool isGMF = !memcmp(header, "GMF\x1", 4); - in->seek(startpos, SEEK_SET); - - uint32 size = in->size() - in->pos(); - if (isGMF) { - if (sfx) { - // Multiple GMF resources are stored in the SFX files, - // but each one is referenced by a pointer at the - // beginning of the file. Those pointers can be used - // to determine file size. - in->seek(0, SEEK_SET); - uint16 value = in->readUint16LE() >> 2; // Number of resources - if (song != value - 1) { - in->seek(song * 2 + 2, SEEK_SET); - value = in->readUint16LE(); - size = value - startpos; - } - in->seek(startpos, SEEK_SET); - } else if (size >= 64000) { - // For GMF resources not in separate - // files, we're going to have to use - // hardcoded size tables. - size = simon1_gmf_size[song]; - } - } - - // When allocating space, add 4 bytes in case - // this is a GMF and we have to tack on our own - // End of Track event. - p->data = (byte *)calloc(size + 4, 1); - in->read(p->data, size); - - uint32 timerRate = _driver->getBaseTempo(); - - if (isGMF) { - // The GMF header - // 3 BYTES: 'GMF' - // 1 BYTE : Major version - // 1 BYTE : Minor version - // 1 BYTE : Ticks (Ranges from 2 - 8, always 2 for SFX) - // 1 BYTE : Loop control. 0 = no loop, 1 = loop (Music only) - if (!sfx) { - // In the original, the ticks value indicated how many - // times the music timer was called before it actually - // did something. The larger the value the slower the - // music. - // - // We, on the other hand, have a timer rate which is - // used to control by how much the music advances on - // each onTimer() call. The larger the value, the - // faster the music. - // - // It seems that 4 corresponds to our base tempo, so - // this should be the right way to calculate it. - timerRate = (4 * _driver->getBaseTempo()) / p->data[5]; - - // According to bug #1706 calling setLoop() from - // within a lock causes a lockup, though I have no - // idea when this actually happens. - _loopTrack = (p->data[6] != 0); - } - } - - MidiParser *parser = MidiParser::createParser_SMF(); - parser->property(MidiParser::mpMalformedPitchBends, 1); - parser->setMidiDriver(this); - parser->setTimerRate(timerRate); - if (!parser->loadMusic(p->data, size)) { - warning("Error reading track"); - delete parser; - parser = nullptr; - } - - if (!sfx) { - _currentTrack = 255; - resetVolumeTable(); - } - p->parser = parser; // That plugs the power cord into the wall -} - -void MidiPlayer::loadMultipleSMF(Common::SeekableReadStream *in, bool sfx) { - // This is a special case for Simon 2 Windows. - // Instead of having multiple sequences as - // separate tracks in a Type 2 file, simon2win - // has multiple songs, each of which is a Type 1 - // file. Thus, preceding the songs is a single - // byte specifying how many songs are coming. - // We need to load ALL the songs and then - // treat them as separate tracks -- for the - // purpose of jumps, anyway. - Common::StackLock lock(_mutex); - - MusicInfo *p = sfx ? &_sfx : &_music; - clearConstructs(*p); - - p->num_songs = in->readByte(); - if (p->num_songs > 16) { - warning("playMultipleSMF: %d is too many songs to keep track of", (int)p->num_songs); - return; - } - - byte i; - for (i = 0; i < p->num_songs; ++i) { - byte buf[5]; - uint32 pos = in->pos(); - - // Make sure there's a MThd - in->read(buf, 4); - if (memcmp(buf, "MThd", 4) != 0) { - warning("Expected MThd but found '%c%c%c%c' instead", buf[0], buf[1], buf[2], buf[3]); - return; - } - in->seek(in->readUint32BE(), SEEK_CUR); - - // Now skip all the MTrk blocks - while (true) { - in->read(buf, 4); - if (memcmp(buf, "MTrk", 4) != 0) - break; - in->seek(in->readUint32BE(), SEEK_CUR); - } - - uint32 pos2 = in->pos() - 4; - uint32 size = pos2 - pos; - p->songs[i] = (byte *)calloc(size, 1); - in->seek(pos, SEEK_SET); - in->read(p->songs[i], size); - p->song_sizes[i] = size; - } - - if (!sfx) { - _currentTrack = 255; - resetVolumeTable(); - } -} - -void MidiPlayer::loadXMIDI(Common::SeekableReadStream *in, bool sfx) { - Common::StackLock lock(_mutex); - MusicInfo *p = sfx ? &_sfx : &_music; - clearConstructs(*p); - - char buf[4]; - uint32 pos = in->pos(); - uint32 size = 4; - in->read(buf, 4); - if (!memcmp(buf, "FORM", 4)) { - int i; - for (i = 0; i < 16; ++i) { - if (!memcmp(buf, "CAT ", 4)) - break; - size += 2; - memcpy(buf, &buf[2], 2); - in->read(&buf[2], 2); - } - if (memcmp(buf, "CAT ", 4) != 0) { - error("Could not find 'CAT ' tag to determine resource size"); - } - size += 4 + in->readUint32BE(); - in->seek(pos, 0); - p->data = (byte *)calloc(size, 1); - in->read(p->data, size); - } else { - error("Expected 'FORM' tag but found '%c%c%c%c' instead", buf[0], buf[1], buf[2], buf[3]); - } - - // In the DOS version of Simon the Sorcerer 2, the music contains lots - // of XMIDI callback controller events. As far as we know, they aren't - // actually used, so we disable the callback handler explicitly. - - MidiParser *parser = MidiParser::createParser_XMIDI(nullptr); - parser->setMidiDriver(this); - parser->setTimerRate(_driver->getBaseTempo()); - if (!parser->loadMusic(p->data, size)) - error("Error reading track"); - - if (!sfx) { - _currentTrack = 255; - resetVolumeTable(); - } - p->parser = parser; // That plugs the power cord into the wall -} - -void MidiPlayer::loadS1D(Common::SeekableReadStream *in, bool sfx) { - Common::StackLock lock(_mutex); - MusicInfo *p = sfx ? &_sfx : &_music; - clearConstructs(*p); - - uint16 size = in->readUint16LE(); - if (size != in->size() - 2) { - error("Size mismatch in MUS file (%ld versus reported %d)", (long)in->size() - 2, (int)size); - } - - p->data = (byte *)calloc(size, 1); - in->read(p->data, size); - - MidiParser *parser = MidiParser_createS1D(); - parser->setMidiDriver(this); - parser->setTimerRate(_driver->getBaseTempo()); - if (!parser->loadMusic(p->data, size)) - error("Error reading track"); - - if (!sfx) { - _currentTrack = 255; - resetVolumeTable(); - } - p->parser = parser; // That plugs the power cord into the wall -} - #define MIDI_SETUP_BUNDLE_HEADER_SIZE 56 #define MIDI_SETUP_BUNDLE_FILEHEADER_SIZE 48 #define MIDI_SETUP_BUNDLE_FILENAME_MAX_SIZE 12 diff --git a/engines/agos/midi.h b/engines/agos/midi.h index 2b4f3b9f69b..a4f4f837327 100644 --- a/engines/agos/midi.h +++ b/engines/agos/midi.h @@ -22,6 +22,8 @@ #ifndef AGOS_MIDI_H #define AGOS_MIDI_H +#include "agos/sfxparser_accolade.h" + #include "audio/mididrv.h" #include "audio/mididrv_ms.h" #include "audio/midiparser.h" @@ -33,139 +35,95 @@ class File; namespace AGOS { -enum kMusicMode { - kMusicModeDisabled = 0, - kMusicModeAccolade = 1, - kMusicModeMilesAudio = 2, - kMusicModeSimon1 = 3, - kMusicModePC98 = 4 -}; - -struct MusicInfo { - MidiParser *parser; - byte *data; - byte num_songs; // For Type 1 SMF resources - byte *songs[16]; // For Type 1 SMF resources - uint32 song_sizes[16]; // For Type 1 SMF resources - - MidiChannel *channel[16]; // Dynamic remapping of channels to resolve conflicts - byte volume[16]; // Current channel volume - - MusicInfo() { clear(); } - void clear() { - parser = 0; data = 0; num_songs = 0; - memset(songs, 0, sizeof(songs)); - memset(song_sizes, 0, sizeof(song_sizes)); - memset(channel, 0, sizeof(channel)); - } -}; - -class MidiPlayer : public MidiDriver_BASE { +class MidiPlayer { protected: // Instrument map specifically for remapping the instruments of the GM // version of Simon 2 track 10 subtracks 2 and 3 to MT-32. - static byte SIMON2_TRACK10_GM_MT32_INSTRUMENT_REMAPPING[]; + static const byte SIMON2_TRACK10_GM_MT32_INSTRUMENT_REMAPPING[]; AGOSEngine *_vm; Common::Mutex _mutex; + // Driver used for music. This points to the same object as _driverMsMusic, + // except if a PC-98 driver is used (these do not implement the Multisource + // interface). MidiDriver *_driver; // Multisource driver used for music. Provides access to multisource // methods without casting. If this is not nullptr, it points to the same // object as _driver. MidiDriver_Multisource *_driverMsMusic; - // Multisource driver used for sound effects. Only used for Simon The - // Sorcerer DOS floppy AdLib sound effects. + // Multisource driver used for sound effects. Only used for Elvira 2, + // Waxworks and Simon The Sorcerer DOS floppy AdLib sound effects. // If AdLib is also used for music, this points to the same object as // _driverMsMusic and _driver. MidiDriver_Multisource *_driverMsSfx; - bool _map_mt32_to_gm; - bool _nativeMT32; - - MusicInfo _music; - MusicInfo _sfx; - MusicInfo *_current; // Allows us to establish current context for operations. + // MIDI parser and data used for music. MidiParser *_parserMusic; byte *_musicData; + // MIDI parser and data used for SFX (Simon 1 DOS floppy). MidiParser *_parserSfx; byte *_sfxData; + // Parser used for SFX (Elvira 2 and Waxworks DOS). + SfxParser_Accolade *_parserSfxAccolade; - // These are maintained for both music and SFX - byte _masterVolume; // 0-255 - byte _musicVolume; - byte _sfxVolume; bool _paused; - // These are only used for music. - byte _currentTrack; - bool _loopTrack; + // Queued music track data (Simon 2). byte _queuedTrack; bool _loopQueuedTrack; protected: static void onTimer(void *data); - void clearConstructs(); - void clearConstructs(MusicInfo &info); - void resetVolumeTable(); public: MidiPlayer(AGOSEngine *vm); - ~MidiPlayer() override; + ~MidiPlayer(); - // Loads music data supported by the MidiParser used for the detected - // version of the game. Specify sfx to indicate that this is a MIDI sound - // effect. - void loadMusic(Common::SeekableReadStream *in, int32 size = -1, bool sfx = false); + // Creates and opens the relevant parsers and drivers for the game version + // and selected sound device. + int open(); + + // Loads music or SFX data supported by the MidiParser or SfxParser used + // for the detected version of the game. Specify sfx to indicate that this + // is a synthesized sound effect. + void load(Common::SeekableReadStream *in, int32 size = -1, bool sfx = false); /** - * Plays the currently loaded music data. If the loaded MIDI data has + * Plays the currently loaded music or SFX data. If the loaded data has * multiple tracks, specify track to select the track to play. * * @param track The track to play. Default 0. - * @param sfx True if the SFX MIDI data should be played, otherwise the - * loaded music data will be played. Default false. + * @param sfx True if the SFX data should be played, otherwise the loaded + * music data will be played. Default false. * @param sfxUsesRhythm True if the sound effect uses OPL rhythm * instruments. Default false. * @param queued True if this track was queued; false if it was played * directly. Default false. */ void play(int track = 0, bool sfx = false, bool sfxUsesRhythm = false, bool queued = false); - - void loadSMF(Common::SeekableReadStream *in, int song, bool sfx = false); - void loadMultipleSMF(Common::SeekableReadStream *in, bool sfx = false); - void loadXMIDI(Common::SeekableReadStream *in, bool sfx = false); - void loadS1D(Common::SeekableReadStream *in, bool sfx = false); - + // Returns true if the playback device uses MT-32 MIDI data; false it it // uses a different data type. bool usesMT32Data() const; - bool hasAdLibSfx() const; + // Returns true if the game version and selected sound device can use MIDI + // (or synthesized) sound effects. + bool hasMidiSfx() const; void setLoop(bool loop); // Activates or deactivates remapping GM to MT-32 instruments for // Simon 2 track 10. void setSimon2Remapping(bool remap); - void startTrack(int track); void queueTrack(int track, bool loop); bool isPlaying(bool checkQueued = false); - void stop(); - void stopSfx(); + void stop(bool sfx = false); void pause(bool b); void fadeOut(); - void setVolume(int musicVol, int sfxVol); void syncSoundSettings(); -public: - int open(int gameType, Common::Platform platform, bool isDemo); - - // MidiDriver_BASE interface implementation - void send(uint32 b) override; - void metaEvent(byte type, byte *data, uint16 length) override; - private: - kMusicMode _musicMode; + bool _pc98; // The type of the music device selected for playback. MusicType _deviceType; // The type of the MIDI data of the game (MT-32 or GM). diff --git a/engines/agos/midiparser_s1d.cpp b/engines/agos/midiparser_s1d.cpp index 19fbfae0219..ede991aa038 100644 --- a/engines/agos/midiparser_s1d.cpp +++ b/engines/agos/midiparser_s1d.cpp @@ -41,16 +41,37 @@ private: byte *start, *end; } _loops[16]; + // Data for monophonic chords mode. + // If this is activated, when multiple notes are played at the same time on + // a melodic channel (0-5), only the highest note will be played. + // This functionality is used by Elvira 2 (although there are no chords in + // the MIDI data), Waxworks and the Simon 1 floppy demo. + + // The highest note played at _lastPlayedNoteTime for each channel. + byte _highestNote[6]; + // The timestamp at which the last note was played at each channel. + uint32 _lastPlayedNoteTime[6]; + + // True if, for notes played at the same time, only the highest note should + // be played. If false, all notes of a chord will be sent to the driver. + bool _monophonicChords; + uint32 readVLQ2(byte *&data); - void chainEvent(EventInfo &info); protected: void parseNextEvent(EventInfo &info) override; + bool processEvent(const EventInfo &info, bool fireEvents = true) override; void resetTracking() override; -public: - MidiParser_S1D() : _data(nullptr), _noDelta(false) {} + public: + MidiParser_S1D(uint8 source = 0, bool monophonicChords = false) : MidiParser(source), + _monophonicChords(monophonicChords), _data(nullptr), _noDelta(false) { + Common::fill(_loops, _loops + ARRAYSIZE(_loops), Loop { 0, 0, 0 }); + Common::fill(_highestNote, _highestNote + ARRAYSIZE(_highestNote), 0); + Common::fill(_lastPlayedNoteTime, _lastPlayedNoteTime + ARRAYSIZE(_lastPlayedNoteTime), 0); + } bool loadMusic(byte *data, uint32 size) override; + int32 determineDataSize(Common::SeekableReadStream *stream) override; }; uint32 MidiParser_S1D::readVLQ2(byte *&data) { @@ -66,17 +87,11 @@ uint32 MidiParser_S1D::readVLQ2(byte *&data) { return delta; } -void MidiParser_S1D::chainEvent(EventInfo &info) { - // When we chain an event, we add up the old delta. - uint32 delta = info.delta; - parseNextEvent(info); - info.delta += delta; -} - void MidiParser_S1D::parseNextEvent(EventInfo &info) { info.start = _position._playPos; info.length = 0; info.delta = _noDelta ? 0 : readVLQ2(_position._playPos); + info.noop = false; _noDelta = false; info.event = *_position._playPos++; @@ -126,26 +141,26 @@ void MidiParser_S1D::parseNextEvent(EventInfo &info) { // Go to the start of the loop _position._playPos = _loops[info.channel()].start; + info.loop = true; } } else { - if (_loops[info.channel()].timer) + if (_loops[info.channel()].timer) { _position._playPos = _loops[info.channel()].start; + info.loop = true; + } --_loops[info.channel()].timer; } } - - // We need to read the next midi event here. Since we can not - // safely pass this event to the MIDI event processing. - chainEvent(info); + // Event has been fully processed here. + info.noop = true; } break; case 0xB: // auto stop marker(?) // In case the stop mode(?) is set to 0x80 this will stop the // track. - // We need to read the next midi event here. Since we can not - // safely pass this event to the MIDI event processing. - chainEvent(info); + // Event has been fully processed here. + info.noop = true; break; case 0xC: // program change @@ -157,9 +172,8 @@ void MidiParser_S1D::parseNextEvent(EventInfo &info) { if (_loops[info.channel()].end) _position._playPos = _loops[info.channel()].end; - // We need to read the next midi event here. Since we can not - // safely pass this event to the MIDI event processing. - chainEvent(info); + // Event has been fully processed here. + info.noop = true; break; default: @@ -167,14 +181,38 @@ void MidiParser_S1D::parseNextEvent(EventInfo &info) { // not to be MIDI related. warning("MidiParser_S1D: default case %d", info.channel()); - // We need to read the next midi event here. Since we can not - // safely pass this event to the MIDI event processing. - chainEvent(info); + // Event has been fully processed here. + info.noop = true; break; } } } +bool MidiParser_S1D::processEvent(const EventInfo &info, bool fireEvents) { + byte channel = info.channel(); + if (_monophonicChords && channel < 6 && info.command() == 0x9 && info.basic.param2 > 0) { + // In monophonic chords mode, when multiple notes are played at the + // same time on a melodic channel (0-5), only the highest note should + // be played. + if (_lastPlayedNoteTime[channel] == _position._playTick && _highestNote[channel] > info.basic.param1) { + // This note is lower than a previously played note on the same + // channel and with the same timestamp. Ignore it. + return true; + } else { + // This note either has a different timestamp (i.e. it is not + // played at the same time), or it is higher than the previously + // played note. + // Update the timestamp and note registry and play this note + // (because the channel is monophonic, a previously played lower + // note will be cut off). + _lastPlayedNoteTime[channel] = _position._playTick; + _highestNote[channel] = info.basic.param1; + } + } + + return MidiParser::processEvent(info, fireEvents); +} + bool MidiParser_S1D::loadMusic(byte *data, uint32 size) { unloadMusic(); @@ -182,7 +220,7 @@ bool MidiParser_S1D::loadMusic(byte *data, uint32 size) { return false; // The original actually just ignores the first two bytes. - byte *pos = data; + byte *pos = data + 2; if (*pos == 0xFC) { // SysEx found right at the start // this seems to happen since Elvira 2, we ignore it @@ -230,6 +268,11 @@ bool MidiParser_S1D::loadMusic(byte *data, uint32 size) { return true; } +int32 MidiParser_S1D::determineDataSize(Common::SeekableReadStream* stream) { + // Data size is stored in the first two bytes. + return stream->readUint16LE() + 2; +} + void MidiParser_S1D::resetTracking() { MidiParser::resetTracking(); // The first event never contains any delta. @@ -237,6 +280,6 @@ void MidiParser_S1D::resetTracking() { memset(_loops, 0, sizeof(_loops)); } -MidiParser *MidiParser_createS1D() { return new MidiParser_S1D; } +MidiParser *MidiParser_createS1D(uint8 source, bool monophonicChords) { return new MidiParser_S1D(source, monophonicChords); } } // End of namespace AGOS diff --git a/engines/agos/module.mk b/engines/agos/module.mk index 9e6f7546068..99c2468c856 100644 --- a/engines/agos/module.mk +++ b/engines/agos/module.mk @@ -6,7 +6,6 @@ MODULE_OBJS := \ drivers/accolade/pc98.o \ drivers/accolade/mt32.o \ drivers/simon1/adlib.o \ - drivers/simon1/adlib_win.o \ agos.o \ charset.o \ charset-fontdata.o \ @@ -40,6 +39,7 @@ MODULE_OBJS := \ script_ww.o \ script_s1.o \ script_s2.o \ + sfxparser_accolade.o \ sound.o \ string.o \ string_pn.o \ diff --git a/engines/agos/res_snd.cpp b/engines/agos/res_snd.cpp index 7c424ae77c0..097e9413044 100644 --- a/engines/agos/res_snd.cpp +++ b/engines/agos/res_snd.cpp @@ -137,7 +137,7 @@ void AGOSEngine::loadMusic(uint16 music, bool forceSimon2Gm) { uint16 indexBase = forceSimon2Gm ? MUSIC_INDEX_BASE_SIMON2_GM : _musicIndexBase; _gameFile->seek(_gameOffsetsPtr[indexBase + music - 1], SEEK_SET); - _midi->loadMusic(_gameFile); + _midi->load(_gameFile); // Activate Simon 2 GM to MT-32 remapping if we force GM, otherwise // deactivate it (in case it was previously activated). @@ -279,7 +279,7 @@ void AGOSEngine_Simon1::playMusic(uint16 music, uint16 track) { int size = SIMON1_GMF_SIZE[music]; _gameFile->seek(_gameOffsetsPtr[_musicIndexBase + music], SEEK_SET); - _midi->loadMusic(_gameFile, size); + _midi->load(_gameFile, size); _midi->play(); } else if (getPlatform() == Common::kPlatformDOS) { // DOS floppy version. @@ -292,7 +292,7 @@ void AGOSEngine_Simon1::playMusic(uint16 music, uint16 track) { if (f.isOpen() == false) error("playMusic: Can't load music from '%s'", filename); - _midi->loadMusic(&f, f.size()); + _midi->load(&f, f.size()); if (getFeatures() & GF_DEMO) { // Full version music data has a loop flag in the file header, but // the demo needs to have this set manually. @@ -304,7 +304,7 @@ void AGOSEngine_Simon1::playMusic(uint16 music, uint16 track) { // Windows version uses SMF data in one large data file. _gameFile->seek(_gameOffsetsPtr[_musicIndexBase + music], SEEK_SET); - _midi->loadMusic(_gameFile); + _midi->load(_gameFile); _midi->setLoop(true); _midi->play(); @@ -315,6 +315,47 @@ void AGOSEngine_Simon1::playMusic(uint16 music, uint16 track) { } } +void AGOSEngine_Simon1::playMidiSfx(uint16 sound) { + // The sound effects in floppy disk version of + // Simon the Sorcerer 1 are only meant for AdLib + if (!_midi->hasMidiSfx()) + return; + + // AdLib SFX use GMF data bundled in 9 STINGSx.MUS files. + char filename[16]; + Common::File mus_file; + + sprintf(filename, "STINGS%i.MUS", _soundFileId); + mus_file.open(filename); + if (!mus_file.isOpen()) + error("playSting: Can't load sound effect from '%s'", filename); + + // WORKAROUND Some Simon 1 DOS floppy SFX use the OPL rhythm instruments. + // This can conflict with the music using the rhythm instruments, so the + // original interpreter disables the music rhythm notes while a sound + // effect is playing. However, only some sound effects use rhythm notes, so + // in many cases this is not needed and leads to the music drums needlessly + // being disabled. + // To improve this, the sound effect number is checked against a list of + // SFX using rhythm notes, and only if it is in the list the music drums + // will be disabled while it plays. + bool rhythmSfx = false; + // Search for the file ID / SFX ID combination in the list of SFX that use + // rhythm notes. + byte sfxId = (_soundFileId << 4) | sound; + for (int i = 0; i < ARRAYSIZE(SIMON1_RHYTHM_SFX); i++) { + if (SIMON1_RHYTHM_SFX[i] == sfxId) { + rhythmSfx = true; + break; + } + } + + _midi->stop(true); + + _midi->load(&mus_file, mus_file.size(), true); + _midi->play(sound, true, rhythmSfx); +} + void AGOSEngine::playMusic(uint16 music, uint16 track) { stopMusic(); @@ -337,9 +378,9 @@ void AGOSEngine::playMusic(uint16 music, uint16 track) { str = file; } - _midi->loadS1D(str); - _midi->startTrack(0); - _midi->startTrack(track); + //warning("Playing track %d", music); + _midi->load(str); + _midi->play(); delete str; } } @@ -351,47 +392,6 @@ void AGOSEngine::stopMusic() { _mixer->stopHandle(_modHandle); } -void AGOSEngine::playSting(uint16 soundId) { - // The sound effects in floppy disk version of - // Simon the Sorcerer 1 are only meant for AdLib - if (!_midi->hasAdLibSfx()) - return; - - // AdLib SFX use GMF data bundled in 9 STINGSx.MUS files. - char filename[16]; - Common::File mus_file; - - sprintf(filename, "STINGS%i.MUS", _soundFileId); - mus_file.open(filename); - if (!mus_file.isOpen()) - error("playSting: Can't load sound effect from '%s'", filename); - - // WORKAROUND Some Simon 1 DOS floppy SFX use the OPL rhythm instruments. - // This can conflict with the music using the rhythm instruments, so the - // original interpreter disables the music rhythm notes while a sound - // effect is playing. However, only some sound effects use rhythm notes, so - // in many cases this is not needed and leads to the music drums needlessly - // being disabled. - // To improve this, the sound effect number is checked against a list of - // SFX using rhythm notes, and only if it is in the list the music drums - // will be disabled while it plays. - bool rhythmSfx = false; - // Search for the file ID / SFX ID combination in the list of SFX that use - // rhythm notes. - byte sfxId = (_soundFileId << 4) | soundId; - for (int i = 0; i < ARRAYSIZE(SIMON1_RHYTHM_SFX); i++) { - if (SIMON1_RHYTHM_SFX[i] == sfxId) { - rhythmSfx = true; - break; - } - } - - _midi->stopSfx(); - - _midi->loadMusic(&mus_file, mus_file.size(), true); - _midi->play(soundId, true, rhythmSfx); -} - static const byte elvira1_soundTable[100] = { 0, 2, 0, 1, 0, 0, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, @@ -571,6 +571,14 @@ void AGOSEngine::loadSound(uint16 sound, int16 pan, int16 vol, uint16 type) { _sound->playSfx5Data(dst, sound, pan, vol); } +void AGOSEngine::playSfx(uint16 sound, uint16 freq, uint16 flags, bool canUseMidiSfx) { + if (_useDigitalSfx) { + loadSound(sound, freq, flags); + } else if (canUseMidiSfx) { + playMidiSfx(sound); + } +} + void AGOSEngine::loadSound(uint16 sound, uint16 freq, uint16 flags) { byte *dst; uint32 offs, size = 0; @@ -611,7 +619,6 @@ void AGOSEngine::loadSound(uint16 sound, uint16 freq, uint16 flags) { if (size > _curSfxFileSize) error("loadSound: Reading beyond EOF (%d, %d)", size, _curSfxFileSize); - } size = READ_BE_UINT16(dst + 2); @@ -638,6 +645,29 @@ void AGOSEngine::loadSound(uint16 sound, uint16 freq, uint16 flags) { } } +void AGOSEngine::loadMidiSfx() { + if (!_midi->hasMidiSfx()) + return; + + Common::File fxb_file; + + Common::String filename = getGameType() == GType_ELVIRA2 ? "MYLIB.FXB" : "WAX.FXB"; + fxb_file.open(filename); + if (!fxb_file.isOpen()) + error("loadMidiSfx: Can't open sound effect bank '%s'", filename.c_str()); + + _midi->load(&fxb_file, fxb_file.size(), true); + + fxb_file.close(); +} + +void AGOSEngine::playMidiSfx(uint16 sound) { + if (!_midi->hasMidiSfx()) + return; + + _midi->play(sound, true); +} + void AGOSEngine::loadVoice(uint speechId) { if (getGameType() == GType_PP && speechId == 99) { _sound->stopVoice(); @@ -673,4 +703,10 @@ void AGOSEngine::loadVoice(uint speechId) { } } +void AGOSEngine::stopAllSfx() { + _sound->stopAllSfx(); + if (_midi->hasMidiSfx()) + _midi->stop(true); +} + } // End of namespace AGOS diff --git a/engines/agos/script_e2.cpp b/engines/agos/script_e2.cpp index 73b4af19853..eb5183aa215 100644 --- a/engines/agos/script_e2.cpp +++ b/engines/agos/script_e2.cpp @@ -581,7 +581,7 @@ void AGOSEngine_Elvira2::oe2_ifExitLocked() { void AGOSEngine_Elvira2::oe2_playEffect() { // 174: play sound uint soundId = getVarOrWord(); - loadSound(soundId, 0, 0); + playSfx(soundId, 0, 0, true); } void AGOSEngine_Elvira2::oe2_getDollar2() { diff --git a/engines/agos/script_s1.cpp b/engines/agos/script_s1.cpp index 31750926014..875353165a1 100644 --- a/engines/agos/script_s1.cpp +++ b/engines/agos/script_s1.cpp @@ -378,7 +378,7 @@ void AGOSEngine_Simon1::os1_playEffect() { uint16 soundId = getVarOrWord(); if (getGameId() == GID_SIMON1DOS) - playSting(soundId); + playSfx(soundId, 0, 0, true); else _sound->playEffects(soundId); } diff --git a/engines/agos/sfxparser_accolade.cpp b/engines/agos/sfxparser_accolade.cpp new file mode 100644 index 00000000000..a83baf604fb --- /dev/null +++ b/engines/agos/sfxparser_accolade.cpp @@ -0,0 +1,508 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#include "agos/sfxparser_accolade.h" + +#include "common/stream.h" +#include "common/textconsole.h" + +namespace AGOS { + +SfxParser_Accolade::SfxSlot::SfxSlot() { + clear(); +} + +void SfxParser_Accolade::SfxSlot::reset() { + noteFractionDelta = 0; + vibratoTime = 0; + vibratoCounter = 0; + vibratoDelta = 0; + waitCounter = 0; + loopStart = 0; + loopCounter = 0; +} + +void SfxParser_Accolade::SfxSlot::clear() { + allocated = false; + active = false; + source = -1; + scriptPos = 0; + playTime = 0; + lastEventTime = 0; + lastPlayedNote = -1; + currentNoteFraction = 0; + programChanged = false; + reset(); +} + +bool SfxParser_Accolade::SfxSlot::atEndOfScript() { + return scriptPos >= sfxData->scriptSize; +} + +int16 SfxParser_Accolade::SfxSlot::readScript(bool opCode) { + if (atEndOfScript()) + error("SfxParser_Accolade::SfxData::readScript - attempt to read past the end of the script"); + + int16 data = sfxData->scriptData[scriptPos]; + scriptPos++; + + if (opCode && (data <= 0 || data > 0xC)) { + // Any opcode outside the range 1-B will cause the script to stop. + data = 0xC; + } + + return data; +} + +const byte SfxParser_Accolade::INSTRUMENT_SIZE_MT32; + +SfxParser_Accolade::SfxParser_Accolade() : _driver(nullptr), _timerRate(0), _sfxData(), + _numSfx(0), _sourceAllocations { -1, -1, -1, -1 }, _paused(false) { } + +SfxParser_Accolade::~SfxParser_Accolade() { + stopAll(); + + if (_sfxData) { + delete[] _sfxData; + _sfxData = nullptr; + } +} + +void SfxParser_Accolade::load(Common::SeekableReadStream *in, int32 size) { + // First word is the total data size. + uint16 dataSize = in->readUint16LE(); + if (dataSize > size) + error("SfxParser_Accolade::load - Sound effect bank lists size %d but has file size %d", dataSize, size); + + // Next word is the number of SFX definitions. + _numSfx = in->readUint16LE(); + _sfxData = new SfxData[_numSfx]; + + // Next is a list of start offsets for each SFX definition. Combined with + // the total size the size of each SFX definition can be determined. + int64 indexStartPos = in->pos(); + for (int i = 0; i < _numSfx; i++) { + in->seek(indexStartPos + (i * 2)); + uint16 sfxDataOffset = in->readUint16LE(); + uint16 sfxDataEndOffset = i < _numSfx - 1 ? in->readUint16LE() : dataSize - 4; + in->seek(indexStartPos + sfxDataOffset); + uint16 sfxDataSize = sfxDataEndOffset - sfxDataOffset; + + // Read instrument definition. + readInstrument(&_sfxData[i], in); + + // Instrument data size is fixed; the reset of the SFX definition is + // the script. + int scriptSize = sfxDataSize - INSTRUMENT_SIZE_MT32 - INSTRUMENT_SIZE_ADLIB; + if (scriptSize < 2) + error("SfxParser_Accolade::load - Unexpected script size %d", scriptSize); + if (scriptSize % 2 != 0) + warning("SfxParser_Accolade::load - Script has odd number of bytes %d", scriptSize); + scriptSize >>= 1; + // Script size is stored in words. + _sfxData[i].scriptSize = scriptSize; + + // Read each word into the script data. + for (int j = 0; j < scriptSize; j++) { + _sfxData[i].scriptData[j] = in->readSint16LE(); + } + } +} + +void SfxParser_Accolade::setTimerRate(uint32 rate) { + _timerRate = rate; +} + +void SfxParser_Accolade::play(uint8 sfxNumber) { + Common::StackLock lock(_mutex); + + if (sfxNumber >= _numSfx) { + warning("SfxParser_Accolade::play - Sound effect %d requested but bank has only %d sound effects", sfxNumber, _numSfx); + return; + } + + // Find an unallocated slot. + SfxSlot *sfxSlot = nullptr; + int sfxSlotNum = -1; + for (int i = 0; i < ARRAYSIZE(_sfxSlots); i++) { + if (!_sfxSlots[i].allocated) { + _sfxSlots[i].allocated = true; + sfxSlot = &_sfxSlots[i]; + sfxSlotNum = i; + break; + } + } + + // Note that the original interpreter would only output MIDI data from 2 + // slots simultaneously, but potentially *all* SFX could be active at the + // same time (but the same sound effect could not play more than once at + // the same time). + // This implementation only allows 4 SFX active at the same time, which + // seems to be more than enough for Elvira 2 and Waxworks. + if (!sfxSlot) + return; + + // Allocate a source. + for (int i = 0; i < getNumberOfSfxSources(); i++) { + if (_sourceAllocations[i] == -1) { + _sourceAllocations[i] = sfxSlotNum; + sfxSlot->source = i + 1; + break; + } + } + + // Set the SFX data and load the instrument into the allocated channel. + sfxSlot->sfxData = &_sfxData[sfxNumber]; + sfxSlot->programChanged = loadInstrument(sfxSlot); + + // Activate the slot to start script execution. + sfxSlot->active = true; +} + +void SfxParser_Accolade::stopAll() { + Common::StackLock lock(_mutex); + + for (int i = 0; i < ARRAYSIZE(_sfxSlots); i++) { + if (_sfxSlots[i].active) + stop(&_sfxSlots[i]); + } +} + +void SfxParser_Accolade::pauseAll(bool paused) { + Common::StackLock lock(_mutex); + + if (_paused == paused) + return; + + _paused = paused; + + if (_paused) { + // Stop the current note for all active SFX. + for (int i = 0; i < ARRAYSIZE(_sfxSlots); i++) { + if (_sfxSlots[i].active) + noteOff(&_sfxSlots[i]); + } + } +} + +void SfxParser_Accolade::stop(SfxSlot *sfxSlot) { + noteOff(sfxSlot); + + // Deallocate the source. + if (sfxSlot->source >= 0) { + _driver->deinitSource(sfxSlot->source); + _sourceAllocations[sfxSlot->source - 1] = -1; + } + + // The original interpreter would try to re-assign the source to an active + // sound effect without a source. This is not implemented here because + // Elvira 2 and Waxworks use SFX very sparingly, so it seems very unlikely + // that more than 2 SFX would be active at the same time. + + sfxSlot->clear(); +} + +void SfxParser_Accolade::processOpCode(SfxSlot *sfxSlot, byte opCode) { + switch (opCode) { + case 0x1: + // Set note and note fraction delta. + sfxSlot->noteFractionDelta = sfxSlot->readScript(false); + break; + case 0x2: + // Clear note and note fraction delta. + sfxSlot->noteFractionDelta = 0; + break; + case 0x3: + // Set vibrato. + int16 vibratoTime; + vibratoTime = sfxSlot->readScript(false); + assert(vibratoTime >= 0); + sfxSlot->vibratoTime = vibratoTime; + // The counter starts at half the vibrato time, which causes the note + // frequency to move above and below the note fraction (like a sine + // wave). + sfxSlot->vibratoCounter = (vibratoTime >> 1) | 1; + sfxSlot->vibratoDelta = sfxSlot->readScript(false); + break; + case 0x4: + // Clear vibrato. + sfxSlot->vibratoTime = 0; + sfxSlot->vibratoDelta = 0; + break; + case 0x5: + // Wait. + sfxSlot->waitCounter = sfxSlot->readScript(false); + assert(sfxSlot->waitCounter >= 0); + break; + case 0x6: + // Play note. + noteOff(sfxSlot); + int8 note; + note = sfxSlot->readScript(false) & 0xFF; + assert(note >= 0 && note <= 0x7F); + sfxSlot->currentNoteFraction = note << 8; + noteOn(sfxSlot); + break; + case 0x7: + // Loop start. + + // Just register the loop start position. + sfxSlot->loopStart = sfxSlot->scriptPos; + break; + case 0x8: + // Loop next. + int16 loopParam; + loopParam = sfxSlot->readScript(false); + assert(loopParam >= 0); + if (sfxSlot->loopCounter == 0) { + // Loop counter has not been set yet, so do this now. + if (loopParam == 0) + // Loop infinitely. + loopParam = -1; + sfxSlot->loopCounter = loopParam; + // Go back to loop start. + sfxSlot->scriptPos = sfxSlot->loopStart; + } else { + // Decrease loop counter, unless the loop is infinite. + if (sfxSlot->loopCounter != -1) + sfxSlot->loopCounter--; + if (sfxSlot->loopCounter != 0) + // Go back to loop start. + sfxSlot->scriptPos = sfxSlot->loopStart; + // Else continue the script. + } + break; + case 0x9: + // Stop the current note. + noteOff(sfxSlot); + break; + case 0xA: + // Reset sound effect data. + sfxSlot->reset(); + // The original interpreter does this; not sure why... + sfxSlot->vibratoCounter = 1; + break; + case 0xB: + // Noop. Consumes 1 script parameter. + sfxSlot->scriptPos++; + break; + case 0xC: + default: + // Stop the sound effect. + stop(sfxSlot); + break; + } +} + +void SfxParser_Accolade::noteOn(SfxSlot *sfxSlot) { + byte note = sfxSlot->currentNoteFraction >> 8; + if (sfxSlot->source >= 0) + // Send a note on with maximum velocity. + _driver->send(sfxSlot->source, 0x90 | (note << 8) | (0x7F << 16)); + sfxSlot->lastPlayedNote = note; +} + +void SfxParser_Accolade::noteOff(SfxSlot *sfxSlot) { + if (sfxSlot->lastPlayedNote < 0) + return; + + if (sfxSlot->source >= 0) + // Send a note off. + _driver->send(sfxSlot->source, 0x80 | (sfxSlot->lastPlayedNote << 8)); + sfxSlot->lastPlayedNote = -1; +} + +void SfxParser_Accolade::onTimer() { + Common::StackLock lock(_mutex); + + if (_paused) + return; + + for (int i = 0; i < ARRAYSIZE(_sfxSlots); i++) { + if (!_sfxSlots[i].active) + continue; + + if (!_sfxSlots[i].programChanged) { + // If the SFX instrument has not yet been set on the allocated + // channel, wait until the driver is ready. + if (!_driver->isReady(_sfxSlots[i].source)) + continue; + + // Then change to the SFX instrument. + changeInstrument(&_sfxSlots[i]); + _sfxSlots[i].programChanged = true; + } + + // Note that ScummVM timer callback frequency is limited by what SDL + // can provide, which is every 10ms minimum. The original interpreter + // processes a script tick about every 3.25ms. So every onTimer call + // multiple script ticks are processed at the same time. Because of + // this, the sound effect audio is not entirely accurate. + + // Determine the end time of the timer callback period that will be + // processed. + uint32 endTime = _sfxSlots[i].playTime + _timerRate; + // Process script ticks while the sound effect remains active. + while (_sfxSlots[i].active) { + // Determine the end time of the script tick that will be + // processed. + uint32 eventTime = _sfxSlots[i].lastEventTime + SCRIPT_TIMER_RATE; + if (eventTime > endTime) + // Script tick end time is after this timer callback period, so + // leave it to the next callback invocation. + break; + + // Process this script tick. + _sfxSlots[i].lastEventTime = eventTime; + + // Process vibrato counter. + if (_sfxSlots[i].vibratoCounter > 0) { + // Count down the vibrato counter. + _sfxSlots[i].vibratoCounter--; + } else { + // Vibrato counter has reached zero. The vibrato note fraction + // delta is negated, so that it will now be subtracted from the + // note fraction instead of added, or the other way around. + _sfxSlots[i].vibratoDelta = -_sfxSlots[i].vibratoDelta; + // Reset the vibrato counter so it counts down to the next + // delta negation. + _sfxSlots[i].vibratoCounter = _sfxSlots[i].vibratoTime; + } + + // Calculate the new note and note fraction by adding the deltas. + uint16 newNoteFraction = _sfxSlots[i].currentNoteFraction; + newNoteFraction += _sfxSlots[i].noteFractionDelta; + newNoteFraction += _sfxSlots[i].vibratoDelta; + + if (newNoteFraction != _sfxSlots[i].currentNoteFraction) { + // Update the note fraction. + _sfxSlots[i].currentNoteFraction = newNoteFraction; + updateNote(&_sfxSlots[i]); + } + + // Process the script. + if (_sfxSlots[i].waitCounter > 0) { + // Count down the wait counter. No script opcode will be + // processsed. + _sfxSlots[i].waitCounter--; + } else if (_sfxSlots[i].atEndOfScript()) { + // The end of the script has been reached, so stop the sound + // effect. + // Note that the original interpreter did not do any bounds + // checking. Some scripts are not terminated properly and would + // cause the original interpreter to overread into the + // instrument data of the next sound effect. + stop(&_sfxSlots[i]); + } else { + // Process the next script opcode. + byte opCode = _sfxSlots[i].readScript(true) & 0xFF; + processOpCode(&_sfxSlots[i], opCode); + } + } + + // If the sound effect is still active, update the play timestamp. + if (_sfxSlots[i].active) + _sfxSlots[i].playTime = endTime; + } +} + +void SfxParser_Accolade::timerCallback(void *data) { + ((SfxParser_Accolade *)data)->onTimer(); +} + +void SfxParser_Accolade_AdLib::setMidiDriver(MidiDriver_Multisource *driver) { + _driver = driver; + _adLibDriver = dynamic_cast(driver); + assert(_adLibDriver); +} + +byte SfxParser_Accolade_AdLib::getNumberOfSfxSources() { + // The number of available sources depends on the OPL chip emulation used. + return _adLibDriver->getNumberOfSfxSources(); +} + +void SfxParser_Accolade_AdLib::readInstrument(SfxData *sfxData, Common::SeekableReadStream *in) { + in->skip(INSTRUMENT_SIZE_MT32); + in->read(sfxData->instrumentDefinition, INSTRUMENT_SIZE_ADLIB); +} + +bool SfxParser_Accolade_AdLib::loadInstrument(SfxSlot *sfxSlot) { + if (sfxSlot->source < 0) + return true; + + _adLibDriver->loadSfxInstrument(sfxSlot->source, sfxSlot->sfxData->instrumentDefinition); + // No separate instrument change is necessary for AdLib, so true is + // returned to indicate the instrument is already changed. + return true; +} + +void SfxParser_Accolade_AdLib::noteOn(SfxSlot *sfxSlot) { + if (sfxSlot->source >= 0) + // Set the current note fraction first. + _adLibDriver->setSfxNoteFraction(sfxSlot->source, sfxSlot->currentNoteFraction); + // Then start the note. + SfxParser_Accolade::noteOn(sfxSlot); +} + +void SfxParser_Accolade_AdLib::updateNote(SfxSlot *sfxSlot) { + if (sfxSlot->source < 0) + return; + + // Set the current note fraction first. + _adLibDriver->setSfxNoteFraction(sfxSlot->source, sfxSlot->currentNoteFraction); + // Then update the note. + _adLibDriver->updateSfxNote(sfxSlot->source); +} + +void SfxParser_Accolade_MT32::setMidiDriver(MidiDriver_Multisource *driver) { + _driver = driver; + _mt32Driver = dynamic_cast(driver); + assert(_mt32Driver); +} + +byte SfxParser_Accolade_MT32::getNumberOfSfxSources() { + // MT-32 always uses 2 SFX sources. + return 2; +} + +void SfxParser_Accolade_MT32::readInstrument(SfxData *sfxSlot, Common::SeekableReadStream *in) { + in->read(sfxSlot->instrumentDefinition, INSTRUMENT_SIZE_MT32); + in->skip(INSTRUMENT_SIZE_ADLIB); +} + +bool SfxParser_Accolade_MT32::loadInstrument(SfxSlot *sfxSlot) { + if (sfxSlot->source < 0) + return true; + + _mt32Driver->loadSfxInstrument(sfxSlot->source, sfxSlot->sfxData->instrumentDefinition); + // MT-32 requires a program change after the SysExes to load the instrument + // have been processed. Return false to indicate this. + return false; +} + +void SfxParser_Accolade_MT32::changeInstrument(SfxSlot *sfxData) { + if (sfxData->source < 0) + return; + + _mt32Driver->changeSfxInstrument(sfxData->source); +} + +} // End of namespace AGOS diff --git a/engines/agos/sfxparser_accolade.h b/engines/agos/sfxparser_accolade.h new file mode 100644 index 00000000000..d99040223b1 --- /dev/null +++ b/engines/agos/sfxparser_accolade.h @@ -0,0 +1,220 @@ +/* ScummVM - Graphic Adventure Engine + * + * ScummVM is the legal property of its developers, whose names + * are too numerous to list here. Please refer to the COPYRIGHT + * file distributed with this source distribution. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +#ifndef AGOS_SFXPARSER_ACCOLADE_H +#define AGOS_SFXPARSER_ACCOLADE_H + +#include "agos/drivers/accolade/adlib.h" +#include "agos/drivers/accolade/mt32.h" + +#include "common/mutex.h" +#include "common/stream.h" + +namespace AGOS { + +class SfxParser_Accolade { +public: + // Size in bytes of MT-32 instrument data in the SFX data. + static const byte INSTRUMENT_SIZE_MT32 = 0xF9; + +protected: + // Number of microseconds per script tick. + static const uint16 SCRIPT_TIMER_RATE = 3250; + // Size in bytes of AdLib instrument data in the SFX data. + static const byte INSTRUMENT_SIZE_ADLIB = 0x09; + // Maximum number of words in an SFX script. + static const byte MAX_SCRIPT_SIZE = 0x30; + // Maximum number of simultaneous sources for OPL2. + static const byte OPL2_NUM_SOURCES = 2; + // Maximum number of simultaneous sources for OPL3. + static const byte OPL3_NUM_SOURCES = 4; + + // Data for a single SFX. Taken from the game's SFX bank. + struct SfxData { + // The instrument data for the used sound device (OPL or MT-32). + byte instrumentDefinition[INSTRUMENT_SIZE_MT32]; + // The SFX script. + int16 scriptData[MAX_SCRIPT_SIZE]; + // The size in words of the SFX script. + int scriptSize; + }; + + // State data a SFX playback slot. + struct SfxSlot { + SfxSlot(); + + // The data of the SFX currently played by this slot. + SfxData *sfxData; + + // True if this slot has been allocated to playing a SFX. + bool allocated; + // True if SFX playback is active. + bool active; + // The source used to send data to the MIDI driver. + int8 source; + // Current position in the SFX script. + byte scriptPos; + + // Current playback time in microseconds. + uint32 playTime; + // The timestamp of the last processed script tick. + uint32 lastEventTime; + // The last MIDI note that was sent as a note on event. + int16 lastPlayedNote; + // The current MIDI note (upper byte) and note fraction (1/256th notes; + // lower byte) value. + uint16 currentNoteFraction; + // True if the allocated channel on the MIDI device has been changed to + // the SFX instrument. + bool programChanged; + + // Delta to the note fraction. This is added to/subtracted from the + // note fraction every script tick. + int16 noteFractionDelta; + // The vibrato time. The number of script ticks it takes for the note + // difference to go from lowest to highest (or the other way around). + int16 vibratoTime; + // The number of script ticks that have passed since the vibrato has + // started. + int16 vibratoCounter; + // Vibrato delta to the note fraction. This is added to/subtracted + // from the note fraction every script tick. + int16 vibratoDelta; + // The number of ticks that remain before the next script event is + // processed. + int16 waitCounter; + // The script position at which the current loop has started. + byte loopStart; + // The number of times the looped section will be repeated (-1 for + // infinite loop). + int16 loopCounter; + + // Completely clears the SFX slot data. + void clear(); + // Resets the SFX slot data as needed by the reset opcode. + void reset(); + // True if the current position is at the end of the script. + bool atEndOfScript(); + // Reads the next script word. Specify the opCode flag to return a + // valid opcode. + int16 readScript(bool opCode); + }; + +public: + SfxParser_Accolade(); + virtual ~SfxParser_Accolade(); + + // Loads the specified sound effects bank (FXB file). + void load(Common::SeekableReadStream *in, int32 size); + + // Sets the MIDI driver that should be used to output the SFX. + virtual void setMidiDriver(MidiDriver_Multisource *driver) = 0; + // Sets the number of microseconds between timer callbacks. + void setTimerRate(uint32 rate); + + // Starts playback of the specified sound effect. + void play(uint8 sfxNumber); + // Stops all active SFX. + void stopAll(); + // Pauses or unpauses all active SFX. + void pauseAll(bool paused); + + void onTimer(); + static void timerCallback(void *data); + +protected: + // Stops the sound effect playing in the specified slot. + void stop(SfxSlot *sfxSlot); + // Processes the specified opcode for the specified slot. + void processOpCode(SfxSlot *sfxSlot, byte opCode); + + // Returns the number of sources available for SFX playback. + virtual byte getNumberOfSfxSources() = 0; + // Reads the SFX instrument data into the specified SfxData from the + // specified SFX bank data. This is positioned at the start of the data of + // a sound effect in the bank; when the function returns is should be + // positioned right after all instrument data for the sound effect. + virtual void readInstrument(SfxData *sfxData, Common::SeekableReadStream *in) = 0; + + // Loads the SFX instrument for the specified slot into the channel + // allocated to the sound effect. Returns true if the channel needs to be + // changed to the new instrument when the driver is ready. + virtual bool loadInstrument(SfxSlot *sfxSlot) = 0; + // Changes the channel allocated to the sound effect to the SFX instrument. + virtual void changeInstrument(SfxSlot *sfxSlot) { }; + // Starts a note at the current note / note fraction for the slot. + virtual void noteOn(SfxSlot *sfxSlot); + // Stops the current note for the slot. + virtual void noteOff(SfxSlot *sfxSlot); + // Updates the note / note fraction for the slot. + virtual void updateNote(SfxSlot *sfxSlot) { }; + + Common::Mutex _mutex; + + MidiDriver_Multisource *_driver; + uint32 _timerRate; + + // Array of SFX data loaded from the SFX bank. + SfxData *_sfxData; + // The number of SFX data loaded. + uint16 _numSfx; + // The slots available for SFX playback. + SfxSlot _sfxSlots[4]; + // The slot numbers allocated to the available SFX sources. -1 if no slot + // is using the source. + int8 _sourceAllocations[4]; + + // True if SFX playback is paused. + bool _paused; +}; + +class SfxParser_Accolade_AdLib : public SfxParser_Accolade { +public: + SfxParser_Accolade_AdLib() : _adLibDriver(nullptr) { } + +protected: + void setMidiDriver(MidiDriver_Multisource *driver) override; + byte getNumberOfSfxSources() override; + void readInstrument(SfxData *sfxData, Common::SeekableReadStream *in) override; + bool loadInstrument(SfxSlot *sfxSlot) override; + void noteOn(SfxSlot *sfxSlot) override; + void updateNote(SfxSlot *sfxSlot) override; + + MidiDriver_Accolade_AdLib *_adLibDriver; +}; + +class SfxParser_Accolade_MT32 : public SfxParser_Accolade { +public: + SfxParser_Accolade_MT32() : _mt32Driver(nullptr) { } + +protected: + void setMidiDriver(MidiDriver_Multisource *driver) override; + byte getNumberOfSfxSources() override; + void readInstrument(SfxData *sfxData, Common::SeekableReadStream *in) override; + bool loadInstrument(SfxSlot *sfxSlot) override; + void changeInstrument(SfxSlot *sfxSlot) override; + + MidiDriver_Accolade_MT32 *_mt32Driver; +}; + +} // End of namespace AGOS + +#endif diff --git a/engines/agos/vga.cpp b/engines/agos/vga.cpp index a8a2046b5dc..4e969f94aab 100644 --- a/engines/agos/vga.cpp +++ b/engines/agos/vga.cpp @@ -1161,14 +1161,17 @@ void AGOSEngine::vc28_playSFX() { uint16 flags = vcReadNextWord(); debug(0, "vc28_playSFX: (sound %d, channels %d, frequency %d, flags %d)", sound, chans, freq, flags); - loadSound(sound, freq, flags); + // Waxworks DOS uses 2 opcodes to play SFX: this one for digital SFX and + // vc52 for MIDI SFX. If MIDI SFX are used, this opcode should be ignored; + // vc52 will play the corresponding MIDI SFX (if available). + playSfx(sound, freq, flags, !(getGameType() == GType_WW && getPlatform() == Common::kPlatformDOS)); } void AGOSEngine::vc29_stopAllSounds() { if (getGameType() != GType_PP) _sound->stopVoice(); - _sound->stopAllSfx(); + stopAllSfx(); } void AGOSEngine::vc30_setFrameRate() { diff --git a/engines/agos/vga_e2.cpp b/engines/agos/vga_e2.cpp index 24c86a86174..836a1d3145b 100644 --- a/engines/agos/vga_e2.cpp +++ b/engines/agos/vga_e2.cpp @@ -191,12 +191,8 @@ void AGOSEngine::vc52_playSound() { _sound->playEffects(sound); } else if (getFeatures() & GF_TALKIE) { _sound->playEffects(sound); - } else if (getGameId() == GID_SIMON1DOS) { - playSting(sound); - } else if (getGameType() == GType_WW) { - // TODO: Sound effects in PC version only } else { - loadSound(sound, 0, 0); + playSfx(sound, 0, 0, true); } }