[Scummvm-git-logs] scummvm master -> ad4faece761504eb0590faa6ca8ffc00e8d19e0e

elasota noreply at scummvm.org
Thu Dec 14 06:33:48 UTC 2023


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
ad4faece76 MTROPOLIS: Split MIDI functionality off to a separate plug-in.  This mainly avoids loading a MIDI driver when we don't a


Commit: ad4faece761504eb0590faa6ca8ffc00e8d19e0e
    https://github.com/scummvm/scummvm/commit/ad4faece761504eb0590faa6ca8ffc00e8d19e0e
Author: elasota (1137273+elasota at users.noreply.github.com)
Date: 2023-12-14T01:33:26-05:00

Commit Message:
MTROPOLIS: Split MIDI functionality off to a separate plug-in.  This mainly avoids loading a MIDI driver when we don't actually need it.

Changed paths:
  A engines/mtropolis/plugin/midi.cpp
  A engines/mtropolis/plugin/midi.h
  A engines/mtropolis/plugin/midi_data.cpp
  A engines/mtropolis/plugin/midi_data.h
    engines/mtropolis/boot.cpp
    engines/mtropolis/module.mk
    engines/mtropolis/plugin/standard.cpp
    engines/mtropolis/plugin/standard.h
    engines/mtropolis/plugin/standard_data.cpp
    engines/mtropolis/plugin/standard_data.h
    engines/mtropolis/plugins.h


diff --git a/engines/mtropolis/boot.cpp b/engines/mtropolis/boot.cpp
index cbb9cd8abe9..641f506c096 100644
--- a/engines/mtropolis/boot.cpp
+++ b/engines/mtropolis/boot.cpp
@@ -1078,6 +1078,7 @@ public:
 		kPlugInSPQR,
 		kPlugInStandard,
 		kPlugInObsidian,
+		kPlugInMIDI,
 	};
 
 	enum BitDepth {
@@ -1259,6 +1260,7 @@ void BootScriptContext::setEnhancedBitDepth(BitDepth bitDepth) {
 
 void BootScriptContext::bootObsidianRetailMacEn() {
 	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInMIDI);
 	addPlugIn(kPlugInStandard);
 
 	addArchive(kArchiveTypeStuffIt, "installer", "fs:Obsidian Installer");
@@ -1278,6 +1280,7 @@ void BootScriptContext::bootObsidianRetailMacEn() {
 
 void BootScriptContext::bootObsidianRetailMacJp() {
 	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInMIDI);
 	addPlugIn(kPlugInStandard);
 
 	addArchive(kArchiveTypeStuffIt, "installer", "fs:xn--u9j9ecg0a2fsa1io6k6jkdc2k");
@@ -1295,6 +1298,7 @@ void BootScriptContext::bootObsidianRetailMacJp() {
 
 void BootScriptContext::bootObsidianGeneric() {
 	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInMIDI);
 	addPlugIn(kPlugInStandard);
 
 	addSubtitles("subtitles_lines_obsidian_en.csv", "subtitles_speakers_obsidian_en.csv", "subtitles_asset_mapping_obsidian_en.csv", "subtitles_modifier_mapping_obsidian_en.csv");
@@ -1302,6 +1306,7 @@ void BootScriptContext::bootObsidianGeneric() {
 
 void BootScriptContext::bootObsidianRetailWinDe() {
 	addPlugIn(kPlugInObsidian);
+	addPlugIn(kPlugInMIDI);
 	addPlugIn(kPlugInStandard);
 
 	addArchive(kArchiveTypeInstallShieldV3, "installer", "_SETUP.1");
@@ -1405,7 +1410,8 @@ void BootScriptContext::executeFunction(const Common::String &functionName, cons
 	const EnumBinding plugInEnum[] = {ENUM_BINDING(kPlugInMTI),
 									  ENUM_BINDING(kPlugInStandard),
 									  ENUM_BINDING(kPlugInObsidian),
-									  ENUM_BINDING(kPlugInSPQR)};
+									  ENUM_BINDING(kPlugInSPQR),
+									  ENUM_BINDING(kPlugInMIDI)};
 
 	const EnumBinding bitDepthEnum[] = {ENUM_BINDING(kBitDepthAuto),
 										ENUM_BINDING(kBitDepth8),
@@ -1695,6 +1701,11 @@ Common::SharedPtr<MTropolis::PlugIn> loadStandardPlugIn(const MTropolisGameDescr
 	return standardPlugIn;
 }
 
+Common::SharedPtr<MTropolis::PlugIn> loadMIDIPlugIn(const MTropolisGameDescription &gameDesc) {
+	Common::SharedPtr<MTropolis::PlugIn> midiPlugIn = PlugIns::createMIDI();
+	return midiPlugIn;
+}
+
 Common::SharedPtr<MTropolis::PlugIn> loadObsidianPlugIn(const MTropolisGameDescription &gameDesc, Common::Archive &fs, const Common::Path &pluginsLocation) {
 	bool isMac = (gameDesc.desc.platform == Common::kPlatformMacintosh);
 	bool isRetail = ((gameDesc.desc.flags & ADGF_DEMO) == 0);
@@ -2420,6 +2431,9 @@ BootConfiguration bootProject(const MTropolisGameDescription &gameDesc) {
 		case Boot::BootScriptContext::kPlugInStandard:
 			plugIns.push_back(Boot::loadStandardPlugIn(gameDesc));
 			break;
+		case Boot::BootScriptContext::kPlugInMIDI:
+			plugIns.push_back(Boot::loadMIDIPlugIn(gameDesc));
+			break;
 		case Boot::BootScriptContext::kPlugInObsidian:
 			plugIns.push_back(Boot::loadObsidianPlugIn(gameDesc, *vfs, pluginsLocation));
 			break;
diff --git a/engines/mtropolis/module.mk b/engines/mtropolis/module.mk
index 21f62aec292..7adee786548 100644
--- a/engines/mtropolis/module.mk
+++ b/engines/mtropolis/module.mk
@@ -16,6 +16,8 @@ MODULE_OBJS = \
 	modifiers.o \
 	modifier_factory.o \
 	mtropolis.o \
+	plugin/midi.o \
+	plugin/midi_data.o \
 	plugin/mti.o \
 	plugin/mti_data.o \
 	plugin/obsidian.o \
diff --git a/engines/mtropolis/plugin/midi.cpp b/engines/mtropolis/plugin/midi.cpp
new file mode 100644
index 00000000000..7a5cb868522
--- /dev/null
+++ b/engines/mtropolis/plugin/midi.cpp
@@ -0,0 +1,1973 @@
+/* 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 "common/config-manager.h"
+#include "common/file.h"
+#include "common/random.h"
+
+#include "audio/mididrv.h"
+#include "audio/midiparser.h"
+#include "audio/midiparser_smf.h"
+#include "audio/midiplayer.h"
+
+#include "mtropolis/miniscript.h"
+#include "mtropolis/plugin/midi.h"
+#include "mtropolis/plugins.h"
+
+#include "mtropolis/miniscript.h"
+
+namespace MTropolis {
+
+namespace Midi {
+
+class MultiMidiPlayer;
+
+// I guess this follows QuickTime quirks, but basically, mTropolis pipes multiple inputs to a single
+// output device, and is totally cool with multiple devices stomping each other.
+//
+// Obsidian actually has a timer that plays a MIDI file that fires AllNoteOff on every channel every
+// 30 seconds, presumably to work around stuck notes, along with workarounds for the workaround,
+// i.e. the intro sequence has a silent part exactly 30 seconds in timed to sync up with the mute.
+//
+// NOTE: Due to SharedPtr not currently being atomic, MidiFilePlayers MUST BE DESTROYED ON THE
+// MAIN THREAD so there is no contention over the file refcount.
+class MidiFilePlayer {
+public:
+	virtual ~MidiFilePlayer();
+};
+
+class MidiNotePlayer {
+public:
+	virtual ~MidiNotePlayer();
+};
+
+class MidiCombinerSource : public MidiDriver_BASE {
+public:
+	virtual ~MidiCombinerSource();
+
+	// Do not call this directly, it's not thread-safe, expose via MultiMidiPlayer
+	virtual void setVolume(uint8 volume) = 0;
+	virtual void detach() = 0;
+};
+
+MidiCombinerSource::~MidiCombinerSource() {
+}
+
+class MidiCombiner {
+public:
+	virtual ~MidiCombiner();
+
+	virtual Common::SharedPtr<MidiCombinerSource> createSource() = 0;
+};
+
+MidiCombiner::~MidiCombiner() {
+}
+
+class MidiParser_MTropolis : public MidiParser_SMF {
+public:
+	MidiParser_MTropolis(bool hasTempoOverride, double tempoOverride, uint16 mutedTracks);
+
+	void setTempo(uint32 tempo) override;
+
+	void setTempoOverride(double tempoOverride);
+	void setMutedTracks(uint16 mutedTracks);
+
+protected:
+	bool processEvent(const EventInfo &info, bool fireEvents) override;
+
+private:
+	double _tempoOverride;
+	uint16 _mutedTracks;
+	bool _hasTempoOverride;
+};
+
+MidiParser_MTropolis::MidiParser_MTropolis(bool hasTempoOverride, double tempoOverride, uint16 mutedTracks)
+	: _hasTempoOverride(hasTempoOverride), _tempoOverride(tempoOverride), _mutedTracks(mutedTracks) {
+}
+
+void MidiParser_MTropolis::setTempo(uint32 tempo) {
+	if (_hasTempoOverride)
+		return;
+
+	MidiParser_SMF::setTempo(tempo);
+}
+
+void MidiParser_MTropolis::setTempoOverride(double tempoOverride) {
+	_hasTempoOverride = true;
+
+	if (tempoOverride < 1.0)
+		tempoOverride = 1.0;
+
+	_tempoOverride = tempoOverride;
+
+	uint32 convertedTempo = static_cast<uint32>(60000000.0 / tempoOverride);
+
+	MidiParser_SMF::setTempo(convertedTempo);
+}
+
+void MidiParser_MTropolis::setMutedTracks(uint16 mutedTracks) {
+	_mutedTracks = mutedTracks;
+}
+
+bool MidiParser_MTropolis::processEvent(const EventInfo &info, bool fireEvents) {
+	if ((info.event & 0xf0) == MidiDriver_BASE::MIDI_COMMAND_NOTE_ON) {
+		int track = _noteChannelToTrack[info.event & 0xf];
+		if (track >= 0 && (_mutedTracks & (1 << track)))
+			return true;
+	}
+
+	return MidiParser_SMF::processEvent(info, fireEvents);
+}
+
+class MidiFilePlayerImpl : public MidiFilePlayer {
+public:
+	explicit MidiFilePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, const Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks);
+	~MidiFilePlayerImpl();
+
+	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
+	void stop();
+	void play();
+	void pause();
+	void resume();
+	void setVolume(uint8 volume);
+	void setLoop(bool loop);
+	void setTempoOverride(double tempo);
+	void setMutedTracks(uint16 mutedTracks);
+	void detach();
+	void onTimer();
+
+private:
+	Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> _file;
+	Common::SharedPtr<MidiParser_MTropolis> _parser;
+	Common::SharedPtr<MidiCombinerSource> _outputDriver;
+	uint16 _mutedTracks;
+	bool _loop;
+};
+
+class MidiNotePlayerImpl : public MidiNotePlayer {
+public:
+	explicit MidiNotePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, uint32 timerRate);
+	~MidiNotePlayerImpl();
+
+	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
+	void onTimer();
+	void play(uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration);
+	void stop();
+	void detach();
+
+private:
+	Common::SharedPtr<MidiCombinerSource> _outputDriver;
+	uint64 _durationRemaining;
+	uint32 _timerRate;
+	uint8 _channel;
+	uint8 _note;
+	uint8 _volume;
+	// uint8 _program;
+
+	bool _initialized;
+};
+
+class MultiMidiPlayer : public Audio::MidiPlayer {
+public:
+	explicit MultiMidiPlayer(bool useDynamicMidiMixer);
+	~MultiMidiPlayer();
+
+	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> &file, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks);
+	MidiNotePlayer *createNotePlayer();
+	void deleteFilePlayer(MidiFilePlayer *player);
+	void deleteNotePlayer(MidiNotePlayer *player);
+
+	Common::SharedPtr<MidiCombinerSource> createSource();
+
+	void setPlayerVolume(MidiFilePlayer *player, uint8 volume);
+	void setPlayerLoop(MidiFilePlayer *player, bool loop);
+	void setPlayerTempo(MidiFilePlayer *player, double tempo);
+	void setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks);
+	void stopPlayer(MidiFilePlayer *player);
+	void playPlayer(MidiFilePlayer *player);
+	void pausePlayer(MidiFilePlayer *player);
+	void resumePlayer(MidiFilePlayer *player);
+
+	void playNote(MidiNotePlayer *player, uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration);
+	void stopNote(MidiNotePlayer *player);
+
+	uint32 getBaseTempo() const;
+
+	void send(uint32 b) override;
+
+private:
+	void onTimer() override;
+
+	static void timerCallback(void *refCon);
+
+	Common::Mutex _mutex;
+	Common::Array<Common::SharedPtr<MidiFilePlayerImpl> > _filePlayers;
+	Common::Array<Common::SharedPtr<MidiNotePlayerImpl> > _notePlayers;
+	Common::SharedPtr<MidiCombiner> _combiner;
+};
+
+MidiFilePlayer::~MidiFilePlayer() {
+}
+
+MidiNotePlayer::~MidiNotePlayer() {
+}
+
+MidiFilePlayerImpl::MidiFilePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, const Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, bool hasTempoOverride, double tempo, uint8 volume, bool loop, uint16 mutedTracks)
+	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _loop(loop), _mutedTracks(mutedTracks) {
+	Common::SharedPtr<MidiParser_MTropolis> parser(new MidiParser_MTropolis(hasTempoOverride, tempo, mutedTracks));
+
+	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
+		_parser = parser;
+
+		parser->setTrack(0);
+		parser->startPlaying();
+		parser->setMidiDriver(outputDriver.get());
+		parser->setTimerRate(baseTempo);
+		parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
+	}
+}
+
+MidiFilePlayerImpl::~MidiFilePlayerImpl() {
+	assert(!_parser); // Call detach first!
+}
+
+void MidiFilePlayerImpl::stop() {
+	_parser->stopPlaying();
+}
+
+void MidiFilePlayerImpl::play() {
+	_parser->startPlaying();
+}
+
+void MidiFilePlayerImpl::pause() {
+	_parser->pausePlaying();
+}
+
+void MidiFilePlayerImpl::resume() {
+	_parser->resumePlaying();
+}
+
+void MidiFilePlayerImpl::setVolume(uint8 volume) {
+	_outputDriver->setVolume(volume);
+}
+
+void MidiFilePlayerImpl::setMutedTracks(uint16 mutedTracks) {
+	_mutedTracks = mutedTracks;
+	_parser->setMutedTracks(mutedTracks);
+}
+
+void MidiFilePlayerImpl::setLoop(bool loop) {
+	_loop = loop;
+	_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
+}
+
+void MidiFilePlayerImpl::setTempoOverride(double tempo) {
+	_parser->setTempoOverride(tempo);
+}
+
+void MidiFilePlayerImpl::detach() {
+	if (_parser) {
+		_parser->setMidiDriver(nullptr);
+		_parser.reset();
+	}
+
+	if (_outputDriver) {
+		_outputDriver->detach();
+		_outputDriver.reset();
+	}
+}
+
+void MidiFilePlayerImpl::onTimer() {
+	if (_parser)
+		_parser->onTimer();
+}
+
+MidiNotePlayerImpl::MidiNotePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, uint32 timerRate)
+	: _timerRate(timerRate), _durationRemaining(0), _outputDriver(outputDriver), _channel(0), _note(0), /* _program(0), */ _initialized(false), _volume(100) {
+}
+
+MidiNotePlayerImpl::~MidiNotePlayerImpl() {
+}
+
+void MidiNotePlayerImpl::onTimer() {
+	if (_durationRemaining > 0) {
+		if (_durationRemaining <= _timerRate) {
+			stop();
+			assert(_durationRemaining == 0);
+		} else {
+			_durationRemaining -= _timerRate;
+		}
+	}
+}
+
+void MidiNotePlayerImpl::play(uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration) {
+	if (duration < 0.000001)
+		return;
+
+	if (_durationRemaining)
+		stop();
+
+	_initialized = true;
+
+	_durationRemaining = static_cast<uint64>(duration * 1000000);
+	_channel = channel;
+	_note = note;
+	_volume = volume;
+
+	// GM volume scale to linear is x^4 so we need the 4th root of the volume, and need to rescale it from 0-100 to 0-0x3f80
+	// = 0x3f80 / sqrt(sqrt(100))
+	const double volumeMultiplier = 5140.5985643697174420974013458299;
+
+	if (volume > 100)
+		volume = 100;
+	uint16 hpVolume = static_cast<uint16>(floor(sqrt(sqrt(volume)) * volumeMultiplier));
+
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE | _channel, program, 0);
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION, 127);
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_REVERB, 0);
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_VOLUME, (hpVolume >> 7) & 0x7f);
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_VOLUME + 32, hpVolume & 0x7f);
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_NOTE_ON | _channel, note, velocity);
+}
+
+void MidiNotePlayerImpl::stop() {
+	if (!_durationRemaining)
+		return;
+
+	_durationRemaining = 0;
+	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF | _channel, _note, 0);
+}
+
+void MidiNotePlayerImpl::detach() {
+	if (_outputDriver) {
+		if (_durationRemaining)
+			stop();
+
+		_outputDriver->detach();
+		_outputDriver.reset();
+	}
+}
+
+// Simple combiner - Behaves "QuickTime-like" and all commands are passed through directly.
+// This applies volume by modulating note velocity.
+class MidiCombinerSimple;
+
+class MidiCombinerSourceSimple : public MidiCombinerSource {
+public:
+	explicit MidiCombinerSourceSimple(MidiCombinerSimple *combiner);
+
+	void setVolume(uint8 volume) override;
+	void detach() override;
+	void send(uint32 b) override;
+
+private:
+	MidiCombinerSimple *_combiner;
+	uint8 _volume;
+};
+
+class MidiCombinerSimple : public MidiCombiner {
+public:
+	explicit MidiCombinerSimple(MidiDriver_BASE *outputDriver);
+
+	Common::SharedPtr<MidiCombinerSource> createSource() override;
+
+	void send(uint32 b);
+
+private:
+	MidiDriver_BASE *_outputDriver;
+};
+
+MidiCombinerSourceSimple::MidiCombinerSourceSimple(MidiCombinerSimple *combiner) : _combiner(combiner), _volume(255) {
+}
+
+void MidiCombinerSourceSimple::setVolume(uint8 volume) {
+	_volume = volume;
+}
+
+void MidiCombinerSourceSimple::detach() {
+}
+
+void MidiCombinerSourceSimple::send(uint32 b) {
+	byte command = (b & 0xF0);
+
+	if (command == MIDI_COMMAND_NOTE_ON || command == MIDI_COMMAND_NOTE_OFF) {
+		byte velocity = (b >> 16) & 0xFF;
+		velocity = (velocity * _volume * 257 + 256) >> 16;
+		b = (b & 0xff00ffff) | (velocity << 16);
+	}
+
+	_combiner->send(b);
+}
+
+MidiCombinerSimple::MidiCombinerSimple(MidiDriver_BASE *outputDriver) : _outputDriver(outputDriver) {
+}
+
+Common::SharedPtr<MidiCombinerSource> MidiCombinerSimple::createSource() {
+	return Common::SharedPtr<MidiCombinerSource>(new MidiCombinerSourceSimple(this));
+}
+
+void MidiCombinerSimple::send(uint32 b) {
+	_outputDriver->send(b);
+}
+
+// Dynamic combiner - Dynamic channel allocation, accurate volume control
+class MidiCombinerDynamic;
+
+class MidiCombinerSourceDynamic : public MidiCombinerSource {
+public:
+	MidiCombinerSourceDynamic(MidiCombinerDynamic *combiner, uint sourceID);
+	~MidiCombinerSourceDynamic();
+
+	void setVolume(uint8 volume) override;
+	void send(uint32 b) override;
+
+	void detach() override;
+
+private:
+	MidiCombinerDynamic *_combiner;
+	uint _sourceID;
+};
+
+class MidiCombinerDynamic : public MidiCombiner {
+public:
+	MidiCombinerDynamic(MidiDriver_BASE *outputDriver);
+
+	Common::SharedPtr<MidiCombinerSource> createSource() override;
+
+	void deallocateSource(uint sourceID);
+
+	void setSourceVolume(uint sourceID, uint8 volume);
+	void sendFromSource(uint sourceID, uint32 b);
+	void sendFromSource(uint sourceID, uint8 cmd, uint8 channel, uint8 param1, uint8 param2);
+
+private:
+	static const uint kMSBMask = 0x3f80u;
+	static const uint kLSBMask = 0x7fu;
+	static const uint kLRControllerStart = 64;
+	static const uint kSostenutoOnThreshold = 64;
+	static const uint kSustainOnThreshold = 64;
+
+	enum DataEntryState {
+		kDataEntryStateNone,
+		kDataEntryStateRPN,
+		kDataEntryStateNRPN,
+	};
+
+	struct MidiChannelState {
+		MidiChannelState();
+
+		void reset();
+		void softReset(); // Executes changes corresponding to Reset All Controllers message
+
+		uint16 _program;
+		uint16 _aftertouch;
+		uint16 _pitchBend;
+		uint16 _rpnNumber;
+		uint16 _nrpnNumber;
+		DataEntryState _dataEntryState;
+
+		uint16 _hrControllers[32];
+		uint8 _lrControllers[32];
+
+		uint16 _registeredParams[5];
+	};
+
+	struct SourceChannelState {
+		SourceChannelState();
+		void reset();
+
+		MidiChannelState _midiChannelState;
+	};
+
+	struct SourceState {
+		SourceState();
+
+		void allocate();
+		void deallocate();
+
+		SourceChannelState _sourceChannelState[MidiDriver_BASE::MIDI_CHANNEL_COUNT];
+		uint16 _root4MasterVolume;
+		bool _isAllocated;
+	};
+
+	struct OutputChannelState {
+		OutputChannelState();
+
+		bool _hasSource;
+		bool _volumeIsAmbiguous;
+		uint _sourceID;
+		uint _channelID;
+		uint _noteOffCounter;
+
+		MidiChannelState _midiChannelState;
+
+		uint _numActiveNotes;
+	};
+
+	struct MidiActiveNote {
+		uint8 _outputChannel;
+		uint16 _tone;
+		bool _affectedBySostenuto;
+
+		// If either of these are set, then the note is off, but is sustained by a pedal
+		bool _isSustainedBySustain;
+		bool _isSustainedBySostenuto;
+	};
+
+	void doNoteOn(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doNoteOff(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doPolyphonicAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doControlChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doProgramChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doChannelAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doPitchBend(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+
+	void doHighRangeControlChange(uint sourceID, uint8 channel, uint8 hrParam, uint16 value);
+	void doLowRangeControlChange(uint sourceID, uint8 channel, uint8 lrParam, uint8 value);
+
+	void doDataEntry(uint sourceID, uint8 channel, int16 existingValueMask, int16 offset);
+	void doChannelMode(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
+	void doAllNotesOff(uint sourceID, uint8 channel, uint8 param2);
+	void doAllSoundOff(uint sourceID, uint8 channel, uint8 param2);
+	void doResetAllControllers(uint sourceID, uint8 channel, uint8 param2);
+
+	void sendToOutput(uint8 command, uint8 channel, uint8 param1, uint8 param2);
+
+	void syncSourceConfiguration(uint outputChannel, OutputChannelState &outChState, const SourceState &sourceState, const SourceChannelState &sourceChState);
+	void syncSourceHRController(uint outputChannel, OutputChannelState &outChState, const SourceState &sourceState, const SourceChannelState &sourceChState, uint hrController);
+	void syncSourceLRController(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint lrController);
+	void syncSourceRegisteredParam(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint rpn);
+
+	void tryCleanUpUnsustainedNote(uint noteIndex);
+
+	Common::Array<SourceState> _sources;
+	Common::Array<MidiActiveNote> _notes;
+	OutputChannelState _outputChannels[MidiDriver_BASE::MIDI_CHANNEL_COUNT];
+	uint _noteOffCounter;
+
+	MidiDriver_BASE *_outputDriver;
+
+	Common::SharedPtr<Common::DumpFile> _dumpFile;
+	int _eventCounter;
+};
+
+MidiCombinerSourceDynamic::MidiCombinerSourceDynamic(MidiCombinerDynamic *combiner, uint sourceID) : _combiner(combiner), _sourceID(sourceID) {
+}
+
+MidiCombinerSourceDynamic::~MidiCombinerSourceDynamic() {
+	assert(_combiner == nullptr); // Call detach first!
+}
+
+void MidiCombinerSourceDynamic::detach() {
+	_combiner->deallocateSource(_sourceID);
+	_combiner = nullptr;
+}
+
+void MidiCombinerSourceDynamic::setVolume(uint8 volume) {
+	_combiner->setSourceVolume(_sourceID, volume);
+}
+
+void MidiCombinerSourceDynamic::send(uint32 b) {
+	_combiner->sendFromSource(_sourceID, b);
+}
+
+MidiCombinerDynamic::MidiCombinerDynamic(MidiDriver_BASE *outputDriver) : _outputDriver(outputDriver), _noteOffCounter(1) {
+#if 0
+	_dumpFile.reset(new Common::DumpFile());
+	_dumpFile->open("mididump.csv");
+	_dumpFile->writeString("event\ttime\tchannel\tcmd\tparam1\tparam2\n");
+#endif
+
+	_eventCounter = 0;
+}
+
+Common::SharedPtr<MidiCombinerSource> MidiCombinerDynamic::createSource() {
+	uint sourceID = _sources.size();
+
+	for (uint i = 0; i < _sources.size(); i++) {
+		if (!_sources[i]._isAllocated) {
+			sourceID = i;
+			break;
+		}
+	}
+
+	if (sourceID == _sources.size())
+		_sources.push_back(SourceState());
+
+	_sources[sourceID].allocate();
+
+	return Common::SharedPtr<MidiCombinerSource>(new MidiCombinerSourceDynamic(this, sourceID));
+}
+
+void MidiCombinerDynamic::deallocateSource(uint sourceID) {
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+		if (!ch._hasSource || ch._sourceID != sourceID)
+			continue;
+
+		// Stop any outputs and release sustain
+		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN, 0);
+		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO, 0);
+		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+
+		ch._hasSource = false;
+		assert(ch._numActiveNotes == 0);
+	}
+
+	_sources[sourceID].deallocate();
+}
+
+void MidiCombinerDynamic::setSourceVolume(uint sourceID, uint8 volume) {
+	SourceState &src = _sources[sourceID];
+	src._root4MasterVolume = static_cast<uint16>(floor(sqrt(sqrt(volume)) * 16400.0));
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+		if (!ch._hasSource || ch._sourceID != sourceID)
+			continue;
+
+		// Synchronize volume control
+		syncSourceHRController(i, ch, src, src._sourceChannelState[ch._channelID], MidiDriver_BASE::MIDI_CONTROLLER_VOLUME);
+	}
+}
+
+void MidiCombinerDynamic::sendFromSource(uint sourceID, uint32 b) {
+	uint8 cmd = static_cast<uint8>(b & 0xf0);
+	uint8 channel = static_cast<uint8>(b & 0x0f);
+	uint8 param1 = static_cast<uint8>((b >> 8) & 0xff);
+	uint8 param2 = static_cast<uint8>((b >> 16) & 0xff);
+
+	sendFromSource(sourceID, cmd, channel, param1, param2);
+}
+
+void MidiCombinerDynamic::sendFromSource(uint sourceID, uint8 cmd, uint8 channel, uint8 param1, uint8 param2) {
+	switch (cmd) {
+	case MidiDriver_BASE::MIDI_COMMAND_NOTE_ON:
+		doNoteOn(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF:
+		doNoteOff(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH:
+		doPolyphonicAftertouch(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE:
+		doControlChange(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE:
+		doProgramChange(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH:
+		doChannelAftertouch(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND:
+		doPitchBend(sourceID, channel, param1, param2);
+		break;
+	case MidiDriver_BASE::MIDI_COMMAND_SYSTEM:
+		break;
+	}
+}
+
+void MidiCombinerDynamic::doNoteOn(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	uint outputChannel = 0;
+
+	if (channel == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL) {
+		outputChannel = MidiDriver_BASE::MIDI_RHYTHM_CHANNEL;
+	} else {
+		bool foundChannel = false;
+
+		// Find an existing exactly-matching channel
+		for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+			OutputChannelState &ch = _outputChannels[i];
+			if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+				foundChannel = true;
+				outputChannel = i;
+				break;
+			}
+		}
+
+		if (!foundChannel) {
+			// Find an inactive channel
+			for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+				if (i == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL)
+					continue;
+
+				if (!_outputChannels[i]._hasSource) {
+					foundChannel = true;
+					outputChannel = i;
+					break;
+				}
+			}
+		}
+
+		if (!foundChannel) {
+			uint bestOffCounter = 0xffffffffu;
+
+			// Find the channel that went quiet the longest time ago
+			for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+				if (i == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL)
+					continue;
+
+				if (_outputChannels[i]._numActiveNotes == 0 && _outputChannels[i]._noteOffCounter < bestOffCounter) {
+					foundChannel = true;
+					outputChannel = i;
+					bestOffCounter = _outputChannels[i]._noteOffCounter;
+				}
+			}
+		}
+
+		// All eligible channels are playing already
+		if (!foundChannel)
+			return;
+	}
+
+	OutputChannelState &ch = _outputChannels[outputChannel];
+
+	if (!ch._hasSource || ch._sourceID != sourceID || ch._channelID != channel) {
+		ch._sourceID = sourceID;
+		ch._channelID = channel;
+		ch._hasSource = true;
+
+		const SourceState &sourceState = _sources[sourceID];
+		syncSourceConfiguration(outputChannel, ch, sourceState, sourceState._sourceChannelState[channel]);
+	}
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_NOTE_ON, outputChannel, param1, param2);
+
+	MidiActiveNote note;
+	note._outputChannel = outputChannel;
+	note._tone = param1;
+	note._affectedBySostenuto = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold);
+	note._isSustainedBySostenuto = false;
+	note._isSustainedBySustain = false;
+	_notes.push_back(note);
+
+	ch._numActiveNotes++;
+}
+
+void MidiCombinerDynamic::doNoteOff(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF, i, param1, param2);
+
+			for (uint ani = 0; ani < _notes.size(); ani++) {
+				MidiActiveNote &note = _notes[ani];
+				if (note._outputChannel == i && note._tone == param1 && !note._isSustainedBySostenuto && !note._isSustainedBySustain) {
+					if (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] >= kSustainOnThreshold)
+						note._isSustainedBySustain = true;
+
+					if (note._affectedBySostenuto && ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold)
+						note._isSustainedBySostenuto = true;
+
+					tryCleanUpUnsustainedNote(ani);
+					break;
+				}
+			}
+
+			break;
+		}
+	}
+}
+
+void MidiCombinerDynamic::doPolyphonicAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		const OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH, i, param1, param2);
+			break;
+		}
+	}
+}
+
+void MidiCombinerDynamic::doControlChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	SourceState &src = _sources[sourceID];
+	SourceChannelState &sch = src._sourceChannelState[channel];
+
+	if (param1 == MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB) {
+		doDataEntry(sourceID, channel, kLSBMask, param2 << 7);
+		return;
+	} else if (param1 == MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB) {
+		doDataEntry(sourceID, channel, kMSBMask, param2);
+		return;
+	} else if (param1 < 32) {
+		uint16 ctrl = ((sch._midiChannelState._hrControllers[param1] & kLSBMask) | ((param2 & 0x7f)) << 7);
+		doHighRangeControlChange(sourceID, channel, param1, ctrl);
+		return;
+	} else if (param1 < 64) {
+		uint16 ctrl = ((sch._midiChannelState._hrControllers[param1 - 32] & kMSBMask) | (param2 & 0x7f));
+		doHighRangeControlChange(sourceID, channel, param1 - 32, ctrl);
+		return;
+	} else if (param1 < 96) {
+		doLowRangeControlChange(sourceID, channel, param1 - 64, param2);
+		return;
+	}
+
+	switch (param1) {
+	case 96:
+		// Data increment
+		doDataEntry(sourceID, channel, 0x3fff, 1);
+		break;
+	case 97:
+		// Data decrement
+		doDataEntry(sourceID, channel, 0x3fff, -1);
+		break;
+	case 98:
+		// NRPN LSB
+		sch._midiChannelState._nrpnNumber = ((sch._midiChannelState._nrpnNumber & kMSBMask) | (param2 & 0x7f));
+		sch._midiChannelState._dataEntryState = kDataEntryStateNRPN;
+		break;
+	case 99:
+		// NRPN MSB
+		sch._midiChannelState._nrpnNumber = ((sch._midiChannelState._nrpnNumber & kLSBMask) | ((param2 & 0x7f) << 7));
+		sch._midiChannelState._dataEntryState = kDataEntryStateNRPN;
+		break;
+	case 100:
+		// RPN LSB
+		sch._midiChannelState._rpnNumber = ((sch._midiChannelState._rpnNumber & kMSBMask) | (param2 & 0x7f));
+		sch._midiChannelState._dataEntryState = kDataEntryStateRPN;
+		break;
+	case 101:
+		// RPN MSB
+		sch._midiChannelState._rpnNumber = ((sch._midiChannelState._rpnNumber & kLSBMask) | ((param2 & 0x7f) << 7));
+		sch._midiChannelState._dataEntryState = kDataEntryStateRPN;
+		break;
+	default:
+		if (param1 >= 120 && param1 < 128)
+			doChannelMode(sourceID, channel, param1, param2);
+		break;
+	};
+}
+
+void MidiCombinerDynamic::doProgramChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE, i, param1, param2);
+			ch._midiChannelState._program = param1;
+			break;
+		}
+	}
+
+	_sources[sourceID]._sourceChannelState[channel]._midiChannelState._program = param1;
+}
+
+void MidiCombinerDynamic::doChannelAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH, i, param1, param2);
+			ch._midiChannelState._aftertouch = param1;
+			break;
+		}
+	}
+}
+
+void MidiCombinerDynamic::doPitchBend(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	uint16 pitchBend = (param1 & 0x7f) | ((param2 & 0x7f) << 7);
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND, i, param1, param2);
+			ch._midiChannelState._pitchBend = pitchBend;
+			break;
+		}
+	}
+
+	_sources[sourceID]._sourceChannelState[channel]._midiChannelState._pitchBend = pitchBend;
+}
+
+void MidiCombinerDynamic::doHighRangeControlChange(uint sourceID, uint8 channel, uint8 hrParam, uint16 value) {
+	SourceState &src = _sources[sourceID];
+	SourceChannelState &srcCh = src._sourceChannelState[channel];
+	srcCh._midiChannelState._hrControllers[hrParam] = value;
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			syncSourceHRController(i, ch, src, srcCh, hrParam);
+			break;
+		}
+	}
+}
+
+void MidiCombinerDynamic::doLowRangeControlChange(uint sourceID, uint8 channel, uint8 lrParam, uint8 value) {
+	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
+	srcCh._midiChannelState._lrControllers[lrParam] = value;
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			if (lrParam == MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart && value < kSustainOnThreshold) {
+				for (uint rni = _notes.size(); rni > 0; rni--) {
+					uint noteIndex = rni - 1;
+					MidiActiveNote &note = _notes[noteIndex];
+					if (note._isSustainedBySustain) {
+						note._isSustainedBySustain = false;
+						tryCleanUpUnsustainedNote(noteIndex);
+					}
+				}
+			} else if (lrParam == MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart && value < kSostenutoOnThreshold) {
+				for (uint rni = _notes.size(); rni > 0; rni--) {
+					uint noteIndex = rni - 1;
+					MidiActiveNote &note = _notes[noteIndex];
+					if (note._isSustainedBySostenuto) {
+						note._isSustainedBySostenuto = false;
+						tryCleanUpUnsustainedNote(noteIndex);
+					}
+				}
+			}
+
+			syncSourceLRController(i, ch, srcCh, lrParam);
+			break;
+		}
+	}
+}
+
+void MidiCombinerDynamic::doDataEntry(uint sourceID, uint8 channel, int16 existingValueMask, int16 offset) {
+	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
+
+	if (srcCh._midiChannelState._dataEntryState == kDataEntryStateRPN && srcCh._midiChannelState._rpnNumber < ARRAYSIZE(srcCh._midiChannelState._registeredParams)) {
+		int32 rp = srcCh._midiChannelState._registeredParams[srcCh._midiChannelState._rpnNumber];
+		rp &= existingValueMask;
+		rp += offset;
+
+		srcCh._midiChannelState._registeredParams[srcCh._midiChannelState._rpnNumber] = (rp & existingValueMask) + offset;
+
+		for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+			OutputChannelState &ch = _outputChannels[i];
+
+			if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+				syncSourceRegisteredParam(i, ch, srcCh, srcCh._midiChannelState._rpnNumber);
+				break;
+			}
+		}
+	}
+}
+
+void MidiCombinerDynamic::doChannelMode(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
+	// Remap omni/poly/mono modes to all notes off, since we don't do anything with omni/poly
+	switch (param1) {
+	case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_OFF:
+	case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_ON:
+	case MidiDriver_BASE::MIDI_CONTROLLER_MONO_ON:
+	case MidiDriver_BASE::MIDI_CONTROLLER_POLY_ON:
+	case MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF:
+		doAllNotesOff(sourceID, channel, param2);
+		break;
+	case MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF:
+		doAllSoundOff(sourceID, channel, param2);
+		break;
+	case MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS:
+		doResetAllControllers(sourceID, channel, param2);
+		break;
+	case 122: // Local control (ignore)
+	default:
+		break;
+	}
+}
+
+void MidiCombinerDynamic::doAllNotesOff(uint sourceID, uint8 channel, uint8 param2) {
+	uint outputChannel = 0;
+	bool foundChannel = false;
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			foundChannel = true;
+			outputChannel = i;
+			break;
+		}
+	}
+
+	if (!foundChannel)
+		return;
+
+	OutputChannelState &ch = _outputChannels[outputChannel];
+
+	bool sustainOn = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] >= kSustainOnThreshold);
+	bool sostenutoOn = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold);
+
+	for (uint rni = _notes.size(); rni > 0; rni--) {
+		uint noteIndex = rni - 1;
+		MidiActiveNote &note = _notes[noteIndex];
+		if (note._outputChannel == outputChannel) {
+			if (note._affectedBySostenuto && sostenutoOn)
+				note._isSustainedBySostenuto = true;
+			if (sustainOn)
+				note._isSustainedBySustain = true;
+
+			tryCleanUpUnsustainedNote(noteIndex);
+		}
+	}
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF, param2);
+}
+
+void MidiCombinerDynamic::doAllSoundOff(uint sourceID, uint8 channel, uint8 param2) {
+	uint outputChannel = 0;
+	bool foundChannel = false;
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			foundChannel = true;
+			outputChannel = i;
+			break;
+		}
+	}
+
+	if (!foundChannel)
+		return;
+
+	OutputChannelState &ch = _outputChannels[outputChannel];
+
+	for (uint rni = _notes.size(); rni > 0; rni--) {
+		uint noteIndex = rni - 1;
+		MidiActiveNote &note = _notes[noteIndex];
+		if (note._outputChannel == outputChannel) {
+			note._isSustainedBySostenuto = false;
+			note._isSustainedBySustain = false;
+
+			tryCleanUpUnsustainedNote(noteIndex);
+		}
+	}
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF, param2);
+	ch._noteOffCounter = 0; // All sound is off so this can be recycled quickly
+}
+
+void MidiCombinerDynamic::doResetAllControllers(uint sourceID, uint8 channel, uint8 param2) {
+	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
+
+	srcCh._midiChannelState.softReset();
+
+	uint outputChannel = 0;
+	bool foundChannel = false;
+
+	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
+		OutputChannelState &ch = _outputChannels[i];
+		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
+			foundChannel = true;
+			outputChannel = i;
+			break;
+		}
+	}
+
+	if (!foundChannel)
+		return;
+
+	OutputChannelState &ch = _outputChannels[outputChannel];
+	ch._midiChannelState.softReset();
+
+	// Release all sustained notes
+	for (uint rni = _notes.size(); rni > 0; rni--) {
+		uint noteIndex = rni - 1;
+		MidiActiveNote &note = _notes[noteIndex];
+		if (note._outputChannel == outputChannel) {
+			if (note._isSustainedBySostenuto || note._isSustainedBySustain) {
+				note._isSustainedBySostenuto = false;
+				note._isSustainedBySustain = false;
+				tryCleanUpUnsustainedNote(noteIndex);
+			}
+		}
+	}
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
+}
+
+void MidiCombinerDynamic::sendToOutput(uint8 command, uint8 channel, uint8 param1, uint8 param2) {
+	uint32 output = static_cast<uint32>(command) | static_cast<uint32>(channel) | static_cast<uint32>(param1 << 8) | static_cast<uint32>(param2 << 16);
+
+	if (_dumpFile) {
+		const int timestamp = g_system->getMillis(true);
+
+		const char *cmdName = "Unknown Command";
+		switch (command) {
+		case MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH:
+			cmdName = "ChannelAftertouch";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF:
+			cmdName = "NoteOff";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_NOTE_ON:
+			cmdName = "NoteOn";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH:
+			cmdName = "PolyAftertouch";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE:
+			cmdName = "ControlChange";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE:
+			cmdName = "ProgramChange";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND:
+			cmdName = "PitchBend";
+			break;
+		case MidiDriver_BASE::MIDI_COMMAND_SYSTEM:
+			cmdName = "System";
+			break;
+		default:
+			cmdName = "Unknown";
+		}
+
+		if (command == MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE) {
+			Common::String ctrlName = "Unknown";
+
+			switch (param1) {
+			case MidiDriver_BASE::MIDI_CONTROLLER_BANK_SELECT_MSB:
+				ctrlName = "BankSelect";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_MODULATION:
+				ctrlName = "Modulation";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB:
+				ctrlName = "DataEntryMSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_VOLUME:
+				ctrlName = "VolumeMSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_VOLUME + 32:
+				ctrlName = "VolumeLSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_BALANCE:
+				ctrlName = "Balance";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_PANNING:
+				ctrlName = "Panning";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION:
+				ctrlName = "Expression";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_BANK_SELECT_LSB:
+				ctrlName = "BankSelectLSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB:
+				ctrlName = "DataEntryLSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN:
+				ctrlName = "Sustain";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_PORTAMENTO:
+				ctrlName = "Portamento";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO:
+				ctrlName = "Sostenuto";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_SOFT:
+				ctrlName = "Soft";
+				break;
+
+			case MidiDriver_BASE::MIDI_CONTROLLER_REVERB:
+				ctrlName = "Reverb";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_CHORUS:
+				ctrlName = "Chorus";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_RPN_LSB:
+				ctrlName = "RPNLSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_RPN_MSB:
+				ctrlName = "RPNMSB";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF:
+				ctrlName = "AllSoundOff";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS:
+				ctrlName = "ResetAllControllers";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF:
+				ctrlName = "AllNotesOff";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_ON:
+				ctrlName = "OmniOn";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_OFF:
+				ctrlName = "OmniOff";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_MONO_ON:
+				ctrlName = "MonoOn";
+				break;
+			case MidiDriver_BASE::MIDI_CONTROLLER_POLY_ON:
+				ctrlName = "PolyOn";
+				break;
+			default:
+				ctrlName = Common::String::format("Unknown%02x", static_cast<int>(param1));
+			}
+
+			_dumpFile->writeString(Common::String::format("%i\t%i\t%i\t%s\t%s\t%i\n", _eventCounter, timestamp, static_cast<int>(channel), cmdName, ctrlName.c_str(), static_cast<int>(param2)));
+		} else
+			_dumpFile->writeString(Common::String::format("%i\t%i\t%i\t%s\t%i\t%i\n", _eventCounter, timestamp, static_cast<int>(channel), cmdName, static_cast<int>(param1), static_cast<int>(param2)));
+
+		_eventCounter++;
+	}
+
+	_outputDriver->send(output);
+}
+
+void MidiCombinerDynamic::syncSourceConfiguration(uint outputChannel, OutputChannelState &outChState, const SourceState &srcState, const SourceChannelState &sourceChState) {
+	const MidiChannelState &srcMidiChState = sourceChState._midiChannelState;
+	MidiChannelState &outState = outChState._midiChannelState;
+
+	if (outState._program != srcMidiChState._program) {
+		outState._program = srcMidiChState._program;
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE, outputChannel, srcMidiChState._program, 0);
+	}
+
+	if (outState._aftertouch != srcMidiChState._aftertouch) {
+		outState._aftertouch = srcMidiChState._aftertouch;
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH, outputChannel, srcMidiChState._aftertouch, 0);
+	}
+
+	if (outState._pitchBend != srcMidiChState._pitchBend) {
+		outState._pitchBend = srcMidiChState._pitchBend;
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND, outputChannel, (srcMidiChState._pitchBend & kLSBMask), (srcMidiChState._pitchBend & kMSBMask) >> 7);
+	}
+
+	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._hrControllers); i++)
+		syncSourceHRController(outputChannel, outChState, srcState, sourceChState, i);
+
+	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._lrControllers); i++)
+		syncSourceLRController(outputChannel, outChState, sourceChState, i);
+
+	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._registeredParams); i++)
+		syncSourceRegisteredParam(outputChannel, outChState, sourceChState, i);
+}
+
+void MidiCombinerDynamic::syncSourceHRController(uint outputChannel, OutputChannelState &outChState, const SourceState &srcState, const SourceChannelState &sourceChState, uint hrController) {
+	const MidiChannelState &srcMidiChState = sourceChState._midiChannelState;
+	MidiChannelState &outState = outChState._midiChannelState;
+
+	uint16 effectiveValue = srcMidiChState._hrControllers[hrController];
+
+	if (hrController == MidiDriver_BASE::MIDI_CONTROLLER_VOLUME) {
+		// GM volume to gain control is 40*log10(V/127)
+		// This means linear scale is (volume/0x3f80)^4
+		// To modulate the volume linearly, we must multiply the volume by the 4th root
+		// of the volume.
+		uint32 effectiveValueScaled = static_cast<uint32>(srcState._root4MasterVolume) * static_cast<uint32>(effectiveValue);
+		effectiveValueScaled += (effectiveValueScaled >> 16) + 1u;
+		effectiveValue = static_cast<uint16>(effectiveValueScaled >> 16);
+	}
+
+	if (outState._hrControllers[hrController] == effectiveValue)
+		return;
+
+	uint16 deltaBits = (outState._hrControllers[hrController] ^ effectiveValue);
+
+	if (deltaBits & kMSBMask)
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, hrController, (effectiveValue & kMSBMask) >> 7);
+	if (deltaBits & kLSBMask)
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, hrController + 32, effectiveValue & kLSBMask);
+
+	outState._hrControllers[hrController] = effectiveValue;
+}
+
+void MidiCombinerDynamic::syncSourceLRController(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint lrController) {
+	const MidiChannelState &srcState = sourceChState._midiChannelState;
+	MidiChannelState &outState = outChState._midiChannelState;
+
+	if (outState._lrControllers[lrController] == srcState._lrControllers[lrController])
+		return;
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, lrController + kLRControllerStart, srcState._lrControllers[lrController] & kLSBMask);
+
+	outState._lrControllers[lrController] = srcState._lrControllers[lrController];
+}
+
+void MidiCombinerDynamic::syncSourceRegisteredParam(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint rpn) {
+	const MidiChannelState &srcState = sourceChState._midiChannelState;
+	MidiChannelState &outState = outChState._midiChannelState;
+
+	if (outState._registeredParams[rpn] == srcState._registeredParams[rpn])
+		return;
+
+	outState._registeredParams[rpn] = srcState._registeredParams[rpn];
+
+	if (outState._dataEntryState != kDataEntryStateRPN || outState._rpnNumber != srcState._rpnNumber) {
+		outState._dataEntryState = kDataEntryStateRPN;
+		outState._rpnNumber = srcState._rpnNumber;
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RPN_LSB, rpn & kLSBMask);
+		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RPN_MSB, (rpn & kMSBMask) >> 7);
+	}
+
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB, srcState._registeredParams[rpn] & kLSBMask);
+	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB, (srcState._registeredParams[rpn] & kMSBMask) >> 7);
+}
+
+void MidiCombinerDynamic::tryCleanUpUnsustainedNote(uint noteIndex) {
+	MidiActiveNote &note = _notes[noteIndex];
+
+	if (!note._isSustainedBySostenuto && !note._isSustainedBySustain) {
+		OutputChannelState &outCh = _outputChannels[note._outputChannel];
+		assert(outCh._numActiveNotes > 0);
+		outCh._numActiveNotes--;
+		if (!outCh._numActiveNotes)
+			outCh._noteOffCounter = _noteOffCounter++;
+
+		_notes.remove_at(noteIndex);
+	}
+}
+
+MidiCombinerDynamic::MidiChannelState::MidiChannelState() {
+	reset();
+}
+
+void MidiCombinerDynamic::MidiChannelState::reset() {
+	_program = 0;
+	_aftertouch = 0;
+	_pitchBend = 0x2000;
+
+	for (uint i = 0; i < ARRAYSIZE(_hrControllers); i++)
+		_hrControllers[i] = 0;
+	for (uint i = 0; i < ARRAYSIZE(_lrControllers); i++)
+		_lrControllers[i] = 0;
+	for (uint i = 0; i < ARRAYSIZE(_registeredParams); i++)
+		_registeredParams[i] = 0;
+
+	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_BALANCE] = (64 << 7);
+	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_PANNING] = (64 << 7);
+	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_VOLUME] = (127 << 7);
+
+	_dataEntryState = kDataEntryStateNone;
+	_rpnNumber = 0;
+	_nrpnNumber = 0;
+}
+
+void MidiCombinerDynamic::MidiChannelState::softReset() {
+	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_MODULATION] = 0;
+	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] = 0;
+	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_PORTAMENTO - kLRControllerStart] = 0;
+	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] = 0;
+	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOFT - kLRControllerStart] = 0;
+	_dataEntryState = kDataEntryStateNone;
+	_rpnNumber = 0;
+	_nrpnNumber = 0;
+	_aftertouch = 0;
+	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION] = (127 << 7);
+	_pitchBend = (64 << 7);
+}
+
+MidiCombinerDynamic::SourceChannelState::SourceChannelState() {
+	reset();
+}
+
+void MidiCombinerDynamic::SourceChannelState::reset() {
+}
+
+MidiCombinerDynamic::SourceState::SourceState() : _isAllocated(false), _root4MasterVolume(0xffffu) {
+}
+
+void MidiCombinerDynamic::SourceState::allocate() {
+	_isAllocated = true;
+}
+
+void MidiCombinerDynamic::SourceState::deallocate() {
+	_isAllocated = false;
+}
+
+MidiCombinerDynamic::OutputChannelState::OutputChannelState() : _sourceID(0), _volumeIsAmbiguous(true), _channelID(0), _hasSource(false), _noteOffCounter(0), _numActiveNotes(0) {
+}
+
+MultiMidiPlayer::MultiMidiPlayer(bool dynamicMidiMixer) {
+	if (dynamicMidiMixer)
+		_combiner.reset(new MidiCombinerDynamic(this));
+	else
+		_combiner.reset(new MidiCombinerSimple(this));
+
+	createDriver(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
+
+	if (_driver->open() != 0) {
+		_driver->close();
+		delete _driver;
+		_driver = nullptr;
+		return;
+	}
+
+	_driver->setTimerCallback(this, &timerCallback);
+}
+
+MultiMidiPlayer::~MultiMidiPlayer() {
+	Common::StackLock lock(_mutex);
+	_filePlayers.clear();
+	_notePlayers.clear();
+}
+
+void MultiMidiPlayer::timerCallback(void *refCon) {
+	static_cast<MultiMidiPlayer *>(refCon)->onTimer();
+}
+
+void MultiMidiPlayer::onTimer() {
+	Common::StackLock lock(_mutex);
+
+	for (const Common::SharedPtr<MidiFilePlayerImpl> &player : _filePlayers)
+		player->onTimer();
+
+	for (const Common::SharedPtr<MidiNotePlayerImpl> &player : _notePlayers)
+		player->onTimer();
+}
+
+MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> &file, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks) {
+	Common::SharedPtr<MidiCombinerSource> combinerSource = createSource();
+	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(combinerSource, file, getBaseTempo(), hasTempoOverride, tempoOverride, volume, loop, mutedTracks));
+
+	{
+		Common::StackLock lock(_mutex);
+		combinerSource->setVolume(volume);
+		_filePlayers.push_back(filePlayer);
+	}
+
+	return filePlayer.get();
+}
+
+MidiNotePlayer *MultiMidiPlayer::createNotePlayer() {
+	Common::SharedPtr<MidiCombinerSource> combinerSource = createSource();
+	Common::SharedPtr<MidiNotePlayerImpl> notePlayer(new MidiNotePlayerImpl(combinerSource, getBaseTempo()));
+
+	{
+		Common::StackLock lock(_mutex);
+		_notePlayers.push_back(notePlayer);
+	}
+
+	return notePlayer.get();
+}
+
+Common::SharedPtr<MidiCombinerSource> MultiMidiPlayer::createSource() {
+	Common::StackLock lock(_mutex);
+	return _combiner->createSource();
+}
+
+void MultiMidiPlayer::deleteFilePlayer(MidiFilePlayer *player) {
+	Common::SharedPtr<MidiFilePlayerImpl> ref;
+
+	for (Common::Array<Common::SharedPtr<MidiFilePlayerImpl> >::iterator it = _filePlayers.begin(), itEnd = _filePlayers.end(); it != itEnd; ++it) {
+		if (it->get() == player) {
+			{
+				Common::StackLock lock(_mutex);
+				ref = *it;
+				_filePlayers.erase(it);
+				ref->stop();
+			}
+			break;
+		}
+	}
+
+	if (ref)
+		ref->detach();
+}
+
+void MultiMidiPlayer::deleteNotePlayer(MidiNotePlayer *player) {
+	Common::SharedPtr<MidiNotePlayerImpl> ref;
+
+	for (Common::Array<Common::SharedPtr<MidiNotePlayerImpl> >::iterator it = _notePlayers.begin(), itEnd = _notePlayers.end(); it != itEnd; ++it) {
+		if (it->get() == player) {
+			{
+				Common::StackLock lock(_mutex);
+				ref = *it;
+				_notePlayers.erase(it);
+				ref->stop();
+			}
+			break;
+		}
+	}
+
+	if (ref)
+		ref->detach();
+}
+
+void MultiMidiPlayer::setPlayerVolume(MidiFilePlayer *player, uint8 volume) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setVolume(volume);
+}
+
+void MultiMidiPlayer::setPlayerLoop(MidiFilePlayer *player, bool loop) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setLoop(loop);
+}
+
+void MultiMidiPlayer::setPlayerTempo(MidiFilePlayer *player, double tempo) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setTempoOverride(tempo);
+}
+
+void MultiMidiPlayer::setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->setMutedTracks(mutedTracks);
+}
+
+void MultiMidiPlayer::stopPlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->stop();
+}
+
+void MultiMidiPlayer::playPlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->play();
+}
+
+void MultiMidiPlayer::pausePlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->pause();
+}
+
+void MultiMidiPlayer::resumePlayer(MidiFilePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiFilePlayerImpl *>(player)->resume();
+}
+
+void MultiMidiPlayer::playNote(MidiNotePlayer *player, uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiNotePlayerImpl *>(player)->play(volume, channel, program, note, velocity, duration);
+}
+
+void MultiMidiPlayer::stopNote(MidiNotePlayer *player) {
+	Common::StackLock lock(_mutex);
+	static_cast<MidiNotePlayerImpl *>(player)->stop();
+}
+
+uint32 MultiMidiPlayer::getBaseTempo() const {
+	if (_driver)
+		return _driver->getBaseTempo();
+	return 1;
+}
+
+void MultiMidiPlayer::send(uint32 b) {
+	if (_driver)
+		_driver->send(b);
+}
+
+MidiModifier::MidiModifier() : _mode(kModeFile), _volume(100), _mutedTracks(0), /* _singleNoteChannel(0), _singleNoteNote(0), */
+							   _plugIn(nullptr), _filePlayer(nullptr), _notePlayer(nullptr) /*, _runtime(nullptr) */ {
+
+	memset(&this->_modeSpecific, 0, sizeof(this->_modeSpecific));
+}
+
+MidiModifier::~MidiModifier() {
+	if (_filePlayer)
+		_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
+
+	if (_notePlayer)
+		_plugIn->getMidi()->deleteNotePlayer(_notePlayer);
+}
+
+bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Midi::MidiModifier &data) {
+	_plugIn = static_cast<MidiPlugIn *>(context.plugIn);
+
+	if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	if (!_executeWhen.load(data.executeWhen.value.asEvent))
+		return false;
+
+	if (data.terminateWhen.type != Data::PlugInTypeTaggedValue::kEvent)
+		return false;
+
+	if (!_terminateWhen.load(data.terminateWhen.value.asEvent))
+		return false;
+
+	if (data.embeddedFlag) {
+		_mode = kModeFile;
+		_embeddedFile = data.embeddedFile;
+
+		_modeSpecific.file.loop = (data.modeSpecific.embedded.loop != 0);
+		_modeSpecific.file.overrideTempo = (data.modeSpecific.embedded.overrideTempo != 0);
+		_volume = data.modeSpecific.embedded.volume;
+
+		if (data.embeddedFadeIn.type != Data::PlugInTypeTaggedValue::kFloat || data.embeddedFadeOut.type != Data::PlugInTypeTaggedValue::kFloat || data.embeddedTempo.type != Data::PlugInTypeTaggedValue::kFloat)
+			return false;
+
+		_modeSpecific.file.fadeIn = data.embeddedFadeIn.value.asFloat.toXPFloat().toDouble();
+		_modeSpecific.file.fadeOut = data.embeddedFadeOut.value.asFloat.toXPFloat().toDouble();
+		_modeSpecific.file.tempo = data.embeddedTempo.value.asFloat.toXPFloat().toDouble();
+	} else {
+		_mode = kModeSingleNote;
+
+		if (data.singleNoteDuration.type != Data::PlugInTypeTaggedValue::kFloat)
+			return false;
+
+		_modeSpecific.singleNote.channel = data.modeSpecific.singleNote.channel;
+		_modeSpecific.singleNote.note = data.modeSpecific.singleNote.note;
+		_modeSpecific.singleNote.velocity = data.modeSpecific.singleNote.velocity;
+		_modeSpecific.singleNote.program = data.modeSpecific.singleNote.program;
+		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toXPFloat().toDouble();
+
+		_volume = 100;
+	}
+
+	return true;
+}
+
+bool MidiModifier::respondsToEvent(const Event &evt) const {
+	return _executeWhen.respondsTo(evt) || _terminateWhen.respondsTo(evt);
+}
+
+VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
+	if (_executeWhen.respondsTo(msg->getEvent())) {
+		const SubtitleTables &subtitleTables = runtime->getProject()->getSubtitles();
+		if (subtitleTables.modifierMapping) {
+			const Common::String *subSetIDPtr = subtitleTables.modifierMapping->findSubtitleSetForModifierGUID(getStaticGUID());
+			if (subSetIDPtr) {
+				// Don't currently support anything except play-on-start subs for MIDI
+				SubtitlePlayer subtitlePlayer(runtime, *subSetIDPtr, subtitleTables);
+				subtitlePlayer.update(0, 1);
+			}
+		}
+
+		if (_mode == kModeFile) {
+			if (_embeddedFile) {
+				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
+
+				const double tempo = _modeSpecific.file.overrideTempo ? _modeSpecific.file.tempo : 120.0;
+				if (!_filePlayer)
+					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _modeSpecific.file.overrideTempo, tempo, getBoostedVolume(runtime) * 255 / 100, _modeSpecific.file.loop, _mutedTracks);
+				_plugIn->getMidi()->playPlayer(_filePlayer);
+			} else {
+				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
+			}
+		} else if (_mode == kModeSingleNote) {
+			playSingleNote();
+		}
+	}
+	if (_terminateWhen.respondsTo(msg->getEvent())) {
+		disable(runtime);
+	}
+
+	return kVThreadReturn;
+}
+
+void MidiModifier::disable(Runtime *runtime) {
+	if (_filePlayer) {
+		_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
+		_filePlayer = nullptr;
+	}
+	if (_notePlayer) {
+		_plugIn->getMidi()->deleteNotePlayer(_notePlayer);
+		_notePlayer = nullptr;
+	}
+}
+
+void MidiModifier::playSingleNote() {
+	if (!_notePlayer)
+		_notePlayer = _plugIn->getMidi()->createNotePlayer();
+	_plugIn->getMidi()->playNote(_notePlayer, _volume, _modeSpecific.singleNote.channel, _modeSpecific.singleNote.program, _modeSpecific.singleNote.note, _modeSpecific.singleNote.velocity, _modeSpecific.singleNote.duration);
+}
+
+void MidiModifier::stopSingleNote() {
+	if (_notePlayer)
+		_plugIn->getMidi()->stopNote(_notePlayer);
+}
+
+bool MidiModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
+	if (attrib == "volume") {
+		result.setInt(_volume);
+		return true;
+	}
+
+	return Modifier::readAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
+	if (attrib == "volume") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetVolume, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "notevelocity") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteVelocity, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "noteduration") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteDuration, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "notenum") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteNum, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "loop") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetLoop, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "playnote") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetPlayNote, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "tempo") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetTempo, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	} else if (attrib == "mutetrack") {
+		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetMuteTrack, true>::create(this, result);
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttribute(thread, result, attrib);
+}
+
+MiniscriptInstructionOutcome MidiModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) {
+	if (attrib == "mutetrack") {
+		int32 asInteger = 0;
+		if (!index.roundToInt(asInteger) || asInteger < 1) {
+			thread->error("Invalid index for mutetrack");
+			return kMiniscriptInstructionOutcomeFailed;
+		}
+
+		result.pod.objectRef = this;
+		result.pod.ptrOrOffset = static_cast<uintptr>(asInteger) - 1;
+		result.pod.ifc = DynamicValueWriteInterfaceGlue<MuteTrackProxyInterface>::getInstance();
+
+		return kMiniscriptInstructionOutcomeContinue;
+	}
+
+	return Modifier::writeRefAttributeIndexed(thread, result, attrib, index);
+}
+
+uint MidiModifier::getBoostedVolume(Runtime *runtime) const {
+	uint boostedVolume = (_volume * runtime->getHacks().midiVolumeScale) >> 8;
+	if (boostedVolume > 100)
+		boostedVolume = 100;
+	return boostedVolume;
+}
+
+Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
+	Common::SharedPtr<MidiModifier> clone(new MidiModifier(*this));
+
+	clone->_notePlayer = nullptr;
+	clone->_filePlayer = nullptr;
+
+	return clone;
+}
+
+const char *MidiModifier::getDefaultName() const {
+	return "MIDI Modifier";
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 100)
+		asInteger = 100;
+
+	_volume = asInteger;
+
+	if (_mode == kModeFile) {
+		debug(2, "MIDI (%x '%s'): Changing volume to %i", getStaticGUID(), getName().c_str(), _volume);
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerVolume(_filePlayer, getBoostedVolume(thread->getRuntime()) * 255 / 100);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 127)
+		asInteger = 127;
+
+	if (_mode == kModeSingleNote) {
+		debug(2, "MIDI (%x '%s'): Changing note velocity to %i", getStaticGUID(), getName().c_str(), asInteger);
+		_modeSpecific.singleNote.velocity = asInteger;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetNoteDuration(MiniscriptThread *thread, const DynamicValue &value) {
+	double asDouble = 0.0;
+	if (value.getType() == DynamicValueTypes::kFloat) {
+		asDouble = value.getFloat();
+	} else {
+		DynamicValue converted;
+		if (!value.convertToType(DynamicValueTypes::kFloat, converted))
+			return kMiniscriptInstructionOutcomeFailed;
+		asDouble = converted.getFloat();
+	}
+
+	if (_mode == kModeSingleNote) {
+		debug(2, "MIDI (%x '%s'): Changing note duration to %g", getStaticGUID(), getName().c_str(), asDouble);
+		_modeSpecific.singleNote.duration = asDouble;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value) {
+	int32 asInteger = 0;
+	if (!value.roundToInt(asInteger))
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (asInteger < 0)
+		asInteger = 0;
+	else if (asInteger > 255)
+		asInteger = 255;
+
+	if (_mode == kModeSingleNote) {
+		debug(2, "MIDI (%x '%s'): Changing note number to %i", getStaticGUID(), getName().c_str(), asInteger);
+		_modeSpecific.singleNote.note = asInteger;
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean)
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (_mode == kModeFile) {
+		const bool loop = value.getBool();
+
+		debug(2, "MIDI (%x '%s'): Changing loop state to %s", getStaticGUID(), getName().c_str(), loop ? "true" : "false");
+		if (_modeSpecific.file.loop != loop) {
+			_modeSpecific.file.loop = loop;
+
+			if (_filePlayer)
+				_plugIn->getMidi()->setPlayerLoop(_filePlayer, loop);
+		}
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetTempo(MiniscriptThread *thread, const DynamicValue &value) {
+	double tempo = 0.0;
+	if (value.getType() == DynamicValueTypes::kInteger)
+		tempo = value.getInt();
+	else if (value.getType() == DynamicValueTypes::kFloat)
+		tempo = value.getFloat();
+	else
+		return kMiniscriptInstructionOutcomeFailed;
+
+	if (_mode == kModeFile) {
+		debug(2, "MIDI (%x '%s'): Changing tempo to %g", getStaticGUID(), getName().c_str(), tempo);
+
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerTempo(_filePlayer, tempo);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetPlayNote(MiniscriptThread *thread, const DynamicValue &value) {
+	if (miniscriptEvaluateTruth(value))
+		playSingleNote();
+	else
+		stopSingleNote();
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetMuteTrack(MiniscriptThread *thread, const DynamicValue &value) {
+	if (value.getType() != DynamicValueTypes::kBoolean) {
+		thread->error("Invalid type for mutetrack");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	uint16 mutedTracks = value.getBool() ? 0xffffu : 0u;
+
+	if (mutedTracks != _mutedTracks) {
+		_mutedTracks = mutedTracks;
+
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerMutedTracks(_filePlayer, mutedTracks);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::scriptSetMuteTrackIndexed(MiniscriptThread *thread, size_t trackIndex, bool muted) {
+	if (trackIndex >= 16) {
+		thread->error("Invalid track index for mutetrack");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	uint16 mutedTracks = _mutedTracks;
+	uint16 trackMask = 1 << trackIndex;
+
+	if (muted)
+		mutedTracks |= trackMask;
+	else
+		mutedTracks -= (mutedTracks & trackMask);
+
+	if (mutedTracks != _mutedTracks) {
+		_mutedTracks = mutedTracks;
+
+		if (_filePlayer)
+			_plugIn->getMidi()->setPlayerMutedTracks(_filePlayer, mutedTracks);
+	}
+
+	return kMiniscriptInstructionOutcomeContinue;
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) {
+	if (value.getType() != DynamicValueTypes::kBoolean) {
+		thread->error("Invalid type for mutetrack");
+		return kMiniscriptInstructionOutcomeFailed;
+	}
+
+	return static_cast<MidiModifier *>(objectRef)->scriptSetMuteTrackIndexed(thread, ptrOrOffset, value.getBool());
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) {
+	return kMiniscriptInstructionOutcomeFailed;
+}
+
+MidiPlugIn::MidiPlugIn(bool useDynamicMidi)
+	: _midiModifierFactory(this) {
+	_midi.reset(new MultiMidiPlayer(useDynamicMidi));
+}
+
+MidiPlugIn::~MidiPlugIn() {
+}
+
+void MidiPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) const {
+	registrar->registerPlugInModifier("MIDIModf", &_midiModifierFactory);
+}
+
+MultiMidiPlayer *MidiPlugIn::getMidi() const {
+	return _midi.get();
+}
+
+} // End of namespace Midi
+
+namespace PlugIns {
+
+Common::SharedPtr<PlugIn> createMIDI() {
+	const bool useDynamicMidi = ConfMan.getBool("mtropolis_mod_dynamic_midi");
+
+	return Common::SharedPtr<PlugIn>(new Midi::MidiPlugIn(useDynamicMidi));
+}
+
+} // namespace PlugIns
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin/midi.h b/engines/mtropolis/plugin/midi.h
new file mode 100644
index 00000000000..3dd0fc087ec
--- /dev/null
+++ b/engines/mtropolis/plugin/midi.h
@@ -0,0 +1,151 @@
+/* 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 MTROPOLIS_PLUGIN_MIDI_H
+#define MTROPOLIS_PLUGIN_MIDI_H
+
+#include "mtropolis/modifier_factory.h"
+#include "mtropolis/modifiers.h"
+#include "mtropolis/plugin/midi_data.h"
+#include "mtropolis/runtime.h"
+
+class MidiDriver;
+
+namespace MTropolis {
+
+class Runtime;
+
+namespace Midi {
+
+class MidiPlugIn;
+class MidiFilePlayer;
+class MidiNotePlayer;
+class MultiMidiPlayer;
+class MidiCombinerSource;
+
+class MidiModifier : public Modifier {
+public:
+	MidiModifier();
+	~MidiModifier();
+
+	bool load(const PlugInModifierLoaderContext &context, const Data::Midi::MidiModifier &data);
+
+	bool respondsToEvent(const Event &evt) const override;
+	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
+	void disable(Runtime *runtime) override;
+
+	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
+	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) override;
+
+#ifdef MTROPOLIS_DEBUG_ENABLE
+	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
+	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
+#endif
+
+private:
+	struct MuteTrackProxyInterface {
+		static MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset);
+		static MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib);
+		static MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index);
+	};
+
+	Common::SharedPtr<Modifier> shallowClone() const override;
+	const char *getDefaultName() const override;
+
+	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetNoteDuration(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetPlayNote(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetTempo(MiniscriptThread *thread, const DynamicValue &value);
+	MiniscriptInstructionOutcome scriptSetMuteTrack(MiniscriptThread *thread, const DynamicValue &value);
+
+	MiniscriptInstructionOutcome scriptSetMuteTrackIndexed(MiniscriptThread *thread, size_t trackIndex, bool muted);
+
+	uint getBoostedVolume(Runtime *runtime) const;
+
+	void playSingleNote();
+	void stopSingleNote();
+
+	struct FilePart {
+		bool loop;
+		bool overrideTempo;
+		double tempo;
+		double fadeIn;
+		double fadeOut;
+	};
+
+	struct SingleNotePart {
+		uint8 channel;
+		uint8 note;
+		uint8 velocity;
+		uint8 program;
+		double duration;
+	};
+
+	union ModeSpecificUnion {
+		FilePart file;
+		SingleNotePart singleNote;
+	};
+
+	enum Mode {
+		kModeFile,
+		kModeSingleNote,
+	};
+
+	Event _executeWhen;
+	Event _terminateWhen;
+
+	Mode _mode;
+	ModeSpecificUnion _modeSpecific;
+	uint8 _volume; // We need this always available because scripts will try to set it and then read it even in single note mode
+
+	Common::SharedPtr<Data::Midi::MidiModifier::EmbeddedFile> _embeddedFile;
+
+	uint16 _mutedTracks;
+
+	MidiPlugIn *_plugIn;
+	MidiFilePlayer *_filePlayer;
+	MidiNotePlayer *_notePlayer;
+};
+
+class MidiPlugIn : public MTropolis::PlugIn {
+public:
+	explicit MidiPlugIn(bool useDynamicMidi);
+	~MidiPlugIn();
+
+	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
+
+	MultiMidiPlayer *getMidi() const;
+
+private:
+	PlugInModifierFactory<MidiModifier, Data::Midi::MidiModifier> _midiModifierFactory;
+
+	Common::SharedPtr<MultiMidiPlayer> _midi;
+};
+
+} // End of namespace Midi
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugin/midi_data.cpp b/engines/mtropolis/plugin/midi_data.cpp
new file mode 100644
index 00000000000..1121aec4650
--- /dev/null
+++ b/engines/mtropolis/plugin/midi_data.cpp
@@ -0,0 +1,73 @@
+/* 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 "mtropolis/plugin/midi.h"
+#include "mtropolis/plugin/midi_data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Midi {
+
+MidiModifier::MidiModifier() : embeddedFlag(0) {
+	memset(&this->modeSpecific, 0, sizeof(this->modeSpecific));
+}
+
+DataReadErrorCode MidiModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
+	if (prefix.plugInRevision != 1 && prefix.plugInRevision != 2)
+		return kDataReadErrorUnsupportedRevision;
+
+	if (!executeWhen.load(reader) || !terminateWhen.load(reader) || !reader.readU8(embeddedFlag))
+		return kDataReadErrorReadFailed;
+
+	if (embeddedFlag) {
+		if (!reader.readU8(modeSpecific.embedded.hasFile))
+			return kDataReadErrorReadFailed;
+		if (modeSpecific.embedded.hasFile) {
+			embeddedFile = Common::SharedPtr<EmbeddedFile>(new EmbeddedFile());
+
+			uint8 bigEndianLength[4];
+			if (!reader.readBytes(bigEndianLength))
+				return kDataReadErrorReadFailed;
+
+			uint32 length = (bigEndianLength[0] << 24) + (bigEndianLength[1] << 16) + (bigEndianLength[2] << 8) + bigEndianLength[3];
+
+			embeddedFile->contents.resize(length);
+			if (length > 0 && !reader.read(&embeddedFile->contents[0], length))
+				return kDataReadErrorReadFailed;
+		}
+
+		if (!reader.readU8(modeSpecific.embedded.loop) || !reader.readU8(modeSpecific.embedded.overrideTempo) || !reader.readU8(modeSpecific.embedded.volume) || !embeddedTempo.load(reader) || !embeddedFadeIn.load(reader) || !embeddedFadeOut.load(reader))
+			return kDataReadErrorReadFailed;
+	} else {
+		if (!reader.readU8(modeSpecific.singleNote.channel) || !reader.readU8(modeSpecific.singleNote.note) || !reader.readU8(modeSpecific.singleNote.velocity) || !reader.readU8(modeSpecific.singleNote.program) || !singleNoteDuration.load(reader))
+			return kDataReadErrorReadFailed;
+	}
+
+	return kDataReadErrorNone;
+}
+
+} // End of namespace Midi
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin/midi_data.h b/engines/mtropolis/plugin/midi_data.h
new file mode 100644
index 00000000000..4cb88481259
--- /dev/null
+++ b/engines/mtropolis/plugin/midi_data.h
@@ -0,0 +1,82 @@
+/* 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 MTROPOLIS_PLUGIN_MIDI_DATA_H
+#define MTROPOLIS_PLUGIN_MIDI_DATA_H
+
+#include "mtropolis/data.h"
+
+namespace MTropolis {
+
+namespace Data {
+
+namespace Midi {
+
+struct MidiModifier : public PlugInModifierData {
+	struct EmbeddedFile {
+		Common::Array<uint8> contents;
+	};
+
+	struct EmbeddedPart {
+		uint8 hasFile;
+		uint8 loop;
+		uint8 overrideTempo;
+		uint8 volume;
+	};
+
+	struct SingleNotePart {
+		uint8 channel;
+		uint8 note;
+		uint8 velocity;
+		uint8 program;
+	};
+
+	union ModeSpecificUnion {
+		EmbeddedPart embedded;
+		SingleNotePart singleNote;
+	};
+
+	MidiModifier();
+
+	PlugInTypeTaggedValue executeWhen;
+	PlugInTypeTaggedValue terminateWhen;
+
+	uint8 embeddedFlag;
+	ModeSpecificUnion modeSpecific;
+
+	PlugInTypeTaggedValue embeddedTempo;      // Float
+	PlugInTypeTaggedValue embeddedFadeIn;     // Float
+	PlugInTypeTaggedValue embeddedFadeOut;    // Float
+	PlugInTypeTaggedValue singleNoteDuration; // Float
+
+	Common::SharedPtr<EmbeddedFile> embeddedFile;
+
+protected:
+	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
+};
+
+} // End of namespace Midi
+
+} // End of namespace Data
+
+} // End of namespace MTropolis
+
+#endif
diff --git a/engines/mtropolis/plugin/standard.cpp b/engines/mtropolis/plugin/standard.cpp
index 9521e25b661..5dbed826cfd 100644
--- a/engines/mtropolis/plugin/standard.cpp
+++ b/engines/mtropolis/plugin/standard.cpp
@@ -23,1546 +23,15 @@
 #include "common/config-manager.h"
 #include "common/file.h"
 
-#include "audio/mididrv.h"
-#include "audio/midiplayer.h"
-#include "audio/midiparser.h"
-#include "audio/midiparser_smf.h"
-
-#include "mtropolis/miniscript.h"
-#include "mtropolis/plugin/standard.h"
-#include "mtropolis/plugins.h"
-
-#include "mtropolis/miniscript.h"
-
-namespace MTropolis {
-
-namespace Standard {
-
-class MultiMidiPlayer;
-
-// I guess this follows QuickTime quirks, but basically, mTropolis pipes multiple inputs to a single
-// output device, and is totally cool with multiple devices stomping each other.
-//
-// Obsidian actually has a timer that plays a MIDI file that fires AllNoteOff on every channel every
-// 30 seconds, presumably to work around stuck notes, along with workarounds for the workaround,
-// i.e. the intro sequence has a silent part exactly 30 seconds in timed to sync up with the mute.
-//
-// NOTE: Due to SharedPtr not currently being atomic, MidiFilePlayers MUST BE DESTROYED ON THE
-// MAIN THREAD so there is no contention over the file refcount.
-class MidiFilePlayer {
-public:
-	virtual ~MidiFilePlayer();
-};
-
-class MidiNotePlayer {
-public:
-	virtual ~MidiNotePlayer();
-};
-
-class MidiCombinerSource : public MidiDriver_BASE {
-public:
-	virtual ~MidiCombinerSource();
-
-	// Do not call this directly, it's not thread-safe, expose via MultiMidiPlayer
-	virtual void setVolume(uint8 volume) = 0;
-	virtual void detach() = 0;
-};
-
-MidiCombinerSource::~MidiCombinerSource() {
-}
-
-class MidiCombiner {
-public:
-	virtual ~MidiCombiner();
-
-	virtual Common::SharedPtr<MidiCombinerSource> createSource() = 0;
-};
-
-MidiCombiner::~MidiCombiner() {
-}
-
-class MidiParser_MTropolis : public MidiParser_SMF {
-public:
-	MidiParser_MTropolis(bool hasTempoOverride, double tempoOverride, uint16 mutedTracks);
-
-	void setTempo(uint32 tempo) override;
-
-	void setTempoOverride(double tempoOverride);
-	void setMutedTracks(uint16 mutedTracks);
-
-protected:
-	bool processEvent(const EventInfo &info, bool fireEvents) override;
-
-private:
-	double _tempoOverride;
-	uint16 _mutedTracks;
-	bool _hasTempoOverride;
-};
-
-MidiParser_MTropolis::MidiParser_MTropolis(bool hasTempoOverride, double tempoOverride, uint16 mutedTracks)
-	: _hasTempoOverride(hasTempoOverride), _tempoOverride(tempoOverride), _mutedTracks(mutedTracks) {
-}
-
-void MidiParser_MTropolis::setTempo(uint32 tempo) {
-	if (_hasTempoOverride)
-		return;
-
-	MidiParser_SMF::setTempo(tempo);
-}
-
-void MidiParser_MTropolis::setTempoOverride(double tempoOverride) {
-	_hasTempoOverride = true;
-
-	if (tempoOverride < 1.0)
-		tempoOverride = 1.0;
-
-	_tempoOverride = tempoOverride;
-
-	uint32 convertedTempo = static_cast<uint32>(60000000.0 / tempoOverride);
-
-	MidiParser_SMF::setTempo(convertedTempo);
-}
-
-void MidiParser_MTropolis::setMutedTracks(uint16 mutedTracks) {
-	_mutedTracks = mutedTracks;
-}
-
-bool MidiParser_MTropolis::processEvent(const EventInfo &info, bool fireEvents) {
-	if ((info.event & 0xf0) == MidiDriver_BASE::MIDI_COMMAND_NOTE_ON) {
-		int track = _noteChannelToTrack[info.event & 0xf];
-		if (track >= 0 && (_mutedTracks & (1 << track)))
-			return true;
-	}
-
-	return MidiParser_SMF::processEvent(info, fireEvents);
-}
-
-class MidiFilePlayerImpl : public MidiFilePlayer {
-public:
-	explicit MidiFilePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks);
-	~MidiFilePlayerImpl();
-
-	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
-	void stop();
-	void play();
-	void pause();
-	void resume();
-	void setVolume(uint8 volume);
-	void setLoop(bool loop);
-	void setTempoOverride(double tempo);
-	void setMutedTracks(uint16 mutedTracks);
-	void detach();
-	void onTimer();
-
-private:
-	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _file;
-	Common::SharedPtr<MidiParser_MTropolis> _parser;
-	Common::SharedPtr<MidiCombinerSource> _outputDriver;
-	uint16 _mutedTracks;
-	bool _loop;
-};
-
-class MidiNotePlayerImpl : public MidiNotePlayer {
-public:
-	explicit MidiNotePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, uint32 timerRate);
-	~MidiNotePlayerImpl();
-
-	// Do not call any of these directly since they're not thread-safe, expose them via MultiMidiPlayer
-	void onTimer();
-	void play(uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration);
-	void stop();
-	void detach();
-
-private:
-	Common::SharedPtr<MidiCombinerSource> _outputDriver;
-	uint64 _durationRemaining;
-	uint32 _timerRate;
-	uint8 _channel;
-	uint8 _note;
-	uint8 _volume;
-	//uint8 _program;
-
-	bool _initialized;
-};
-
-class MultiMidiPlayer : public Audio::MidiPlayer {
-public:
-	explicit MultiMidiPlayer(bool useDynamicMidiMixer);
-	~MultiMidiPlayer();
-
-	MidiFilePlayer *createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks);
-	MidiNotePlayer *createNotePlayer();
-	void deleteFilePlayer(MidiFilePlayer *player);
-	void deleteNotePlayer(MidiNotePlayer *player);
-
-	Common::SharedPtr<MidiCombinerSource> createSource();
-
-	void setPlayerVolume(MidiFilePlayer *player, uint8 volume);
-	void setPlayerLoop(MidiFilePlayer *player, bool loop);
-	void setPlayerTempo(MidiFilePlayer *player, double tempo);
-	void setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks);
-	void stopPlayer(MidiFilePlayer *player);
-	void playPlayer(MidiFilePlayer *player);
-	void pausePlayer(MidiFilePlayer *player);
-	void resumePlayer(MidiFilePlayer *player);
-
-	void playNote(MidiNotePlayer *player, uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration);
-	void stopNote(MidiNotePlayer *player);
-
-	uint32 getBaseTempo() const;
-
-	void send(uint32 b) override;
-
-private:
-	void onTimer() override;
-
-	static void timerCallback(void *refCon);
-
-	Common::Mutex _mutex;
-	Common::Array<Common::SharedPtr<MidiFilePlayerImpl> > _filePlayers;
-	Common::Array<Common::SharedPtr<MidiNotePlayerImpl> > _notePlayers;
-	Common::SharedPtr<MidiCombiner> _combiner;
-};
-
-MidiFilePlayer::~MidiFilePlayer() {
-}
-
-MidiNotePlayer::~MidiNotePlayer() {
-}
-
-MidiFilePlayerImpl::MidiFilePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, uint32 baseTempo, bool hasTempoOverride, double tempo, uint8 volume, bool loop, uint16 mutedTracks)
-	: _file(file), _outputDriver(outputDriver), _parser(nullptr), _loop(loop), _mutedTracks(mutedTracks) {
-	Common::SharedPtr<MidiParser_MTropolis> parser(new MidiParser_MTropolis(hasTempoOverride, tempo, mutedTracks));
-
-	if (file->contents.size() != 0 && parser->loadMusic(&file->contents[0], file->contents.size())) {
-		_parser = parser;
-
-		parser->setTrack(0);
-		parser->startPlaying();
-		parser->setMidiDriver(outputDriver.get());
-		parser->setTimerRate(baseTempo);
-		parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
-	}
-}
-
-MidiFilePlayerImpl::~MidiFilePlayerImpl() {
-	assert(!_parser); // Call detach first!
-}
-
-void MidiFilePlayerImpl::stop() {
-	_parser->stopPlaying();
-}
-
-void MidiFilePlayerImpl::play() {
-	_parser->startPlaying();
-}
-
-void MidiFilePlayerImpl::pause() {
-	_parser->pausePlaying();
-}
-
-void MidiFilePlayerImpl::resume() {
-	_parser->resumePlaying();
-}
-
-void MidiFilePlayerImpl::setVolume(uint8 volume) {
-	_outputDriver->setVolume(volume);
-}
-
-void MidiFilePlayerImpl::setMutedTracks(uint16 mutedTracks) {
-	_mutedTracks = mutedTracks;
-	_parser->setMutedTracks(mutedTracks);
-}
-
-void MidiFilePlayerImpl::setLoop(bool loop) {
-	_loop = loop;
-	_parser->property(MidiParser::mpAutoLoop, loop ? 1 : 0);
-}
-
-void MidiFilePlayerImpl::setTempoOverride(double tempo) {
-	_parser->setTempoOverride(tempo);
-}
-
-void MidiFilePlayerImpl::detach() {
-	if (_parser) {
-		_parser->setMidiDriver(nullptr);
-		_parser.reset();
-	}
-
-	if (_outputDriver) {
-		_outputDriver->detach();
-		_outputDriver.reset();
-	}
-}
-
-void MidiFilePlayerImpl::onTimer() {
-	if (_parser)
-		_parser->onTimer();
-}
-
-MidiNotePlayerImpl::MidiNotePlayerImpl(const Common::SharedPtr<MidiCombinerSource> &outputDriver, uint32 timerRate)
-	: _timerRate(timerRate), _durationRemaining(0), _outputDriver(outputDriver), _channel(0), _note(0), /* _program(0), */_initialized(false), _volume(100) {
-}
-
-MidiNotePlayerImpl::~MidiNotePlayerImpl() {
-}
-
-void MidiNotePlayerImpl::onTimer() {
-	if (_durationRemaining > 0) {
-		if (_durationRemaining <= _timerRate) {
-			stop();
-			assert(_durationRemaining == 0);
-		} else {
-			_durationRemaining -= _timerRate;
-		}
-	}
-}
-
-void MidiNotePlayerImpl::play(uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration) {
-	if (duration < 0.000001)
-		return;
-
-	if (_durationRemaining)
-		stop();
-
-	_initialized = true;
-
-	_durationRemaining = static_cast<uint64>(duration * 1000000);
-	_channel = channel;
-	_note = note;
-	_volume = volume;
-
-	// GM volume scale to linear is x^4 so we need the 4th root of the volume, and need to rescale it from 0-100 to 0-0x3f80
-	// = 0x3f80 / sqrt(sqrt(100))
-	const double volumeMultiplier = 5140.5985643697174420974013458299;
-
-	if (volume > 100)
-		volume = 100;
-	uint16 hpVolume = static_cast<uint16>(floor(sqrt(sqrt(volume)) * volumeMultiplier));
-
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE | _channel, program, 0);
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION, 127);
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_REVERB, 0);
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_VOLUME, (hpVolume >> 7) & 0x7f);
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE | _channel, MidiDriver_BASE::MIDI_CONTROLLER_VOLUME + 32, hpVolume & 0x7f);
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_NOTE_ON | _channel, note, velocity);
-}
-
-void MidiNotePlayerImpl::stop() {
-	if (!_durationRemaining)
-		return;
-
-	_durationRemaining = 0;
-	_outputDriver->send(MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF | _channel, _note, 0);
-}
-
-void MidiNotePlayerImpl::detach() {
-	if (_outputDriver) {
-		if (_durationRemaining)
-			stop();
-
-		_outputDriver->detach();
-		_outputDriver.reset();
-	}
-}
-
-// Simple combiner - Behaves "QuickTime-like" and all commands are passed through directly.
-// This applies volume by modulating note velocity.
-class MidiCombinerSimple;
-
-class MidiCombinerSourceSimple : public MidiCombinerSource {
-public:
-	explicit MidiCombinerSourceSimple(MidiCombinerSimple *combiner);
-
-	void setVolume(uint8 volume) override;
-	void detach() override;
-	void send(uint32 b) override;
-
-private:
-	MidiCombinerSimple *_combiner;
-	uint8 _volume;
-};
-
-class MidiCombinerSimple : public MidiCombiner {
-public:
-	explicit MidiCombinerSimple(MidiDriver_BASE *outputDriver);
-
-	Common::SharedPtr<MidiCombinerSource> createSource() override;
-
-	void send(uint32 b);
-
-private:
-	MidiDriver_BASE *_outputDriver;
-};
-
-
-MidiCombinerSourceSimple::MidiCombinerSourceSimple(MidiCombinerSimple *combiner) : _combiner(combiner), _volume(255) {
-}
-
-void MidiCombinerSourceSimple::setVolume(uint8 volume) {
-	_volume = volume;
-}
-
-void MidiCombinerSourceSimple::detach() {
-}
-
-void MidiCombinerSourceSimple::send(uint32 b) {
-	byte command = (b & 0xF0);
-
-	if (command == MIDI_COMMAND_NOTE_ON || command == MIDI_COMMAND_NOTE_OFF) {
-		byte velocity = (b >> 16) & 0xFF;
-		velocity = (velocity * _volume * 257 + 256) >> 16;
-		b = (b & 0xff00ffff) | (velocity << 16);
-	}
-
-	_combiner->send(b);
-}
-
-MidiCombinerSimple::MidiCombinerSimple(MidiDriver_BASE *outputDriver) : _outputDriver(outputDriver) {
-}
-
-Common::SharedPtr<MidiCombinerSource> MidiCombinerSimple::createSource() {
-	return Common::SharedPtr<MidiCombinerSource>(new MidiCombinerSourceSimple(this));
-}
-
-void MidiCombinerSimple::send(uint32 b) {
-	_outputDriver->send(b);
-}
-
-
-// Dynamic combiner - Dynamic channel allocation, accurate volume control
-class MidiCombinerDynamic;
-
-class MidiCombinerSourceDynamic : public MidiCombinerSource {
-public:
-	MidiCombinerSourceDynamic(MidiCombinerDynamic *combiner, uint sourceID);
-	~MidiCombinerSourceDynamic();
-
-	void setVolume(uint8 volume) override;
-	void send(uint32 b) override;
-
-	void detach() override;
-
-private:
-	MidiCombinerDynamic *_combiner;
-	uint _sourceID;
-};
-
-class MidiCombinerDynamic : public MidiCombiner {
-public:
-	MidiCombinerDynamic(MidiDriver_BASE *outputDriver);
-
-	Common::SharedPtr<MidiCombinerSource> createSource() override;
-
-	void deallocateSource(uint sourceID);
-
-	void setSourceVolume(uint sourceID, uint8 volume);
-	void sendFromSource(uint sourceID, uint32 b);
-	void sendFromSource(uint sourceID, uint8 cmd, uint8 channel, uint8 param1, uint8 param2);
-
-private:
-	static const uint kMSBMask = 0x3f80u;
-	static const uint kLSBMask = 0x7fu;
-	static const uint kLRControllerStart = 64;
-	static const uint kSostenutoOnThreshold = 64;
-	static const uint kSustainOnThreshold = 64;
-
-	enum DataEntryState {
-		kDataEntryStateNone,
-		kDataEntryStateRPN,
-		kDataEntryStateNRPN,
-	};
-
-	struct MidiChannelState {
-		MidiChannelState();
-
-		void reset();
-		void softReset();	// Executes changes corresponding to Reset All Controllers message
-
-		uint16 _program;
-		uint16 _aftertouch;
-		uint16 _pitchBend;
-		uint16 _rpnNumber;
-		uint16 _nrpnNumber;
-		DataEntryState _dataEntryState;
-
-		uint16 _hrControllers[32];
-		uint8 _lrControllers[32];
-
-		uint16 _registeredParams[5];
-	};
-
-	struct SourceChannelState {
-		SourceChannelState();
-		void reset();
-
-		MidiChannelState _midiChannelState;
-	};
-
-	struct SourceState {
-		SourceState();
-
-		void allocate();
-		void deallocate();
-
-		SourceChannelState _sourceChannelState[MidiDriver_BASE::MIDI_CHANNEL_COUNT];
-		uint16 _root4MasterVolume;
-		bool _isAllocated;
-	};
-
-	struct OutputChannelState {
-		OutputChannelState();
-
-		bool _hasSource;
-		bool _volumeIsAmbiguous;
-		uint _sourceID;
-		uint _channelID;
-		uint _noteOffCounter;
-
-		MidiChannelState _midiChannelState;
-
-		uint _numActiveNotes;
-	};
-
-	struct MidiActiveNote {
-		uint8 _outputChannel;
-		uint16 _tone;
-		bool _affectedBySostenuto;
-
-		// If either of these are set, then the note is off, but is sustained by a pedal
-		bool _isSustainedBySustain;
-		bool _isSustainedBySostenuto;
-	};
-
-	void doNoteOn(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doNoteOff(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doPolyphonicAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doControlChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doProgramChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doChannelAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doPitchBend(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-
-	void doHighRangeControlChange(uint sourceID, uint8 channel, uint8 hrParam, uint16 value);
-	void doLowRangeControlChange(uint sourceID, uint8 channel, uint8 lrParam, uint8 value);
-
-	void doDataEntry(uint sourceID, uint8 channel, int16 existingValueMask, int16 offset);
-	void doChannelMode(uint sourceID, uint8 channel, uint8 param1, uint8 param2);
-	void doAllNotesOff(uint sourceID, uint8 channel, uint8 param2);
-	void doAllSoundOff(uint sourceID, uint8 channel, uint8 param2);
-	void doResetAllControllers(uint sourceID, uint8 channel, uint8 param2);
-
-	void sendToOutput(uint8 command, uint8 channel, uint8 param1, uint8 param2);
-
-	void syncSourceConfiguration(uint outputChannel, OutputChannelState &outChState, const SourceState &sourceState, const SourceChannelState &sourceChState);
-	void syncSourceHRController(uint outputChannel, OutputChannelState &outChState, const SourceState &sourceState, const SourceChannelState &sourceChState, uint hrController);
-	void syncSourceLRController(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint lrController);
-	void syncSourceRegisteredParam(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint rpn);
-
-	void tryCleanUpUnsustainedNote(uint noteIndex);
-
-	Common::Array<SourceState> _sources;
-	Common::Array<MidiActiveNote> _notes;
-	OutputChannelState _outputChannels[MidiDriver_BASE::MIDI_CHANNEL_COUNT];
-	uint _noteOffCounter;
-
-	MidiDriver_BASE *_outputDriver;
-
-	Common::SharedPtr<Common::DumpFile> _dumpFile;
-	int _eventCounter;
-};
-
-MidiCombinerSourceDynamic::MidiCombinerSourceDynamic(MidiCombinerDynamic *combiner, uint sourceID) : _combiner(combiner), _sourceID(sourceID) {
-}
-
-MidiCombinerSourceDynamic::~MidiCombinerSourceDynamic() {
-	assert(_combiner == nullptr);	// Call detach first!
-}
-
-void MidiCombinerSourceDynamic::detach() {
-	_combiner->deallocateSource(_sourceID);
-	_combiner = nullptr;
-}
-
-void MidiCombinerSourceDynamic::setVolume(uint8 volume) {
-	_combiner->setSourceVolume(_sourceID, volume);
-}
-
-void MidiCombinerSourceDynamic::send(uint32 b) {
-	_combiner->sendFromSource(_sourceID, b);
-}
-
-MidiCombinerDynamic::MidiCombinerDynamic(MidiDriver_BASE *outputDriver) : _outputDriver(outputDriver), _noteOffCounter(1) {
-#if 0
-	_dumpFile.reset(new Common::DumpFile());
-	_dumpFile->open("mididump.csv");
-	_dumpFile->writeString("event\ttime\tchannel\tcmd\tparam1\tparam2\n");
-#endif
-
-	_eventCounter = 0;
-}
-
-Common::SharedPtr<MidiCombinerSource> MidiCombinerDynamic::createSource() {
-	uint sourceID = _sources.size();
-
-	for (uint i = 0; i < _sources.size(); i++) {
-		if (!_sources[i]._isAllocated) {
-			sourceID = i;
-			break;
-		}
-	}
-
-	if (sourceID == _sources.size())
-		_sources.push_back(SourceState());
-
-	_sources[sourceID].allocate();
-
-	return Common::SharedPtr<MidiCombinerSource>(new MidiCombinerSourceDynamic(this, sourceID));
-}
-
-void MidiCombinerDynamic::deallocateSource(uint sourceID) {
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-		if (!ch._hasSource || ch._sourceID != sourceID)
-			continue;
-
-		// Stop any outputs and release sustain
-		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN, 0);
-		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO, 0);
-		sendFromSource(sourceID, MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, i, MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
-
-		ch._hasSource = false;
-		assert(ch._numActiveNotes == 0);
-	}
-
-	_sources[sourceID].deallocate();
-}
-
-void MidiCombinerDynamic::setSourceVolume(uint sourceID, uint8 volume) {
-	SourceState &src = _sources[sourceID];
-	src._root4MasterVolume = static_cast<uint16>(floor(sqrt(sqrt(volume)) * 16400.0));
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-		if (!ch._hasSource || ch._sourceID != sourceID)
-			continue;
-
-		// Synchronize volume control
-		syncSourceHRController(i, ch, src, src._sourceChannelState[ch._channelID], MidiDriver_BASE::MIDI_CONTROLLER_VOLUME);
-	}
-}
-
-void MidiCombinerDynamic::sendFromSource(uint sourceID, uint32 b) {
-	uint8 cmd = static_cast<uint8>(b & 0xf0);
-	uint8 channel = static_cast<uint8>(b & 0x0f);
-	uint8 param1 = static_cast<uint8>((b >> 8) & 0xff);
-	uint8 param2 = static_cast<uint8>((b >> 16) & 0xff);
-
-	sendFromSource(sourceID, cmd, channel, param1, param2);
-}
-
-void MidiCombinerDynamic::sendFromSource(uint sourceID, uint8 cmd, uint8 channel, uint8 param1, uint8 param2) {
-	switch (cmd) {
-	case MidiDriver_BASE::MIDI_COMMAND_NOTE_ON:
-		doNoteOn(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF:
-		doNoteOff(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH:
-		doPolyphonicAftertouch(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE:
-		doControlChange(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE:
-		doProgramChange(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH:
-		doChannelAftertouch(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND:
-		doPitchBend(sourceID, channel, param1, param2);
-		break;
-	case MidiDriver_BASE::MIDI_COMMAND_SYSTEM:
-		break;
-	}
-}
-
-void MidiCombinerDynamic::doNoteOn(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	uint outputChannel = 0;
-
-	if (channel == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL) {
-		outputChannel = MidiDriver_BASE::MIDI_RHYTHM_CHANNEL;
-	} else {
-		bool foundChannel = false;
-
-		// Find an existing exactly-matching channel
-		for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-			OutputChannelState &ch = _outputChannels[i];
-			if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-				foundChannel = true;
-				outputChannel = i;
-				break;
-			}
-		}
-
-		if (!foundChannel) {
-			// Find an inactive channel
-			for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-				if (i == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL)
-					continue;
-
-				if (!_outputChannels[i]._hasSource) {
-					foundChannel = true;
-					outputChannel = i;
-					break;
-				}
-			}
-		}
-
-		if (!foundChannel) {
-			uint bestOffCounter = 0xffffffffu;
-
-			// Find the channel that went quiet the longest time ago
-			for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-				if (i == MidiDriver_BASE::MIDI_RHYTHM_CHANNEL)
-					continue;
-
-				if (_outputChannels[i]._numActiveNotes == 0 && _outputChannels[i]._noteOffCounter < bestOffCounter) {
-					foundChannel = true;
-					outputChannel = i;
-					bestOffCounter = _outputChannels[i]._noteOffCounter;
-				}
-			}
-		}
-
-		// All eligible channels are playing already
-		if (!foundChannel)
-			return;
-	}
-
-	OutputChannelState &ch = _outputChannels[outputChannel];
-
-	if (!ch._hasSource || ch._sourceID != sourceID || ch._channelID != channel) {
-		ch._sourceID = sourceID;
-		ch._channelID = channel;
-		ch._hasSource = true;
-
-		const SourceState &sourceState = _sources[sourceID];
-		syncSourceConfiguration(outputChannel, ch, sourceState, sourceState._sourceChannelState[channel]);
-	}
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_NOTE_ON, outputChannel, param1, param2);
-
-	MidiActiveNote note;
-	note._outputChannel = outputChannel;
-	note._tone = param1;
-	note._affectedBySostenuto = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold);
-	note._isSustainedBySostenuto = false;
-	note._isSustainedBySustain = false;
-	_notes.push_back(note);
-
-	ch._numActiveNotes++;
-}
-
-void MidiCombinerDynamic::doNoteOff(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF, i, param1, param2);
-
-			for (uint ani = 0; ani < _notes.size(); ani++) {
-				MidiActiveNote &note = _notes[ani];
-				if (note._outputChannel == i && note._tone == param1 && !note._isSustainedBySostenuto && !note._isSustainedBySustain) {
-					if (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] >= kSustainOnThreshold)
-						note._isSustainedBySustain = true;
-
-					if (note._affectedBySostenuto && ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold)
-						note._isSustainedBySostenuto = true;
-
-					tryCleanUpUnsustainedNote(ani);
-					break;
-				}
-			}
-
-			break;
-		}
-	}
-}
-
-void MidiCombinerDynamic::doPolyphonicAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		const OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH, i, param1, param2);
-			break;
-		}
-	}
-}
-
-void MidiCombinerDynamic::doControlChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	SourceState &src = _sources[sourceID];
-	SourceChannelState &sch = src._sourceChannelState[channel];
-
-	if (param1 == MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB) {
-		doDataEntry(sourceID, channel, kLSBMask, param2 << 7);
-		return;
-	} else if (param1 == MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB) {
-		doDataEntry(sourceID, channel, kMSBMask, param2);
-		return;
-	} else if (param1 < 32) {
-		uint16 ctrl = ((sch._midiChannelState._hrControllers[param1] & kLSBMask) | ((param2 & 0x7f)) << 7);
-		doHighRangeControlChange(sourceID, channel, param1, ctrl);
-		return;
-	} else if (param1 < 64) {
-		uint16 ctrl = ((sch._midiChannelState._hrControllers[param1 - 32] & kMSBMask) | (param2 & 0x7f));
-		doHighRangeControlChange(sourceID, channel, param1 - 32, ctrl);
-		return;
-	} else if (param1 < 96) {
-		doLowRangeControlChange(sourceID, channel, param1 - 64, param2);
-		return;
-	}
-
-	switch (param1) {
-	case 96:
-		// Data increment
-		doDataEntry(sourceID, channel, 0x3fff, 1);
-		break;
-	case 97:
-		// Data decrement
-		doDataEntry(sourceID, channel, 0x3fff, -1);
-		break;
-	case 98:
-		// NRPN LSB
-		sch._midiChannelState._nrpnNumber = ((sch._midiChannelState._nrpnNumber & kMSBMask) | (param2 & 0x7f));
-		sch._midiChannelState._dataEntryState = kDataEntryStateNRPN;
-		break;
-	case 99:
-		// NRPN MSB
-		sch._midiChannelState._nrpnNumber = ((sch._midiChannelState._nrpnNumber & kLSBMask) | ((param2 & 0x7f) << 7));
-		sch._midiChannelState._dataEntryState = kDataEntryStateNRPN;
-		break;
-	case 100:
-		// RPN LSB
-		sch._midiChannelState._rpnNumber = ((sch._midiChannelState._rpnNumber & kMSBMask) | (param2 & 0x7f));
-		sch._midiChannelState._dataEntryState = kDataEntryStateRPN;
-		break;
-	case 101:
-		// RPN MSB
-		sch._midiChannelState._rpnNumber = ((sch._midiChannelState._rpnNumber & kLSBMask) | ((param2 & 0x7f) << 7));
-		sch._midiChannelState._dataEntryState = kDataEntryStateRPN;
-		break;
-	default:
-		if (param1 >= 120 && param1 < 128)
-			doChannelMode(sourceID, channel, param1, param2);
-		break;
-	};
-}
-
-void MidiCombinerDynamic::doProgramChange(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE, i, param1, param2);
-			ch._midiChannelState._program = param1;
-			break;
-		}
-	}
-
-	_sources[sourceID]._sourceChannelState[channel]._midiChannelState._program = param1;
-}
-
-void MidiCombinerDynamic::doChannelAftertouch(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH, i, param1, param2);
-			ch._midiChannelState._aftertouch = param1;
-			break;
-		}
-	}
-}
-
-void MidiCombinerDynamic::doPitchBend(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	uint16 pitchBend = (param1 & 0x7f) | ((param2 & 0x7f) << 7);
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND, i, param1, param2);
-			ch._midiChannelState._pitchBend = pitchBend;
-			break;
-		}
-	}
-
-	_sources[sourceID]._sourceChannelState[channel]._midiChannelState._pitchBend = pitchBend;
-}
-
-void MidiCombinerDynamic::doHighRangeControlChange(uint sourceID, uint8 channel, uint8 hrParam, uint16 value) {
-	SourceState &src = _sources[sourceID];
-	SourceChannelState &srcCh = src._sourceChannelState[channel];
-	srcCh._midiChannelState._hrControllers[hrParam] = value;
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			syncSourceHRController(i, ch, src, srcCh, hrParam);
-			break;
-		}
-	}
-}
-
-void MidiCombinerDynamic::doLowRangeControlChange(uint sourceID, uint8 channel, uint8 lrParam, uint8 value) {
-	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
-	srcCh._midiChannelState._lrControllers[lrParam] = value;
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			if (lrParam == MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart && value < kSustainOnThreshold) {
-				for (uint rni = _notes.size(); rni > 0; rni--) {
-					uint noteIndex = rni - 1;
-					MidiActiveNote &note = _notes[noteIndex];
-					if (note._isSustainedBySustain) {
-						note._isSustainedBySustain = false;
-						tryCleanUpUnsustainedNote(noteIndex);
-					}
-				}
-			} else if (lrParam == MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart && value < kSostenutoOnThreshold) {
-				for (uint rni = _notes.size(); rni > 0; rni--) {
-					uint noteIndex = rni - 1;
-					MidiActiveNote &note = _notes[noteIndex];
-					if (note._isSustainedBySostenuto) {
-						note._isSustainedBySostenuto = false;
-						tryCleanUpUnsustainedNote(noteIndex);
-					}
-				}
-			}
-
-			syncSourceLRController(i, ch, srcCh, lrParam);
-			break;
-		}
-	}
-}
-
-void MidiCombinerDynamic::doDataEntry(uint sourceID, uint8 channel, int16 existingValueMask, int16 offset) {
-	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
-
-	if (srcCh._midiChannelState._dataEntryState == kDataEntryStateRPN && srcCh._midiChannelState._rpnNumber < ARRAYSIZE(srcCh._midiChannelState._registeredParams)) {
-		int32 rp = srcCh._midiChannelState._registeredParams[srcCh._midiChannelState._rpnNumber];
-		rp &= existingValueMask;
-		rp += offset;
-
-		srcCh._midiChannelState._registeredParams[srcCh._midiChannelState._rpnNumber] = (rp & existingValueMask) + offset;
-
-		for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-			OutputChannelState &ch = _outputChannels[i];
-
-			if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-				syncSourceRegisteredParam(i, ch, srcCh, srcCh._midiChannelState._rpnNumber);
-				break;
-			}
-		}
-	}
-}
-
-void MidiCombinerDynamic::doChannelMode(uint sourceID, uint8 channel, uint8 param1, uint8 param2) {
-	// Remap omni/poly/mono modes to all notes off, since we don't do anything with omni/poly
-	switch (param1) {
-	case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_OFF:
-	case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_ON:
-	case MidiDriver_BASE::MIDI_CONTROLLER_MONO_ON:
-	case MidiDriver_BASE::MIDI_CONTROLLER_POLY_ON:
-	case MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF:
-		doAllNotesOff(sourceID, channel, param2);
-		break;
-	case MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF:
-		doAllSoundOff(sourceID, channel, param2);
-		break;
-	case MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS:
-		doResetAllControllers(sourceID, channel, param2);
-		break;
-	case 122: // Local control (ignore)
-	default:
-		break;
-	}
-}
-
-void MidiCombinerDynamic::doAllNotesOff(uint sourceID, uint8 channel, uint8 param2) {
-	uint outputChannel = 0;
-	bool foundChannel = false;
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			foundChannel = true;
-			outputChannel = i;
-			break;
-		}
-	}
-
-	if (!foundChannel)
-		return;
-
-	OutputChannelState &ch = _outputChannels[outputChannel];
-
-	bool sustainOn = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] >= kSustainOnThreshold);
-	bool sostenutoOn = (ch._midiChannelState._lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] >= kSostenutoOnThreshold);
-
-	for (uint rni = _notes.size(); rni > 0; rni--) {
-		uint noteIndex = rni - 1;
-		MidiActiveNote &note = _notes[noteIndex];
-		if (note._outputChannel == outputChannel) {
-			if (note._affectedBySostenuto && sostenutoOn)
-				note._isSustainedBySostenuto = true;
-			if (sustainOn)
-				note._isSustainedBySustain = true;
-
-			tryCleanUpUnsustainedNote(noteIndex);
-		}
-	}
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF, param2);
-}
-
-void MidiCombinerDynamic::doAllSoundOff(uint sourceID, uint8 channel, uint8 param2) {
-	uint outputChannel = 0;
-	bool foundChannel = false;
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			foundChannel = true;
-			outputChannel = i;
-			break;
-		}
-	}
-
-	if (!foundChannel)
-		return;
-
-	OutputChannelState &ch = _outputChannels[outputChannel];
-
-	for (uint rni = _notes.size(); rni > 0; rni--) {
-		uint noteIndex = rni - 1;
-		MidiActiveNote &note = _notes[noteIndex];
-		if (note._outputChannel == outputChannel) {
-			note._isSustainedBySostenuto = false;
-			note._isSustainedBySustain = false;
-
-			tryCleanUpUnsustainedNote(noteIndex);
-		}
-	}
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF, param2);
-	ch._noteOffCounter = 0;	// All sound is off so this can be recycled quickly
-}
-
-void MidiCombinerDynamic::doResetAllControllers(uint sourceID, uint8 channel, uint8 param2) {
-	SourceChannelState &srcCh = _sources[sourceID]._sourceChannelState[channel];
-
-	srcCh._midiChannelState.softReset();
-
-	uint outputChannel = 0;
-	bool foundChannel = false;
-
-	for (uint i = 0; i < ARRAYSIZE(_outputChannels); i++) {
-		OutputChannelState &ch = _outputChannels[i];
-		if (ch._hasSource && ch._sourceID == sourceID && ch._channelID == channel) {
-			foundChannel = true;
-			outputChannel = i;
-			break;
-		}
-	}
-
-	if (!foundChannel)
-		return;
-
-	OutputChannelState &ch = _outputChannels[outputChannel];
-	ch._midiChannelState.softReset();
-
-	// Release all sustained notes
-	for (uint rni = _notes.size(); rni > 0; rni--) {
-		uint noteIndex = rni - 1;
-		MidiActiveNote &note = _notes[noteIndex];
-		if (note._outputChannel == outputChannel) {
-			if (note._isSustainedBySostenuto || note._isSustainedBySustain) {
-				note._isSustainedBySostenuto = false;
-				note._isSustainedBySustain = false;
-				tryCleanUpUnsustainedNote(noteIndex);
-			}
-		}
-	}
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
-}
-
-void MidiCombinerDynamic::sendToOutput(uint8 command, uint8 channel, uint8 param1, uint8 param2) {
-	uint32 output = static_cast<uint32>(command) | static_cast<uint32>(channel) | static_cast<uint32>(param1 << 8) | static_cast<uint32>(param2 << 16);
-
-	if (_dumpFile) {
-		const int timestamp = g_system->getMillis(true);
-
-		const char *cmdName = "Unknown Command";
-		switch (command) {
-		case MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH:
-			cmdName = "ChannelAftertouch";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_NOTE_OFF:
-			cmdName = "NoteOff";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_NOTE_ON:
-			cmdName = "NoteOn";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_POLYPHONIC_AFTERTOUCH:
-			cmdName = "PolyAftertouch";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE:
-			cmdName = "ControlChange";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE:
-			cmdName = "ProgramChange";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND:
-			cmdName = "PitchBend";
-			break;
-		case MidiDriver_BASE::MIDI_COMMAND_SYSTEM:
-			cmdName = "System";
-			break;
-		default:
-			cmdName = "Unknown";
-		}
-
-		if (command == MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE) {
-			Common::String ctrlName = "Unknown";
-
-			switch (param1) {
-			case MidiDriver_BASE::MIDI_CONTROLLER_BANK_SELECT_MSB:
-				ctrlName = "BankSelect";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_MODULATION:
-				ctrlName = "Modulation";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB:
-				ctrlName = "DataEntryMSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_VOLUME:
-				ctrlName = "VolumeMSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_VOLUME + 32:
-				ctrlName = "VolumeLSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_BALANCE:
-				ctrlName = "Balance";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_PANNING:
-				ctrlName = "Panning";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION:
-				ctrlName = "Expression";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_BANK_SELECT_LSB:
-				ctrlName = "BankSelectLSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB:
-				ctrlName = "DataEntryLSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN:
-				ctrlName = "Sustain";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_PORTAMENTO:
-				ctrlName = "Portamento";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO:
-				ctrlName = "Sostenuto";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_SOFT:
-				ctrlName = "Soft";
-				break;
-
-			case MidiDriver_BASE::MIDI_CONTROLLER_REVERB:
-				ctrlName = "Reverb";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_CHORUS:
-				ctrlName = "Chorus";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_RPN_LSB:
-				ctrlName = "RPNLSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_RPN_MSB:
-				ctrlName = "RPNMSB";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_ALL_SOUND_OFF:
-				ctrlName = "AllSoundOff";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS:
-				ctrlName = "ResetAllControllers";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_ALL_NOTES_OFF:
-				ctrlName = "AllNotesOff";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_ON:
-				ctrlName = "OmniOn";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_OMNI_OFF:
-				ctrlName = "OmniOff";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_MONO_ON:
-				ctrlName = "MonoOn";
-				break;
-			case MidiDriver_BASE::MIDI_CONTROLLER_POLY_ON:
-				ctrlName = "PolyOn";
-				break;
-			default:
-				ctrlName = Common::String::format("Unknown%02x", static_cast<int>(param1));
-			}
-
-			_dumpFile->writeString(Common::String::format("%i\t%i\t%i\t%s\t%s\t%i\n", _eventCounter, timestamp, static_cast<int>(channel), cmdName, ctrlName.c_str(), static_cast<int>(param2)));
-		} else
-			_dumpFile->writeString(Common::String::format("%i\t%i\t%i\t%s\t%i\t%i\n", _eventCounter, timestamp, static_cast<int>(channel), cmdName, static_cast<int>(param1), static_cast<int>(param2)));
-
-		_eventCounter++;
-	}
-
-	_outputDriver->send(output);
-}
-
-void MidiCombinerDynamic::syncSourceConfiguration(uint outputChannel, OutputChannelState &outChState, const SourceState &srcState, const SourceChannelState &sourceChState) {
-	const MidiChannelState &srcMidiChState = sourceChState._midiChannelState;
-	MidiChannelState &outState = outChState._midiChannelState;
-
-	if (outState._program != srcMidiChState._program) {
-		outState._program = srcMidiChState._program;
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PROGRAM_CHANGE, outputChannel, srcMidiChState._program, 0);
-	}
-
-	if (outState._aftertouch != srcMidiChState._aftertouch) {
-		outState._aftertouch = srcMidiChState._aftertouch;
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CHANNEL_AFTERTOUCH, outputChannel, srcMidiChState._aftertouch, 0);
-	}
-
-	if (outState._pitchBend != srcMidiChState._pitchBend) {
-		outState._pitchBend = srcMidiChState._pitchBend;
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_PITCH_BEND, outputChannel, (srcMidiChState._pitchBend & kLSBMask), (srcMidiChState._pitchBend & kMSBMask) >> 7);
-	}
-
-	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._hrControllers); i++)
-		syncSourceHRController(outputChannel, outChState, srcState, sourceChState, i);
-
-	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._lrControllers); i++)
-		syncSourceLRController(outputChannel, outChState, sourceChState, i);
-
-	for (uint i = 0; i < ARRAYSIZE(srcMidiChState._registeredParams); i++)
-		syncSourceRegisteredParam(outputChannel, outChState, sourceChState, i);
-}
-
-void MidiCombinerDynamic::syncSourceHRController(uint outputChannel, OutputChannelState &outChState, const SourceState &srcState, const SourceChannelState &sourceChState, uint hrController) {
-	const MidiChannelState &srcMidiChState = sourceChState._midiChannelState;
-	MidiChannelState &outState = outChState._midiChannelState;
-
-	uint16 effectiveValue = srcMidiChState._hrControllers[hrController];
-
-	if (hrController == MidiDriver_BASE::MIDI_CONTROLLER_VOLUME) {
-		// GM volume to gain control is 40*log10(V/127)
-		// This means linear scale is (volume/0x3f80)^4
-		// To modulate the volume linearly, we must multiply the volume by the 4th root
-		// of the volume.
-		uint32 effectiveValueScaled = static_cast<uint32>(srcState._root4MasterVolume) * static_cast<uint32>(effectiveValue);
-		effectiveValueScaled += (effectiveValueScaled >> 16) + 1u;
-		effectiveValue = static_cast<uint16>(effectiveValueScaled >> 16);
-	}
-
-	if (outState._hrControllers[hrController] == effectiveValue)
-		return;
-
-	uint16 deltaBits = (outState._hrControllers[hrController] ^ effectiveValue);
-
-	if (deltaBits & kMSBMask)
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, hrController, (effectiveValue & kMSBMask) >> 7);
-	if (deltaBits & kLSBMask)
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, hrController + 32, effectiveValue & kLSBMask);
-
-	outState._hrControllers[hrController] = effectiveValue;
-}
-
-void MidiCombinerDynamic::syncSourceLRController(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint lrController) {
-	const MidiChannelState &srcState = sourceChState._midiChannelState;
-	MidiChannelState &outState = outChState._midiChannelState;
-
-	if (outState._lrControllers[lrController] == srcState._lrControllers[lrController])
-		return;
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, lrController + kLRControllerStart, srcState._lrControllers[lrController] & kLSBMask);
-
-	outState._lrControllers[lrController] = srcState._lrControllers[lrController];
-}
-
-void MidiCombinerDynamic::syncSourceRegisteredParam(uint outputChannel, OutputChannelState &outChState, const SourceChannelState &sourceChState, uint rpn) {
-	const MidiChannelState &srcState = sourceChState._midiChannelState;
-	MidiChannelState &outState = outChState._midiChannelState;
-
-	if (outState._registeredParams[rpn] == srcState._registeredParams[rpn])
-		return;
-
-	outState._registeredParams[rpn] = srcState._registeredParams[rpn];
-
-	if (outState._dataEntryState != kDataEntryStateRPN || outState._rpnNumber != srcState._rpnNumber) {
-		outState._dataEntryState = kDataEntryStateRPN;
-		outState._rpnNumber = srcState._rpnNumber;
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RPN_LSB, rpn & kLSBMask);
-		sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_RPN_MSB, (rpn & kMSBMask) >> 7);
-	}
-
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_LSB, srcState._registeredParams[rpn] & kLSBMask);
-	sendToOutput(MidiDriver_BASE::MIDI_COMMAND_CONTROL_CHANGE, outputChannel, MidiDriver_BASE::MIDI_CONTROLLER_DATA_ENTRY_MSB, (srcState._registeredParams[rpn] & kMSBMask) >> 7);
-}
-
-void MidiCombinerDynamic::tryCleanUpUnsustainedNote(uint noteIndex) {
-	MidiActiveNote &note = _notes[noteIndex];
-
-	if (!note._isSustainedBySostenuto && !note._isSustainedBySustain) {
-		OutputChannelState &outCh = _outputChannels[note._outputChannel];
-		assert(outCh._numActiveNotes > 0);
-		outCh._numActiveNotes--;
-		if (!outCh._numActiveNotes)
-			outCh._noteOffCounter = _noteOffCounter++;
-
-		_notes.remove_at(noteIndex);
-	}
-}
-
-MidiCombinerDynamic::MidiChannelState::MidiChannelState() {
-	reset();
-}
-
-void MidiCombinerDynamic::MidiChannelState::reset() {
-	_program = 0;
-	_aftertouch = 0;
-	_pitchBend = 0x2000;
-
-	for (uint i = 0; i < ARRAYSIZE(_hrControllers); i++)
-		_hrControllers[i] = 0;
-	for (uint i = 0; i < ARRAYSIZE(_lrControllers); i++)
-		_lrControllers[i] = 0;
-	for (uint i = 0; i < ARRAYSIZE(_registeredParams); i++)
-		_registeredParams[i] = 0;
-
-	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_BALANCE] = (64 << 7);
-	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_PANNING] = (64 << 7);
-	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_VOLUME] = (127 << 7);
-
-	_dataEntryState = kDataEntryStateNone;
-	_rpnNumber = 0;
-	_nrpnNumber = 0;
-}
-
-void MidiCombinerDynamic::MidiChannelState::softReset() {
-	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_MODULATION] = 0;
-	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SUSTAIN - kLRControllerStart] = 0;
-	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_PORTAMENTO - kLRControllerStart] = 0;
-	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOSTENUTO - kLRControllerStart] = 0;
-	_lrControllers[MidiDriver_BASE::MIDI_CONTROLLER_SOFT - kLRControllerStart] = 0;
-	_dataEntryState = kDataEntryStateNone;
-	_rpnNumber = 0;
-	_nrpnNumber = 0;
-	_aftertouch = 0;
-	_hrControllers[MidiDriver_BASE::MIDI_CONTROLLER_EXPRESSION] = (127 << 7);
-	_pitchBend = (64 << 7);
-}
-
-MidiCombinerDynamic::SourceChannelState::SourceChannelState() {
-	reset();
-}
-
-void MidiCombinerDynamic::SourceChannelState::reset() {
-}
-
-MidiCombinerDynamic::SourceState::SourceState() : _isAllocated(false), _root4MasterVolume(0xffffu) {
-}
-
-void MidiCombinerDynamic::SourceState::allocate() {
-	_isAllocated = true;
-}
-
-void MidiCombinerDynamic::SourceState::deallocate() {
-	_isAllocated = false;
-}
-
-MidiCombinerDynamic::OutputChannelState::OutputChannelState() : _sourceID(0), _volumeIsAmbiguous(true), _channelID(0), _hasSource(false), _noteOffCounter(0), _numActiveNotes(0) {
-}
-
-MultiMidiPlayer::MultiMidiPlayer(bool dynamicMidiMixer) {
-	if (dynamicMidiMixer)
-		_combiner.reset(new MidiCombinerDynamic(this));
-	else
-		_combiner.reset(new MidiCombinerSimple(this));
-
-	createDriver(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
-
-	if (_driver->open() != 0) {
-		_driver->close();
-		delete _driver;
-		_driver = nullptr;
-		return;
-	}
-
-	_driver->setTimerCallback(this, &timerCallback);
-}
-
-MultiMidiPlayer::~MultiMidiPlayer() {
-	Common::StackLock lock(_mutex);
-	_filePlayers.clear();
-	_notePlayers.clear();
-}
-
-void MultiMidiPlayer::timerCallback(void *refCon) {
-	static_cast<MultiMidiPlayer *>(refCon)->onTimer();
-}
-
-void MultiMidiPlayer::onTimer() {
-	Common::StackLock lock(_mutex);
-
-	for (const Common::SharedPtr<MidiFilePlayerImpl> &player : _filePlayers)
-		player->onTimer();
-
-	for (const Common::SharedPtr<MidiNotePlayerImpl> &player : _notePlayers)
-		player->onTimer();
-}
-
-MidiFilePlayer *MultiMidiPlayer::createFilePlayer(const Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> &file, bool hasTempoOverride, double tempoOverride, uint8 volume, bool loop, uint16 mutedTracks) {
-	Common::SharedPtr<MidiCombinerSource> combinerSource = createSource();
-	Common::SharedPtr<MidiFilePlayerImpl> filePlayer(new MidiFilePlayerImpl(combinerSource, file, getBaseTempo(), hasTempoOverride, tempoOverride, volume, loop, mutedTracks));
-
-	{
-		Common::StackLock lock(_mutex);
-		combinerSource->setVolume(volume);
-		_filePlayers.push_back(filePlayer);
-	}
-
-	return filePlayer.get();
-}
-
-MidiNotePlayer *MultiMidiPlayer::createNotePlayer() {
-	Common::SharedPtr<MidiCombinerSource> combinerSource = createSource();
-	Common::SharedPtr<MidiNotePlayerImpl> notePlayer(new MidiNotePlayerImpl(combinerSource, getBaseTempo()));
-
-	{
-		Common::StackLock lock(_mutex);
-		_notePlayers.push_back(notePlayer);
-	}
-
-	return notePlayer.get();
-}
-
-Common::SharedPtr<MidiCombinerSource> MultiMidiPlayer::createSource() {
-	Common::StackLock lock(_mutex);
-	return _combiner->createSource();
-}
-
-void MultiMidiPlayer::deleteFilePlayer(MidiFilePlayer *player) {
-	Common::SharedPtr<MidiFilePlayerImpl> ref;
-
-	for (Common::Array<Common::SharedPtr<MidiFilePlayerImpl> >::iterator it = _filePlayers.begin(), itEnd = _filePlayers.end(); it != itEnd; ++it) {
-		if (it->get() == player) {
-			{
-				Common::StackLock lock(_mutex);
-				ref = *it;
-				_filePlayers.erase(it);
-				ref->stop();
-			}
-			break;
-		}
-	}
-
-	if (ref)
-		ref->detach();
-}
-
-void MultiMidiPlayer::deleteNotePlayer(MidiNotePlayer *player) {
-	Common::SharedPtr<MidiNotePlayerImpl> ref;
-
-	for (Common::Array<Common::SharedPtr<MidiNotePlayerImpl> >::iterator it = _notePlayers.begin(), itEnd = _notePlayers.end(); it != itEnd; ++it) {
-		if (it->get() == player) {
-			{
-				Common::StackLock lock(_mutex);
-				ref = *it;
-				_notePlayers.erase(it);
-				ref->stop();
-			}
-			break;
-		}
-	}
-
-	if (ref)
-		ref->detach();
-}
-
-void MultiMidiPlayer::setPlayerVolume(MidiFilePlayer *player, uint8 volume) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->setVolume(volume);
-}
-
-void MultiMidiPlayer::setPlayerLoop(MidiFilePlayer *player, bool loop) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->setLoop(loop);
-}
-
-void MultiMidiPlayer::setPlayerTempo(MidiFilePlayer *player, double tempo) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->setTempoOverride(tempo);
-}
-
-void MultiMidiPlayer::setPlayerMutedTracks(MidiFilePlayer *player, uint16 mutedTracks) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->setMutedTracks(mutedTracks);
-}
-
-void MultiMidiPlayer::stopPlayer(MidiFilePlayer *player) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->stop();
-}
-
-void MultiMidiPlayer::playPlayer(MidiFilePlayer *player) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->play();
-}
-
-void MultiMidiPlayer::pausePlayer(MidiFilePlayer *player) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->pause();
-}
-
-void MultiMidiPlayer::resumePlayer(MidiFilePlayer *player) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiFilePlayerImpl *>(player)->resume();
-}
-
-void MultiMidiPlayer::playNote(MidiNotePlayer *player, uint8 volume, uint8 channel, uint8 program, uint8 note, uint8 velocity, double duration) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiNotePlayerImpl *>(player)->play(volume, channel, program, note, velocity, duration);
-}
+#include "mtropolis/miniscript.h"
+#include "mtropolis/plugin/standard.h"
+#include "mtropolis/plugins.h"
 
-void MultiMidiPlayer::stopNote(MidiNotePlayer *player) {
-	Common::StackLock lock(_mutex);
-	static_cast<MidiNotePlayerImpl *>(player)->stop();
-}
+#include "mtropolis/miniscript.h"
 
-uint32 MultiMidiPlayer::getBaseTempo() const {
-	if (_driver)
-		return _driver->getBaseTempo();
-	return 1;
-}
+namespace MTropolis {
 
-void MultiMidiPlayer::send(uint32 b) {
-	if (_driver)
-		_driver->send(b);
-}
+namespace Standard {
 
 CursorModifier::CursorModifier() : _cursorID(0) {
 }
@@ -2366,391 +835,6 @@ bool ObjectReferenceVariableStorage::SaveLoad::loadInternal(Common::ReadStream *
 	return true;
 }
 
-
-MidiModifier::MidiModifier() : _mode(kModeFile), _volume(100), _mutedTracks(0), /* _singleNoteChannel(0), _singleNoteNote(0), */
-							   _plugIn(nullptr), _filePlayer(nullptr), _notePlayer(nullptr) /*, _runtime(nullptr) */ {
-
-	memset(&this->_modeSpecific, 0, sizeof(this->_modeSpecific));
-}
-
-MidiModifier::~MidiModifier() {
-	if (_filePlayer)
-		_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
-
-	if (_notePlayer)
-		_plugIn->getMidi()->deleteNotePlayer(_notePlayer);
-}
-
-bool MidiModifier::load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data) {
-	_plugIn = static_cast<StandardPlugIn *>(context.plugIn);
-
-	if (data.executeWhen.type != Data::PlugInTypeTaggedValue::kEvent)
-		return false;
-
-	if (!_executeWhen.load(data.executeWhen.value.asEvent))
-		return false;
-
-	if (data.terminateWhen.type != Data::PlugInTypeTaggedValue::kEvent)
-		return false;
-
-	if (!_terminateWhen.load(data.terminateWhen.value.asEvent))
-		return false;
-
-	if (data.embeddedFlag) {
-		_mode = kModeFile;
-		_embeddedFile = data.embeddedFile;
-
-		_modeSpecific.file.loop = (data.modeSpecific.embedded.loop != 0);
-		_modeSpecific.file.overrideTempo = (data.modeSpecific.embedded.overrideTempo != 0);
-		_volume = data.modeSpecific.embedded.volume;
-
-		if (data.embeddedFadeIn.type != Data::PlugInTypeTaggedValue::kFloat
-			|| data.embeddedFadeOut.type != Data::PlugInTypeTaggedValue::kFloat
-			|| data.embeddedTempo.type != Data::PlugInTypeTaggedValue::kFloat)
-			return false;
-
-		_modeSpecific.file.fadeIn = data.embeddedFadeIn.value.asFloat.toXPFloat().toDouble();
-		_modeSpecific.file.fadeOut = data.embeddedFadeOut.value.asFloat.toXPFloat().toDouble();
-		_modeSpecific.file.tempo = data.embeddedTempo.value.asFloat.toXPFloat().toDouble();
-	} else {
-		_mode = kModeSingleNote;
-
-		if (data.singleNoteDuration.type != Data::PlugInTypeTaggedValue::kFloat)
-			return false;
-
-		_modeSpecific.singleNote.channel = data.modeSpecific.singleNote.channel;
-		_modeSpecific.singleNote.note = data.modeSpecific.singleNote.note;
-		_modeSpecific.singleNote.velocity = data.modeSpecific.singleNote.velocity;
-		_modeSpecific.singleNote.program = data.modeSpecific.singleNote.program;
-		_modeSpecific.singleNote.duration = data.singleNoteDuration.value.asFloat.toXPFloat().toDouble();
-
-		_volume = 100;
-	}
-
-	return true;
-}
-
-bool MidiModifier::respondsToEvent(const Event &evt) const {
-	return _executeWhen.respondsTo(evt) || _terminateWhen.respondsTo(evt);
-}
-
-VThreadState MidiModifier::consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) {
-	if (_executeWhen.respondsTo(msg->getEvent())) {
-		const SubtitleTables &subtitleTables = runtime->getProject()->getSubtitles();
-		if (subtitleTables.modifierMapping) {
-			const Common::String *subSetIDPtr = subtitleTables.modifierMapping->findSubtitleSetForModifierGUID(getStaticGUID());
-			if (subSetIDPtr) {
-				// Don't currently support anything except play-on-start subs for MIDI
-				SubtitlePlayer subtitlePlayer(runtime, *subSetIDPtr, subtitleTables);
-				subtitlePlayer.update(0, 1);
-			}
-		}
-
-		if (_mode == kModeFile) {
-			if (_embeddedFile) {
-				debug(2, "MIDI (%x '%s'): Playing embedded file", getStaticGUID(), getName().c_str());
-
-				const double tempo = _modeSpecific.file.overrideTempo ? _modeSpecific.file.tempo : 120.0;
-				if (!_filePlayer)
-					_filePlayer = _plugIn->getMidi()->createFilePlayer(_embeddedFile, _modeSpecific.file.overrideTempo, tempo, getBoostedVolume(runtime) * 255 / 100, _modeSpecific.file.loop, _mutedTracks);
-				_plugIn->getMidi()->playPlayer(_filePlayer);
-			} else {
-				debug(2, "MIDI (%x '%s'): Digested execute event but don't have anything to play", getStaticGUID(), getName().c_str());
-			}
-		} else if (_mode == kModeSingleNote) {
-			playSingleNote();
-		}
-	}
-	if (_terminateWhen.respondsTo(msg->getEvent())) {
-		disable(runtime);
-	}
-
-	return kVThreadReturn;
-}
-
-void MidiModifier::disable(Runtime *runtime) {
-	if (_filePlayer) {
-		_plugIn->getMidi()->deleteFilePlayer(_filePlayer);
-		_filePlayer = nullptr;
-	}
-	if (_notePlayer) {
-		_plugIn->getMidi()->deleteNotePlayer(_notePlayer);
-		_notePlayer = nullptr;
-	}
-}
-
-void MidiModifier::playSingleNote() {
-	if (!_notePlayer)
-		_notePlayer = _plugIn->getMidi()->createNotePlayer();
-	_plugIn->getMidi()->playNote(_notePlayer, _volume, _modeSpecific.singleNote.channel, _modeSpecific.singleNote.program, _modeSpecific.singleNote.note, _modeSpecific.singleNote.velocity, _modeSpecific.singleNote.duration);
-}
-
-void MidiModifier::stopSingleNote() {
-	if (_notePlayer)
-		_plugIn->getMidi()->stopNote(_notePlayer);
-}
-
-bool MidiModifier::readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) {
-	if (attrib == "volume") {
-		result.setInt(_volume);
-		return true;
-	}
-
-	return Modifier::readAttribute(thread, result, attrib);
-}
-
-MiniscriptInstructionOutcome MidiModifier::writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) {
-	if (attrib == "volume") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetVolume, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "notevelocity") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteVelocity, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "noteduration") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteDuration, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "notenum") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetNoteNum, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "loop") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetLoop, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "playnote") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetPlayNote, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "tempo") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetTempo, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	} else if (attrib == "mutetrack") {
-		DynamicValueWriteFuncHelper<MidiModifier, &MidiModifier::scriptSetMuteTrack, true>::create(this, result);
-		return kMiniscriptInstructionOutcomeContinue;
-	}
-
-	return Modifier::writeRefAttribute(thread, result, attrib);
-}
-
-MiniscriptInstructionOutcome MidiModifier::writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) {
-	if (attrib == "mutetrack") {
-		int32 asInteger = 0;
-		if (!index.roundToInt(asInteger) || asInteger < 1) {
-			thread->error("Invalid index for mutetrack");
-			return kMiniscriptInstructionOutcomeFailed;
-		}
-
-		result.pod.objectRef = this;
-		result.pod.ptrOrOffset = static_cast<uintptr>(asInteger) - 1;
-		result.pod.ifc = DynamicValueWriteInterfaceGlue<MuteTrackProxyInterface>::getInstance();
-
-		return kMiniscriptInstructionOutcomeContinue;
-	}
-
-	return Modifier::writeRefAttributeIndexed(thread, result, attrib, index);
-}
-
-
-uint MidiModifier::getBoostedVolume(Runtime *runtime) const {
-	uint boostedVolume = (_volume * runtime->getHacks().midiVolumeScale) >> 8;
-	if (boostedVolume > 100)
-		boostedVolume = 100;
-	return boostedVolume;
-}
-
-Common::SharedPtr<Modifier> MidiModifier::shallowClone() const {
-	Common::SharedPtr<MidiModifier> clone(new MidiModifier(*this));
-
-	clone->_notePlayer = nullptr;
-	clone->_filePlayer = nullptr;
-
-	return clone;
-}
-
-const char *MidiModifier::getDefaultName() const {
-	return "MIDI Modifier";
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value) {
-	int32 asInteger = 0;
-	if (!value.roundToInt(asInteger))
-		return kMiniscriptInstructionOutcomeFailed;
-
-	if (asInteger < 0)
-		asInteger = 0;
-	else if (asInteger > 100)
-		asInteger = 100;
-
-	_volume = asInteger;
-
-	if (_mode == kModeFile) {
-		debug(2, "MIDI (%x '%s'): Changing volume to %i", getStaticGUID(), getName().c_str(), _volume);
-		if (_filePlayer)
-			_plugIn->getMidi()->setPlayerVolume(_filePlayer, getBoostedVolume(thread->getRuntime()) * 255 / 100);
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value) {
-	int32 asInteger = 0;
-	if (!value.roundToInt(asInteger))
-		return kMiniscriptInstructionOutcomeFailed;
-
-	if (asInteger < 0)
-		asInteger = 0;
-	else if (asInteger > 127)
-		asInteger = 127;
-
-	if (_mode == kModeSingleNote) {
-		debug(2, "MIDI (%x '%s'): Changing note velocity to %i", getStaticGUID(), getName().c_str(), asInteger);
-		_modeSpecific.singleNote.velocity = asInteger;
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetNoteDuration(MiniscriptThread *thread, const DynamicValue &value) {
-	double asDouble = 0.0;
-	if (value.getType() == DynamicValueTypes::kFloat) {
-		asDouble = value.getFloat();
-	} else {
-		DynamicValue converted;
-		if (!value.convertToType(DynamicValueTypes::kFloat, converted))
-			return kMiniscriptInstructionOutcomeFailed;
-		asDouble = converted.getFloat();
-	}
-
-	if (_mode == kModeSingleNote) {
-		debug(2, "MIDI (%x '%s'): Changing note duration to %g", getStaticGUID(), getName().c_str(), asDouble);
-		_modeSpecific.singleNote.duration = asDouble;
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value) {
-	int32 asInteger = 0;
-	if (!value.roundToInt(asInteger))
-		return kMiniscriptInstructionOutcomeFailed;
-
-	if (asInteger < 0)
-		asInteger = 0;
-	else if (asInteger > 255)
-		asInteger = 255;
-
-	if (_mode == kModeSingleNote) {
-		debug(2, "MIDI (%x '%s'): Changing note number to %i", getStaticGUID(), getName().c_str(), asInteger);
-		_modeSpecific.singleNote.note = asInteger;
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value) {
-	if (value.getType() != DynamicValueTypes::kBoolean)
-		return kMiniscriptInstructionOutcomeFailed;
-
-	if (_mode == kModeFile) {
-		const bool loop = value.getBool();
-
-		debug(2, "MIDI (%x '%s'): Changing loop state to %s", getStaticGUID(), getName().c_str(), loop ? "true" : "false");
-		if (_modeSpecific.file.loop != loop) {
-			_modeSpecific.file.loop = loop;
-
-			if (_filePlayer)
-				_plugIn->getMidi()->setPlayerLoop(_filePlayer, loop);
-		}
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetTempo(MiniscriptThread *thread, const DynamicValue &value) {
-	double tempo = 0.0;
-	if (value.getType() == DynamicValueTypes::kInteger)
-		tempo = value.getInt();
-	else if (value.getType() == DynamicValueTypes::kFloat)
-		tempo = value.getFloat();
-	else
-		return kMiniscriptInstructionOutcomeFailed;
-
-	if (_mode == kModeFile) {
-		debug(2, "MIDI (%x '%s'): Changing tempo to %g", getStaticGUID(), getName().c_str(), tempo);
-
-		if (_filePlayer)
-			_plugIn->getMidi()->setPlayerTempo(_filePlayer, tempo);
-
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetPlayNote(MiniscriptThread *thread, const DynamicValue &value) {
-	if (miniscriptEvaluateTruth(value))
-		playSingleNote();
-	else
-		stopSingleNote();
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetMuteTrack(MiniscriptThread *thread, const DynamicValue &value) {
-	if (value.getType() != DynamicValueTypes::kBoolean) {
-		thread->error("Invalid type for mutetrack");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
-
-	uint16 mutedTracks = value.getBool() ? 0xffffu : 0u;
-
-	if (mutedTracks != _mutedTracks) {
-		_mutedTracks = mutedTracks;
-
-		if (_filePlayer)
-			_plugIn->getMidi()->setPlayerMutedTracks(_filePlayer, mutedTracks);
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::scriptSetMuteTrackIndexed(MiniscriptThread *thread, size_t trackIndex, bool muted) {
-	if (trackIndex >= 16) {
-		thread->error("Invalid track index for mutetrack");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
-
-	uint16 mutedTracks = _mutedTracks;
-	uint16 trackMask = 1 << trackIndex;
-
-	if (muted)
-		mutedTracks |= trackMask;
-	else
-		mutedTracks -= (mutedTracks & trackMask);
-
-	if (mutedTracks != _mutedTracks) {
-		_mutedTracks = mutedTracks;
-
-		if (_filePlayer)
-			_plugIn->getMidi()->setPlayerMutedTracks(_filePlayer, mutedTracks);
-	}
-
-	return kMiniscriptInstructionOutcomeContinue;
-}
-
-MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::write(MiniscriptThread *thread, const DynamicValue &value, void *objectRef, uintptr ptrOrOffset) {
-	if (value.getType() != DynamicValueTypes::kBoolean) {
-		thread->error("Invalid type for mutetrack");
-		return kMiniscriptInstructionOutcomeFailed;
-	}
-
-	return static_cast<MidiModifier *>(objectRef)->scriptSetMuteTrackIndexed(thread, ptrOrOffset, value.getBool());
-}
-
-MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib) {
-	return kMiniscriptInstructionOutcomeFailed;
-}
-
-MiniscriptInstructionOutcome MidiModifier::MuteTrackProxyInterface::refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index) {
-	return kMiniscriptInstructionOutcomeFailed;
-}
-
 ListVariableModifier::ListVariableModifier() : VariableModifier(Common::SharedPtr<VariableStorage>(new ListVariableStorage())) {
 }
 
@@ -3336,16 +1420,14 @@ const char *PanningModifier::getDefaultName() const {
 StandardPlugInHacks::StandardPlugInHacks() : allowGarbledListModData(false) {
 }
 
-StandardPlugIn::StandardPlugIn(bool useDynamicMidi)
+StandardPlugIn::StandardPlugIn()
 	: _cursorModifierFactory(this)
 	, _sTransCtModifierFactory(this)
 	, _mediaCueModifierFactory(this)
 	, _objRefVarModifierFactory(this)
-	, _midiModifierFactory(this)
 	, _listVarModifierFactory(this)
 	, _sysInfoModifierFactory(this)
 	, _panningModifierFactory(this) {
-	_midi.reset(new MultiMidiPlayer(useDynamicMidi));
 }
 
 StandardPlugIn::~StandardPlugIn() {
@@ -3356,7 +1438,6 @@ void StandardPlugIn::registerModifiers(IPlugInModifierRegistrar *registrar) cons
 	registrar->registerPlugInModifier("STransCt", &_sTransCtModifierFactory);
 	registrar->registerPlugInModifier("MediaCue", &_mediaCueModifierFactory);
 	registrar->registerPlugInModifier("ObjRefP", &_objRefVarModifierFactory);
-	registrar->registerPlugInModifier("MIDIModf", &_midiModifierFactory);
 	registrar->registerPlugInModifier("ListMod", &_listVarModifierFactory);
 	registrar->registerPlugInModifier("SysInfo", &_sysInfoModifierFactory);
 
@@ -3371,18 +1452,12 @@ StandardPlugInHacks &StandardPlugIn::getHacks() {
 	return _hacks;
 }
 
-MultiMidiPlayer *StandardPlugIn::getMidi() const {
-	return _midi.get();
-}
-
 } // End of namespace Standard
 
 namespace PlugIns {
 
 Common::SharedPtr<PlugIn> createStandard() {
-	const bool useDynamicMidi = ConfMan.getBool("mtropolis_mod_dynamic_midi");
-
-	return Common::SharedPtr<PlugIn>(new Standard::StandardPlugIn(useDynamicMidi));
+	return Common::SharedPtr<PlugIn>(new Standard::StandardPlugIn());
 }
 
 } // End of namespace MTropolis
diff --git a/engines/mtropolis/plugin/standard.h b/engines/mtropolis/plugin/standard.h
index ddb6c59031e..f3bebf57b9f 100644
--- a/engines/mtropolis/plugin/standard.h
+++ b/engines/mtropolis/plugin/standard.h
@@ -27,8 +27,6 @@
 #include "mtropolis/runtime.h"
 #include "mtropolis/plugin/standard_data.h"
 
-class MidiDriver;
-
 namespace MTropolis {
 
 class Runtime;
@@ -36,10 +34,6 @@ class Runtime;
 namespace Standard {
 
 class StandardPlugIn;
-class MidiFilePlayer;
-class MidiNotePlayer;
-class MultiMidiPlayer;
-class MidiCombinerSource;
 
 class CursorModifier : public Modifier {
 public:
@@ -241,94 +235,6 @@ private:
 	mutable ObjectReference _object;
 };
 
-class MidiModifier : public Modifier {
-public:
-	MidiModifier();
-	~MidiModifier();
-
-	bool load(const PlugInModifierLoaderContext &context, const Data::Standard::MidiModifier &data);
-
-	bool respondsToEvent(const Event &evt) const override;
-	VThreadState consumeMessage(Runtime *runtime, const Common::SharedPtr<MessageProperties> &msg) override;
-	void disable(Runtime *runtime) override;
-
-	bool readAttribute(MiniscriptThread *thread, DynamicValue &result, const Common::String &attrib) override;
-	MiniscriptInstructionOutcome writeRefAttribute(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib) override;
-	MiniscriptInstructionOutcome writeRefAttributeIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &result, const Common::String &attrib, const DynamicValue &index) override;
-
-#ifdef MTROPOLIS_DEBUG_ENABLE
-	const char *debugGetTypeName() const override { return "MIDI Modifier"; }
-	SupportStatus debugGetSupportStatus() const override { return kSupportStatusDone; }
-#endif
-
-private:
-	struct MuteTrackProxyInterface {
-		static MiniscriptInstructionOutcome write(MiniscriptThread *thread, const DynamicValue &dest, void *objectRef, uintptr ptrOrOffset);
-		static MiniscriptInstructionOutcome refAttrib(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib);
-		static MiniscriptInstructionOutcome refAttribIndexed(MiniscriptThread *thread, DynamicValueWriteProxy &proxy, void *objectRef, uintptr ptrOrOffset, const Common::String &attrib, const DynamicValue &index);
-	};
-
-	Common::SharedPtr<Modifier> shallowClone() const override;
-	const char *getDefaultName() const override;
-
-	MiniscriptInstructionOutcome scriptSetVolume(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetNoteVelocity(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetNoteDuration(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetNoteNum(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetLoop(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetPlayNote(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetTempo(MiniscriptThread *thread, const DynamicValue &value);
-	MiniscriptInstructionOutcome scriptSetMuteTrack(MiniscriptThread *thread, const DynamicValue &value);
-
-	MiniscriptInstructionOutcome scriptSetMuteTrackIndexed(MiniscriptThread *thread, size_t trackIndex, bool muted);
-
-	uint getBoostedVolume(Runtime *runtime) const;
-
-	void playSingleNote();
-	void stopSingleNote();
-
-	struct FilePart {
-		bool loop;
-		bool overrideTempo;
-		double tempo;
-		double fadeIn;
-		double fadeOut;
-	};
-
-	struct SingleNotePart {
-		uint8 channel;
-		uint8 note;
-		uint8 velocity;
-		uint8 program;
-		double duration;
-	};
-
-	union ModeSpecificUnion {
-		FilePart file;
-		SingleNotePart singleNote;
-	};
-
-	enum Mode {
-		kModeFile,
-		kModeSingleNote,
-	};
-
-	Event _executeWhen;
-	Event _terminateWhen;
-
-	Mode _mode;
-	ModeSpecificUnion _modeSpecific;
-	uint8 _volume;	// We need this always available because scripts will try to set it and then read it even in single note mode
-
-	Common::SharedPtr<Data::Standard::MidiModifier::EmbeddedFile> _embeddedFile;
-
-	uint16 _mutedTracks;
-
-	StandardPlugIn *_plugIn;
-	MidiFilePlayer *_filePlayer;
-	MidiNotePlayer *_notePlayer;
-};
-
 class ListVariableStorage : public VariableStorage {
 public:
 	friend class ListVariableModifier;
@@ -440,7 +346,7 @@ private:
 
 class StandardPlugIn : public MTropolis::PlugIn {
 public:
-	explicit StandardPlugIn(bool useDynamicMidi);
+	StandardPlugIn();
 	~StandardPlugIn();
 
 	void registerModifiers(IPlugInModifierRegistrar *registrar) const override;
@@ -448,19 +354,15 @@ public:
 	const StandardPlugInHacks &getHacks() const;
 	StandardPlugInHacks &getHacks();
 
-	MultiMidiPlayer *getMidi() const;
-
 private:
 	PlugInModifierFactory<CursorModifier, Data::Standard::CursorModifier> _cursorModifierFactory;
 	PlugInModifierFactory<STransCtModifier, Data::Standard::STransCtModifier> _sTransCtModifierFactory;
 	PlugInModifierFactory<MediaCueMessengerModifier, Data::Standard::MediaCueMessengerModifier> _mediaCueModifierFactory;
 	PlugInModifierFactory<ObjectReferenceVariableModifier, Data::Standard::ObjectReferenceVariableModifier> _objRefVarModifierFactory;
-	PlugInModifierFactory<MidiModifier, Data::Standard::MidiModifier> _midiModifierFactory;
 	PlugInModifierFactory<ListVariableModifier, Data::Standard::ListVariableModifier> _listVarModifierFactory;
 	PlugInModifierFactory<SysInfoModifier, Data::Standard::SysInfoModifier> _sysInfoModifierFactory;
 	PlugInModifierFactory<PanningModifier, Data::Standard::PanningModifier> _panningModifierFactory;
 
-	Common::SharedPtr<MultiMidiPlayer> _midi;
 	StandardPlugInHacks _hacks;
 };
 
diff --git a/engines/mtropolis/plugin/standard_data.cpp b/engines/mtropolis/plugin/standard_data.cpp
index b7ee40404a5..998059b89c6 100644
--- a/engines/mtropolis/plugin/standard_data.cpp
+++ b/engines/mtropolis/plugin/standard_data.cpp
@@ -107,47 +107,6 @@ DataReadErrorCode ObjectReferenceVariableModifier::load(PlugIn &plugIn, const Pl
 	return kDataReadErrorNone;
 }
 
-MidiModifier::MidiModifier() : embeddedFlag(0) {
-	memset(&this->modeSpecific, 0, sizeof(this->modeSpecific));
-}
-
-DataReadErrorCode MidiModifier::load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) {
-	if (prefix.plugInRevision != 1 && prefix.plugInRevision != 2)
-		return kDataReadErrorUnsupportedRevision;
-
-	if (!executeWhen.load(reader) || !terminateWhen.load(reader) || !reader.readU8(embeddedFlag))
-		return kDataReadErrorReadFailed;
-
-	if (embeddedFlag) {
-		if (!reader.readU8(modeSpecific.embedded.hasFile))
-			return kDataReadErrorReadFailed;
-		if (modeSpecific.embedded.hasFile) {
-			embeddedFile = Common::SharedPtr<EmbeddedFile>(new EmbeddedFile());
-
-			uint8 bigEndianLength[4];
-			if (!reader.readBytes(bigEndianLength))
-				return kDataReadErrorReadFailed;
-
-			uint32 length = (bigEndianLength[0] << 24) + (bigEndianLength[1] << 16) + (bigEndianLength[2] << 8) + bigEndianLength[3];
-
-			embeddedFile->contents.resize(length);
-			if (length > 0 && !reader.read(&embeddedFile->contents[0], length))
-				return kDataReadErrorReadFailed;
-		}
-
-		if (!reader.readU8(modeSpecific.embedded.loop) || !reader.readU8(modeSpecific.embedded.overrideTempo)
-			|| !reader.readU8(modeSpecific.embedded.volume) || !embeddedTempo.load(reader)
-			|| !embeddedFadeIn.load(reader) || !embeddedFadeOut.load(reader))
-			return kDataReadErrorReadFailed;
-	} else {
-		if (!reader.readU8(modeSpecific.singleNote.channel) || !reader.readU8(modeSpecific.singleNote.note) || !reader.readU8(modeSpecific.singleNote.velocity)
-			|| !reader.readU8(modeSpecific.singleNote.program) || !singleNoteDuration.load(reader))
-			return kDataReadErrorReadFailed;
-	}
-
-	return kDataReadErrorNone;
-}
-
 ListVariableModifier::ListVariableModifier() : unknown1(0), contentsType(0), unknown2{0, 0, 0, 0},
 	havePersistentData(false), numValues(0), values(nullptr), persistentValuesGarbled(false) {
 }
diff --git a/engines/mtropolis/plugin/standard_data.h b/engines/mtropolis/plugin/standard_data.h
index 14620418b8a..ab17f31816b 100644
--- a/engines/mtropolis/plugin/standard_data.h
+++ b/engines/mtropolis/plugin/standard_data.h
@@ -96,49 +96,6 @@ protected:
 	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
 };
 
-struct MidiModifier : public PlugInModifierData {
-	struct EmbeddedFile {
-		Common::Array<uint8> contents;
-	};
-
-	struct EmbeddedPart {
-		uint8 hasFile;
-		uint8 loop;
-		uint8 overrideTempo;
-		uint8 volume;
-	};
-
-	struct SingleNotePart {
-		uint8 channel;
-		uint8 note;
-		uint8 velocity;
-		uint8 program;
-	};
-
-	union ModeSpecificUnion {
-		EmbeddedPart embedded;
-		SingleNotePart singleNote;
-	};
-
-	MidiModifier();
-
-	PlugInTypeTaggedValue executeWhen;
-	PlugInTypeTaggedValue terminateWhen;
-
-	uint8 embeddedFlag;
-	ModeSpecificUnion modeSpecific;
-
-	PlugInTypeTaggedValue embeddedTempo;		// Float
-	PlugInTypeTaggedValue embeddedFadeIn;		// Float
-	PlugInTypeTaggedValue embeddedFadeOut;		// Float
-	PlugInTypeTaggedValue singleNoteDuration;	// Float
-
-	Common::SharedPtr<EmbeddedFile> embeddedFile;
-
-protected:
-	DataReadErrorCode load(PlugIn &plugIn, const PlugInModifier &prefix, DataReader &reader) override;
-};
-
 struct ListVariableModifier : public PlugInModifierData {
 	enum ContentsType {
 		kContentsTypeInteger = 1,
diff --git a/engines/mtropolis/plugins.h b/engines/mtropolis/plugins.h
index 484cdbf6027..415c42d620a 100644
--- a/engines/mtropolis/plugins.h
+++ b/engines/mtropolis/plugins.h
@@ -38,6 +38,7 @@ class PlugIn;
 
 namespace PlugIns {
 
+Common::SharedPtr<PlugIn> createMIDI();
 Common::SharedPtr<PlugIn> createStandard();
 Common::SharedPtr<PlugIn> createObsidian(const Common::SharedPtr<Obsidian::WordGameData> &wgData);
 Common::SharedPtr<PlugIn> createMTI();




More information about the Scummvm-git-logs mailing list