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.
This commit is contained in:
Coen Rampen 2022-04-23 20:39:25 +02:00
parent 97745050c2
commit 66bb075f1c
24 changed files with 1892 additions and 2095 deletions

View File

@ -2,3 +2,4 @@ engines/agos/saveload.cpp
engines/agos/animation.cpp
engines/agos/metaengine.cpp
engines/agos/midi.cpp
engines/agos/detection.cpp

View File

@ -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();

View File

@ -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<uint32> &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;

View File

@ -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);

File diff suppressed because it is too large Load Diff

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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;

View File

@ -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

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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

File diff suppressed because it is too large Load Diff

View File

@ -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).

View File

@ -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

View File

@ -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 \

View File

@ -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

View File

@ -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() {

View File

@ -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);
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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<MidiDriver_Accolade_AdLib *>(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<MidiDriver_Accolade_MT32 *>(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

View File

@ -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 <http://www.gnu.org/licenses/>.
*
*/
#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

View File

@ -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() {

View File

@ -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);
}
}