[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