[Scummvm-cvs-logs] scummvm master -> 57aa0984e57172ed4f76579f580d7e179a5a132f

m-kiewitz m_kiewitz at users.sourceforge.net
Fri Jun 26 02:53:26 CEST 2015


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:
57aa0984e5 SHERLOCK: Tattoo: Miles Audio 3 AdLib driver


Commit: 57aa0984e57172ed4f76579f580d7e179a5a132f
    https://github.com/scummvm/scummvm/commit/57aa0984e57172ed4f76579f580d7e179a5a132f
Author: Martin Kiewitz (m_kiewitz at users.sourceforge.net)
Date: 2015-06-26T02:52:07+02:00

Commit Message:
SHERLOCK: Tattoo: Miles Audio 3 AdLib driver

- implement Miles Audio 3 AdLib driver
- also cleanup of Scalpel AdLib driver
- work in progress, sustain and for example OPL-3 missing

Changed paths:
  A engines/sherlock/tattoo/drivers/tattoo_adlib.cpp
  A engines/sherlock/tattoo/drivers/tattoo_mididriver.h
    engines/sherlock/module.mk
    engines/sherlock/music.cpp
    engines/sherlock/scalpel/drivers/adlib.cpp
    engines/sherlock/scalpel/drivers/mididriver.h



diff --git a/engines/sherlock/module.mk b/engines/sherlock/module.mk
index badd288..6e573ab 100644
--- a/engines/sherlock/module.mk
+++ b/engines/sherlock/module.mk
@@ -18,6 +18,7 @@ MODULE_OBJS = \
 	scalpel/scalpel_talk.o \
 	scalpel/scalpel_user_interface.o \
 	scalpel/settings.o \
+	tattoo/drivers/tattoo_adlib.o \
 	tattoo/tattoo.o \
 	tattoo/tattoo_journal.o \
 	tattoo/tattoo_map.o \
diff --git a/engines/sherlock/music.cpp b/engines/sherlock/music.cpp
index 63e4a84..c8ca49c 100644
--- a/engines/sherlock/music.cpp
+++ b/engines/sherlock/music.cpp
@@ -25,6 +25,7 @@
 #include "sherlock/sherlock.h"
 #include "sherlock/music.h"
 #include "sherlock/scalpel/drivers/mididriver.h"
+#include "sherlock/tattoo/drivers/tattoo_mididriver.h"
 // for 3DO digital music
 #include "audio/decoders/aiff.h"
 
@@ -240,7 +241,7 @@ Music::Music(SherlockEngine *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) {
 
 		switch (_musicType) {
 		case MT_ADLIB:
-			_midiDriver = MidiDriver_AdLib_create();
+			_midiDriver = MidiDriver_SH_AdLib_create();
 			break;
 		case MT_MT32:
 			_midiDriver = MidiDriver_MT32_create();
@@ -266,8 +267,21 @@ Music::Music(SherlockEngine *vm, Audio::Mixer *mixer) : _vm(vm), _mixer(mixer) {
 
 		_midiParser = MidiParser::createParser_XMIDI();
 		dev = MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
-		_musicType = MT_GM;
-		_midiDriver = MidiDriver::createMidi(dev);
+		_musicType = MidiDriver::getMusicType(dev);
+
+		switch (_musicType) {
+		case MT_ADLIB:
+			// SAMPLE.AD  -> regular AdLib instrument data
+			// SAMPLE.OPL -> OPL-3 instrument data
+			// although in case of Rose Tattoo both files are exactly the same
+			_midiDriver = MidiDriver_Miles_AdLib_create("SAMPLE.AD", "SAMPLE.OPL");
+			break;
+		default:
+			// HACK
+			_musicType = MT_GM;
+			_midiDriver = MidiDriver::createMidi(dev);
+			break;
+		}
 	}
 
 	if (_midiDriver) {
@@ -437,21 +451,22 @@ bool Music::playMusic(const Common::String &name) {
 			}
 		}
 
-		switch (_musicType) {
-		case MT_ADLIB:
-			MidiDriver_AdLib_newMusicData(_midiDriver, dataPos, dataSize);
-			break;
-
-		case MT_MT32:
-			MidiDriver_MT32_newMusicData(_midiDriver, dataPos, dataSize);
-			break;
+		if (IS_SERRATED_SCALPEL) {
+			// Pass the music data to the driver as well
+			// because channel mapping and a few other things inside the header
+			switch (_musicType) {
+			case MT_ADLIB:
+				MidiDriver_SH_AdLib_newMusicData(_midiDriver, dataPos, dataSize);
+				break;
 
-		case MT_GM:
-			break;
+			case MT_MT32:
+				MidiDriver_MT32_newMusicData(_midiDriver, dataPos, dataSize);
+				break;
 
-		default:
-			// should never happen
-			break;
+			default:
+				// should never happen
+				break;
+			}
 		}
 
 		_midiParser->loadMusic(midiMusicData, midiMusicDataSize);
diff --git a/engines/sherlock/scalpel/drivers/adlib.cpp b/engines/sherlock/scalpel/drivers/adlib.cpp
index 372db5e..cc7867a 100644
--- a/engines/sherlock/scalpel/drivers/adlib.cpp
+++ b/engines/sherlock/scalpel/drivers/adlib.cpp
@@ -35,21 +35,21 @@ namespace Sherlock {
 #define SHERLOCK_ADLIB_VOICES_COUNT 9
 #define SHERLOCK_ADLIB_NOTES_COUNT 96
 
-byte adlib_Operator1Register[SHERLOCK_ADLIB_VOICES_COUNT] = {
+byte operator1Register[SHERLOCK_ADLIB_VOICES_COUNT] = {
 	0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12
 };
 
-byte adlib_Operator2Register[SHERLOCK_ADLIB_VOICES_COUNT] = {
+byte operator2Register[SHERLOCK_ADLIB_VOICES_COUNT] = {
 	0x03, 0x04, 0x05, 0x0B, 0x0C, 0x0D, 0x13, 0x14, 0x15
 };
 
-struct adlib_percussionChannelEntry {
+struct percussionChannelEntry {
 	byte requiredNote;
 	byte replacementNote;
 };
 
 // hardcoded, dumped from ADHOM.DRV
-const adlib_percussionChannelEntry adlib_percussionChannelTable[SHERLOCK_ADLIB_VOICES_COUNT] = {
+const percussionChannelEntry percussionChannelTable[SHERLOCK_ADLIB_VOICES_COUNT] = {
 	{ 0x00, 0x00 },
 	{ 0x00, 0x00 },
 	{ 0x00, 0x00 },
@@ -61,7 +61,7 @@ const adlib_percussionChannelEntry adlib_percussionChannelTable[SHERLOCK_ADLIB_V
 	{ 0x26, 0x1E }
 };
 
-struct adlib_InstrumentEntry {
+struct InstrumentEntry {
 	byte reg20op1;
 	byte reg40op1;
 	byte reg60op1;
@@ -77,7 +77,7 @@ struct adlib_InstrumentEntry {
 };
 
 // hardcoded, dumped from ADHOM.DRV
-const adlib_InstrumentEntry adlib_instrumentTable[] = {
+const InstrumentEntry instrumentTable[] = {
 	{ 0x71, 0x89, 0x51, 0x11, 0x00, 0x61, 0x23, 0x42, 0x15, 0x01, 0x02, 0xF4 },
 	{ 0x22, 0x20, 0x97, 0x89, 0x00, 0xA2, 0x1F, 0x70, 0x07, 0x00, 0x0A, 0xF4 },
 	{ 0x70, 0x1A, 0x64, 0x13, 0x00, 0x20, 0x1F, 0x53, 0x46, 0x00, 0x0E, 0xF4 },
@@ -203,7 +203,7 @@ const adlib_InstrumentEntry adlib_instrumentTable[] = {
 };
 
 // hardcoded, dumped from ADHOM.DRV
-uint16 adlib_FrequencyLookUpTable[SHERLOCK_ADLIB_NOTES_COUNT] = {
+uint16 frequencyLookUpTable[SHERLOCK_ADLIB_NOTES_COUNT] = {
 	0x0158, 0x016C, 0x0182, 0x0199, 0x01B1, 0x01CB, 0x01E6, 0x0203, 0x0222, 0x0242,
 	0x0265, 0x0289, 0x0558, 0x056C, 0x0582, 0x0599, 0x05B1, 0x05CB, 0x05E6, 0x0603,
 	0x0622, 0x0642, 0x0665, 0x0689, 0x0958, 0x096C, 0x0982, 0x0999, 0x09B1, 0x09CB,
@@ -216,13 +216,13 @@ uint16 adlib_FrequencyLookUpTable[SHERLOCK_ADLIB_NOTES_COUNT] = {
 	0x1DE6, 0x1E03, 0x1E22, 0x1E42, 0x1E65, 0x1E89
 };
 
-class MidiDriver_AdLib : public MidiDriver_Emulated {
+class MidiDriver_SH_AdLib : public MidiDriver_Emulated {
 public:
-	MidiDriver_AdLib(Audio::Mixer *mixer)
+	MidiDriver_SH_AdLib(Audio::Mixer *mixer)
 		: MidiDriver_Emulated(mixer), _masterVolume(15), _opl(0) {
 		memset(_voiceChannelMapping, 0, sizeof(_voiceChannelMapping));
 	}
-	virtual ~MidiDriver_AdLib() { }
+	virtual ~MidiDriver_SH_AdLib() { }
 
 	// MidiDriver
 	int open();
@@ -249,7 +249,7 @@ private:
 	struct adlib_ChannelEntry {
 		bool   inUse;
 		uint16 inUseTimer;
-		const  adlib_InstrumentEntry *currentInstrumentPtr;
+		const  InstrumentEntry *currentInstrumentPtr;
 		byte   currentNote;
 		byte   currentA0hReg;
 		byte   currentB0hReg;
@@ -284,7 +284,7 @@ private:
 	void pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2);
 };
 
-int MidiDriver_AdLib::open() {
+int MidiDriver_SH_AdLib::open() {
 	int rate = _mixer->getOutputRate();
 
 	debugC(kDebugLevelAdLibDriver, "AdLib: starting driver");
@@ -303,13 +303,13 @@ int MidiDriver_AdLib::open() {
 	return 0;
 }
 
-void MidiDriver_AdLib::close() {
+void MidiDriver_SH_AdLib::close() {
 	_mixer->stopHandle(_mixerSoundHandle);
 
 	delete _opl;
 }
 
-void MidiDriver_AdLib::setVolume(byte volume) {
+void MidiDriver_SH_AdLib::setVolume(byte volume) {
 	_masterVolume = volume;
 	//renewNotes(-1, true);
 }
@@ -317,7 +317,7 @@ void MidiDriver_AdLib::setVolume(byte volume) {
 // this should/must get called per tick
 // original driver did this before MIDI data processing on each tick
 // we do it atm after MIDI data processing
-void MidiDriver_AdLib::onTimer() {
+void MidiDriver_SH_AdLib::onTimer() {
 	for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) {
 		if (_channels[FMvoiceChannel].inUse) {
 			_channels[FMvoiceChannel].inUseTimer++;
@@ -326,7 +326,7 @@ void MidiDriver_AdLib::onTimer() {
 }
 
 // Called when a music track got loaded into memory
-void MidiDriver_AdLib::newMusicData(byte *musicData, int32 musicDataSize) {
+void MidiDriver_SH_AdLib::newMusicData(byte *musicData, int32 musicDataSize) {
 	assert(musicDataSize >= 0x7F);
 	// MIDI Channel <-> FM Voice Channel mapping at offset 0x22 of music data
 	memcpy(&_voiceChannelMapping, musicData + 0x22, 9);
@@ -338,7 +338,7 @@ void MidiDriver_AdLib::newMusicData(byte *musicData, int32 musicDataSize) {
 	memset(&_channels, 0, sizeof(_channels));
 }
 
-void MidiDriver_AdLib::resetAdLib() {
+void MidiDriver_SH_AdLib::resetAdLib() {
 
 	setRegister(0x01, 0x20); // enable waveform control on both operators
 	setRegister(0x04, 0xE0); // Timer control
@@ -357,7 +357,7 @@ void MidiDriver_AdLib::resetAdLib() {
 	resetAdLib_OperatorRegisters(0x40, 0x3F);
 }
 
-void MidiDriver_AdLib::resetAdLib_OperatorRegisters(byte baseRegister, byte value) {
+void MidiDriver_SH_AdLib::resetAdLib_OperatorRegisters(byte baseRegister, byte value) {
 	byte operatorIndex;
 
 	for (operatorIndex = 0; operatorIndex < 0x16; operatorIndex++) {
@@ -373,7 +373,7 @@ void MidiDriver_AdLib::resetAdLib_OperatorRegisters(byte baseRegister, byte valu
 	}
 }
 
-void MidiDriver_AdLib::resetAdLib_FMVoiceChannelRegisters(byte baseRegister, byte value) {
+void MidiDriver_SH_AdLib::resetAdLib_FMVoiceChannelRegisters(byte baseRegister, byte value) {
 	byte FMvoiceChannel;
 
 	for (FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) {
@@ -382,7 +382,7 @@ void MidiDriver_AdLib::resetAdLib_FMVoiceChannelRegisters(byte baseRegister, byt
 }
 
 // MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php
-void MidiDriver_AdLib::send(uint32 b) {
+void MidiDriver_SH_AdLib::send(uint32 b) {
 	byte command = b & 0xf0;
 	byte channel = b & 0xf;
 	byte op1 = (b >> 8) & 0xff;
@@ -417,11 +417,11 @@ void MidiDriver_AdLib::send(uint32 b) {
 	}
 }
 
-void MidiDriver_AdLib::generateSamples(int16 *data, int len) {
+void MidiDriver_SH_AdLib::generateSamples(int16 *data, int len) {
 	_opl->readBuffer(data, len);
 }
 
-void MidiDriver_AdLib::noteOn(byte MIDIchannel, byte note, byte velocity) {
+void MidiDriver_SH_AdLib::noteOn(byte MIDIchannel, byte note, byte velocity) {
 	int16  oldestInUseChannel = -1;
 	uint16 oldestInUseTimer   = 0;
 
@@ -470,11 +470,11 @@ void MidiDriver_AdLib::noteOn(byte MIDIchannel, byte note, byte velocity) {
 		// Percussion channel
 		for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) {
 			if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) {
-				if (note == adlib_percussionChannelTable[FMvoiceChannel].requiredNote) {
+				if (note == percussionChannelTable[FMvoiceChannel].requiredNote) {
 					_channels[FMvoiceChannel].inUse = true;
 					_channels[FMvoiceChannel].currentNote = note;
 
-					voiceOnOff(FMvoiceChannel, true, adlib_percussionChannelTable[FMvoiceChannel].replacementNote, velocity);
+					voiceOnOff(FMvoiceChannel, true, percussionChannelTable[FMvoiceChannel].replacementNote, velocity);
 					return;
 				}
 			}
@@ -483,7 +483,7 @@ void MidiDriver_AdLib::noteOn(byte MIDIchannel, byte note, byte velocity) {
 	}
 }
 
-void MidiDriver_AdLib::noteOff(byte MIDIchannel, byte note) {
+void MidiDriver_SH_AdLib::noteOff(byte MIDIchannel, byte note) {
 	for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) {
 		if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) {
 			if (_channels[FMvoiceChannel].currentNote == note) {
@@ -495,7 +495,7 @@ void MidiDriver_AdLib::noteOff(byte MIDIchannel, byte note) {
 					// not-percussion
 					voiceOnOff(FMvoiceChannel, false, note, 0);
 				} else {
-					voiceOnOff(FMvoiceChannel, false, adlib_percussionChannelTable[FMvoiceChannel].replacementNote, 0);
+					voiceOnOff(FMvoiceChannel, false, percussionChannelTable[FMvoiceChannel].replacementNote, 0);
 				}
 				return;
 			}
@@ -503,7 +503,7 @@ void MidiDriver_AdLib::noteOff(byte MIDIchannel, byte note) {
 	}
 }
 
-void MidiDriver_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, byte velocity) {
+void MidiDriver_SH_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, byte velocity) {
 	byte frequencyOffset = 0;
 	uint16 frequency = 0;
 	byte op2RegAdjust = 0;
@@ -521,7 +521,7 @@ void MidiDriver_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, by
 		warning("CRITICAL - AdLib driver: bad note!!!");
 		return;
 	}
-	frequency = adlib_FrequencyLookUpTable[frequencyOffset];
+	frequency = frequencyLookUpTable[frequencyOffset];
 
 	if (keyOn) {
 		// adjust register 40h
@@ -529,7 +529,7 @@ void MidiDriver_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, by
 			regValue40h = _channels[FMvoiceChannel].currentInstrumentPtr->reg40op2;
 		}
 		regValue40h = regValue40h - (velocity >> 3);
-		op2RegAdjust = adlib_Operator2Register[FMvoiceChannel];
+		op2RegAdjust = operator2Register[FMvoiceChannel];
 		setRegister(0x40 + op2RegAdjust, regValue40h);
 	}
 
@@ -545,7 +545,7 @@ void MidiDriver_AdLib::voiceOnOff(byte FMvoiceChannel, bool keyOn, byte note, by
 	_channels[FMvoiceChannel].currentB0hReg = regValueB0h;
 }
 
-void MidiDriver_AdLib::pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2) {
+void MidiDriver_SH_AdLib::pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2) {
 	uint16 channelFrequency              = 0;
 	byte   channelRegB0hWithoutFrequency = 0;
 	uint16 parameter                     = 0;
@@ -584,20 +584,20 @@ void MidiDriver_AdLib::pitchBendChange(byte MIDIchannel, byte parameter1, byte p
 	}
 }
 
-void MidiDriver_AdLib::programChange(byte MIDIchannel, byte op1) {
-	const adlib_InstrumentEntry *instrumentPtr;
+void MidiDriver_SH_AdLib::programChange(byte MIDIchannel, byte op1) {
+	const InstrumentEntry *instrumentPtr;
 	byte op1Reg = 0;
 	byte op2Reg = 0;
 
 	// setup instrument
-	instrumentPtr = &adlib_instrumentTable[op1];
+	instrumentPtr = &instrumentTable[op1];
 	//warning("program change for MIDI channel %d, instrument id %d", MIDIchannel, op1);
 
 	for (byte FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_ADLIB_VOICES_COUNT; FMvoiceChannel++) {
 		if (_voiceChannelMapping[FMvoiceChannel] == MIDIchannel) {
 
-			op1Reg = adlib_Operator1Register[FMvoiceChannel];
-			op2Reg = adlib_Operator2Register[FMvoiceChannel];
+			op1Reg = operator1Register[FMvoiceChannel];
+			op2Reg = operator2Register[FMvoiceChannel];
 
 			setRegister(0x20 + op1Reg, instrumentPtr->reg20op1);
 			setRegister(0x40 + op1Reg, instrumentPtr->reg40op1);
@@ -618,21 +618,21 @@ void MidiDriver_AdLib::programChange(byte MIDIchannel, byte op1) {
 		}
 	}
 }
-void MidiDriver_AdLib::setRegister(int reg, int value) {
+void MidiDriver_SH_AdLib::setRegister(int reg, int value) {
 	_opl->write(0x220, reg);
 	_opl->write(0x221, value);
 }
 
-uint32 MidiDriver_AdLib::property(int prop, uint32 param) {
+uint32 MidiDriver_SH_AdLib::property(int prop, uint32 param) {
 	return 0;
 }
 
-MidiDriver *MidiDriver_AdLib_create() {
-	return new MidiDriver_AdLib(g_system->getMixer());
+MidiDriver *MidiDriver_SH_AdLib_create() {
+	return new MidiDriver_SH_AdLib(g_system->getMixer());
 }
 
-void MidiDriver_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize) {
-	static_cast<MidiDriver_AdLib *>(driver)->newMusicData(musicData, musicDataSize);
+void MidiDriver_SH_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize) {
+	static_cast<MidiDriver_SH_AdLib *>(driver)->newMusicData(musicData, musicDataSize);
 }
 
 } // End of namespace Sherlock
diff --git a/engines/sherlock/scalpel/drivers/mididriver.h b/engines/sherlock/scalpel/drivers/mididriver.h
index 3cdc321..1b8ceed 100644
--- a/engines/sherlock/scalpel/drivers/mididriver.h
+++ b/engines/sherlock/scalpel/drivers/mididriver.h
@@ -20,8 +20,8 @@
  *
  */
 
-#ifndef SHERLOCK_SOFTSEQ_MIDIDRIVER_H
-#define SHERLOCK_SOFTSEQ_MIDIDRIVER_H
+#ifndef SHERLOCK_SCALPEL_DRIVERS_MIDIDRIVER_H
+#define SHERLOCK_SCALPEL_DRIVERS_MIDIDRIVER_H
 
 #include "sherlock/sherlock.h"
 #include "audio/mididrv.h"
@@ -29,8 +29,8 @@
 
 namespace Sherlock {
 
-extern MidiDriver *MidiDriver_AdLib_create();
-extern void MidiDriver_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize);
+extern MidiDriver *MidiDriver_SH_AdLib_create();
+extern void MidiDriver_SH_AdLib_newMusicData(MidiDriver *driver, byte *musicData, int32 musicDataSize);
 
 extern MidiDriver *MidiDriver_MT32_create();
 extern void MidiDriver_MT32_uploadPatches(MidiDriver *driver, byte *driverData, int32 driverSize);
@@ -38,4 +38,4 @@ extern void MidiDriver_MT32_newMusicData(MidiDriver *driver, byte *musicData, in
 
 } // End of namespace Sherlock
 
-#endif // SHERLOCK_SOFTSEQ_MIDIDRIVER_H
+#endif // SHERLOCK_SCALPEL_DRIVERS_MIDIDRIVER_H
diff --git a/engines/sherlock/tattoo/drivers/tattoo_adlib.cpp b/engines/sherlock/tattoo/drivers/tattoo_adlib.cpp
new file mode 100644
index 0000000..6f2008a
--- /dev/null
+++ b/engines/sherlock/tattoo/drivers/tattoo_adlib.cpp
@@ -0,0 +1,1045 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#include "sherlock/sherlock.h"
+#include "sherlock/tattoo/drivers/tattoo_mididriver.h"
+
+#include "common/file.h"
+#include "common/system.h"
+#include "common/textconsole.h"
+
+#include "audio/fmopl.h"
+#include "audio/softsynth/emumidi.h"
+
+namespace Sherlock {
+
+#define SHERLOCK_MILES_MIDI_CHANNEL_COUNT 16
+#define SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT 9
+#define SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT 16
+
+#define SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT_MAX 20
+
+#define SHERLOCK_MILES_ADLIB_PERCUSSION_BANK 127
+
+enum kMilesAdLibUpdateFlags {
+	kMilesAdLibUpdateFlags_None    = 0,
+	kMilesAdLibUpdateFlags_Reg_20  = 1 << 0,
+	kMilesAdLibUpdateFlags_Reg_40  = 1 << 1,
+	kMilesAdLibUpdateFlags_Reg_60  = 1 << 2, // register 0x6x + 0x8x
+	kMilesAdLibUpdateFlags_Reg_C0  = 1 << 3,
+	kMilesAdLibUpdateFlags_Reg_E0  = 1 << 4,
+	kMilesAdLibUpdateFlags_Reg_A0  = 1 << 5, // register 0xAx + 0xBx
+	kMilesAdLibUpdateFlags_Reg_All = 0x3F
+};
+
+byte milesOperator1Register[SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT] = {
+	0x00, 0x01, 0x02, 0x08, 0x09, 0x0A, 0x10, 0x11, 0x12
+};
+
+byte milesOperator2Register[SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT] = {
+	0x03, 0x04, 0x05, 0x0B, 0x0C, 0x0D, 0x13, 0x14, 0x15
+};
+
+struct InstrumentEntry {
+	byte bankId;
+	byte patchId;
+	int16 transposition;
+	byte reg20op1;
+	byte reg40op1;
+	byte reg60op1;
+	byte reg80op1;
+	byte regE0op1;
+	byte reg20op2;
+	byte reg40op2;
+	byte reg60op2;
+	byte reg80op2;
+	byte regE0op2;
+	byte regC0;
+};
+
+// hardcoded, dumped from ADLIB.MDI
+uint16 milesFrequencyLookUpTable[] = {
+	0x02B2, 0x02B4, 0x02B7, 0x02B9, 0x02BC, 0x02BE, 0x02C1, 0x02C3, 0x02C6, 0x02C9, 0x02CB, 0x02CE,
+	0x02D0, 0x02D3, 0x02D6, 0x02D8, 0x02DB, 0x02DD, 0x02E0, 0x02E3, 0x02E5, 0x02E8, 0x02EB, 0x02ED,
+	0x02F0, 0x02F3, 0x02F6, 0x02F8, 0x02FB, 0x02FE, 0x0301, 0x0303, 0x0306, 0x0309, 0x030C, 0x030F,
+	0x0311, 0x0314, 0x0317, 0x031A, 0x031D, 0x0320, 0x0323, 0x0326, 0x0329, 0x032B, 0x032E, 0x0331,
+	0x0334, 0x0337, 0x033A, 0x033D, 0x0340, 0x0343, 0x0346, 0x0349, 0x034C, 0x034F, 0x0352, 0x0356,
+	0x0359, 0x035C, 0x035F, 0x0362, 0x0365, 0x0368, 0x036B, 0x036F, 0x0372, 0x0375, 0x0378, 0x037B,
+	0x037F, 0x0382, 0x0385, 0x0388, 0x038C, 0x038F, 0x0392, 0x0395, 0x0399, 0x039C, 0x039F, 0x03A3,
+	0x03A6, 0x03A9, 0x03AD, 0x03B0, 0x03B4, 0x03B7, 0x03BB, 0x03BE, 0x03C1, 0x03C5, 0x03C8, 0x03CC,
+	0x03CF, 0x03D3, 0x03D7, 0x03DA, 0x03DE, 0x03E1, 0x03E5, 0x03E8, 0x03EC, 0x03F0, 0x03F3, 0x03F7,
+	0x03FB, 0x03FE, 0xFE01, 0xFE03, 0xFE05, 0xFE07, 0xFE08, 0xFE0A, 0xFE0C, 0xFE0E, 0xFE10, 0xFE12,
+	0xFE14, 0xFE16, 0xFE18, 0xFE1A, 0xFE1C, 0xFE1E, 0xFE20, 0xFE21, 0xFE23, 0xFE25, 0xFE27, 0xFE29,
+	0xFE2B, 0xFE2D, 0xFE2F, 0xFE31, 0xFE34, 0xFE36, 0xFE38, 0xFE3A, 0xFE3C, 0xFE3E, 0xFE40, 0xFE42,
+	0xFE44, 0xFE46, 0xFE48, 0xFE4A, 0xFE4C, 0xFE4F, 0xFE51, 0xFE53, 0xFE55, 0xFE57, 0xFE59, 0xFE5C,
+	0xFE5E, 0xFE60, 0xFE62, 0xFE64, 0xFE67, 0xFE69, 0xFE6B, 0xFE6D, 0xFE6F, 0xFE72, 0xFE74, 0xFE76,
+	0xFE79, 0xFE7B, 0xFE7D, 0xFE7F, 0xFE82, 0xFE84, 0xFE86, 0xFE89, 0xFE8B, 0xFE8D, 0xFE90, 0xFE92,
+	0xFE95, 0xFE97, 0xFE99, 0xFE9C, 0xFE9E, 0xFEA1, 0xFEA3, 0xFEA5, 0xFEA8, 0xFEAA, 0xFEAD, 0xFEAF
+};
+
+// hardcoded, dumped from ADLIB.MDI
+uint16 milesVolumeSensitivityTable[] = {
+	82, 85, 88, 91, 94, 97, 100, 103, 106, 109, 112, 115, 118, 121, 124, 127
+};
+
+
+class MidiDriver_Miles_AdLib : public MidiDriver_Emulated {
+public:
+	MidiDriver_Miles_AdLib(Audio::Mixer *mixer, InstrumentEntry *instrumentTablePtr, uint16 instrumentTableCount);
+	virtual ~MidiDriver_Miles_AdLib();
+
+	// MidiDriver
+	int open();
+	void close();
+	void send(uint32 b);
+	MidiChannel *allocateChannel() { return NULL; }
+	MidiChannel *getPercussionChannel() { return NULL; }
+
+	// AudioStream
+	bool isStereo() const { return false; }
+	int getRate() const { return _mixer->getOutputRate(); }
+	int getPolyphony() const { return SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT; }
+	bool hasRhythmChannel() const { return false; }
+
+	// MidiDriver_Emulated
+	void generateSamples(int16 *buf, int len);
+
+	void setVolume(byte volume);
+	virtual uint32 property(int prop, uint32 param);
+
+private:
+	// Structure to hold information about current status of MIDI Channels
+	struct MidiChannelEntry {
+		byte   currentPatchBank;
+		const  InstrumentEntry *currentInstrumentPtr;
+		uint16 currentPitchBender;
+		byte   currentPitchRange;
+		byte   currentVoiceProtection;
+
+		byte   currentVolume;
+		byte   currentVolumeExpression;
+
+		byte   currentModulation;
+		byte   currentSustain;
+
+		byte   currentActiveVoicesCount;
+
+		MidiChannelEntry() : currentPatchBank(0),
+							currentInstrumentPtr(NULL),
+							currentPitchBender(SHERLOCK_MILES_PITCHBENDER_DEFAULT),
+							currentVoiceProtection(0),
+							currentVolume(0), currentVolumeExpression(0),
+							currentModulation(0),
+							currentSustain(0),
+							currentActiveVoicesCount(0) { }
+	};
+
+	// Structure to hold information about current status of virtual FM Voices
+	struct VirtualFmVoiceEntry {
+		bool   inUse;
+		byte   actualMidiChannel;
+
+		const  InstrumentEntry *currentInstrumentPtr;
+
+		bool   isPhysical;
+		byte   physicalFmVoice;
+
+		uint16 currentPriority;
+
+		byte   currentNote;
+		int16  currentTransposition;
+		byte   currentVelocity;
+
+		bool   sustained;
+
+		VirtualFmVoiceEntry(): inUse(false),
+								actualMidiChannel(0),
+								currentInstrumentPtr(NULL),
+								isPhysical(false), physicalFmVoice(0),
+								currentPriority(0),
+								currentNote(0),
+								currentTransposition(0),
+								currentVelocity(0),
+								sustained(false) { }
+	};
+
+	// Structure to hold information about current status of physical FM Voices
+	struct PhysicalFmVoiceEntry {
+		bool   inUse;
+		byte   virtualFmVoice;
+
+		byte   currentA0hReg;
+		byte   currentB0hReg;
+
+		PhysicalFmVoiceEntry(): inUse(false),
+								virtualFmVoice(0),
+								currentA0hReg(0), currentB0hReg(0) { }
+	};
+
+	OPL::OPL *_opl;
+	int _masterVolume;
+
+	// stores information about all MIDI channels (not the actual OPL FM voice channels!)
+	MidiChannelEntry _midiChannels[SHERLOCK_MILES_MIDI_CHANNEL_COUNT];
+
+	// stores information about all virtual OPL FM voices
+	VirtualFmVoiceEntry _virtualFmVoices[SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT];
+
+	// stores information about all physical OPL FM voices
+	PhysicalFmVoiceEntry _physicalFmVoices[SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT];
+
+	// holds all instruments
+	InstrumentEntry *_instrumentTablePtr;
+	uint16           _instrumentTableCount;
+
+protected:
+	void onTimer();
+
+private:
+	void resetData();
+	void resetAdLib();
+	void resetAdLib_OperatorRegisters(byte baseRegister, byte value);
+	void resetAdLib_FMVoiceChannelRegisters(byte baseRegister, byte value);
+
+	void setRegister(int reg, int value);
+
+	int16 searchFreeVirtualFmVoiceChannel();
+	int16 searchFreePhysicalFmVoiceChannel();
+
+	void noteOn(byte midiChannel, byte note, byte velocity);
+	void noteOff(byte midiChannel, byte note);
+	//void voiceOnOff(byte fmVoiceChannel, bool KeyOn, byte note, byte velocity);
+
+	void prioritySort();
+
+	void releaseFmVoice(byte virtualFmVoice);
+
+	void updatePhysicalFmVoice(byte virtualFmVoice, bool keyOn, uint16 registerUpdateFlags);
+
+	void controlChange(byte midiChannel, byte controllerNumber, byte controllerValue);
+	void programChange(byte midiChannel, byte patchId);
+
+	const InstrumentEntry *searchInstrument(byte bankId, byte patchId);
+
+	void pitchBendChange(byte MIDIchannel, byte parameter1, byte parameter2);
+};
+
+MidiDriver_Miles_AdLib::MidiDriver_Miles_AdLib(Audio::Mixer *mixer, InstrumentEntry *instrumentTablePtr, uint16 instrumentTableCount)
+	: MidiDriver_Emulated(mixer), _masterVolume(15), _opl(0) {
+
+	_instrumentTablePtr = instrumentTablePtr;
+	_instrumentTableCount = instrumentTableCount;
+
+	resetData();
+}
+
+MidiDriver_Miles_AdLib::~MidiDriver_Miles_AdLib() {
+	delete[] _instrumentTablePtr; // is created in factory MidiDriver_Miles_AdLib_create()
+}
+
+int MidiDriver_Miles_AdLib::open() {
+	int rate = _mixer->getOutputRate();
+
+	_opl = OPL::Config::create(OPL::Config::kOpl2);
+
+	if (!_opl)
+		return -1;
+
+	_opl->init(rate);
+
+	MidiDriver_Emulated::open();
+
+	_mixer->playStream(Audio::Mixer::kPlainSoundType, &_mixerSoundHandle, this, -1, _mixer->kMaxChannelVolume, 0, DisposeAfterUse::NO);
+
+	resetAdLib();
+
+	return 0;
+}
+
+void MidiDriver_Miles_AdLib::close() {
+	_mixer->stopHandle(_mixerSoundHandle);
+
+	delete _opl;
+}
+
+void MidiDriver_Miles_AdLib::setVolume(byte volume) {
+	_masterVolume = volume;
+	//renewNotes(-1, true);
+}
+
+void MidiDriver_Miles_AdLib::onTimer() {
+}
+
+void MidiDriver_Miles_AdLib::resetData() {
+	memset(_midiChannels, 0, sizeof(_midiChannels));
+	memset(_virtualFmVoices, 0, sizeof(_virtualFmVoices));
+	memset(_physicalFmVoices, 0, sizeof(_physicalFmVoices));
+
+	for (byte midiChannel = 0; midiChannel < SHERLOCK_MILES_MIDI_CHANNEL_COUNT; midiChannel++) {
+		_midiChannels[midiChannel].currentPitchBender = SHERLOCK_MILES_PITCHBENDER_DEFAULT;
+		_midiChannels[midiChannel].currentPitchRange = 12;
+		// Miles Audio 2: hardcoded pitch range as a global (not channel specific), set to 12
+		// Miles Audio 3: pitch range per MIDI channel
+		_midiChannels[midiChannel].currentVolumeExpression = 127;
+	}
+
+}
+
+void MidiDriver_Miles_AdLib::resetAdLib() {
+	setRegister(0x01, 0x20); // enable waveform control on both operators
+	setRegister(0x04, 0xE0); // Timer control
+
+	setRegister(0x08, 0); // select FM music mode
+	setRegister(0xBD, 0); // disable Rhythm
+
+	// reset FM voice instrument data
+	resetAdLib_OperatorRegisters(0x20, 0);
+	resetAdLib_OperatorRegisters(0x60, 0);
+	resetAdLib_OperatorRegisters(0x80, 0);
+	resetAdLib_FMVoiceChannelRegisters(0xA0, 0);
+	resetAdLib_FMVoiceChannelRegisters(0xB0, 0);
+	resetAdLib_FMVoiceChannelRegisters(0xC0, 0);
+	resetAdLib_OperatorRegisters(0xE0, 0);
+	resetAdLib_OperatorRegisters(0x40, 0x3F);
+}
+
+void MidiDriver_Miles_AdLib::resetAdLib_OperatorRegisters(byte baseRegister, byte value) {
+	byte operatorIndex;
+
+	for (operatorIndex = 0; operatorIndex < 0x16; operatorIndex++) {
+		switch (operatorIndex) {
+		case 0x06:
+		case 0x07:
+		case 0x0E:
+		case 0x0F:
+			break;
+		default:
+			setRegister(baseRegister + operatorIndex, value);
+		}
+	}
+}
+
+void MidiDriver_Miles_AdLib::resetAdLib_FMVoiceChannelRegisters(byte baseRegister, byte value) {
+	byte FMvoiceChannel;
+
+	for (FMvoiceChannel = 0; FMvoiceChannel < SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT; FMvoiceChannel++) {
+		setRegister(baseRegister + FMvoiceChannel, value);
+	}
+}
+
+// MIDI messages can be found at http://www.midi.org/techspecs/midimessages.php
+void MidiDriver_Miles_AdLib::send(uint32 b) {
+	byte command = b & 0xf0;
+	byte channel = b & 0xf;
+	byte op1 = (b >> 8) & 0xff;
+	byte op2 = (b >> 16) & 0xff;
+
+	switch (command) {
+	case 0x80:
+		noteOff(channel, op1);
+		break;
+	case 0x90:
+		noteOn(channel, op1, op2);
+		break;
+	case 0xb0: // Control change
+		controlChange(channel, op1, op2);
+		break;
+	case 0xc0: // Program Change
+		programChange(channel, op1);
+		break;
+	case 0xa0: // Polyphonic key pressure (aftertouch)
+	case 0xd0: // Channel pressure (aftertouch)
+		// Aftertouch doesn't seem to be implemented in the Sherlock Holmes adlib driver
+		break;
+	case 0xe0:
+		pitchBendChange(channel, op1, op2);
+		break;
+	case 0xf0: // SysEx
+		warning("MILES-ADLIB: SysEx: %x", b);
+		break;
+	default:
+		warning("MILES-ADLIB: Unknown event %02x", command);
+	}
+}
+
+void MidiDriver_Miles_AdLib::generateSamples(int16 *data, int len) {
+	_opl->readBuffer(data, len);
+}
+
+int16 MidiDriver_Miles_AdLib::searchFreeVirtualFmVoiceChannel() {
+	for (byte virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+		if (!_virtualFmVoices[virtualFmVoice].inUse)
+			return virtualFmVoice;
+	}
+	return -1;
+}
+
+int16 MidiDriver_Miles_AdLib::searchFreePhysicalFmVoiceChannel() {
+	for (byte physicalFmVoice = 0; physicalFmVoice < SHERLOCK_MILES_ADLIB_PHYSICAL_FMVOICES_COUNT; physicalFmVoice++) {
+		if (!_physicalFmVoices[physicalFmVoice].inUse)
+			return physicalFmVoice;
+	}
+	return -1;
+}
+
+void MidiDriver_Miles_AdLib::noteOn(byte midiChannel, byte note, byte velocity) {
+	const InstrumentEntry *instrumentPtr = NULL;
+
+	//warning("Note On: channel %d, note %d, velocity %d", midiChannel, note, velocity);
+	if (velocity == 0) {
+		noteOff(midiChannel, note);
+		return;
+	}
+
+	if (midiChannel == 9) {
+		// percussion channel
+		// search for instrument according to given note
+		instrumentPtr = searchInstrument(SHERLOCK_MILES_ADLIB_PERCUSSION_BANK, note);
+	} else {
+		// directly get instrument of channel
+		instrumentPtr = _midiChannels[midiChannel].currentInstrumentPtr;
+	}
+	if (!instrumentPtr) {
+		warning("MILES-ADLIB: noteOn: invalid instrument");
+		return;
+	}
+
+	// look for free virtual FM voice
+	int16 virtualFmVoice = searchFreeVirtualFmVoiceChannel();
+
+	if (virtualFmVoice == -1) {
+		// Out of virtual voices,  can't do anything about it
+		return;
+	}
+
+	// Scale back velocity
+	velocity = (velocity & 0x7F) >> 3;
+	velocity = milesVolumeSensitivityTable[velocity];
+
+	if (midiChannel != 9) {
+		_virtualFmVoices[virtualFmVoice].currentNote = note;
+		_virtualFmVoices[virtualFmVoice].currentTransposition = instrumentPtr->transposition;
+	} else {
+		// Percussion channel
+		_virtualFmVoices[virtualFmVoice].currentNote = instrumentPtr->transposition;
+		_virtualFmVoices[virtualFmVoice].currentTransposition = 0;
+	}
+
+	_virtualFmVoices[virtualFmVoice].inUse = true;
+	_virtualFmVoices[virtualFmVoice].actualMidiChannel = midiChannel;
+	_virtualFmVoices[virtualFmVoice].currentInstrumentPtr = instrumentPtr;
+	_virtualFmVoices[virtualFmVoice].currentVelocity = velocity;
+	_virtualFmVoices[virtualFmVoice].isPhysical = false;
+	_virtualFmVoices[virtualFmVoice].sustained = false;
+	_virtualFmVoices[virtualFmVoice].currentPriority = 32767;
+
+	int16 physicalFmVoice = searchFreePhysicalFmVoiceChannel();
+	if (physicalFmVoice == -1) {
+		// None found
+		// go through priorities and reshuffle voices
+		prioritySort();
+		return;
+	}
+
+	// Another voice active on this MIDI channel
+	_midiChannels[midiChannel].currentActiveVoicesCount++;
+
+	// Mark virtual FM-Voice as being connected to physical FM-Voice
+	_virtualFmVoices[virtualFmVoice].isPhysical = true;
+	_virtualFmVoices[virtualFmVoice].physicalFmVoice = physicalFmVoice;
+
+	// Mark physical FM-Voice as being connected to virtual FM-Voice
+	_physicalFmVoices[physicalFmVoice].inUse = true;
+	_physicalFmVoices[physicalFmVoice].virtualFmVoice = virtualFmVoice;
+
+	// Update the physical FM-Voice
+	updatePhysicalFmVoice(virtualFmVoice, true, kMilesAdLibUpdateFlags_Reg_All);
+}
+
+void MidiDriver_Miles_AdLib::noteOff(byte midiChannel, byte note) {
+	//warning("Note Off: channel %d, note %d", midiChannel, note);
+
+	// Search through all virtual FM-Voices for current midiChannel + note
+	for (byte virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+		if (_virtualFmVoices[virtualFmVoice].inUse) {
+			if ((_virtualFmVoices[virtualFmVoice].actualMidiChannel == midiChannel) && (_virtualFmVoices[virtualFmVoice].currentNote == note)) {
+				// found one
+				if (_midiChannels[midiChannel].currentSustain >= 64) {
+					_virtualFmVoices[virtualFmVoice].sustained = true;
+					continue;
+				}
+				// 
+				releaseFmVoice(virtualFmVoice);
+				_virtualFmVoices[virtualFmVoice].inUse = false;
+			}
+		}
+	}
+}
+
+void MidiDriver_Miles_AdLib::prioritySort() {
+	byte   virtualFmVoice = 0;
+	uint16 virtualPriority = 0;
+	uint16 virtualPriorities[SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT_MAX];
+	uint16 virtualFmVoicesCount = 0;
+	byte   midiChannel = 0;
+
+	memset(&virtualPriorities, 0, sizeof(virtualPriorities));
+
+	//warning("prioritysort");
+
+	// First calculate priorities for all virtual FM voices, that are in use
+	for (virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+		if (_virtualFmVoices[virtualFmVoice].inUse) {
+			virtualFmVoicesCount++;
+
+			midiChannel = _virtualFmVoices[virtualFmVoice].actualMidiChannel;
+			if (_midiChannels[midiChannel].currentVoiceProtection >= 64) {
+				// Voice protection enabled
+				virtualPriority = 0xFFFF;
+			} else {
+				virtualPriority = _virtualFmVoices[virtualFmVoice].currentPriority;
+			}
+			byte currentActiveVoicesCount = _midiChannels[midiChannel].currentActiveVoicesCount;
+			if (virtualPriority >= currentActiveVoicesCount) {
+				virtualPriority -= _midiChannels[midiChannel].currentActiveVoicesCount;
+			} else {
+				virtualPriority = 0; // overflow, should never happen
+			}
+			virtualPriorities[virtualFmVoice] = virtualPriority;
+		}
+	}
+
+	// 	
+	while (virtualFmVoicesCount) {
+		uint16 unvoicedHighestPriority = 0;
+		byte   unvoicedHighestFmVoice = 0;
+		uint16 voicedLowestPriority = 65535;
+		byte   voicedLowestFmVoice = 0;
+
+		for (virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+			if (_virtualFmVoices[virtualFmVoice].inUse) {
+				virtualPriority = virtualPriorities[virtualFmVoice];
+				if (!_virtualFmVoices[virtualFmVoice].isPhysical) {
+					// currently not physical, so unvoiced
+					if (virtualPriority >= unvoicedHighestPriority) {
+						unvoicedHighestPriority = virtualPriority;
+						unvoicedHighestFmVoice  = virtualFmVoice;
+					}
+				} else {
+					// currently physical, so voiced
+					if (virtualPriority <= voicedLowestPriority) {
+						voicedLowestPriority = virtualPriority;
+						voicedLowestFmVoice  = virtualFmVoice;
+					}
+				}
+			}
+		}
+
+		if (unvoicedHighestPriority < voicedLowestPriority)
+			break; // We are done
+
+		if (unvoicedHighestPriority == 0)
+			break;
+
+		// Safety checks
+		assert(_virtualFmVoices[voicedLowestFmVoice].isPhysical);
+		assert(!_virtualFmVoices[unvoicedHighestFmVoice].isPhysical);
+
+		// Steal this physical voice
+		byte physicalFmVoice = _virtualFmVoices[voicedLowestFmVoice].physicalFmVoice;
+
+		//warning("MILES-ADLIB: stealing physical FM-Voice %d from virtual FM-Voice %d for virtual FM-Voice %d", physicalFmVoice, voicedLowestFmVoice, unvoicedHighestFmVoice);
+		//warning("priority old %d, priority new %d", unvoicedHighestPriority, voicedLowestPriority);
+
+		releaseFmVoice(voicedLowestFmVoice);
+		_virtualFmVoices[voicedLowestFmVoice].inUse = false;
+
+		// Get some data of the unvoiced highest priority virtual FM Voice
+		midiChannel = _virtualFmVoices[unvoicedHighestFmVoice].actualMidiChannel;
+
+		// Another voice active on this MIDI channel
+		_midiChannels[midiChannel].currentActiveVoicesCount++;
+
+		// Mark virtual FM-Voice as being connected to physical FM-Voice
+		_virtualFmVoices[unvoicedHighestFmVoice].isPhysical = true;
+		_virtualFmVoices[unvoicedHighestFmVoice].physicalFmVoice = physicalFmVoice;
+
+		// Mark physical FM-Voice as being connected to virtual FM-Voice
+		_physicalFmVoices[physicalFmVoice].inUse = true;
+		_physicalFmVoices[physicalFmVoice].virtualFmVoice = unvoicedHighestFmVoice;
+
+		// Update the physical FM-Voice
+		updatePhysicalFmVoice(unvoicedHighestFmVoice, true, kMilesAdLibUpdateFlags_Reg_All);
+
+		virtualFmVoicesCount--;
+	}
+}
+
+void MidiDriver_Miles_AdLib::releaseFmVoice(byte virtualFmVoice) {
+	// virtual Voice not actually played? -> exit
+	if (!_virtualFmVoices[virtualFmVoice].isPhysical) {
+		return;
+	}
+
+	byte midiChannel = _virtualFmVoices[virtualFmVoice].actualMidiChannel;
+	byte physicalFmVoice = _virtualFmVoices[virtualFmVoice].physicalFmVoice;
+
+	// stop note from playing
+	updatePhysicalFmVoice(virtualFmVoice, false, kMilesAdLibUpdateFlags_Reg_A0);
+
+	// this virtual FM voice isn't physical anymore
+	_virtualFmVoices[virtualFmVoice].isPhysical = false;
+
+	// Remove physical FM-Voice from being active
+	_physicalFmVoices[physicalFmVoice].inUse = false;
+
+	// One less voice active on this MIDI channel
+	assert(_midiChannels[midiChannel].currentActiveVoicesCount);
+	_midiChannels[midiChannel].currentActiveVoicesCount--;	
+}
+
+void MidiDriver_Miles_AdLib::updatePhysicalFmVoice(byte virtualFmVoice, bool keyOn, uint16 registerUpdateFlags) {
+	byte midiChannel = _virtualFmVoices[virtualFmVoice].actualMidiChannel;
+
+	if (!_virtualFmVoices[virtualFmVoice].isPhysical) {
+		// virtual FM-Voice has no physical FM-Voice assigned? -> ignore
+		return;
+	}
+
+	byte                   physicalFmVoice = _virtualFmVoices[virtualFmVoice].physicalFmVoice;
+	const InstrumentEntry *instrumentPtr = _virtualFmVoices[virtualFmVoice].currentInstrumentPtr;
+
+	byte op1Reg = milesOperator1Register[physicalFmVoice];
+	byte op2Reg = milesOperator2Register[physicalFmVoice];
+
+	uint16 compositeVolume = 0;
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_40) {
+		// Calculate new volume
+		byte midiVolume = _midiChannels[midiChannel].currentVolume;
+		byte midiVolumeExpression = _midiChannels[midiChannel].currentVolumeExpression;
+		compositeVolume = midiVolume * midiVolumeExpression * 2;
+
+		compositeVolume = compositeVolume >> 8; // get upmost 8 bits
+		if (compositeVolume)
+			compositeVolume++; // round up in case result wasn't 0
+
+		compositeVolume = compositeVolume * _virtualFmVoices[virtualFmVoice].currentVelocity * 2;
+		compositeVolume = compositeVolume >> 8; // get upmost 8 bits
+		if (compositeVolume)
+			compositeVolume++; // round up in case result wasn't 0
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_20) {
+		// Amplitude Modulation / Vibrato / Envelope Generator Type / Keyboard Scaling Rate / Modulator Frequency Multiple
+		byte reg20op1 = instrumentPtr->reg20op1;
+		byte reg20op2 = instrumentPtr->reg20op2;
+
+		if (_midiChannels[midiChannel].currentModulation >= 64) {
+			// set bit 6 (Vibrato)
+			reg20op1 |= 0x40;
+			reg20op2 |= 0x40;
+		}
+		setRegister(0x20 + op1Reg, reg20op1);
+		setRegister(0x20 + op2Reg, reg20op2);
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_40) {
+		// Volume (Level Key Scaling / Total Level)
+		byte reg40op1 = instrumentPtr->reg40op1;
+		byte reg40op2 = instrumentPtr->reg40op2;
+
+		uint16 volumeOp1 = (~reg40op1) & 0x3F;
+		uint16 volumeOp2 = (~reg40op2) & 0x3F;
+
+		if (instrumentPtr->regC0 & 1) {
+			// operator 2 enabled
+			// scale volume factor
+			volumeOp1 = (volumeOp1 * compositeVolume) / 127;
+			// 2nd operator always scaled
+		}
+
+		volumeOp2 = (volumeOp2 * compositeVolume) / 127;
+
+		volumeOp1 = (~volumeOp1) & 0x3F; // negate it, so we get the proper value for the register
+		volumeOp2 = (~volumeOp2) & 0x3F; // ditto
+		reg40op1  = (reg40op1 & 0xC0) | volumeOp1; // keep "scaling level" and merge in our volume
+		reg40op2  = (reg40op2 & 0xC0) | volumeOp2;
+
+		setRegister(0x40 + op1Reg, reg40op1);
+		setRegister(0x40 + op2Reg, reg40op2);
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_60) {
+		// Attack Rate / Decay Rate
+		// Sustain Level / Release Rate
+		byte reg60op1 = instrumentPtr->reg60op1;
+		byte reg60op2 = instrumentPtr->reg60op2;
+		byte reg80op1 = instrumentPtr->reg80op1;
+		byte reg80op2 = instrumentPtr->reg80op2;
+
+		setRegister(0x60 + op1Reg, reg60op1);
+		setRegister(0x60 + op2Reg, reg60op2);
+		setRegister(0x80 + op1Reg, reg80op1);
+		setRegister(0x80 + op2Reg, reg80op2);
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_E0) {
+		// Waveform Select
+		byte regE0op1 = instrumentPtr->regE0op1;
+		byte regE0op2 = instrumentPtr->regE0op2;
+
+		setRegister(0xE0 + op1Reg, regE0op1);
+		setRegister(0xE0 + op2Reg, regE0op2);
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_C0) {
+		// Feedback / Algorithm
+		byte regC0 = instrumentPtr->regC0;
+
+		setRegister(0xC0 + physicalFmVoice, regC0);
+	}
+
+	if (registerUpdateFlags & kMilesAdLibUpdateFlags_Reg_A0) {
+		// Frequency / Key-On
+		// Octave / F-Number / Key-On
+		if (!keyOn) {
+			// turn off note
+			byte regB0 = _physicalFmVoices[physicalFmVoice].currentB0hReg & 0x1F; // remove bit 5 "key on"
+			setRegister(0xB0 + physicalFmVoice, regB0);
+
+		} else {
+			// turn on note, calculate frequency, octave...
+			int16 pitchBender = _midiChannels[midiChannel].currentPitchBender;
+			byte  pitchRange = _midiChannels[midiChannel].currentPitchRange;
+			int16 currentNote = _virtualFmVoices[virtualFmVoice].currentNote;
+			int16 physicalNote = 0;
+			int16 halfTone = 0;
+			uint16 frequency = 0;
+			uint16 frequencyIdx = 0;
+			byte   octave = 0;
+
+			pitchBender -= 0x2000;
+			pitchBender = pitchBender >> 5; // divide by 32
+			pitchBender = pitchBender * pitchRange; // pitchrange 12: now +0x0C00 to -0xC00
+			// difference between Miles Audio 2 + 3
+			// Miles Audio 2 used a pitch range of 12, which was basically hardcoded
+			// Miles Audio 3 used an array, which got set by control change events
+
+			currentNote += _virtualFmVoices->currentTransposition;
+
+			// Normalize note
+			currentNote -= 24;
+			do {
+				currentNote += 12;
+			} while (currentNote < 0);
+			currentNote += 12;
+
+			do {
+				currentNote -= 12;
+			} while (currentNote > 95);
+
+			// combine note + pitchbender, also adjust by 8 for rounding
+			currentNote = (currentNote << 8) + pitchBender + 8;
+
+			currentNote = currentNote >> 4; // get actual note
+
+			// Normalize
+			currentNote -= (12 * 16);
+			do {
+				currentNote += (12 * 16);
+			} while (currentNote < 0);
+
+			currentNote += (12 * 16);
+			do {
+				currentNote -= (12 * 16);
+			} while (currentNote > ((96 * 16) - 1));
+
+			physicalNote = currentNote >> 4;
+
+			halfTone = physicalNote % 12; // remainder of physicalNote / 12
+
+			frequencyIdx = (halfTone << 4) + (currentNote & 0x0F);
+			assert(frequencyIdx < sizeof(milesFrequencyLookUpTable));
+			frequency = milesFrequencyLookUpTable[frequencyIdx];
+
+			octave = (physicalNote / 12) - 1;
+
+			if (frequency & 0x8000)
+				octave++;
+
+			if (octave & 0x80) {
+				octave++;
+				frequency = frequency >> 1;
+			}
+
+			byte regA0 = frequency & 0xFF;
+			byte regB0 = ((frequency >> 8) & 0x03) | (octave << 2) | 0x20;
+
+			setRegister(0xA0 + physicalFmVoice, regA0);
+			setRegister(0xB0 + physicalFmVoice, regB0);
+
+			_physicalFmVoices[physicalFmVoice].currentB0hReg = regB0;
+		}
+	}
+
+	//warning("end of update voice");
+}
+
+void MidiDriver_Miles_AdLib::controlChange(byte midiChannel, byte controllerNumber, byte controllerValue) {
+	uint16 registerUpdateFlags = kMilesAdLibUpdateFlags_None;
+
+	switch (controllerNumber) {
+	case SHERLOCK_MILES_CONTROLLER_SELECT_PATCH_BANK:
+		_midiChannels[midiChannel].currentPatchBank = controllerValue;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_PROTECT_VOICE:
+		_midiChannels[midiChannel].currentVoiceProtection = controllerValue;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_PROTECT_TIMBRE:
+		// It seems that this can get ignored, because we don't cache timbres at all
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_MODULATION:
+		_midiChannels[midiChannel].currentModulation = controllerValue;
+		registerUpdateFlags = kMilesAdLibUpdateFlags_Reg_20;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_VOLUME:
+		_midiChannels[midiChannel].currentVolume = controllerValue;
+		registerUpdateFlags = kMilesAdLibUpdateFlags_Reg_40;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_EXPRESSION:
+		_midiChannels[midiChannel].currentVolumeExpression = controllerValue;
+		registerUpdateFlags = kMilesAdLibUpdateFlags_Reg_40;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_PANNING:
+		//warning("MILES-ADLIB: controlChange: panning");
+		registerUpdateFlags = kMilesAdLibUpdateFlags_Reg_C0;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_SUSTAIN:
+		_midiChannels[midiChannel].currentSustain = controllerValue;
+		if (controllerValue < 64) {
+			// release sustain TODO
+		}
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_PITCH_RANGE:
+		// Miles Audio 3 feature
+		_midiChannels[midiChannel].currentPitchRange = controllerValue;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_RESET_ALL:
+		_midiChannels[midiChannel].currentSustain = 0;
+		// release sustain TODO
+		_midiChannels[midiChannel].currentModulation = 0;
+		_midiChannels[midiChannel].currentVolumeExpression = 127;
+		_midiChannels[midiChannel].currentPitchBender = SHERLOCK_MILES_PITCHBENDER_DEFAULT;
+		registerUpdateFlags = kMilesAdLibUpdateFlags_Reg_20 | kMilesAdLibUpdateFlags_Reg_40 | kMilesAdLibUpdateFlags_Reg_A0;
+		break;
+
+	case SHERLOCK_MILES_CONTROLLER_ALL_NOTES_OFF:
+		for (byte virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+			if (_virtualFmVoices[virtualFmVoice].inUse) {
+				// used
+				if (_virtualFmVoices[virtualFmVoice].actualMidiChannel == midiChannel) {
+					// by our current MIDI channel -> noteOff
+					noteOff(midiChannel, _virtualFmVoices[virtualFmVoice].currentNote);
+				}
+			}
+		}
+		break;
+
+	default:
+		//warning("MILES-ADLIB: Unsupported control change %d", controllerNumber);
+		break;
+	}
+
+	if (registerUpdateFlags) {
+		for (byte virtualFmVoice = 0; virtualFmVoice < SHERLOCK_MILES_ADLIB_VIRTUAL_FMVOICES_COUNT; virtualFmVoice++) {
+			if (_virtualFmVoices[virtualFmVoice].inUse) {
+				// used
+				if (_virtualFmVoices[virtualFmVoice].actualMidiChannel == midiChannel) {
+					// by our current MIDI channel -> update
+					updatePhysicalFmVoice(virtualFmVoice, true, registerUpdateFlags);
+				}
+			}
+		}
+	}
+}
+
+void MidiDriver_Miles_AdLib::programChange(byte midiChannel, byte patchId) {
+	const InstrumentEntry *instrumentPtr = NULL;
+	byte patchBank = _midiChannels[midiChannel].currentPatchBank;
+
+	// we check, if we actually have data for the requested instrument...
+	instrumentPtr = searchInstrument(patchBank, patchId);
+	if (!instrumentPtr) {
+		warning("MILES-ADLIB: unknown instrument requested (%d, %d)", patchBank, patchId);
+		return;
+	}
+
+	// and remember it in that case for the current MIDI-channel
+	_midiChannels[midiChannel].currentInstrumentPtr = instrumentPtr;
+}
+
+const InstrumentEntry *MidiDriver_Miles_AdLib::searchInstrument(byte bankId, byte patchId) {
+	const InstrumentEntry *instrumentPtr = _instrumentTablePtr;
+
+	for (uint16 instrumentNr = 0; instrumentNr < _instrumentTableCount; instrumentNr++) {
+		if ((instrumentPtr->bankId == bankId) && (instrumentPtr->patchId == patchId)) {
+			return instrumentPtr;
+		}
+		instrumentPtr++;
+	}
+
+	return NULL;
+}
+
+void MidiDriver_Miles_AdLib::pitchBendChange(byte midiChannel, byte parameter1, byte parameter2) {
+	// Miles Audio actually didn't shift parameter 2 1 down in here
+	// which means in memory it used a 15-bit pitch bender, which also means the default was 0x4000
+	if ((parameter1 & 0x80) || (parameter2 & 0x80)) {
+		warning("MILES-ADLIB: invalid pitch bend change");
+		return;
+	}
+	_midiChannels[midiChannel].currentPitchBender = parameter1 | (parameter2 << 7);
+}
+
+void MidiDriver_Miles_AdLib::setRegister(int reg, int value) {
+	_opl->write(0x220, reg);
+	_opl->write(0x221, value);
+	//warning("OPL write %x %x (%d)", reg, value, value);
+}
+
+uint32 MidiDriver_Miles_AdLib::property(int prop, uint32 param) {
+	return 0;
+}
+
+MidiDriver *MidiDriver_Miles_AdLib_create(const Common::String instrumentDataFilename, const Common::String instrumentDataFilenameOPL3) {
+	// Load adlib instrument data from file SAMPLE.AD (OPL3: SAMPLE.OPL)
+	Common::File *fileStream = new Common::File();
+	uint32        fileSize = 0;
+	byte         *fileDataPtr = NULL;
+	uint32        fileDataOffset = 0;
+	uint32        fileDataLeft = 0;
+
+	byte curBankId = 0;
+	byte curPatchId = 0;
+
+	InstrumentEntry *instrumentTablePtr = NULL;
+	uint16           instrumentTableCount = 0;
+	InstrumentEntry *instrumentPtr = NULL;
+	uint32           instrumentOffset = 0;
+	uint16           instrumentDataSize = 0;
+
+	if (!fileStream->open(instrumentDataFilename))
+		error("MILES-ADLIB: coult not open instrument file");
+
+	fileSize = fileStream->size();
+
+	fileDataPtr = new byte[fileSize];
+
+	if (fileStream->read(fileDataPtr, fileSize) != fileSize)
+		error("MILES-ADLIB: error while reading instrument file");
+	fileStream->close();
+	delete fileStream;
+
+	// File is like this:
+	// [patch:BYTE] [bank:BYTE] [patchoffset:UINT32]
+	// ...
+	// until patch + bank are both 0xFF, which signals end of header
+
+	// First we check how many entries there are
+	fileDataOffset = 0;
+	fileDataLeft = fileSize;
+	while (1) {
+		if (fileDataLeft < 6)
+			error("MILES-ADLIB: unexpected EOF in instrument file");
+
+		curPatchId = fileDataPtr[fileDataOffset++];
+		curBankId  = fileDataPtr[fileDataOffset++];
+
+		if ((curBankId == 0xFF) && (curPatchId == 0xFF))
+			break;
+
+		fileDataOffset += 4; // skip over offset
+		instrumentTableCount++;
+	}
+
+	if (instrumentTableCount == 0)
+		error("MILES-ADLIB: no instruments in instrument file");
+
+	// Allocate space for instruments
+	instrumentTablePtr = new InstrumentEntry[instrumentTableCount];
+
+	// Now actually read all entries
+	instrumentPtr = instrumentTablePtr;
+
+	fileDataOffset = 0;
+	fileDataLeft = fileSize;
+	while (1) {
+		curPatchId = fileDataPtr[fileDataOffset++];
+		curBankId  = fileDataPtr[fileDataOffset++];
+
+		if ((curBankId == 0xFF) && (curPatchId == 0xFF))
+			break;
+
+		instrumentOffset = READ_LE_UINT32(fileDataPtr + fileDataOffset);
+		fileDataOffset += 4;
+
+		instrumentPtr->bankId = curBankId;
+		instrumentPtr->patchId = curPatchId;
+
+		instrumentDataSize = READ_LE_UINT16(fileDataPtr + instrumentOffset);
+		if (instrumentDataSize != 14)
+			error("MILES-ADLIB: unsupported instrument size");
+
+		instrumentPtr->transposition = (signed char)fileDataPtr[instrumentOffset + 2];
+		instrumentPtr->reg20op1 = fileDataPtr[instrumentOffset + 3];
+		instrumentPtr->reg40op1 = fileDataPtr[instrumentOffset + 4];
+		instrumentPtr->reg60op1 = fileDataPtr[instrumentOffset + 5];
+		instrumentPtr->reg80op1 = fileDataPtr[instrumentOffset + 6];
+		instrumentPtr->regE0op1 = fileDataPtr[instrumentOffset + 7];
+		instrumentPtr->regC0    = fileDataPtr[instrumentOffset + 8];
+		instrumentPtr->reg20op2 = fileDataPtr[instrumentOffset + 9];
+		instrumentPtr->reg40op2 = fileDataPtr[instrumentOffset + 10];
+		instrumentPtr->reg60op2 = fileDataPtr[instrumentOffset + 11];
+		instrumentPtr->reg80op2 = fileDataPtr[instrumentOffset + 12];
+		instrumentPtr->regE0op2 = fileDataPtr[instrumentOffset + 13];
+
+		// Instrument read, next instrument please
+		instrumentPtr++;
+	}
+
+	// Free instrument file data
+	delete[] fileDataPtr;
+
+	return new MidiDriver_Miles_AdLib(g_system->getMixer(), instrumentTablePtr, instrumentTableCount);
+}
+
+} // End of namespace Sherlock
diff --git a/engines/sherlock/tattoo/drivers/tattoo_mididriver.h b/engines/sherlock/tattoo/drivers/tattoo_mididriver.h
new file mode 100644
index 0000000..537e154
--- /dev/null
+++ b/engines/sherlock/tattoo/drivers/tattoo_mididriver.h
@@ -0,0 +1,56 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU General Public License
+ * as published by the Free Software Foundation; either version 2
+ * of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ *
+ */
+
+#ifndef SHERLOCK_TATTOO_DRIVERS_MIDIDRIVER_H
+#define SHERLOCK_TATTOO_DRIVERS_MIDIDRIVER_H
+
+#include "sherlock/sherlock.h"
+#include "audio/mididrv.h"
+#include "common/error.h"
+
+namespace Sherlock {
+
+#define SHERLOCK_MILES_MIDI_CHANNEL_COUNT 16
+
+// Miles Audio supported controllers for control change messages
+#define SHERLOCK_MILES_CONTROLLER_SELECT_PATCH_BANK 114
+#define SHERLOCK_MILES_CONTROLLER_PROTECT_VOICE 112
+#define SHERLOCK_MILES_CONTROLLER_PROTECT_TIMBRE 113
+#define SHERLOCK_MILES_CONTROLLER_MODULATION 1
+#define SHERLOCK_MILES_CONTROLLER_VOLUME 7
+#define SHERLOCK_MILES_CONTROLLER_EXPRESSION 11
+#define SHERLOCK_MILES_CONTROLLER_PANNING 10
+#define SHERLOCK_MILES_CONTROLLER_SUSTAIN 64
+#define SHERLOCK_MILES_CONTROLLER_PITCH_RANGE 6
+#define SHERLOCK_MILES_CONTROLLER_RESET_ALL 121
+#define SHERLOCK_MILES_CONTROLLER_ALL_NOTES_OFF 123
+
+// Miles Audio actually used 0x4000, because they didn't shift the 2 bytes properly
+#define SHERLOCK_MILES_PITCHBENDER_DEFAULT 0x2000
+
+extern MidiDriver *MidiDriver_Miles_AdLib_create(const Common::String instrumentDataFilename, const Common::String instrumentDataFilenameOPL3);
+
+//extern MidiDriver *MidiDriver_Tattoo_MT32_create();
+
+} // End of namespace Sherlock
+
+#endif // SHERLOCK_TATTOO_DRIVERS_MIDIDRIVER_H






More information about the Scummvm-git-logs mailing list