[Scummvm-git-logs] scummvm master -> 9825e9a8cc862e5f0f2574678b97b11ce0fc0aca

dreammaster noreply at scummvm.org
Sat Aug 15 07:56:11 UTC 2026


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

Summary:
fcb9692887 TSAGE: Add General MIDI support to Blue Force
3583c08120 TSAGE: Improve Sound Blaster sample playback
6cb5d91fc2 TSAGE: Pause sound sequencing with the engine
21a97bb9d5 TSAGE: Add MT-32 support to Blue Force
9825e9a8cc TSAGE: Enable MIDI support for Ringworld games


Commit: fcb9692887059b4a455b95a449721fa0ffa70ab7
    https://github.com/scummvm/scummvm/commit/fcb9692887059b4a455b95a449721fa0ffa70ab7
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-15T17:56:03+10:00

Commit Message:
TSAGE: Add General MIDI support to Blue Force

Add a tSAGE General MIDI driver, select the configured MIDI device with
an AdLib fallback, and expose the option for verified Blue Force CD
releases.

Correct the shared voice routing and driver-group parsing needed for
MIDI and digitized effects to coexist.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/tsage/configure.engine
    engines/tsage/detection.h
    engines/tsage/detection_tables.h
    engines/tsage/sound.cpp
    engines/tsage/sound.h


diff --git a/engines/tsage/configure.engine b/engines/tsage/configure.engine
index 5e05c03f9d2..42accc33824 100644
--- a/engines/tsage/configure.engine
+++ b/engines/tsage/configure.engine
@@ -1,6 +1,6 @@
 # This file is included from the main "configure" script
 # add_engine [name] [desc] [build-by-default] [subengines] [base games] [deps] [components]
-add_engine tsage "TsAGE" yes "ringworld blueforce ringworld2"
+add_engine tsage "TsAGE" yes "ringworld blueforce ringworld2" "" "" "midi"
 add_engine ringworld "Ringworld: Revenge of the Patriarch" yes
 add_engine blueforce "Blue Force" yes
 add_engine ringworld2 "Return to Ringworld" yes
diff --git a/engines/tsage/detection.h b/engines/tsage/detection.h
index c4514fee6c5..28a89ae5a76 100644
--- a/engines/tsage/detection.h
+++ b/engines/tsage/detection.h
@@ -38,7 +38,8 @@ enum {
 	GF_CD			= 1 << 1,
 	GF_FLOPPY		= 1 << 2,
 	GF_ALT_REGIONS	= 1 << 3,
-	GF_UNINSTALLED	= 1 << 4
+	GF_UNINSTALLED	= 1 << 4,
+	GF_GENERAL_MIDI	= 1 << 5
 };
 
 struct tSageGameDescription {
diff --git a/engines/tsage/detection_tables.h b/engines/tsage/detection_tables.h
index 51432752856..796d63a3ccc 100644
--- a/engines/tsage/detection_tables.h
+++ b/engines/tsage/detection_tables.h
@@ -165,10 +165,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO2(GUIO_NOSPEECH, GUIO_NOSFX)
+			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIGM)
 		},
 		GType_BlueForce,
-		GF_CD | GF_ALT_REGIONS
+		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI
 	},
 
 	// Blue Force Spanish Floppy and CD use the same files
@@ -195,10 +195,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO2(GUIO_NOSPEECH, GUIO_NOSFX)
+			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIGM)
 		},
 		GType_BlueForce,
-		GF_CD | GF_ALT_REGIONS
+		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI
 	},
 
 	// Found on Wacky Funsters!
diff --git a/engines/tsage/sound.cpp b/engines/tsage/sound.cpp
index 2f38c374b1b..d4703b570e6 100644
--- a/engines/tsage/sound.cpp
+++ b/engines/tsage/sound.cpp
@@ -21,6 +21,7 @@
 
 #include "audio/fmopl.h"
 #include "audio/decoders/raw.h"
+#include "audio/mididrv.h"
 #include "common/config-manager.h"
 #include "common/timer.h"
 #include "audio/audiostream.h"
@@ -34,6 +35,10 @@ namespace TsAGE {
 
 static SoundManager *_soundManager = NULL;
 
+static bool supportsGeneralMidi() {
+	return g_vm->getFeatures() & GF_GENERAL_MIDI;
+}
+
 /*--------------------------------------------------------------------------*/
 
 SoundManager::SoundManager() {
@@ -55,6 +60,8 @@ SoundManager::SoundManager() {
 
 SoundManager::~SoundManager() {
 	if (_sndmgrReady) {
+		g_system->getTimerManager()->removeTimerProc(&sfSoundServer);
+
 		Common::StackLock slock(_serverDisabledMutex);
 		g_vm->_mixer->stopAll();
 
@@ -69,8 +76,6 @@ SoundManager::~SoundManager() {
 			delete driver;
 		}
 		sfTerminate();
-
-		g_system->getTimerManager()->removeTimerProc(&sfSoundServer);
 	}
 
 	// Free any allocated voice type structures
@@ -146,8 +151,17 @@ Common::List<SoundDriverEntry> &SoundManager::buildDriverList(bool detectFlag) {
 	assert(_sndmgrReady);
 	_availableDrivers.clear();
 
-	// Build up a list of available drivers. Currently we only implement an Adlib music
-	// and SoundBlaster FX driver
+	// Build up a list of available drivers
+	if (supportsGeneralMidi()) {
+		SoundDriverEntry sdMidi;
+		sdMidi._driverNum = GENERAL_MIDI_DRIVER_NUM;
+		sdMidi._status = detectFlag ? SNDSTATUS_DETECTED : SNDSTATUS_SKIPPED;
+		sdMidi._field2 = 0;
+		sdMidi._field6 = 10010;
+		sdMidi._shortDescription = "External General MIDI Device";
+		sdMidi._longDescription = "GenMidi";
+		_availableDrivers.push_back(sdMidi);
+	}
 
 	// Adlib driver
 	SoundDriverEntry sd;
@@ -174,7 +188,30 @@ Common::List<SoundDriverEntry> &SoundManager::buildDriverList(bool detectFlag) {
 }
 
 void SoundManager::installConfigDrivers() {
-	installDriver(ADLIB_DRIVER_NUM);
+	if (supportsGeneralMidi()) {
+		const MidiDriver::DeviceHandle device =
+			MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
+		const MusicType musicType = MidiDriver::getMusicType(device);
+
+		switch (musicType) {
+		case MT_GM:
+		case MT_GS:
+			if (!installDriver(GENERAL_MIDI_DRIVER_NUM, device)) {
+				warning("Could not initialize General MIDI output, falling back to AdLib");
+				installDriver(ADLIB_DRIVER_NUM);
+			}
+			break;
+		case MT_MT32:
+		case MT_ADLIB:
+			installDriver(ADLIB_DRIVER_NUM);
+			break;
+		default:
+			break;
+		}
+	} else {
+		installDriver(ADLIB_DRIVER_NUM);
+	}
+
 	installDriver(SBLASTER_DRIVER_NUM);
 }
 
@@ -192,15 +229,15 @@ void SoundManager::dumpDriverList() {
 /**
  * Install the specified driver number
  */
-void SoundManager::installDriver(int driverNum) {
+bool SoundManager::installDriver(int driverNum, MidiDriver::DeviceHandle midiDevice) {
 	// If driver is already installed, no need to install it
 	if (isInstalled(driverNum))
-		return;
+		return true;
 
 	// Instantiate the sound driver
-	SoundDriver *driver = instantiateDriver(driverNum);
+	SoundDriver *driver = instantiateDriver(driverNum, midiDevice);
 	if (!driver)
-		return;
+		return false;
 
 	assert((_ourDrvResVersion >= driver->_minVersion) && (_ourDrvResVersion <= driver->_maxVersion));
 
@@ -211,8 +248,15 @@ void SoundManager::installDriver(int driverNum) {
 		(*i)->mute(true);
 
 	// Install the driver
-	if (!sfInstallDriver(driver))
-		error("Sound driver initialization failed");
+	if (!sfInstallDriver(driver)) {
+		delete driver;
+
+		// Unmute currently active sounds
+		for (Common::List<Sound *>::iterator i = _playList.begin(); i != _playList.end(); ++i)
+			(*i)->mute(false);
+
+		return false;
+	}
 
 	switch (driverNum) {
 	case ROLAND_DRIVER_NUM:
@@ -236,17 +280,21 @@ void SoundManager::installDriver(int driverNum) {
 	default:
 		break;
 	}
+
+	return true;
 }
 
 /**
  * Instantiate a driver class for the specified driver number
  */
-SoundDriver *SoundManager::instantiateDriver(int driverNum) {
+SoundDriver *SoundManager::instantiateDriver(int driverNum, MidiDriver::DeviceHandle midiDevice) {
 	switch (driverNum) {
 	case ADLIB_DRIVER_NUM:
 		return new AdlibSoundDriver();
 	case SBLASTER_DRIVER_NUM:
 		return new SoundBlasterDriver();
+	case GENERAL_MIDI_DRIVER_NUM:
+		return new GeneralMidiSoundDriver(midiDevice);
 	default:
 		error("Unknown sound driver - %d", driverNum);
 	}
@@ -711,6 +759,7 @@ void SoundManager::sfRethinkSoundDrivers() {
 								VoiceStructEntry ve;
 								memset(&ve, 0, sizeof(VoiceStructEntry));
 
+								ve._voiceNum = byteVal & 0x7f;
 								ve._field1 = (byteVal & 0x80) ? 0 : 1;
 								ve._driver = driver;
 								ve._type0._sound = NULL;
@@ -741,7 +790,9 @@ void SoundManager::sfRethinkSoundDrivers() {
 							}
 						}
 					} else {
-						if (*groupData++ != 0) {
+						// Type 0 lists voices up to a 0xff terminator; type 1
+						// uses a fixed-size count/reserved pair.
+						if (*groupData++ == 0) {
 							while (*groupData != 0xff)
 								++groupData;
 						} else {
@@ -978,7 +1029,7 @@ void SoundManager::sfRethinkVoiceTypes() {
 						vte._sound2 = sound;
 						vte._channelNum2 = foundIndex;
 						vte._priority2 = foundPriority;
-						vte._field12 = false;
+						vte._field12 = true;
 						continue;
 					}
 
@@ -1099,8 +1150,9 @@ void SoundManager::sfRethinkVoiceTypes() {
 		if (vs->_voiceType == VOICETYPE_0) {
 			// Type 0
 			for (uint idx = 0; idx < vs->_entries.size(); ++idx) {
-				VoiceStructEntryType0 &vse = vs->_entries[idx]._type0;
-				SoundDriver *driver = vs->_entries[idx]._driver;
+				VoiceStructEntry &voice = vs->_entries[idx];
+				VoiceStructEntryType0 &vse = voice._type0;
+				SoundDriver *driver = voice._driver;
 				assert(driver);
 
 				if (vse._field12) {
@@ -1118,15 +1170,15 @@ void SoundManager::sfRethinkVoiceTypes() {
 					vse._sound2 = NULL;
 
 					if ((total) && vse._sound) {
-						driver->proc24(vse._channelNum, idx, vse._sound, 123, 0);
-						driver->proc24(vse._channelNum, idx, vse._sound, 1, vse._sound->_chModulation[vse._channelNum]);
-						driver->proc24(vse._channelNum, idx, vse._sound, 7,
+						driver->controlChange(voice._voiceNum, 123, 0);
+						driver->controlChange(voice._voiceNum, 1, vse._sound->_chModulation[vse._channelNum]);
+						driver->controlChange(voice._voiceNum, 7,
 							vse._sound->_chVolume[vse._channelNum] * vse._sound->_volume / 127);
-						driver->proc24(vse._channelNum, idx, vse._sound, 10, vse._sound->_chPan[vse._channelNum]);
-						driver->proc24(vse._channelNum, idx, vse._sound, 64, vse._sound->_chDamper[vse._channelNum]);
+						driver->controlChange(voice._voiceNum, 10, vse._sound->_chPan[vse._channelNum]);
+						driver->controlChange(voice._voiceNum, 64, vse._sound->_chDamper[vse._channelNum]);
 
-						driver->setProgram(vse._channelNum, vse._sound->_chProgram[vse._channelNum]);
-						driver->setPitchBlend(vse._channelNum, vse._sound->_chPitchBlend[vse._channelNum]);
+						driver->setProgram(voice._voiceNum, vse._sound->_chProgram[vse._channelNum]);
+						driver->setPitchBlend(voice._voiceNum, vse._sound->_chPitchBlend[vse._channelNum]);
 
 						vse._sound3 = NULL;
 					}
@@ -1148,7 +1200,7 @@ void SoundManager::sfRethinkVoiceTypes() {
 
 				for (uint entryIndex = 0; entryIndex < vs->_entries.size(); ++entryIndex) {
 					VoiceStructEntryType0 &vteCur = vs->_entries[entryIndex]._type0;
-					if ((vteCur._sound3 != sound) || (vteCur._channelNum3 != channelNum)) {
+					if ((vteCur._sound3 == sound) && (vteCur._channelNum3 == channelNum)) {
 						// Found match
 						vteCur._sound = sound;
 						vteCur._channelNum = channelNum;
@@ -1179,25 +1231,25 @@ void SoundManager::sfRethinkVoiceTypes() {
 				}
 				assert(foundIndex != -1);
 
-				VoiceStructEntryType0 &vseFound = vs->_entries[foundIndex]._type0;
+				VoiceStructEntry &voiceFound = vs->_entries[foundIndex];
+				VoiceStructEntryType0 &vseFound = voiceFound._type0;
 
 				vseFound._sound = vse._sound2;
 				vseFound._channelNum = vse._channelNum2;
 				vseFound._priority = vse._priority2;
 				vseFound._fieldA = false;
 
-				SoundDriver *driver = vs->_entries[foundIndex]._driver;
+				SoundDriver *driver = voiceFound._driver;
 				assert(driver);
 
-				driver->proc24(vseFound._channelNum, voiceIndex, vseFound._sound, 123, 0);
-				driver->proc24(vseFound._channelNum, voiceIndex, vseFound._sound,
-					1, vseFound._sound->_chModulation[vseFound._channelNum]);
-				driver->proc24(vseFound._channelNum, voiceIndex, vseFound._sound,
-					7, vseFound._sound->_chVolume[vseFound._channelNum] * vseFound._sound->_volume / 127);
-				driver->proc24(vseFound._channelNum, voiceIndex, vseFound._sound,
-					10, vseFound._sound->_chPan[vseFound._channelNum]);
-				driver->setProgram(vseFound._channelNum, vseFound._sound->_chProgram[vseFound._channelNum]);
-				driver->setPitchBlend(vseFound._channelNum, vseFound._sound->_chPitchBlend[vseFound._channelNum]);
+				driver->controlChange(voiceFound._voiceNum, 123, 0);
+				driver->controlChange(voiceFound._voiceNum, 1, vseFound._sound->_chModulation[vseFound._channelNum]);
+				driver->controlChange(voiceFound._voiceNum, 7,
+					vseFound._sound->_chVolume[vseFound._channelNum] * vseFound._sound->_volume / 127);
+				driver->controlChange(voiceFound._voiceNum, 10, vseFound._sound->_chPan[vseFound._channelNum]);
+				driver->controlChange(voiceFound._voiceNum, 64, vseFound._sound->_chDamper[vseFound._channelNum]);
+				driver->setProgram(voiceFound._voiceNum, vseFound._sound->_chProgram[vseFound._channelNum]);
+				driver->setPitchBlend(voiceFound._voiceNum, vseFound._sound->_chPitchBlend[vseFound._channelNum]);
 			}
 
 			// Final loop
@@ -1207,7 +1259,7 @@ void SoundManager::sfRethinkVoiceTypes() {
 				if (!vse._sound && (vse._sound3)) {
 					SoundDriver *driver = vs->_entries[idx]._driver;
 					assert(driver);
-					driver->proc24(vs->_entries[idx]._voiceNum, voiceIndex, vse._sound3, 123, 0);
+					driver->controlChange(vs->_entries[idx]._voiceNum, 123, 0);
 				}
 			}
 
@@ -1445,7 +1497,7 @@ void SoundManager::sfDoUpdateVolume(Sound *sound) {
 			if (vs->_voiceType == VOICETYPE_0) {
 				if (vse._type0._sound && vse._type0._sound == sound) {
 					int vol = sound->_volume * sound->_chVolume[vse._type0._channelNum] / 127;
-					driver->proc24(vse._voiceNum, voiceIndex, sound, 7, vol);
+					driver->controlChange(vse._voiceNum, 7, vol);
 				}
 			} else {
 				if (vse._type1._sound && vse._type1._sound == sound ) {
@@ -1967,8 +2019,8 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 			voiceType = vtStruct->_voiceType;
 			if (voiceType == VOICETYPE_0) {
 				for (uint idx = 0; idx < vtStruct->_entries.size(); ++idx) {
-					if (!vtStruct->_entries[idx]._type0._sound &&
-							(vtStruct->_entries[idx]._type0._channelNum != channelNum)) {
+					if ((vtStruct->_entries[idx]._type0._sound == this) &&
+							(vtStruct->_entries[idx]._type0._channelNum == channelNum)) {
 						voiceNum = vtStruct->_entries[idx]._voiceNum;
 						driver = vtStruct->_entries[idx]._driver;
 						break;
@@ -1991,7 +2043,7 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 						soUpdateDamper(vtStruct, channelNum, chVoiceType, v);
 					} else if (voiceNum != -1) {
 						assert(driver);
-						driver->proc18(voiceNum, chVoiceType);
+						driver->noteOff(voiceNum, v);
 					}
 				}
 			}
@@ -2014,7 +2066,7 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 							soPlaySound(vtStruct, channelData, channelNum, chVoiceType, v, b);
 					} else if (voiceNum != -1) {
 						assert(driver);
-						driver->proc20(voiceNum, chVoiceType);
+						driver->noteOn(voiceNum, v, b);
 					}
 				}
 			} else {
@@ -2061,7 +2113,7 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 						soProc38(vtStruct, channelNum, chVoiceType, cmdVal, b);
 					} else if (voiceNum != -1) {
 						assert(driver);
-						driver->proc24(voiceNum, chVoiceType, this, cmdVal, b);
+						driver->controlChange(voiceNum, cmdVal, b);
 					}
 				}
 			}
@@ -2092,7 +2144,7 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 						soProc38(vtStruct, channelNum, chVoiceType, cmd, value);
 					} else if (voiceNum != -1) {
 						assert(driver);
-						driver->proc24(voiceNum, chVoiceType, this, cmd, value);
+						driver->controlChange(voiceNum, cmd, value);
 					}
 				}
 			} else if (soDoUpdateTracks(cmd, value)) {
@@ -2112,7 +2164,7 @@ void Sound::soServiceTrackType0(int trackIndex, const byte *channelData) {
 						soProc40(vtStruct, channelNum, pitchBlend);
 					} else if (voiceNum != -1) {
 						assert(driver);
-						driver->setPitchBlend(channel, pitchBlend);
+						driver->setPitchBlend(voiceNum, pitchBlend);
 					}
 				}
 			} else {
@@ -2709,6 +2761,169 @@ SoundDriver::SoundDriver() {
 
 /*--------------------------------------------------------------------------*/
 
+// Voice type 6 uses MIDI channels 0-9. The high bit on channel 9 is an
+// allocation flag from the original driver data; the two 0xff bytes terminate
+// the channel list and group table.
+const byte general_midi_group_data[] = {
+	6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0x89, 0xff, 0xff
+};
+
+GeneralMidiSoundDriver::GeneralMidiSoundDriver(MidiDriver::DeviceHandle device) : SoundDriver(),
+		_midiDriver(NULL), _device(device), _masterVolume(127) {
+	_driverResID = GENERAL_MIDI_DRIVER_NUM;
+	_minVersion = _maxVersion = 0x10A;
+	_shortDescription = "External General MIDI Device";
+	_longDescription = "GenMidi";
+
+	_groupData._groupMask = 0x40;
+	_groupData._pData = general_midi_group_data;
+
+	Common::fill(_modulation, _modulation + SOUND_ARR_SIZE, 0xff);
+	Common::fill(_channelVolume, _channelVolume + SOUND_ARR_SIZE, 0xff);
+	Common::fill(_pan, _pan + SOUND_ARR_SIZE, 0xff);
+	Common::fill(_damper, _damper + SOUND_ARR_SIZE, 0xff);
+	Common::fill(_program, _program + SOUND_ARR_SIZE, 0xff);
+	Common::fill(_pitchBlend, _pitchBlend + SOUND_ARR_SIZE, 0xffff);
+	Common::fill(_active, _active + SOUND_ARR_SIZE, true);
+}
+
+GeneralMidiSoundDriver::~GeneralMidiSoundDriver() {
+	close();
+}
+
+bool GeneralMidiSoundDriver::open() {
+	assert(!_midiDriver);
+
+	_midiDriver = MidiDriver::createMidi(_device);
+	if (!_midiDriver) {
+		warning("Could not create MIDI device");
+		return false;
+	}
+
+	int result = _midiDriver->open();
+	if (result != 0) {
+		warning("Could not open MIDI device: %s", MidiDriver::getErrorName(result));
+		delete _midiDriver;
+		_midiDriver = NULL;
+		return false;
+	}
+
+	_midiDriver->sendGMReset();
+	return true;
+}
+
+void GeneralMidiSoundDriver::close() {
+	if (!_midiDriver)
+		return;
+
+	for (int channel = 0; channel < SOUND_ARR_SIZE; ++channel)
+		send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | channel,
+			MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+
+	_midiDriver->close();
+	delete _midiDriver;
+	_midiDriver = NULL;
+}
+
+const GroupData *GeneralMidiSoundDriver::getGroupData() {
+	return &_groupData;
+}
+
+int GeneralMidiSoundDriver::setMasterVolume(int volume) {
+	int oldVolume = _masterVolume;
+	_masterVolume = CLIP<int>(volume, 0, 127);
+
+	for (int channel = 0; channel < SOUND_ARR_SIZE; ++channel) {
+		if (_channelVolume[channel] != 0xff)
+			send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | channel,
+				MidiDriver::MIDI_CONTROLLER_VOLUME,
+				_channelVolume[channel] * _masterVolume / 127);
+	}
+
+	return oldVolume;
+}
+
+void GeneralMidiSoundDriver::noteOff(int channel, int note) {
+	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
+	send(MidiDriver::MIDI_COMMAND_NOTE_ON | channel, CLIP<int>(note, 0, 127), 0);
+}
+
+void GeneralMidiSoundDriver::noteOn(int channel, int note, int velocity) {
+	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
+	_active[channel] = true;
+	send(MidiDriver::MIDI_COMMAND_NOTE_ON | channel, CLIP<int>(note, 0, 127),
+		CLIP<int>(velocity, 0, 127));
+}
+
+void GeneralMidiSoundDriver::controlChange(int channel, int controller, int value) {
+	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
+	value = CLIP<int>(value, 0, 127);
+
+	byte *currentValue;
+	switch (controller) {
+	case MidiDriver::MIDI_CONTROLLER_MODULATION:
+		currentValue = &_modulation[channel];
+		break;
+	case MidiDriver::MIDI_CONTROLLER_VOLUME:
+		currentValue = &_channelVolume[channel];
+		break;
+	case MidiDriver::MIDI_CONTROLLER_PANNING:
+		currentValue = &_pan[channel];
+		break;
+	case MidiDriver::MIDI_CONTROLLER_SUSTAIN:
+		currentValue = &_damper[channel];
+		break;
+	case MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF:
+		if (!_active[channel])
+			return;
+		_active[channel] = false;
+		currentValue = NULL;
+		break;
+	default:
+		return;
+	}
+
+	if (currentValue) {
+		if (*currentValue == value)
+			return;
+		*currentValue = value;
+	}
+
+	if (controller == MidiDriver::MIDI_CONTROLLER_VOLUME)
+		value = value * _masterVolume / 127;
+
+	send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | channel,
+		controller, value);
+}
+
+void GeneralMidiSoundDriver::setProgram(int channel, int program) {
+	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
+	program = CLIP<int>(program, 0, 127);
+	if (_program[channel] == program)
+		return;
+
+	_program[channel] = program;
+	send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | channel, program, 0);
+}
+
+void GeneralMidiSoundDriver::setPitchBlend(int channel, int pitchBlend) {
+	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
+	pitchBlend = CLIP<int>(pitchBlend, 0, 0x3fff);
+	if (_pitchBlend[channel] == pitchBlend)
+		return;
+
+	_pitchBlend[channel] = pitchBlend;
+	send(MidiDriver::MIDI_COMMAND_PITCH_BEND | channel,
+		pitchBlend & 0x7f, (pitchBlend >> 7) & 0x7f);
+}
+
+void GeneralMidiSoundDriver::send(byte status, byte firstOp, byte secondOp) {
+	if (_midiDriver)
+		_midiDriver->send(status, firstOp, secondOp);
+}
+
+/*--------------------------------------------------------------------------*/
+
 const byte adlib_group_data[] = { 1, 1, 9, 1, 0xff };
 
 const byte adlib_operator1_offset[] = { 0, 1, 2, 8, 9, 10, 16, 17, 18 };
@@ -2731,6 +2946,7 @@ const int v440D4[48] = {
 };
 
 AdlibSoundDriver::AdlibSoundDriver(): SoundDriver() {
+	_driverResID = ADLIB_DRIVER_NUM;
 	_minVersion = 0x102;
 	_maxVersion = 0x10A;
 	_masterVolume = 0;
@@ -3016,11 +3232,12 @@ void AdlibSoundDriver::onTimer() {
 
 
 SoundBlasterDriver::SoundBlasterDriver(): SoundDriver() {
+	_driverResID = SBLASTER_DRIVER_NUM;
 	_minVersion = 0x102;
 	_maxVersion = 0x10A;
 	_masterVolume = 0;
 
-	_groupData._groupMask = 1;
+	_groupData._groupMask = 8;
 	static byte const group_data[] = { 3, 1, 1, 0, 0xff };
 	_groupData._pData = group_data;
 
diff --git a/engines/tsage/sound.h b/engines/tsage/sound.h
index 6e40073b637..0b70eced978 100644
--- a/engines/tsage/sound.h
+++ b/engines/tsage/sound.h
@@ -25,6 +25,7 @@
 #include "common/scummsys.h"
 #include "common/mutex.h"
 #include "common/queue.h"
+#include "audio/mididrv.h"
 #include "audio/mixer.h"
 #include "common/list.h"
 #include "tsage/saveload.h"
@@ -46,6 +47,7 @@ class Sound;
 #define ROLAND_DRIVER_NUM 2
 #define ADLIB_DRIVER_NUM 3
 #define SBLASTER_DRIVER_NUM 4
+#define GENERAL_MIDI_DRIVER_NUM 6
 #define CALLBACKS_PER_SECOND 60
 
 struct trackInfoStruct {
@@ -104,10 +106,10 @@ public:
 	virtual void method6() {}									// Method #6
 	virtual int setMasterVolume(int volume) { return 0; }		// Method #7
 	virtual void proc16() {}									// Method #8
-	virtual void proc18(int al, VoiceType voiceType) {}			// Method #9
-	virtual void proc20(int al, VoiceType voiceType) {}			// Method #10
+	virtual void noteOff(int channel, int note) {}				// Method #9
+	virtual void noteOn(int channel, int note, int velocity) {}	// Method #10
 	virtual void proc22(int al, VoiceType voiceType, int v3) {}	// Method #11
-	virtual void proc24(int channel, int voiceIndex, Sound *sound, int v1, int v2) {}
+	virtual void controlChange(int channel, int controller, int value) {} // Method #12
 	virtual void setProgram(int channel, int program) {}			// Method #13
 	virtual void setVolume1(int channel, int v2, int v3, int volume) {}
 	virtual void setPitchBlend(int channel, int pitchBlend) {}		// Method #15
@@ -169,7 +171,7 @@ public:
 
 class SoundManager : public SaveListener {
 private:
-	SoundDriver *instantiateDriver(int driverNum);
+	SoundDriver *instantiateDriver(int driverNum, MidiDriver::DeviceHandle midiDevice);
 public:
 	bool _sndmgrReady;
 	int _ourSndResVersion, _ourDrvResVersion;
@@ -205,7 +207,7 @@ public:
 	Common::List<SoundDriverEntry> &buildDriverList(bool detectFlag);
 	Common::List<SoundDriverEntry> &getDriverList(bool detectFlag);
 	void dumpDriverList();
-	void installDriver(int driverNum);
+	bool installDriver(int driverNum, MidiDriver::DeviceHandle midiDevice = 0);
 	bool isInstalled(int driverNum) const;
 	void unInstallDriver(int driverNum);
 	void checkResVersion(const byte *soundData);
@@ -449,6 +451,36 @@ public:
 
 #define ADLIB_CHANNEL_COUNT 9
 
+class GeneralMidiSoundDriver : public SoundDriver {
+private:
+	GroupData _groupData;
+	MidiDriver *_midiDriver;
+	MidiDriver::DeviceHandle _device;
+	int _masterVolume;
+	byte _modulation[SOUND_ARR_SIZE];
+	byte _channelVolume[SOUND_ARR_SIZE];
+	byte _pan[SOUND_ARR_SIZE];
+	byte _damper[SOUND_ARR_SIZE];
+	byte _program[SOUND_ARR_SIZE];
+	uint16 _pitchBlend[SOUND_ARR_SIZE];
+	bool _active[SOUND_ARR_SIZE];
+
+	void send(byte status, byte firstOp, byte secondOp);
+public:
+	explicit GeneralMidiSoundDriver(MidiDriver::DeviceHandle device);
+	~GeneralMidiSoundDriver() override;
+
+	bool open() override;
+	void close() override;
+	const GroupData *getGroupData() override;
+	int setMasterVolume(int volume) override;
+	void noteOff(int channel, int note) override;
+	void noteOn(int channel, int note, int velocity) override;
+	void controlChange(int channel, int controller, int value) override;
+	void setProgram(int channel, int program) override;
+	void setPitchBlend(int channel, int pitchBlend) override;
+};
+
 class AdlibSoundDriver: public SoundDriver {
 private:
 	GroupData _groupData;


Commit: 3583c08120c4e4b9511465505d3b146334ea5f95
    https://github.com/scummvm/scummvm/commit/3583c08120c4e4b9511465505d3b146334ea5f95
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-15T17:56:03+10:00

Commit Message:
TSAGE: Improve Sound Blaster sample playback

Use the sample rate and initial channel volume stored by the sound
resources, and let triggered digitized samples play to completion after
their scene-level owner is released.

Starting another sample still interrupts the active Sound Blaster
channel.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/tsage/sound.cpp
    engines/tsage/sound.h


diff --git a/engines/tsage/sound.cpp b/engines/tsage/sound.cpp
index d4703b570e6..b8cbf758a2b 100644
--- a/engines/tsage/sound.cpp
+++ b/engines/tsage/sound.cpp
@@ -3242,10 +3242,7 @@ SoundBlasterDriver::SoundBlasterDriver(): SoundDriver() {
 	_groupData._pData = group_data;
 
 	_mixer = g_vm->_mixer;
-	_sampleRate = _mixer->getOutputRate();
-	_audioStream = NULL;
-	_channelData = NULL;
-	_channelVolume = 0;
+	_channelVolume = 127;
 }
 
 SoundBlasterDriver::~SoundBlasterDriver() {
@@ -3274,48 +3271,52 @@ int SoundBlasterDriver::setMasterVolume(int volume) {
 	return oldVolume;
 }
 
+byte SoundBlasterDriver::getVolume() const {
+	// tSAGE channel volumes use 0-127, while the mixer uses 0-255.
+	return _channelVolume * 2;
+}
+
+void SoundBlasterDriver::updateVolume() {
+	if (_mixer->isSoundHandleActive(_soundHandle))
+		_mixer->setChannelVolume(_soundHandle, getVolume());
+}
+
 void SoundBlasterDriver::playSound(const byte *channelData, int dataOffset, int program, int channel, int v0, int v1) {
 	if (program != -1)
 		return;
 
 	assert(channel == 0);
 
-	// If sound data has been previously set, then release it
-	if (_channelData)
-		updateVoice(channel);
+	if (_mixer->isSoundHandleActive(_soundHandle))
+		_mixer->stopHandle(_soundHandle);
 
-	// Set the new channel data
-	_channelData = channelData + dataOffset + 18;
+	// Digitized tracks have an 18-byte header. The sample rate is a little-
+	// endian word at offset 4 and unsigned 8-bit PCM follows the header.
+	const byte *sampleHeader = channelData + dataOffset;
+	const int sampleHeaderSize = 18;
+	const int sampleRate = READ_LE_UINT16(sampleHeader + 4);
+	const byte *sampleData = sampleHeader + sampleHeaderSize;
 
 	// Make a copy of the buffer
-	int dataSize = g_vm->_memoryManager.getSize(channelData);
-	dataSize -= 18;
-
-	byte *soundData = (byte *)malloc(dataSize - dataOffset);
-	Common::copy(_channelData, _channelData + (dataSize - dataOffset), soundData);
-
-	_audioStream = Audio::makeQueuingAudioStream(11025, false);
-	_audioStream->queueBuffer(soundData, dataSize - dataOffset, DisposeAfterUse::YES, Audio::FLAG_UNSIGNED);
+	const int dataSize = g_vm->_memoryManager.getSize(channelData) - dataOffset - sampleHeaderSize;
+	byte *soundData = (byte *)malloc(dataSize);
+	Common::copy(sampleData, sampleData + dataSize, soundData);
+	Audio::AudioStream *audioStream = Audio::makeRawStream(soundData, dataSize, sampleRate, Audio::FLAG_UNSIGNED);
 
 	// Start the new sound
-	if (!_mixer->isSoundHandleActive(_soundHandle))
-		_mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundHandle, _audioStream);
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundHandle, audioStream, -1, getVolume());
 }
 
 void SoundBlasterDriver::updateVoice(int channel) {
-	// Stop the playing voice
-	if (_mixer->isSoundHandleActive(_soundHandle))
-		_mixer->stopHandle(_soundHandle);
-
-	_audioStream = NULL;
-	_channelData = NULL;
+	// Digitized sounds play to completion after being triggered. A new sample
+	// stops the current one in playSound().
 }
 
 void SoundBlasterDriver::proc38(int channel, int cmd, int value) {
 	if (cmd == 7) {
 		// Set channel volume
 		_channelVolume = value;
-		_mixer->setChannelVolume(_soundHandle, (byte)MIN(255, value * 2));
+		updateVolume();
 	}
 }
 
@@ -3324,14 +3325,7 @@ void SoundBlasterDriver::proc42(int channel, int cmd, int value, int *v1, int *v
 	*v1 = 0;
 	*v2 = 0;
 
-	// Note: Checking whether a playing Fx sound had finished was originally done in another
-	// method in the sample playing code. But since we're using the ScummVM audio soundsystem,
-	// it's easier simply to do the check right here
-	if (_audioStream && (_audioStream->numQueuedStreams() == 0)) {
-		updateVoice(channel);
-	}
-
-	if (!_channelData)
+	if (!_mixer->isSoundHandleActive(_soundHandle))
 		// Flag that sound isn't playing
 		*v1 = 1;
 }
diff --git a/engines/tsage/sound.h b/engines/tsage/sound.h
index 0b70eced978..fca3757f7ce 100644
--- a/engines/tsage/sound.h
+++ b/engines/tsage/sound.h
@@ -534,12 +534,12 @@ private:
 	GroupData _groupData;
 	Audio::Mixer *_mixer;
 	Audio::SoundHandle _soundHandle;
-	Audio::QueuingAudioStream *_audioStream;
-	int _sampleRate;
 
 	byte _masterVolume;
 	byte _channelVolume;
-	const byte *_channelData;
+
+	byte getVolume() const;
+	void updateVolume();
 public:
 	SoundBlasterDriver();
 	~SoundBlasterDriver() override;


Commit: 6cb5d91fc292a6d3edb94de9d1700e19328b62be
    https://github.com/scummvm/scummvm/commit/6cb5d91fc292a6d3edb94de9d1700e19328b62be
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-15T17:56:03+10:00

Commit Message:
TSAGE: Pause sound sequencing with the engine

Freeze the timer-driven sound server before pausing mixer channels so
that it cannot start a new stream while a modal dialog is open. Resume
the server only after the mixer.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/tsage/sound.cpp
    engines/tsage/sound.h
    engines/tsage/tsage.cpp
    engines/tsage/tsage.h


diff --git a/engines/tsage/sound.cpp b/engines/tsage/sound.cpp
index b8cbf758a2b..8f487dcfee1 100644
--- a/engines/tsage/sound.cpp
+++ b/engines/tsage/sound.cpp
@@ -56,6 +56,7 @@ SoundManager::SoundManager() {
 	_needToRethink = false;
 
 	_soTimeIndexFlag = false;
+	_serverPaused = false;
 }
 
 SoundManager::~SoundManager() {
@@ -351,6 +352,11 @@ int SoundManager::getMasterVol() const {
 	return _masterVol;
 }
 
+void SoundManager::setPaused(bool paused) {
+	Common::StackLock slock(_serverSuspendedMutex);
+	_serverPaused = paused;
+}
+
 void SoundManager::loadSound(int soundNum, bool showErrors) {
 	// This method preloaded the data associated with a given sound, so is now redundant
 }
@@ -427,6 +433,9 @@ void SoundManager::sfSoundServer(void *) {
 	Common::StackLock slock1(SoundManager::sfManager()._serverDisabledMutex);
 	Common::StackLock slock2(SoundManager::sfManager()._serverSuspendedMutex);
 
+	if (SoundManager::sfManager()._serverPaused)
+		return;
+
 	if (sfManager()._needToRethink) {
 		sfRethinkVoiceTypes();
 		sfManager()._needToRethink = false;
diff --git a/engines/tsage/sound.h b/engines/tsage/sound.h
index fca3757f7ce..508ab5af523 100644
--- a/engines/tsage/sound.h
+++ b/engines/tsage/sound.h
@@ -172,6 +172,7 @@ public:
 class SoundManager : public SaveListener {
 private:
 	SoundDriver *instantiateDriver(int driverNum, MidiDriver::DeviceHandle midiDevice);
+	bool _serverPaused;
 public:
 	bool _sndmgrReady;
 	int _ourSndResVersion, _ourDrvResVersion;
@@ -226,6 +227,7 @@ public:
 	void updateSoundLoop(Sound *sound);
 	void setMasterVol(int volume);
 	int getMasterVol() const;
+	void setPaused(bool paused);
 	void loadSound(int soundNum, bool showErrors);
 	void unloadSound(int soundNum);
 	bool isFading();
diff --git a/engines/tsage/tsage.cpp b/engines/tsage/tsage.cpp
index f873e46edf6..3e204f472e1 100644
--- a/engines/tsage/tsage.cpp
+++ b/engines/tsage/tsage.cpp
@@ -191,4 +191,17 @@ void TSageEngine::syncSoundSettings() {
 	g_globals->_soundManager.syncSounds();
 }
 
+void TSageEngine::pauseEngineIntern(bool pause) {
+	// The sound server runs from a timer independently of the game loop. Stop it
+	// before pausing mixer channels so it cannot start a new stream while a
+	// modal dialog is open; reverse the order when resuming.
+	if (pause && g_globals)
+		g_globals->_soundManager.setPaused(true);
+
+	Engine::pauseEngineIntern(pause);
+
+	if (!pause && g_globals)
+		g_globals->_soundManager.setPaused(false);
+}
+
 } // End of namespace TsAGE
diff --git a/engines/tsage/tsage.h b/engines/tsage/tsage.h
index 1aa117717be..bd36fbbfeb2 100644
--- a/engines/tsage/tsage.h
+++ b/engines/tsage/tsage.h
@@ -93,6 +93,7 @@ public:
 	Common::Error loadGameState(int slot) override;
 	Common::Error saveGameState(int slot, const Common::String &desc, bool isAutosave = false) override;
 	void syncSoundSettings() override;
+	void pauseEngineIntern(bool pause) override;
 
 	void initialize();
 	void deinitialize();


Commit: 21a97bb9d53855a35cbffc86c9ee7f987f9462db
    https://github.com/scummvm/scummvm/commit/21a97bb9d53855a35cbffc86c9ee7f987f9462db
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-15T17:56:03+10:00

Commit Message:
TSAGE: Add MT-32 support to Blue Force

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/tsage/detection.h
    engines/tsage/detection_tables.h
    engines/tsage/sound.cpp
    engines/tsage/sound.h


diff --git a/engines/tsage/detection.h b/engines/tsage/detection.h
index 28a89ae5a76..23caf6422e9 100644
--- a/engines/tsage/detection.h
+++ b/engines/tsage/detection.h
@@ -39,7 +39,8 @@ enum {
 	GF_FLOPPY		= 1 << 2,
 	GF_ALT_REGIONS	= 1 << 3,
 	GF_UNINSTALLED	= 1 << 4,
-	GF_GENERAL_MIDI	= 1 << 5
+	GF_GENERAL_MIDI	= 1 << 5,
+	GF_MT32			= 1 << 6
 };
 
 struct tSageGameDescription {
diff --git a/engines/tsage/detection_tables.h b/engines/tsage/detection_tables.h
index 796d63a3ccc..7fe673f93f0 100644
--- a/engines/tsage/detection_tables.h
+++ b/engines/tsage/detection_tables.h
@@ -165,10 +165,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIGM)
+			GUIO5(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM)
 		},
 		GType_BlueForce,
-		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI
+		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI | GF_MT32
 	},
 
 	// Blue Force Spanish Floppy and CD use the same files
@@ -195,10 +195,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::RU_RUS,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIGM)
+			GUIO5(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM)
 		},
 		GType_BlueForce,
-		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI
+		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI | GF_MT32
 	},
 
 	// Found on Wacky Funsters!
diff --git a/engines/tsage/sound.cpp b/engines/tsage/sound.cpp
index 8f487dcfee1..7a21c948e72 100644
--- a/engines/tsage/sound.cpp
+++ b/engines/tsage/sound.cpp
@@ -39,6 +39,10 @@ static bool supportsGeneralMidi() {
 	return g_vm->getFeatures() & GF_GENERAL_MIDI;
 }
 
+static bool supportsMt32() {
+	return g_vm->getFeatures() & GF_MT32;
+}
+
 /*--------------------------------------------------------------------------*/
 
 SoundManager::SoundManager() {
@@ -153,6 +157,17 @@ Common::List<SoundDriverEntry> &SoundManager::buildDriverList(bool detectFlag) {
 	_availableDrivers.clear();
 
 	// Build up a list of available drivers
+	if (supportsMt32()) {
+		SoundDriverEntry sdMt32;
+		sdMt32._driverNum = ROLAND_DRIVER_NUM;
+		sdMt32._status = detectFlag ? SNDSTATUS_DETECTED : SNDSTATUS_SKIPPED;
+		sdMt32._field2 = 0;
+		sdMt32._field6 = 10000;
+		sdMt32._shortDescription = "LAPC-I";
+		sdMt32._longDescription = "Roland LAPC-I / MT-32";
+		_availableDrivers.push_back(sdMt32);
+	}
+
 	if (supportsGeneralMidi()) {
 		SoundDriverEntry sdMidi;
 		sdMidi._driverNum = GENERAL_MIDI_DRIVER_NUM;
@@ -189,20 +204,27 @@ Common::List<SoundDriverEntry> &SoundManager::buildDriverList(bool detectFlag) {
 }
 
 void SoundManager::installConfigDrivers() {
-	if (supportsGeneralMidi()) {
+	if (supportsGeneralMidi() || supportsMt32()) {
+		const MidiDriverFlags preferredMidiType =
+			supportsMt32() ? MDT_PREFER_MT32 : MDT_PREFER_GM;
 		const MidiDriver::DeviceHandle device =
-			MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | MDT_PREFER_GM);
+			MidiDriver::detectDevice(MDT_MIDI | MDT_ADLIB | preferredMidiType);
 		const MusicType musicType = MidiDriver::getMusicType(device);
 
 		switch (musicType) {
 		case MT_GM:
 		case MT_GS:
-			if (!installDriver(GENERAL_MIDI_DRIVER_NUM, device)) {
+			if (!supportsGeneralMidi() || !installDriver(GENERAL_MIDI_DRIVER_NUM, device)) {
 				warning("Could not initialize General MIDI output, falling back to AdLib");
 				installDriver(ADLIB_DRIVER_NUM);
 			}
 			break;
 		case MT_MT32:
+			if (!supportsMt32() || !installDriver(ROLAND_DRIVER_NUM, device)) {
+				warning("Could not initialize MT-32 output, falling back to AdLib");
+				installDriver(ADLIB_DRIVER_NUM);
+			}
+			break;
 		case MT_ADLIB:
 			installDriver(ADLIB_DRIVER_NUM);
 			break;
@@ -290,12 +312,14 @@ bool SoundManager::installDriver(int driverNum, MidiDriver::DeviceHandle midiDev
  */
 SoundDriver *SoundManager::instantiateDriver(int driverNum, MidiDriver::DeviceHandle midiDevice) {
 	switch (driverNum) {
+	case ROLAND_DRIVER_NUM:
+		return new MidiSoundDriver(midiDevice, MT_MT32);
 	case ADLIB_DRIVER_NUM:
 		return new AdlibSoundDriver();
 	case SBLASTER_DRIVER_NUM:
 		return new SoundBlasterDriver();
 	case GENERAL_MIDI_DRIVER_NUM:
-		return new GeneralMidiSoundDriver(midiDevice);
+		return new MidiSoundDriver(midiDevice, MT_GM);
 	default:
 		error("Unknown sound driver - %d", driverNum);
 	}
@@ -2770,22 +2794,36 @@ SoundDriver::SoundDriver() {
 
 /*--------------------------------------------------------------------------*/
 
-// Voice type 6 uses MIDI channels 0-9. The high bit on channel 9 is an
+// Voice types 4 and 6 use MIDI channels 0-9. The high bit on channel 9 is an
 // allocation flag from the original driver data; the two 0xff bytes terminate
-// the channel list and group table.
+// the channel list and group table. MT-32 tracks use resource group 0x1000.
+const byte mt32_group_data[] = {
+	4, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0x89, 0xff, 0xff
+};
+
 const byte general_midi_group_data[] = {
 	6, 0, 1, 2, 3, 4, 5, 6, 7, 8, 0x89, 0xff, 0xff
 };
 
-GeneralMidiSoundDriver::GeneralMidiSoundDriver(MidiDriver::DeviceHandle device) : SoundDriver(),
-		_midiDriver(NULL), _device(device), _masterVolume(127) {
-	_driverResID = GENERAL_MIDI_DRIVER_NUM;
+MidiSoundDriver::MidiSoundDriver(MidiDriver::DeviceHandle device, MusicType musicType) : SoundDriver(),
+		_midiDriver(NULL), _device(device), _musicType(musicType), _masterVolume(127) {
+	assert(_musicType == MT_MT32 || _musicType == MT_GM);
+
 	_minVersion = _maxVersion = 0x10A;
-	_shortDescription = "External General MIDI Device";
-	_longDescription = "GenMidi";
 
-	_groupData._groupMask = 0x40;
-	_groupData._pData = general_midi_group_data;
+	if (_musicType == MT_MT32) {
+		_driverResID = ROLAND_DRIVER_NUM;
+		_shortDescription = "LAPC-I";
+		_longDescription = "Roland LAPC-I / MT-32";
+		_groupData._groupMask = 0x1000;
+		_groupData._pData = mt32_group_data;
+	} else {
+		_driverResID = GENERAL_MIDI_DRIVER_NUM;
+		_shortDescription = "External General MIDI Device";
+		_longDescription = "GenMidi";
+		_groupData._groupMask = 0x40;
+		_groupData._pData = general_midi_group_data;
+	}
 
 	Common::fill(_modulation, _modulation + SOUND_ARR_SIZE, 0xff);
 	Common::fill(_channelVolume, _channelVolume + SOUND_ARR_SIZE, 0xff);
@@ -2796,11 +2834,11 @@ GeneralMidiSoundDriver::GeneralMidiSoundDriver(MidiDriver::DeviceHandle device)
 	Common::fill(_active, _active + SOUND_ARR_SIZE, true);
 }
 
-GeneralMidiSoundDriver::~GeneralMidiSoundDriver() {
+MidiSoundDriver::~MidiSoundDriver() {
 	close();
 }
 
-bool GeneralMidiSoundDriver::open() {
+bool MidiSoundDriver::open() {
 	assert(!_midiDriver);
 
 	_midiDriver = MidiDriver::createMidi(_device);
@@ -2817,11 +2855,14 @@ bool GeneralMidiSoundDriver::open() {
 		return false;
 	}
 
-	_midiDriver->sendGMReset();
+	if (_musicType == MT_MT32)
+		_midiDriver->sendMT32Reset();
+	else
+		_midiDriver->sendGMReset();
 	return true;
 }
 
-void GeneralMidiSoundDriver::close() {
+void MidiSoundDriver::close() {
 	if (!_midiDriver)
 		return;
 
@@ -2834,14 +2875,63 @@ void GeneralMidiSoundDriver::close() {
 	_midiDriver = NULL;
 }
 
-const GroupData *GeneralMidiSoundDriver::getGroupData() {
+const GroupData *MidiSoundDriver::getGroupData() {
 	return &_groupData;
 }
 
-int GeneralMidiSoundDriver::setMasterVolume(int volume) {
+void MidiSoundDriver::installPatch(const byte *data, int size) {
+	if (_musicType != MT_MT32)
+		return;
+
+	// The MT-32 bank contains system settings, rhythm parameters, all 128
+	// patches and 64 custom timbres in the same layout used by the DOS driver.
+	static const int kPatchBankSize = 0x429a;
+	if (size < kPatchBankSize) {
+		warning("MT-32 patch bank is smaller than expected");
+		return;
+	}
+
+	struct PatchBlock {
+		uint16 offset;
+		uint16 size;
+		uint32 address;
+	};
+
+	static const PatchBlock patchBlocks[] = {
+		{ 0x0000, 0x0009, 0x100004 },
+		{ 0x002a, 0x00f0, 0x030110 },
+		{ 0x011a, 0x0100, 0x050000 },
+		{ 0x021a, 0x0100, 0x050200 },
+		{ 0x031a, 0x0100, 0x050400 },
+		{ 0x041a, 0x0100, 0x050600 }
+	};
+
+	for (uint i = 0; i < ARRAYSIZE(patchBlocks); ++i) {
+		const PatchBlock &block = patchBlocks[i];
+		sendMt32SysEx(block.address, data + block.offset, block.size);
+	}
+
+	const byte *timbreData = data + 0x051a;
+	for (int timbre = 0; timbre < 64; ++timbre) {
+		sendMt32SysEx(0x080000 + (timbre << 9), timbreData, 0x00f6);
+		timbreData += 0x00f6;
+	}
+}
+
+int MidiSoundDriver::setMasterVolume(int volume) {
 	int oldVolume = _masterVolume;
 	_masterVolume = CLIP<int>(volume, 0, 127);
 
+	if (_musicType == MT_MT32) {
+		static const byte volumeTable[] = {
+			0, 5, 10, 16, 22, 26, 33, 37,
+			42, 48, 53, 58, 64, 69, 74, 80
+		};
+		const byte mt32Volume = volumeTable[_masterVolume >> 3];
+		sendMt32SysEx(0x100016, &mt32Volume, 1);
+		return oldVolume;
+	}
+
 	for (int channel = 0; channel < SOUND_ARR_SIZE; ++channel) {
 		if (_channelVolume[channel] != 0xff)
 			send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | channel,
@@ -2852,19 +2942,19 @@ int GeneralMidiSoundDriver::setMasterVolume(int volume) {
 	return oldVolume;
 }
 
-void GeneralMidiSoundDriver::noteOff(int channel, int note) {
+void MidiSoundDriver::noteOff(int channel, int note) {
 	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
 	send(MidiDriver::MIDI_COMMAND_NOTE_ON | channel, CLIP<int>(note, 0, 127), 0);
 }
 
-void GeneralMidiSoundDriver::noteOn(int channel, int note, int velocity) {
+void MidiSoundDriver::noteOn(int channel, int note, int velocity) {
 	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
 	_active[channel] = true;
 	send(MidiDriver::MIDI_COMMAND_NOTE_ON | channel, CLIP<int>(note, 0, 127),
 		CLIP<int>(velocity, 0, 127));
 }
 
-void GeneralMidiSoundDriver::controlChange(int channel, int controller, int value) {
+void MidiSoundDriver::controlChange(int channel, int controller, int value) {
 	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
 	value = CLIP<int>(value, 0, 127);
 
@@ -2898,14 +2988,14 @@ void GeneralMidiSoundDriver::controlChange(int channel, int controller, int valu
 		*currentValue = value;
 	}
 
-	if (controller == MidiDriver::MIDI_CONTROLLER_VOLUME)
+	if (_musicType == MT_GM && controller == MidiDriver::MIDI_CONTROLLER_VOLUME)
 		value = value * _masterVolume / 127;
 
 	send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | channel,
 		controller, value);
 }
 
-void GeneralMidiSoundDriver::setProgram(int channel, int program) {
+void MidiSoundDriver::setProgram(int channel, int program) {
 	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
 	program = CLIP<int>(program, 0, 127);
 	if (_program[channel] == program)
@@ -2915,7 +3005,7 @@ void GeneralMidiSoundDriver::setProgram(int channel, int program) {
 	send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | channel, program, 0);
 }
 
-void GeneralMidiSoundDriver::setPitchBlend(int channel, int pitchBlend) {
+void MidiSoundDriver::setPitchBlend(int channel, int pitchBlend) {
 	assert(channel >= 0 && channel < SOUND_ARR_SIZE);
 	pitchBlend = CLIP<int>(pitchBlend, 0, 0x3fff);
 	if (_pitchBlend[channel] == pitchBlend)
@@ -2926,11 +3016,33 @@ void GeneralMidiSoundDriver::setPitchBlend(int channel, int pitchBlend) {
 		pitchBlend & 0x7f, (pitchBlend >> 7) & 0x7f);
 }
 
-void GeneralMidiSoundDriver::send(byte status, byte firstOp, byte secondOp) {
+void MidiSoundDriver::send(byte status, byte firstOp, byte secondOp) {
 	if (_midiDriver)
 		_midiDriver->send(status, firstOp, secondOp);
 }
 
+void MidiSoundDriver::sendMt32SysEx(uint32 address, const byte *data, uint16 size) {
+	assert(size <= 256);
+
+	byte message[264];
+	message[0] = 0x41;
+	message[1] = 0x10;
+	message[2] = 0x16;
+	message[3] = 0x12;
+	message[4] = (address >> 16) & 0x7f;
+	message[5] = (address >> 8) & 0x7f;
+	message[6] = address & 0x7f;
+	memcpy(message + 7, data, size);
+
+	byte checksum = 0;
+	for (uint16 i = 4; i < size + 7; ++i)
+		checksum -= message[i];
+	message[size + 7] = checksum & 0x7f;
+
+	if (_midiDriver)
+		_midiDriver->sysEx(message, size + 8);
+}
+
 /*--------------------------------------------------------------------------*/
 
 const byte adlib_group_data[] = { 1, 1, 9, 1, 0xff };
diff --git a/engines/tsage/sound.h b/engines/tsage/sound.h
index 508ab5af523..99beadcfda0 100644
--- a/engines/tsage/sound.h
+++ b/engines/tsage/sound.h
@@ -453,11 +453,12 @@ public:
 
 #define ADLIB_CHANNEL_COUNT 9
 
-class GeneralMidiSoundDriver : public SoundDriver {
+class MidiSoundDriver : public SoundDriver {
 private:
 	GroupData _groupData;
 	MidiDriver *_midiDriver;
 	MidiDriver::DeviceHandle _device;
+	MusicType _musicType;
 	int _masterVolume;
 	byte _modulation[SOUND_ARR_SIZE];
 	byte _channelVolume[SOUND_ARR_SIZE];
@@ -468,13 +469,15 @@ private:
 	bool _active[SOUND_ARR_SIZE];
 
 	void send(byte status, byte firstOp, byte secondOp);
+	void sendMt32SysEx(uint32 address, const byte *data, uint16 size);
 public:
-	explicit GeneralMidiSoundDriver(MidiDriver::DeviceHandle device);
-	~GeneralMidiSoundDriver() override;
+	MidiSoundDriver(MidiDriver::DeviceHandle device, MusicType musicType);
+	~MidiSoundDriver() override;
 
 	bool open() override;
 	void close() override;
 	const GroupData *getGroupData() override;
+	void installPatch(const byte *data, int size) override;
 	int setMasterVolume(int volume) override;
 	void noteOff(int channel, int note) override;
 	void noteOn(int channel, int note, int velocity) override;


Commit: 9825e9a8cc862e5f0f2574678b97b11ce0fc0aca
    https://github.com/scummvm/scummvm/commit/9825e9a8cc862e5f0f2574678b97b11ce0fc0aca
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-15T17:56:03+10:00

Commit Message:
TSAGE: Enable MIDI support for Ringworld games

Expose the native MT-32 scores in the English Ringworld CD and demos,
and both the native MT-32 and General MIDI scores in the English Return
to Ringworld CD and demo.

Assisted-by: Codex:GPT-5.4

Changed paths:
    engines/tsage/detection_tables.h


diff --git a/engines/tsage/detection_tables.h b/engines/tsage/detection_tables.h
index 7fe673f93f0..35f5a304d11 100644
--- a/engines/tsage/detection_tables.h
+++ b/engines/tsage/detection_tables.h
@@ -34,10 +34,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO2(GUIO_NOSPEECH, GUIO_NOSFX)
+			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 		},
 		GType_Ringworld,
-		GF_CD | GF_ALT_REGIONS
+		GF_CD | GF_ALT_REGIONS | GF_MT32
 	},
 	// Ringworld Spanish Floppy and CD use the same files
 	{
@@ -90,10 +90,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_DEMO,
-			GUIO2(GUIO_NOSPEECH, GUIO_NOSFX)
+			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 		},
 		GType_Ringworld,
-		GF_FLOPPY | GF_DEMO
+		GF_FLOPPY | GF_DEMO | GF_MT32
 	},
 
 	// Ringworld English Floppy Demo #2 version
@@ -105,10 +105,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_DEMO,
-			GUIO2(GUIO_NOSPEECH, GUIO_NOSFX)
+			GUIO4(GUIO_NOSPEECH, GUIO_NOSFX, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 		},
 		GType_Ringworld,
-		GF_FLOPPY | GF_DEMO | GF_ALT_REGIONS
+		GF_FLOPPY | GF_DEMO | GF_ALT_REGIONS | GF_MT32
 	},
 
 	// Found on Wacky Funsters!
@@ -240,10 +240,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO0()
+			GUIO3(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM)
 		},
 		GType_Ringworld2,
-		GF_CD | GF_ALT_REGIONS
+		GF_CD | GF_ALT_REGIONS | GF_GENERAL_MIDI | GF_MT32
 	},
 	// Return to Ringworld - Demo
 	{
@@ -254,10 +254,10 @@ static const tSageGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_CD,
-			GUIO0()
+			GUIO3(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM)
 		},
 		GType_Ringworld2,
-		GF_CD | GF_ALT_REGIONS | GF_DEMO
+		GF_CD | GF_ALT_REGIONS | GF_DEMO | GF_GENERAL_MIDI | GF_MT32
 	},
 
 	// Return to Ringworld. Spanish fan translation.




More information about the Scummvm-git-logs mailing list