[Scummvm-git-logs] scummvm master -> 019a7afd0ae92e42d3e99ca8f0659afd5dfe29a8

dreammaster noreply at scummvm.org
Wed Aug 12 06:19:06 UTC 2026


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

Summary:
0f083f0577 MADS: Add native sound host timer
abdc6de059 MADS: NEBULAR: Restore native ASOUND playback
bfef55a7cb MADS: PHANTOM: Restore native ASOUND playback
a1a5f33871 MADS: DRAGONSPHERE: Restore native ASOUND playback
ab48624493 MADS: NEBULAR: Restore native RSOUND playback
11d6f71550 MADS: PHANTOM: Restore native RSOUND playback
7291754f59 MADS: DRAGONSPHERE: Restore native RSOUND playback
cecf1a0be4 MADS: PHANTOM: Add ISOUND PC speaker support
4affc417b9 MADS: NEBULAR: Add PAS16 PSOUND support
0809b132d4 MADS: PHANTOM: Add PAS16 PSOUND support
fd033eff87 MADS: DRAGONSPHERE: Add PAS16 PSOUND support
b25ea30d59 MADS: DRAGONSPHERE: Reimplement GSOUND overlays
8628b42cd7 MADS: Preserve queued sound command parameters
b91d5844d7 MADS: Return native sound command results
e48ce89c6f MADS: Remove obsolete sound manager state
6ed1126f0f MADS: Warn about unavailable section sound drivers
019a7afd0a MADS: Add sound driver debugger commands


Commit: 0f083f05773b376e0957e965c39556503a469dc6
    https://github.com/scummvm/scummvm/commit/0f083f05773b376e0957e965c39556503a469dc6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Add native sound host timer

Reconstruct the two-stage PIT service and sequence polling cadence used
by the first three MADS games.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/core/native_sound_timer.h
    engines/mads/core/sound_manager.h


diff --git a/engines/mads/core/native_sound_timer.h b/engines/mads/core/native_sound_timer.h
new file mode 100644
index 00000000000..4fefba1bcf6
--- /dev/null
+++ b/engines/mads/core/native_sound_timer.h
@@ -0,0 +1,77 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_CORE_NATIVE_SOUND_TIMER_H
+#define MADS_CORE_NATIVE_SOUND_TIMER_H
+
+#include "common/scummsys.h"
+
+namespace MADS {
+
+/**
+ * Reconstructs the driver-service and sequence-poll cascade supplied by the
+ * original executables' PIT channel 0 handlers.
+ */
+class NativeSoundTimer {
+public:
+	enum {
+		kPitClockHz = 1193182,
+		kHostTimerDivisor = 0x07a8,
+		kHostServiceDivider = 2,
+		kSequenceServiceDivider = 5
+	};
+
+	NativeSoundTimer() : _accumulator(0), _pollCountdown(1) {
+	}
+
+	/**
+	 * Advances time expressed in arbitrary units and returns the number of
+	 * native driver-service ticks which elapsed.
+	 */
+	uint32 advance(uint64 elapsedUnits, uint64 unitsPerSecond) {
+		const uint64 serviceThreshold = unitsPerSecond *
+			kHostTimerDivisor * kHostServiceDivider;
+
+		_accumulator += elapsedUnits * kPitClockHz;
+		const uint32 serviceTicks = _accumulator / serviceThreshold;
+		_accumulator %= serviceThreshold;
+		return serviceTicks;
+	}
+
+	/**
+	 * Advances the native five-service sequence countdown.
+	 */
+	bool pollDue() {
+		if (--_pollCountdown)
+			return false;
+
+		_pollCountdown = kSequenceServiceDivider;
+		return true;
+	}
+
+private:
+	uint64 _accumulator;
+	byte _pollCountdown;
+};
+
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index 888d8062143..d2da049369f 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -51,6 +51,13 @@ protected:
 	explicit SoundDriver(Audio::Mixer *mixer) : _mixer(mixer) {}
 
 public:
+	/**
+	 * Loads a driver data block from an absolute file offset.
+	 *
+	 * For DOS MZ overlays, dataOffset includes the executable header. Offsets
+	 * passed to getDataStream() and stored in sequence data are relative to
+	 * the loaded block instead.
+	 */
 	SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
 		int dataOffset, int dataSize);
 	virtual ~SoundDriver() {}


Commit: abdc6de0596c4fd91cf4d164923eca526f180646
    https://github.com/scummvm/scummvm/commit/abdc6de0596c4fd91cf4d164923eca526f180646
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: NEBULAR: Restore native ASOUND playback

Drive the retail and demo ASOUND overlays from the recovered DOS host
cadence. Preserve command dispatch, sequence behavior, and callbacks
verified against the native overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/nebular/sound/asound.cpp
    engines/mads/nebular/sound/asound.h
    engines/mads/nebular/sound/asound_nebular.cpp
    engines/mads/nebular/sound/asound_nebular.h


diff --git a/engines/mads/nebular/sound/asound.cpp b/engines/mads/nebular/sound/asound.cpp
index 7dcd7c9fd6b..f5c15ae507f 100644
--- a/engines/mads/nebular/sound/asound.cpp
+++ b/engines/mads/nebular/sound/asound.cpp
@@ -30,6 +30,9 @@ namespace RexNebular {
 namespace Sound {
 
 constexpr int CHAN_COMMAND_COUNT = 15;
+constexpr int HOST_CALLBACK_RATE =
+	NativeSoundTimer::kPitClockHz /
+	NativeSoundTimer::kHostTimerDivisor;
 
 bool AdlibChannel::_channelsEnabled;
 
@@ -128,6 +131,7 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset
 	_frameCounter = 0;
 	_isDisabled = false;
 	_masterVolume = 255;
+	_noiseEnabled = false;
 	_noiseTicks1 = 0;
 	_noiseTicks2 = 0;
 	_activeChannelNumber = 0;
@@ -163,7 +167,8 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset
 	// Initialize the OPL instance
 	_opl = OPL::Config::create();
 	_opl->init();
-	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer), CALLBACKS_PER_SECOND);
+	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer),
+		HOST_CALLBACK_RATE);
 
 	// Initialize the Adlib
 	adlibInit();
@@ -593,7 +598,19 @@ void ASound::updateFNumber() {
 }
 
 void ASound::onTimer() {
-	poll();
+	uint32 serviceTicks = _hostTimer.advance(1, HOST_CALLBACK_RATE);
+	while (serviceTicks--) {
+		// The original host calls export 4 before export 3. A poll result
+		// consequently changes noise service beginning with the next tick.
+		if (_noiseEnabled)
+			noise();
+
+		if (_hostTimer.pollDue()) {
+			const int result = poll();
+			if (result)
+				_noiseEnabled = result > 0;
+		}
+	}
 }
 
 void ASound::setVolume(int volume) {
@@ -716,7 +733,7 @@ void ASound::channelCommand(byte *&pSrc, bool &updateFlag) {
 				chan->_innerLoopPtr = chan->_pSrc;
 				chan->_innerLoopCount = 0;
 			} else {
-				chan->_innerLoopCount = *pSrc;
+				chan->_innerLoopCount = (uint16)(int16)(int8)*pSrc;
 				chan->_pSrc = chan->_innerLoopPtr;
 			}
 		} else if (--chan->_innerLoopCount) {
@@ -736,13 +753,13 @@ void ASound::channelCommand(byte *&pSrc, bool &updateFlag) {
 				chan->_innerLoopCount = 0;
 				chan->_outerLoopCount = 0;
 			} else {
-				chan->_outerLoopCount = *pSrc;
+				chan->_outerLoopCount = (uint16)(int16)(int8)*pSrc;
 				chan->_pSrc = chan->_outerLoopPtr;
 				chan->_innerLoopPtr = chan->_outerLoopPtr;
 			}
 		} else if (--chan->_outerLoopCount) {
-			chan->_outerLoopPtr = chan->_pSrc;
-			chan->_innerLoopPtr = chan->_pSrc;
+			chan->_pSrc = chan->_outerLoopPtr;
+			chan->_innerLoopPtr = chan->_outerLoopPtr;
 		} else {
 			chan->_pSrc += 2;
 			chan->_outerLoopPtr = chan->_pSrc;
diff --git a/engines/mads/nebular/sound/asound.h b/engines/mads/nebular/sound/asound.h
index 665ed0cdada..83f2a3b25f5 100644
--- a/engines/mads/nebular/sound/asound.h
+++ b/engines/mads/nebular/sound/asound.h
@@ -24,6 +24,7 @@
 
 #include "audio/fmopl.h"
 #include "mads/core/sound_manager.h"
+#include "mads/core/native_sound_timer.h"
 
 namespace MADS {
 namespace RexNebular {
@@ -57,8 +58,8 @@ public:
 	byte *_pSrc = nullptr;
 	byte *_innerLoopPtr = nullptr;	// inner-loop restart address (opcode 0)
 	byte *_outerLoopPtr = nullptr;	// outer-loop restart address (opcode 1)
-	int _innerLoopCount = 0;    // remaining inner-loop iterations (opcode 0)
-	int _outerLoopCount = 0;    // remaining outer-loop iterations (opcode 1)
+	uint16 _innerLoopCount = 0; // signed byte stored as a 16-bit inner-loop count
+	uint16 _outerLoopCount = 0; // signed byte stored as a 16-bit outer-loop count
 	byte *_soundData = nullptr;
 	int _transpose = 0;         // fine-tune offset added into the frequency table lookup
 	int _volumeOffset = 0;
@@ -138,6 +139,8 @@ private:
 	OPL::OPL *_opl;
 	uint16 _randomSeed;
 	int _masterVolume;
+	NativeSoundTimer _hostTimer;
+	bool _noiseEnabled;
 
 	/**
 	 * Does the initial Adlib initialisation
diff --git a/engines/mads/nebular/sound/asound_nebular.cpp b/engines/mads/nebular/sound/asound_nebular.cpp
index 663008f8fb5..7ad97ab304c 100644
--- a/engines/mads/nebular/sound/asound_nebular.cpp
+++ b/engines/mads/nebular/sound/asound_nebular.cpp
@@ -1362,7 +1362,7 @@ int ASound3::command60() {
 
 /*-----------------------------------------------------------------------*/
 
-const ASound4::CommandPtr ASound4::_commandList[61] = {
+const ASound4::CommandPtr ASound4::_commandList[60] = {
 	&ASound4::command0, &ASound4::command1, &ASound4::command2, &ASound4::command3,
 	&ASound4::command4, &ASound4::command5, &ASound4::command6, &ASound4::command7,
 	&ASound4::command8, &ASound4::nullCommand, &ASound4::command10, &ASound4::nullCommand,
@@ -1377,8 +1377,7 @@ const ASound4::CommandPtr ASound4::_commandList[61] = {
 	&ASound4::nullCommand, &ASound4::nullCommand, &ASound4::nullCommand, &ASound4::nullCommand,
 	&ASound4::nullCommand, &ASound4::nullCommand, &ASound4::nullCommand, &ASound4::nullCommand,
 	&ASound4::command52, &ASound4::command53, &ASound4::command54, &ASound4::command55,
-	&ASound4::command56, &ASound4::command57, &ASound4::command58, &ASound4::command59,
-	&ASound4::command60
+	&ASound4::command56, &ASound4::command57, &ASound4::command58, &ASound4::command59
 };
 
 ASound4::ASound4(Audio::Mixer *mixer) : ASound(mixer, "asound.004", 0x14F0, 0x2930) {
@@ -1389,7 +1388,7 @@ ASound4::ASound4(Audio::Mixer *mixer) : ASound(mixer, "asound.004", 0x14F0, 0x29
 }
 
 int ASound4::command(int commandId, int param) {
-	if (commandId > 60)
+	if (commandId > 59)
 		return 0;
 
 	_commandParam = param;
@@ -1597,12 +1596,6 @@ int ASound4::command59() {
 	return 0;
 }
 
-int ASound4::command60() {
-	playSound(0x28FC);
-
-	return 0;
-}
-
 void ASound4::method1() {
 	byte *pData = loadData(0x2180);
 	if (!isSoundActive(pData)) {
@@ -1637,7 +1630,7 @@ const ASound5::CommandPtr ASound5::_commandList[42] = {
 	&ASound5::command40, &ASound5::command41
 };
 
-ASound5::ASound5(Audio::Mixer *mixer) : ASound(mixer, "asound.005", 0x15E0, 0x2200) {
+ASound5::ASound5(Audio::Mixer *mixer) : ASound(mixer, "asound.005", 0x1440, 0x2200) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x144);
 	for (int i = 0; i < 164; ++i)
diff --git a/engines/mads/nebular/sound/asound_nebular.h b/engines/mads/nebular/sound/asound_nebular.h
index 5f057dc4598..907d586be79 100644
--- a/engines/mads/nebular/sound/asound_nebular.h
+++ b/engines/mads/nebular/sound/asound_nebular.h
@@ -238,7 +238,7 @@ public:
 class ASound4 : public ASound {
 private:
 	typedef int (ASound4:: *CommandPtr)();
-	static const CommandPtr _commandList[61];
+	static const CommandPtr _commandList[60];
 
 	int command10();
 	int command12();
@@ -264,8 +264,6 @@ private:
 	int command57();
 	int command58();
 	int command59();
-	int command60();
-
 	void method1();
 public:
 	ASound4(Audio::Mixer *mixer);


Commit: bfef55a7cbc950bd0da10713d3f805e9534e840d
    https://github.com/scummvm/scummvm/commit/bfef55a7cbc950bd0da10713d3f805e9534e840d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: PHANTOM: Restore native ASOUND playback

Drive the retail and demo ASOUND overlays from the recovered DOS host
cadence. Preserve command dispatch, sequence behavior, and callbacks
verified against the native overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/phantom/sound/asound.cpp
    engines/mads/phantom/sound/asound.h
    engines/mads/phantom/sound/asound_phantom.cpp
    engines/mads/phantom/sound/asound_phantom.h


diff --git a/engines/mads/phantom/sound/asound.cpp b/engines/mads/phantom/sound/asound.cpp
index c439aaf02a8..78118194aa6 100644
--- a/engines/mads/phantom/sound/asound.cpp
+++ b/engines/mads/phantom/sound/asound.cpp
@@ -22,6 +22,7 @@
 #include "audio/fmopl.h"
 #include "common/file.h"
 #include "common/md5.h"
+#include "common/util.h"
 #include "mads/phantom/sound/asound.h"
 
 namespace MADS {
@@ -30,6 +31,9 @@ namespace Sound {
 
 bool AdlibChannel::_isDisabled;
 
+static const uint32 HOST_CALLBACK_RATE =
+	NativeSoundTimer::kPitClockHz / NativeSoundTimer::kHostTimerDivisor;
+
 /* =========================================================================
  * Lookup tables
  * ========================================================================= */
@@ -206,7 +210,8 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset
 	// Initialize the OPL
 	_opl = OPL::Config::create();
 	_opl->init();
-	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer), CALLBACKS_PER_SECOND);
+	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer),
+		HOST_CALLBACK_RATE);
 
 	write(4, 0x60);		// Mask off both adlib timers
 	write(4, 0x80);		// IRQ reset timer flags
@@ -450,6 +455,7 @@ int ASound::command7() {
 	}
 
 	_isDisabled = false;
+	refreshVolumes();
 	return 0;
 }
 
@@ -468,7 +474,7 @@ int ASound::command8() {
 	return result;
 }
 
-void ASound::callFunction(uint16 offset) {
+void ASound::callFunction(uint16 offset, AdlibChannel &) {
 	error("Unsupported call to sound driver function at offset %.4x", offset);
 }
 
@@ -483,7 +489,22 @@ void ASound::write(uint8 reg, uint8 value) {
 
 void ASound::onTimer() {
 	Common::StackLock slock(_driverMutex);
-	poll();
+
+	uint32 serviceTicks = _hostTimer.advance(1, HOST_CALLBACK_RATE);
+	while (serviceTicks--) {
+		// The native host invokes export 4 before export 3. A poll result
+		// therefore changes the noise service beginning with the next tick.
+		if (_noiseEnabled) {
+			for (int i = ADLIB_CHANNEL_COUNT - 1; i >= 0; --i)
+				noise_inner(i);
+		}
+
+		if (_hostTimer.pollDue()) {
+			const int result = poll();
+			if (result)
+				_noiseEnabled = result > 0;
+		}
+	}
 }
 
 uint16 ASound::getRandomNumber() {
@@ -535,6 +556,7 @@ void ASound::writeVolume() {
 	/* Step 2: map velocity through VOL_VEL_TO_ATTEN_STEP */
 	int16 velStep = (int16)VOL_VEL_TO_ATTEN_STEP[(int8)ch->_velocity];
 	int16 var8 = volStep + velStep - 1;   /* combined attenuation step */
+	var8 = CLIP<int16>(var8, 0, 63) * _masterVolume / 255;
 
 	/* Determine carrier operator register for this voice */
 	uint8 chanNum = _activeChannelNumber;
@@ -621,6 +643,30 @@ void ASound::writeVolume() {
 	}
 }
 
+void ASound::refreshVolumes() {
+	AdlibChannel *savedChannel = _activeChannelPtr;
+	const uint8 savedChannelNumber = _activeChannelNumber;
+
+	if (!_isDisabled) {
+		for (int i = 0; i < ADLIB_CHANNEL_COUNT; ++i) {
+			if (_channels[i]->_activeCount == 0)
+				continue;
+
+			_activeChannelPtr = _channels[i];
+			_activeChannelNumber = i;
+			writeVolume();
+		}
+	}
+
+	_activeChannelPtr = savedChannel;
+	_activeChannelNumber = savedChannelNumber;
+}
+
+void ASound::setVolume(int volume) {
+	_masterVolume = CLIP(volume, 0, 255);
+	refreshVolumes();
+}
+
 void ASound::writeFrequency() {
 	AdlibChannel *ch = _activeChannelPtr;
 	uint8 chanNum = _activeChannelNumber;
@@ -1003,7 +1049,7 @@ dispatch:
 			{
 				if (ch->_innerLoopCount == 0) {
 					pSrc++;
-					uint8 cnt = *pSrc;
+					uint16 cnt = (uint16)(int16)(int8)*pSrc;
 					if (cnt == 0) {
 						ch->_pSrc += 2;
 						ch->_innerLoopPtr = ch->_pSrc;
@@ -1027,7 +1073,7 @@ dispatch:
 			{
 				if (ch->_outerLoopCount == 0) {
 					pSrc++;
-					uint8 cnt = *pSrc;
+					uint16 cnt = (uint16)(int16)(int8)*pSrc;
 					if (cnt == 0) {
 						ch->_pSrc += 2;
 						ch->_outerLoopPtr = ch->_pSrc;
@@ -1751,7 +1797,7 @@ branch_skip5:
 			case 6:
 			{
 				uint16 fptr = readWord_impl();
-				callFunction(fptr);
+				callFunction(fptr, *ch);
 				ch->_pSrc += 3;
 				goto dispatch;
 			}
diff --git a/engines/mads/phantom/sound/asound.h b/engines/mads/phantom/sound/asound.h
index 86d8deec233..0fa25776a5a 100644
--- a/engines/mads/phantom/sound/asound.h
+++ b/engines/mads/phantom/sound/asound.h
@@ -26,6 +26,7 @@
 #include "common/mutex.h"
 #include "common/queue.h"
 #include "common/util.h"
+#include "mads/core/native_sound_timer.h"
 #include "mads/core/sound_manager.h"
 
 namespace MADS {
@@ -132,6 +133,8 @@ struct AdlibSample {
 class ASound : public SoundDriver {
 private:
 	OPL::OPL *_opl;
+	NativeSoundTimer _hostTimer;
+	bool _noiseEnabled = false;
 	uint16 _callbackCounter = 0;		// Period counter
 	uint16 _callbackPeriod = 0;			// Period reload
 	AdlibChannel *_activeChannelPtr = NULL;
@@ -144,6 +147,7 @@ private:
 	int16 _resultFlag = 0;
 	uint16 _randomSeed = 0x4D2;
 	uint8  _isDisabled = 0;
+	int _masterVolume = 255;
 	uint8  _findChannelMode = 0;		// findFreeChannel mode
 	uint8  _ch5SweepLive = 0;			// Channel5 savedFreqSweep shadow
 	uint8  _ch5SweepSaved = 0;			// Channel5 savedFreqSweep shadow 2
@@ -190,6 +194,7 @@ private:
 	 *   >= 0x18 -> patch-attenuation-aware mapping using _patchAttenToTL table
 	 */
 	void writeVolume();
+	void refreshVolumes();
 
 	/**
 	 * Computes the OPL F-number / block (octave) from the channel's note,
@@ -431,8 +436,9 @@ protected:
 	/**
 	 * Calls a function at a fixed offset within the sound driver.
 	 * @param offset		Offset of the function
+	 * @param channel	Channel which encountered the callback opcode
 	 */
-	virtual void callFunction(uint16 offset);
+	virtual void callFunction(uint16 offset, AdlibChannel &channel);
 
 public:
 	/**
@@ -471,9 +477,7 @@ public:
 	 */
 	void noise() override;
 
-	void setVolume(int volume) override {
-		// TODO: Does this need implementation?
-	}
+	void setVolume(int volume) override;
 };
 
 } // namespace Sound
diff --git a/engines/mads/phantom/sound/asound_phantom.cpp b/engines/mads/phantom/sound/asound_phantom.cpp
index 59b16595c20..f1605efddd6 100644
--- a/engines/mads/phantom/sound/asound_phantom.cpp
+++ b/engines/mads/phantom/sound/asound_phantom.cpp
@@ -29,7 +29,7 @@ namespace Sound {
 /* ASound1  (asound.ph1)                                                  *
  *-----------------------------------------------------------------------*/
 
-const ASound1::CommandPtr ASound1::_commandList[40] = {
+const ASound1::CommandPtr ASound1::_commandList[77] = {
 	&ASound1::command0,  &ASound1::command1,  &ASound1::command2,  &ASound1::command3,
 	&ASound1::command4,  &ASound1::command5,  &ASound1::command6,  &ASound1::command7,
 	&ASound1::command8,  nullptr,             nullptr,             nullptr,
@@ -39,7 +39,19 @@ const ASound1::CommandPtr ASound1::_commandList[40] = {
 	&ASound1::command24, &ASound1::command25, &ASound1::command26, &ASound1::command27,
 	nullptr,             nullptr,             nullptr,             nullptr,
 	&ASound1::command32, &ASound1::command33, &ASound1::command34, &ASound1::command35,
-	&ASound1::command36, &ASound1::command37, &ASound1::command38, &ASound1::command39
+	&ASound1::command36, &ASound1::command37, &ASound1::command38, &ASound1::command39,
+	// commands 40-63 absent
+	nullptr,             nullptr,             nullptr,             nullptr,
+	nullptr,             nullptr,             nullptr,             nullptr,
+	nullptr,             nullptr,             nullptr,             nullptr,
+	nullptr,             nullptr,             nullptr,             nullptr,
+	nullptr,             nullptr,             nullptr,             nullptr,
+	nullptr,             nullptr,             nullptr,             nullptr,
+	// commands 64-76
+	&ASound1::command64, &ASound1::command65, &ASound1::command66, &ASound1::command67,
+	&ASound1::command68, &ASound1::command69, &ASound1::command70, &ASound1::command71,
+	&ASound1::command72, &ASound1::command73, &ASound1::command74, &ASound1::command75,
+	&ASound1::command76
 };
 
 ASound1::ASound1(Audio::Mixer *mixer)
@@ -51,7 +63,7 @@ ASound1::ASound1(Audio::Mixer *mixer)
 }
 
 int ASound1::command(int commandId, int param) {
-	if (commandId > 39 || !_commandList[commandId])
+	if (commandId > 76 || !_commandList[commandId])
 		return 0;
 	
 	return (this->*_commandList[commandId])();
@@ -286,6 +298,82 @@ int ASound1::command39() {
 	return 0;
 }
 
+int ASound1::command64() {
+	playSound(0x32BA);
+	return 0;
+}
+
+int ASound1::command65() {
+	playSound(0x32CC);
+	playSound(0x32DE);
+	return 0;
+}
+
+int ASound1::command66() {
+	playSound(0x32F2);
+	return 0;
+}
+
+int ASound1::command67() {
+	playSound(0x330A);
+	playSound(0x331B);
+	playSound(0x332C);
+	return 0;
+}
+
+int ASound1::command68() {
+	playSound(0x337A);
+	playSound(0x338B);
+	playSound(0x339C);
+	return 0;
+}
+
+int ASound1::command69() {
+	playSound(0x3336);
+	playSound(0x3342);
+	return 0;
+}
+
+int ASound1::command70() {
+	playSound(0x3350);
+	playSound(0x3360);
+	playSound(0x3370);
+	return 0;
+}
+
+int ASound1::command71() {
+	playSound(0x180D);
+	return 0;
+}
+
+int ASound1::command72() {
+	playSound(0x33AD);
+	return 0;
+}
+
+int ASound1::command73() {
+	playSound(0x33BC);
+	playSound(0x33BF);
+	return 0;
+}
+
+int ASound1::command74() {
+	playSound(0x33F9);
+	playSound(0x3407);
+	return 0;
+}
+
+int ASound1::command75() {
+	playSound(0x33D5);
+	playSound(0x33DB);
+	return 0;
+}
+
+int ASound1::command76() {
+	playSound(0x181C);
+	return 0;
+}
+
 /*-----------------------------------------------------------------------*/
 
 /*-----------------------------------------------------------------------*/
@@ -860,6 +948,25 @@ int ASound4::command6() { return ASound::command6(); }
 int ASound4::command7() { return ASound::command7(); }
 int ASound4::command8() { return ASound::command8(); }
 
+void ASound4::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1CB0) {
+		ASound::callFunction(offset, channel);
+		return;
+	}
+
+	// Keep the two related sequence fragments on the same random variant.
+	const uint16 tableIndex = (getRandomNumber() & 0x0F) * 2;
+	byte *source = loadData(0x0FD9 + tableIndex);
+	byte *destination = loadData(0x0C7F);
+	destination[0] = source[0];
+	destination[1] = source[1];
+
+	source = loadData(0x0FB9 + tableIndex);
+	destination = loadData(0x0C48);
+	destination[0] = source[0];
+	destination[1] = source[1];
+}
+
 // ---------------------------------------------------------------------------
 // command16 - isSoundActive guard, command1, load ch0-6
 // ---------------------------------------------------------------------------
@@ -1546,6 +1653,50 @@ int ASoundDemo::command6() { return ASound::command6(); }
 int ASoundDemo::command7() { return ASound::command7(); }
 int ASoundDemo::command8() { return ASound::command8(); }
 
+void ASoundDemo::callFunction(uint16 offset, AdlibChannel &channel) {
+	uint16 firstValue;
+	uint16 secondValue;
+	uint16 firstDestination;
+	uint16 secondDestination;
+
+	switch (offset) {
+	case 0x1E88:
+		firstValue = 0x325D;
+		secondValue = 0x641B;
+		firstDestination = 0x5F37;
+		secondDestination = 0x5FAC;
+		break;
+	case 0x1EB1:
+		firstValue = 0x4B0D;
+		secondValue = 0x5A0B;
+		firstDestination = 0x5F5D;
+		secondDestination = 0x5FC7;
+		break;
+	case 0x2031:
+		// The native callback writes one status character directly to CGA
+		// video memory. It has no effect on sound playback.
+		return;
+	default:
+		ASound::callFunction(offset, channel);
+		return;
+	}
+
+	// The demo chooses which member of each pair plays first from the high
+	// byte of the native random-number state.
+	if ((getRandomNumber() >> 8) <= 0x80) {
+		const uint16 value = firstValue;
+		firstValue = secondValue;
+		secondValue = value;
+	}
+
+	byte *destination = loadData(firstDestination);
+	destination[0] = firstValue & 0xFF;
+	destination[2] = firstValue >> 8;
+	destination = loadData(secondDestination);
+	destination[0] = secondValue & 0xFF;
+	destination[2] = secondValue >> 8;
+}
+
 int ASoundDemo::command9() {
 	ASound::command1();
 	findFreeChannelFull(loadData(0x59EC));
diff --git a/engines/mads/phantom/sound/asound_phantom.h b/engines/mads/phantom/sound/asound_phantom.h
index 06ded4aa955..65b5a79ca38 100644
--- a/engines/mads/phantom/sound/asound_phantom.h
+++ b/engines/mads/phantom/sound/asound_phantom.h
@@ -37,9 +37,7 @@ namespace Sound {
  *   Table 3: commands 24–27  (max=0x1B, base=0x18, 4 entries)
  *   Table 4: commands 32–39  (max=0x27, base=0x20, 8 entries)
  *
- * A fifth table (commands 64–76) exists but is encoded as raw
- * sound data bytes used as near-pointers — not reconstructible without the
- * binary.  Those commands are silently ignored.
+ *   Table 5: commands 64–76  (max=0x4C, base=0x40, 13 entries)
  *
  * command16: random background-music selector.  Checks whether
  * channel 0 is already playing one of the five known music pieces; if not,
@@ -49,7 +47,7 @@ namespace Sound {
 class ASound1 : public ASound {
 private:
 	typedef int (ASound1::*CommandPtr)();
-	static const CommandPtr _commandList[40];
+	static const CommandPtr _commandList[77];
 
 	// Tracks which music piece was last selected.
 	int _musicIndex = 0;
@@ -85,6 +83,19 @@ private:
 	int command37();
 	int command38();
 	int command39();
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+	int command73();
+	int command74();
+	int command75();
+	int command76();
 
 public:
 	ASound1(Audio::Mixer *mixer);
@@ -250,6 +261,8 @@ private:
 	int command69();
 	int command70();
 
+	void callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound4(Audio::Mixer *mixer);
 	~ASound4() override {}
@@ -406,6 +419,8 @@ private:
 	int command28();
 	int command29();
 
+	void callFunction(uint16 offset, AdlibChannel &channel) override;
+
 	static const CommandPtr _commandList[30];
 
 public:


Commit: a1a5f33871590cc961c225fb585c2b2cf262b3ec
    https://github.com/scummvm/scummvm/commit/a1a5f33871590cc961c225fb585c2b2cf262b3ec
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: DRAGONSPHERE: Restore native ASOUND playback

Drive the retail and demo ASOUND overlays from the recovered DOS host
cadence. Preserve command dispatch, sequence behavior, and callbacks
verified against the native overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/dragonsphere/sound/asound.cpp
    engines/mads/dragonsphere/sound/asound.h
    engines/mads/dragonsphere/sound/asound_dragonsphere.cpp
    engines/mads/dragonsphere/sound/asound_dragonsphere.h
    engines/mads/phantom/sound/asound.cpp
    engines/mads/phantom/sound/asound.h


diff --git a/engines/mads/dragonsphere/sound/asound.cpp b/engines/mads/dragonsphere/sound/asound.cpp
index 38dddee0ed8..e92ac00affb 100644
--- a/engines/mads/dragonsphere/sound/asound.cpp
+++ b/engines/mads/dragonsphere/sound/asound.cpp
@@ -22,12 +22,15 @@
 #include "audio/fmopl.h"
 #include "common/file.h"
 #include "common/md5.h"
+#include "common/util.h"
 #include "mads/dragonsphere/sound/asound.h"
 
 namespace MADS {
 namespace Dragonsphere {
 namespace Sound {
 
+static const uint32 HOST_CALLBACK_RATE =
+	NativeSoundTimer::kPitClockHz / NativeSoundTimer::kHostTimerDivisor;
 
 bool AdlibChannel::_isDisabled;
 
@@ -247,7 +250,7 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename,
 	_opl = OPL::Config::create();
 	_opl->init();
 	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer),
-		CALLBACKS_PER_SECOND);
+		HOST_CALLBACK_RATE);
 
 	/* Standard OPL timer-reset sequence. */
 	write(4, 0x60);
@@ -319,7 +322,7 @@ int ASound::poll() {
 
 void ASound::noise() {
 	Common::StackLock slock(_driverMutex);
-	for (int i = 0; i < ADLIB_CHANNEL_COUNT; ++i)
+	for (int i = ADLIB_CHANNEL_COUNT - 1; i >= 0; --i)
 		noise_inner(i);
 }
 
@@ -442,6 +445,7 @@ int ASound::command7() {
 		signalSoundPlaying();
 
 	_isDisabled = 0;
+	refreshVolumes();
 	return 0;
 }
 
@@ -463,8 +467,9 @@ int ASound::command18() {
 	return command(_musicIndex, 0);
 }
 
-void ASound::callFunction(uint16 offset) {
+bool ASound::callFunction(uint16 offset, AdlibChannel &) {
 	error("Unsupported call to sound driver function at offset %.4x", offset);
+	return false;
 }
 
 void ASound::write(uint8 reg, uint8 value) {
@@ -474,7 +479,22 @@ void ASound::write(uint8 reg, uint8 value) {
 
 void ASound::onTimer() {
 	Common::StackLock slock(_driverMutex);
-	poll();
+
+	uint32 serviceTicks = _hostTimer.advance(1, HOST_CALLBACK_RATE);
+	while (serviceTicks--) {
+		// Both native hosts invoke export 4 before export 3. The poll result
+		// consequently changes noise service beginning with the next tick.
+		if (_noiseEnabled) {
+			for (int i = ADLIB_CHANNEL_COUNT - 1; i >= 0; --i)
+				noise_inner(i);
+		}
+
+		if (_hostTimer.pollDue()) {
+			const int result = poll();
+			if (result)
+				_noiseEnabled = result > 0;
+		}
+	}
 }
 
 uint16 ASound::getRandomNumber() {
@@ -540,6 +560,7 @@ void ASound::writeVolume() {
 	int16  volStep = (int16)(uint16)VOL_VEL_TO_ATTEN_STEP[volIdx];
 	int16  velStep = (int16)(uint16)VOL_VEL_TO_ATTEN_STEP[velIdx];
 	int16  var4 = volStep + velStep - 1;   /* var_4: combined step (shared) */
+	var4 = CLIP<int16>(var4, 0, 63) * _masterVolume / 255;
 
 	/* Check _alg of the first sample to determine loop count. */
 	AdlibSample *smpFirst = &_samples[ch->_sampleIndex * 2];
@@ -630,6 +651,30 @@ void ASound::writeVolume() {
 	ch->_savedFreqSweep = (uint8)(finalSi & 0x3F);
 }
 
+void ASound::refreshVolumes() {
+	AdlibChannel *savedChannel = _activeChannelPtr;
+	const uint8 savedChannelNumber = _activeChannelNumber;
+
+	if (!_isDisabled) {
+		for (int i = 0; i < ADLIB_CHANNEL_COUNT; ++i) {
+			if (_channels[i]->_activeCount == 0)
+				continue;
+
+			_activeChannelPtr = _channels[i];
+			_activeChannelNumber = i;
+			writeVolume();
+		}
+	}
+
+	_activeChannelPtr = savedChannel;
+	_activeChannelNumber = savedChannelNumber;
+}
+
+void ASound::setVolume(int volume) {
+	_masterVolume = CLIP(volume, 0, 255);
+	refreshVolumes();
+}
+
 void ASound::writeFrequency() {
 	AdlibChannel *ch = _activeChannelPtr;
 	uint8 chanNum = _activeChannelNumber;
@@ -1187,7 +1232,7 @@ op2_set_vol:
 				ch = _activeChannelPtr;
 				if (ch->_innerLoopCount == 0) {
 					pSrc++;   /* advance to count byte */
-					uint8 cnt = *pSrc;
+					uint16 cnt = (uint16)(int16)(int8)*pSrc;
 					if (cnt == 0) {
 						ch->_pSrc += 2;
 						ch = _activeChannelPtr;
@@ -1220,7 +1265,7 @@ op2_set_vol:
 				ch = _activeChannelPtr;
 				if (ch->_outerLoopCount == 0) {
 					pSrc++;
-					uint8 cnt = *pSrc;
+					uint16 cnt = (uint16)(int16)(int8)*pSrc;
 					if (cnt == 0) {
 						ch->_pSrc += 2;
 						ch = _activeChannelPtr;
@@ -1377,7 +1422,8 @@ op2_set_vol:
 			case 0x3: /* call function by address (near call in original) */
 			{
 				uint16 fnOffset = readWord_impl();
-				callFunction(fnOffset);
+				if (!callFunction(fnOffset, *ch))
+					return;
 				ch = _activeChannelPtr;
 				ch->_pSrc += 3;
 				goto dispatch;
diff --git a/engines/mads/dragonsphere/sound/asound.h b/engines/mads/dragonsphere/sound/asound.h
index f52340d0e19..b82abef0c02 100644
--- a/engines/mads/dragonsphere/sound/asound.h
+++ b/engines/mads/dragonsphere/sound/asound.h
@@ -26,6 +26,7 @@
 #include "common/mutex.h"
 #include "common/queue.h"
 #include "common/util.h"
+#include "mads/core/native_sound_timer.h"
 #include "mads/core/sound_manager.h"
 
 namespace MADS {
@@ -71,8 +72,8 @@ struct AdlibChannel {
 
 	// ---- word counter fields (offsets 0x20 - 0x29) ----------------------
 
-	uint16 _innerLoopCount = 0; // 0x20  remaining inner-loop iterations (0 = infinite until opcode)
-	uint16 _outerLoopCount = 0; // 0x22  remaining outer-loop iterations (0 = infinite until opcode)
+	uint16 _innerLoopCount = 0; // 0x20  signed byte stored as a 16-bit loop count
+	uint16 _outerLoopCount = 0; // 0x22  signed byte stored as a 16-bit loop count
 	uint16 _noiseFreqMask = 0; // 0x24  AND mask applied to the random number in noise mode
 	uint16 _freqAccum = 0; // 0x26  frequency sweep accumulator (base frequency + swept offset)
 	uint16 _freqStep = 0; // 0x28  per-tick increment added to _freqAccum during a sweep
@@ -195,6 +196,8 @@ protected:
 
 private:
 	OPL::OPL *_opl;
+	NativeSoundTimer _hostTimer;
+	bool _noiseEnabled = false;
 
 	// ---- callback / tick state ------------------------------------------
 	uint16 _callbackCounter = 0;  // per-tick countdown
@@ -219,6 +222,7 @@ private:
 
 	// ---- driver-wide flags ----------------------------------------------
 	uint16 _isDisabled = 0; // non-zero while the engine is paused (command6)
+	int _masterVolume = 255;
 	uint8  _findChannelMode = 0; // 0=full search, 1=ch0-5 only, 2=ch6-8 then pending
 
 	// ---- per-channel sweep shadows (for channel 5 special-casing) -------
@@ -285,6 +289,7 @@ private:
 	 * so command7 can restore levels without a full recalculation.
 	 */
 	void writeVolume();
+	void refreshVolumes();
 
 	/**
 	 * Derives the OPL F-number and block (octave) from _note, _octaveTranspose,
@@ -587,10 +592,14 @@ protected:
 	int command8();
 
 	/**
-	 * Calls a function at a fixed offset within the sound driver.
+	 * Handles an opcode-A3 near call to a fixed offset in the native driver.
+	 * Derived overlays map only targets recovered from their sequence data;
+	 * unknown targets remain fatal.
 	 * @param offset		Offset of the function
+	 * @param channel	Channel which encountered the callback opcode
+	 * @return true to continue interpreting the current stream
 	 */
-	virtual void callFunction(uint16 offset);
+	virtual bool callFunction(uint16 offset, AdlibChannel &channel);
 
 	// =========================================================================
 	// Music-index launcher (called via command18)
@@ -642,9 +651,7 @@ public:
 	 */
 	void playSound(int offset);
 
-	void setVolume(int volume) override {
-		// TODO: implement if needed
-	}
+	void setVolume(int volume) override;
 };
 
 } // namespace Sound
diff --git a/engines/mads/dragonsphere/sound/asound_dragonsphere.cpp b/engines/mads/dragonsphere/sound/asound_dragonsphere.cpp
index b440ee4e7fa..bde6e461450 100644
--- a/engines/mads/dragonsphere/sound/asound_dragonsphere.cpp
+++ b/engines/mads/dragonsphere/sound/asound_dragonsphere.cpp
@@ -34,13 +34,17 @@ namespace Sound {
 #define MAKE_CALLBACK(cls, fn) \
 	reinterpret_cast<ASound::CallbackFunction>(&cls::fn)
 
+#define MAKE_DEMO_CALLBACK(cls, fn) \
+	reinterpret_cast<ASoundDemo::CallbackFunction>(&cls::fn)
+
 const ASound1::CommandPtr ASound1::_commandList[102] = {
 	// commands 0-8  (table 1)
 	&ASound1::command0,   &ASound1::command1,   &ASound1::command2,   &ASound1::command3,
 	&ASound1::command4,   &ASound1::command5,   &ASound1::command6,   &ASound1::command7,
 	&ASound1::command8,
-	// 9-15 absent
-	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+	// commands 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound1::command16, &ASound1::command17, &ASound1::command18, nullptr,
+	&ASound1::command24, &ASound1::command25, &ASound1::command26,
 	// commands 16-18, 19=no-op  (table 2)
 	&ASound1::command16,  &ASound1::command17,  &ASound1::command18,  nullptr,
 	// 20-23 absent
@@ -94,6 +98,46 @@ int ASound1::command6()  { return ASound::command6(); }
 int ASound1::command7()  { return ASound::command7(); }
 int ASound1::command8()  { return ASound::command8(); }
 
+void ASound1::loadCallback1FBA() {
+	resetCallbackTimer(0x50);
+	ASound::command1();
+	_channels[0]->load(loadData(0x30E6));
+	_channels[1]->load(loadData(0x313C));
+	_channels[2]->load(loadData(0x31C5));
+	_channels[3]->load(loadData(0x3125));
+	_channels[4]->load(loadData(0x31AF));
+	_channels[5]->load(loadData(0x326D));
+}
+
+// Opcode A3 reaches four existing music controllers and one transition used
+// by command 34. The offsets are native code addresses, not sound-data roots.
+bool ASound1::callFunction(uint16 offset, AdlibChannel &channel) {
+	switch (offset) {
+	case 0x1C56:
+		command40();
+		return true;
+	case 0x1CA6:
+		command41();
+		return true;
+	case 0x1CF6:
+		command16();
+		return true;
+	case 0x1DD0:
+		command32();
+		return true;
+	case 0x1FBA:
+		if (!isSoundActive(loadData(0x30E6))) {
+			if (isMusicChannelsActive())
+				scheduleCallback(MAKE_CALLBACK(ASound1, loadCallback1FBA));
+			else
+				loadCallback1FBA();
+		}
+		return true;
+	default:
+		return ASound::callFunction(offset, channel);
+	}
+}
+
 // ---------------------------------------------------------------------------
 // command16 - music piece A (castle interior theme)
 // isSoundActive guard + isMusicChannelsActive deferred-callback pattern.
@@ -685,9 +729,9 @@ const ASound2::CommandPtr ASound2::_commandList[76] = {
 	&ASound2::command0,  &ASound2::command1,  &ASound2::command2,  &ASound2::command3,
 	&ASound2::command4,  &ASound2::command5,  &ASound2::command6,  &ASound2::command7,
 	&ASound2::command8,
-	// 9-15: nullptr
-	nullptr,             nullptr,             nullptr,
-	nullptr,             nullptr,             nullptr,             nullptr,
+	// 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound2::command16, &ASound2::command17, &ASound2::command18, nullptr,
+	&ASound2::command24, &ASound2::command25, &ASound2::command26,
 	// 16-19: table 2
 	&ASound2::command16, &ASound2::command17, &ASound2::command18, nullptr,
 	// 20-23: nullptr
@@ -737,6 +781,16 @@ int ASound2::command6() { return ASound::command6(); }
 int ASound2::command7() { return ASound::command7(); }
 int ASound2::command8() { return ASound::command8(); }
 
+// Command 65's fourth effect stream returns from a script subroutine into
+// this A3 call, which re-enters the native command-16 music controller.
+bool ASound2::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1D52)
+		return ASound::callFunction(offset, channel);
+
+	command16();
+	return true;
+}
+
 void ASound2::loadCommand16() {
 	resetCallbackTimer(0x60);
 	setMusicIndex(0x10);
@@ -973,9 +1027,9 @@ const ASound3::CommandPtr ASound3::_commandList[77] = {
 	&ASound3::command0,  &ASound3::command1,  &ASound3::command2,  &ASound3::command3,
 	&ASound3::command4,  &ASound3::command5,  &ASound3::command6,  &ASound3::command7,
 	&ASound3::command8,
-	// 9-15 absent
-	nullptr,             nullptr,             nullptr,
-	nullptr,             nullptr,             nullptr,             nullptr,
+	// 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound3::command16, &ASound3::command17, &ASound3::command18, nullptr,
+	&ASound3::command24, &ASound3::command25, &ASound3::command26,
 	// commands 16-19  (table 2; slot 19 = no-op)
 	&ASound3::command16, &ASound3::command17, &ASound3::command18, nullptr,
 	// 20-23 absent
@@ -1028,6 +1082,31 @@ int ASound3::command6() { return ASound::command6(); }
 int ASound3::command7() { return ASound::command7(); }
 int ASound3::command8() { return ASound::command8(); }
 
+// The native callback selects channel 0, 2, or 3 from the low byte of its
+// inner-loop counter, folds that channel's note into the required octave,
+// and writes the result back into the active sound data.
+bool ASound3::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1BC5)
+		return ASound::callFunction(offset, channel);
+
+	AdlibChannel *sourceChannel = _channels[0];
+	if ((byte)sourceChannel->_innerLoopCount != 0) {
+		sourceChannel = _channels[2];
+		if ((byte)sourceChannel->_innerLoopCount != 0)
+			sourceChannel = _channels[3];
+	}
+
+	byte note = sourceChannel->_note;
+	if ((int8)note >= 0x61) {
+		note -= 12;
+	} else {
+		while (note < 0x58)
+			note += 12;
+	}
+	*loadData(0x1D1C) = note;
+	return true;
+}
+
 // ---------------------------------------------------------------------------
 // command16 (Pattern B deferred): timer=0xA8, musicIndex=0x10, ch0-6
 // ---------------------------------------------------------------------------
@@ -1247,9 +1326,9 @@ const ASound4::CommandPtr ASound4::_commandList[82] = {
 	&ASound4::command0,  &ASound4::command1,  &ASound4::command2,  &ASound4::command3,
 	&ASound4::command4,  nullptr,             &ASound4::command6,  &ASound4::command7,
 	&ASound4::command8,
-	// 9-15 absent
-	nullptr,             nullptr,             nullptr,             nullptr,
-	nullptr,             nullptr,             nullptr,
+	// 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound4::command16, &ASound4::command17, &ASound4::command18, nullptr,
+	&ASound4::command24, &ASound4::command25, &ASound4::command26,
 	// commands 16-19 (table 2; slot 19 = no-op)
 	&ASound4::command16, &ASound4::command17, &ASound4::command18, nullptr,
 	// 20-23 absent
@@ -1609,9 +1688,9 @@ const ASound5::CommandPtr ASound5::_commandList[82] = {
 	&ASound5::command0,  &ASound5::command1,  &ASound5::command2,  &ASound5::command3,
 	&ASound5::command4,  &ASound5::command5,  &ASound5::command6,  &ASound5::command7,
 	&ASound5::command8,
-	// 9-15 absent
-	nullptr,             nullptr,             nullptr,
-	nullptr,             nullptr,             nullptr,             nullptr,
+	// 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound5::command16, &ASound5::command17, &ASound5::command18, nullptr,
+	&ASound5::command24, &ASound5::command25, &ASound5::command26,
 	// commands 16-19 (table 2; slot 19 = no-op)
 	&ASound5::command16, &ASound5::command17, &ASound5::command18, nullptr,
 	// 20-23 absent
@@ -1665,6 +1744,34 @@ int ASound5::command6() { return ASound::command6(); }
 int ASound5::command7() { return ASound::command7(); }
 int ASound5::command8() { return ASound::command8(); }
 
+void ASound5::loadCallback1B7B() {
+	ASound::command1();
+	resetCallbackTimer(0xC0);
+	setMusicIndex(0x10);
+	_channels[0]->load(loadData(0x168A));
+	_channels[1]->load(loadData(0x17F4));
+	_channels[2]->load(loadData(0x18AF));
+	_channels[3]->load(loadData(0x199C));
+	_channels[4]->load(loadData(0x1A65));
+	_channels[5]->load(loadData(0x1C8A));
+	_channels[6]->load(loadData(0x1CA9));
+}
+
+// Several music streams use A3 to switch to this alternate command-16
+// arrangement. It has distinct channel roots and cannot use loadCommand16().
+bool ASound5::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1B7B)
+		return ASound::callFunction(offset, channel);
+
+	if (!isSoundActive(loadData(0x168A))) {
+		if (isMusicChannelsActive())
+			scheduleCallback(MAKE_CALLBACK(ASound5, loadCallback1B7B));
+		else
+			loadCallback1B7B();
+	}
+	return true;
+}
+
 // ---------------------------------------------------------------------------
 // command16 - Pattern B deferred music, timer=0xC0, ch0-6
 // isSoundActive check uses 0x168A (not ch0's load offset 0x167C)
@@ -1941,8 +2048,9 @@ const ASound6::CommandPtr ASound6::_commandList[102] = {
 	&ASound6::command0,  &ASound6::command1,  &ASound6::command2,  &ASound6::command3,
 	&ASound6::command4,  &ASound6::command5,  &ASound6::command6,  &ASound6::command7,
 	&ASound6::command8,
-	// 9-15 absent
-	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+	// 9-11 alias 16-18; 12=no-op; 13-15 alias 24-26
+	&ASound6::command16, &ASound6::command17, &ASound6::command18, nullptr,
+	&ASound6::command24, &ASound6::command25, &ASound6::command26,
 	// commands 16-19 (table 2; slot 19 = no-op)
 	&ASound6::command16, &ASound6::command17, &ASound6::command18, nullptr,
 	// 20-23 absent
@@ -2000,6 +2108,16 @@ int ASound6::command6() { return ASound::command6(); }
 int ASound6::command7() { return ASound::command7(); }
 int ASound6::command8() { return ASound::command8(); }
 
+// The A3 target embedded in command 44's channel-7 stream is the native
+// command-37 controller.
+bool ASound6::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1C94)
+		return ASound::callFunction(offset, channel);
+
+	command37();
+	return true;
+}
+
 // command16 — Pattern B with 5 isSoundActive guards; setMusicIndex(0x10)
 void ASound6::loadCommand16() {
 	resetCallbackTimer(0xC8);
@@ -2457,11 +2575,13 @@ int ASound9::command6() { return ASound::command6(); }
 int ASound9::command7() { return ASound::command7(); }
 int ASound9::command8() { return ASound::command8(); }
 
-void ASound9::callFunction(uint16 offset) {
-	if (offset == 0x1adc)
-		command32();
-	else
-		ASound::callFunction(offset);
+// The main-menu stream calls the native command-32 controller after its rest.
+bool ASound9::callFunction(uint16 offset, AdlibChannel &channel) {
+	if (offset != 0x1ADC)
+		return ASound::callFunction(offset, channel);
+
+	command32();
+	return true;
 }
 
 // ---------------------------------------------------------------------------
@@ -3008,29 +3128,69 @@ int ASound9::command63() {
 /* ASoundDemo1  (asound.dr1 [demo])                                       *
  *-----------------------------------------------------------------------*/
 
+ASoundDemo::ASoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize) :
+		MADS::Phantom::Sound::ASound(mixer, filename, dataOffset, dataSize),
+		_callbackCounter(0), _callbackPeriod(0), _callbackFnPtr(nullptr) {
+}
+
+void ASoundDemo::clearGameCallback() {
+	_callbackFnPtr = nullptr;
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+}
+
+void ASoundDemo::resetGameState() {
+	clearGameCallback();
+}
+
+void ASoundDemo::tickGameCallback() {
+	if (_callbackPeriod == 0)
+		return;
+	if (--_callbackCounter != 0)
+		return;
+
+	_callbackCounter = _callbackPeriod;
+	if (_callbackFnPtr == nullptr)
+		return;
+
+	CallbackFunction fn = _callbackFnPtr;
+	_callbackFnPtr = nullptr;
+	(this->*fn)();
+}
+
+int ASoundDemo::isMusicChannelsActive() const {
+	uint8 result = 0;
+	for (int i = 0; i <= 6; ++i)
+		result |= _channels[i]->_activeCount;
+	return result;
+}
+
 const ASoundDemo1::CommandPtr ASoundDemo1::_commandList[93] = {
 	// commands 0-8  (commands0)
 	&ASoundDemo1::command0,  &ASoundDemo1::command1,  &ASoundDemo1::command2,  &ASoundDemo1::command3,
 	&ASoundDemo1::command4,  &ASoundDemo1::command5,  &ASoundDemo1::command6,  &ASoundDemo1::command7,
 	&ASoundDemo1::command8,
-	// 9-15 absent
-	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+	// commands 9-13 alias commands 16 and 24-27; 14-15 are no-op
+	&ASoundDemo1::command16, &ASoundDemo1::command24, &ASoundDemo1::command25,
+	&ASoundDemo1::command26, &ASoundDemo1::command27, nullptr, nullptr,
 	// command 16  (commands16)
 	&ASoundDemo1::command16,
 	// 17-23 absent
 	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
-	// commands 24-27, 28-29 = no-op, commands 30-43, 44 = no-op  (commands24)
+	// commands 24-27, 28-29 = no-op  (commands24)
 	&ASoundDemo1::command24, &ASoundDemo1::command25, &ASoundDemo1::command26, &ASoundDemo1::command27,
-	nullptr,                 nullptr,
-	&ASoundDemo1::command30, &ASoundDemo1::command31, &ASoundDemo1::command32, &ASoundDemo1::command33,
+	nullptr,                 nullptr,                 nullptr,                 nullptr,
+	// commands 32-45  (commands32)
+	&ASoundDemo1::command32, &ASoundDemo1::command33,
 	&ASoundDemo1::command34, &ASoundDemo1::command35, &ASoundDemo1::command36, &ASoundDemo1::command37,
 	&ASoundDemo1::command38, &ASoundDemo1::command39, &ASoundDemo1::command40, &ASoundDemo1::command41,
-	&ASoundDemo1::command42, &ASoundDemo1::command43,
+	&ASoundDemo1::command42, &ASoundDemo1::command43, &ASoundDemo1::command44, &ASoundDemo1::command45,
 	nullptr,
-	// 45-63 absent
+	// 46-63 absent
 	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
 	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
-	nullptr, nullptr, nullptr, nullptr, nullptr,
+	nullptr, nullptr, nullptr,
 	// commands 64-89 = no-op, commands 90-92  (commands64)
 	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
 	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
@@ -3040,10 +3200,10 @@ const ASoundDemo1::CommandPtr ASoundDemo1::_commandList[93] = {
 };
 
 ASoundDemo1::ASoundDemo1(Audio::Mixer *mixer)
-		: ASound(mixer, "asound.dr1", 0x23e0, 0x4900) {
-	auto samplesStream = getDataStream(0x1dc);
+		: ASoundDemo(mixer, "asound.dr1", 0x23e0, 0x4900) {
+	Common::MemoryReadStream samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
-		_samples.push_back(AdlibSample(samplesStream));
+		_samples.push_back(MADS::Phantom::Sound::AdlibSample(samplesStream));
 }
 
 int ASoundDemo1::command(int commandId, int param) {
@@ -3052,16 +3212,42 @@ int ASoundDemo1::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
-// commands 0-8: delegate to base ASound
-int ASoundDemo1::command0() { return ASound::command0(); }
-int ASoundDemo1::command1() { return ASound::command1(); }
-int ASoundDemo1::command2() { return ASound::command2(); }
-int ASoundDemo1::command3() { return ASound::command3(); }
-int ASoundDemo1::command4() { return ASound::command4(); }
-int ASoundDemo1::command5() { return ASound::command5(); }
-int ASoundDemo1::command6() { return ASound::command6(); }
-int ASoundDemo1::command7() { return ASound::command7(); }
-int ASoundDemo1::command8() { return ASound::command8(); }
+// commands 0-8: delegate to the shared demo runtime
+int ASoundDemo1::command0() { return ASoundDemo::command0(); }
+int ASoundDemo1::command1() { return ASoundDemo::command1(); }
+int ASoundDemo1::command2() { return ASoundDemo::command2(); }
+int ASoundDemo1::command3() { return ASoundDemo::command3(); }
+int ASoundDemo1::command4() { return ASoundDemo::command4(); }
+int ASoundDemo1::command5() { return ASoundDemo::command5(); }
+int ASoundDemo1::command6() { return ASoundDemo::command6(); }
+int ASoundDemo1::command7() { return ASoundDemo::command7(); }
+int ASoundDemo1::command8() { return ASoundDemo::command8(); }
+
+void ASoundDemo1::callFunction(uint16 offset,
+		MADS::Phantom::Sound::AdlibChannel &channel) {
+	// The five C4 sites in the demo's loaded sound data call native music
+	// controllers. Their operands are load-module code offsets, while the
+	// C4 sites themselves are relative to the loaded sound-data block.
+	switch (offset) {
+	case 0x1E36:
+		command41();
+		break;
+	case 0x1DEE:
+		command40();
+		break;
+	case 0x1F14:
+		command32();
+		break;
+	case 0x1E7E:
+		command16();
+		break;
+	case 0x1FBE:
+		command34();
+		break;
+	default:
+		ASoundDemo::callFunction(offset, channel);
+	}
+}
 
 // ---------------------------------------------------------------------------
 // command16 - no isSoundActive guard; if music channels are busy, defer via
@@ -3069,7 +3255,7 @@ int ASoundDemo1::command8() { return ASound::command8(); }
 // ---------------------------------------------------------------------------
 void ASoundDemo1::loadCommand16() {
 	resetCallbackTimer(0x90);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x2BC6));
 	_channels[1]->load(loadData(0x2C74));
 	_channels[2]->load(loadData(0x2CEC));
@@ -3081,7 +3267,7 @@ void ASoundDemo1::loadCommand16() {
 
 int ASoundDemo1::command16() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand16));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand16));
 	else
 		loadCommand16();
 	return 0;
@@ -3107,11 +3293,11 @@ int ASoundDemo1::command27() {
 }
 
 // ---------------------------------------------------------------------------
-// command30 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command32 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand30() {
+void ASoundDemo1::loadCommand32() {
 	resetCallbackTimer(0xB0);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x2F2A));
 	_channels[1]->load(loadData(0x2F73));
 	_channels[2]->load(loadData(0x2FD5));
@@ -3120,21 +3306,21 @@ void ASoundDemo1::loadCommand30() {
 	_channels[5]->load(loadData(0x303E));
 }
 
-int ASoundDemo1::command30() {
+int ASoundDemo1::command32() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand30));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand32));
 	else
-		loadCommand30();
+		loadCommand32();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command31 - isMusicChannelsActive guard, deferred callback, load ch0-5.
+// command33 - isMusicChannelsActive guard, deferred callback, load ch0-5.
 // Note: command1() is called before resetCallbackTimer here (reversed order
-// vs command30/command16).
+// vs command32/command16).
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand31() {
-	ASound::command1();
+void ASoundDemo1::loadCommand33() {
+	ASoundDemo::command1();
 	resetCallbackTimer(0xB0);
 	_channels[0]->load(loadData(0x304E));
 	_channels[1]->load(loadData(0x30F4));
@@ -3144,21 +3330,21 @@ void ASoundDemo1::loadCommand31() {
 	_channels[5]->load(loadData(0x3228));
 }
 
-int ASoundDemo1::command31() {
+int ASoundDemo1::command33() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand31));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand33));
 	else
-		loadCommand31();
+		loadCommand33();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command32 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command34 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // (channels 3-5 load from non-sequential data offsets)
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand32() {
+void ASoundDemo1::loadCommand34() {
 	resetCallbackTimer(0x50);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x32AC));
 	_channels[1]->load(loadData(0x32FE));
 	_channels[2]->load(loadData(0x3387));
@@ -3167,20 +3353,20 @@ void ASoundDemo1::loadCommand32() {
 	_channels[5]->load(loadData(0x3448));
 }
 
-int ASoundDemo1::command32() {
+int ASoundDemo1::command34() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand32));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand34));
 	else
-		loadCommand32();
+		loadCommand34();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command33 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command35 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand33() {
+void ASoundDemo1::loadCommand35() {
 	resetCallbackTimer(0x60);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x3A7E));
 	_channels[1]->load(loadData(0x3B16));
 	_channels[2]->load(loadData(0x3B9C));
@@ -3189,20 +3375,20 @@ void ASoundDemo1::loadCommand33() {
 	_channels[5]->load(loadData(0x3C34));
 }
 
-int ASoundDemo1::command33() {
+int ASoundDemo1::command35() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand33));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand35));
 	else
-		loadCommand33();
+		loadCommand35();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command34 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command36 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand34() {
+void ASoundDemo1::loadCommand36() {
 	resetCallbackTimer(0x80);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x40FE));
 	_channels[1]->load(loadData(0x41C2));
 	_channels[2]->load(loadData(0x42B1));
@@ -3211,20 +3397,20 @@ void ASoundDemo1::loadCommand34() {
 	_channels[5]->load(loadData(0x43C4));
 }
 
-int ASoundDemo1::command34() {
+int ASoundDemo1::command36() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand34));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand36));
 	else
-		loadCommand34();
+		loadCommand36();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command35 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command37 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand35() {
+void ASoundDemo1::loadCommand37() {
 	resetCallbackTimer(0xC0);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x43E8));
 	_channels[1]->load(loadData(0x444B));
 	_channels[2]->load(loadData(0x44B6));
@@ -3233,21 +3419,21 @@ void ASoundDemo1::loadCommand35() {
 	_channels[5]->load(loadData(0x44E0));
 }
 
-int ASoundDemo1::command35() {
+int ASoundDemo1::command37() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand35));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand37));
 	else
-		loadCommand35();
+		loadCommand37();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command36 - isMusicChannelsActive guard, deferred callback, load ch0-5.
+// command38 - isMusicChannelsActive guard, deferred callback, load ch0-5.
 // Note: command1() is called before resetCallbackTimer here (reversed order
-// vs command33-35).
+// vs command35-37).
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand36() {
-	ASound::command1();
+void ASoundDemo1::loadCommand38() {
+	ASoundDemo::command1();
 	resetCallbackTimer(0x60);
 	_channels[0]->load(loadData(0x1906));
 	_channels[1]->load(loadData(0x19ED));
@@ -3257,20 +3443,20 @@ void ASoundDemo1::loadCommand36() {
 	_channels[5]->load(loadData(0x1CFC));
 }
 
-int ASoundDemo1::command36() {
+int ASoundDemo1::command38() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand36));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand38));
 	else
-		loadCommand36();
+		loadCommand38();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command37 - isMusicChannelsActive guard, deferred callback, load ch0-5
-// (command1() before resetCallbackTimer, as in command36)
+// command39 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// (command1() before resetCallbackTimer, as in command38)
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand37() {
-	ASound::command1();
+void ASoundDemo1::loadCommand39() {
+	ASoundDemo::command1();
 	resetCallbackTimer(0xB0);
 	_channels[0]->load(loadData(0x1D40));
 	_channels[1]->load(loadData(0x1D95));
@@ -3280,21 +3466,21 @@ void ASoundDemo1::loadCommand37() {
 	_channels[5]->load(loadData(0x1E6F));
 }
 
-int ASoundDemo1::command37() {
+int ASoundDemo1::command39() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand37));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand39));
 	else
-		loadCommand37();
+		loadCommand39();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command38 - isMusicChannelsActive guard, deferred callback, load ch0-5
-// (resetCallbackTimer before command1(), as in command33-35)
+// command40 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// (resetCallbackTimer before command1(), as in command35-37)
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand38() {
+void ASoundDemo1::loadCommand40() {
 	resetCallbackTimer(0xA8);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x1E7E));
 	_channels[1]->load(loadData(0x1FF7));
 	_channels[2]->load(loadData(0x21AD));
@@ -3303,21 +3489,21 @@ void ASoundDemo1::loadCommand38() {
 	_channels[5]->load(loadData(0x293B));
 }
 
-int ASoundDemo1::command38() {
+int ASoundDemo1::command40() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand38));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand40));
 	else
-		loadCommand38();
+		loadCommand40();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command39 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command41 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // (channels 2-5 load from non-sequential data offsets)
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand39() {
+void ASoundDemo1::loadCommand41() {
 	resetCallbackTimer(0x90);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x2972));
 	_channels[1]->load(loadData(0x29DF));
 	_channels[2]->load(loadData(0x2BB7));
@@ -3326,20 +3512,20 @@ void ASoundDemo1::loadCommand39() {
 	_channels[5]->load(loadData(0x2B8A));
 }
 
-int ASoundDemo1::command39() {
+int ASoundDemo1::command41() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand39));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand41));
 	else
-		loadCommand39();
+		loadCommand41();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command40 - isMusicChannelsActive guard, deferred callback, load ch0-6
+// command42 - isMusicChannelsActive guard, deferred callback, load ch0-6
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand40() {
+void ASoundDemo1::loadCommand42() {
 	resetCallbackTimer(0x90);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x17D6));
 	_channels[1]->load(loadData(0x180C));
 	_channels[2]->load(loadData(0x183F));
@@ -3349,20 +3535,20 @@ void ASoundDemo1::loadCommand40() {
 	_channels[6]->load(loadData(0x18E3));
 }
 
-int ASoundDemo1::command40() {
+int ASoundDemo1::command42() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand40));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand42));
 	else
-		loadCommand40();
+		loadCommand42();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command41 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command43 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand41() {
+void ASoundDemo1::loadCommand43() {
 	resetCallbackTimer(0x50);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x3C44));
 	_channels[1]->load(loadData(0x3CB1));
 	_channels[2]->load(loadData(0x3CCD));
@@ -3371,22 +3557,22 @@ void ASoundDemo1::loadCommand41() {
 	_channels[5]->load(loadData(0x3D03));
 }
 
-int ASoundDemo1::command41() {
+int ASoundDemo1::command43() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand41));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand43));
 	else
-		loadCommand41();
+		loadCommand43();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command42 - isMusicChannelsActive guard, deferred callback, load ch0-5.
+// command44 - isMusicChannelsActive guard, deferred callback, load ch0-5.
 // Uses resetCallbackTimerEx: counter=0x60, period=0xE0 (asymmetric).
 // (channels 2 and 5 load from non-sequential data offsets)
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand42() {
+void ASoundDemo1::loadCommand44() {
 	resetCallbackTimerEx(0x60, 0xE0);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x3D10));
 	_channels[1]->load(loadData(0x3D66));
 	_channels[2]->load(loadData(0x40EE));
@@ -3395,20 +3581,20 @@ void ASoundDemo1::loadCommand42() {
 	_channels[5]->load(loadData(0x4053));
 }
 
-int ASoundDemo1::command42() {
+int ASoundDemo1::command44() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand42));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand44));
 	else
-		loadCommand42();
+		loadCommand44();
 	return 0;
 }
 
 // ---------------------------------------------------------------------------
-// command43 - isMusicChannelsActive guard, deferred callback, load ch0-5
+// command45 - isMusicChannelsActive guard, deferred callback, load ch0-5
 // ---------------------------------------------------------------------------
-void ASoundDemo1::loadCommand43() {
+void ASoundDemo1::loadCommand45() {
 	resetCallbackTimer(0x60);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x347A));
 	_channels[1]->load(loadData(0x35BB));
 	_channels[2]->load(loadData(0x36A8));
@@ -3417,11 +3603,11 @@ void ASoundDemo1::loadCommand43() {
 	_channels[5]->load(loadData(0x3A4D));
 }
 
-int ASoundDemo1::command43() {
+int ASoundDemo1::command45() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand43));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand45));
 	else
-		loadCommand43();
+		loadCommand45();
 	return 0;
 }
 
@@ -3454,7 +3640,7 @@ int ASoundDemo1::command91() {
 // ---------------------------------------------------------------------------
 void ASoundDemo1::loadCommand92() {
 	resetCallbackTimer(0x54);
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x2E16));
 	_channels[1]->load(loadData(0x2E62));
 	_channels[2]->load(loadData(0x2EA9));
@@ -3465,7 +3651,7 @@ void ASoundDemo1::loadCommand92() {
 
 int ASoundDemo1::command92() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo1, loadCommand92));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo1, loadCommand92));
 	else
 		loadCommand92();
 	return 0;
@@ -3480,8 +3666,10 @@ const ASoundDemo9::CommandPtr ASoundDemo9::_commandList[51] = {
 	&ASoundDemo9::command0,  &ASoundDemo9::command1,  &ASoundDemo9::command2,  &ASoundDemo9::command3,
 	&ASoundDemo9::command4,  &ASoundDemo9::command5,  &ASoundDemo9::command6,  &ASoundDemo9::command7,
 	&ASoundDemo9::command8,
-	// 9-15 absent
-	nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+	// commands 9-15 alias later native entries
+	&ASoundDemo9::command16, &ASoundDemo9::command25, &ASoundDemo9::command26,
+	&ASoundDemo9::command27, &ASoundDemo9::command28_32,
+	&ASoundDemo9::command29_33, &ASoundDemo9::command34,
 	// command 16  (commands16_24)
 	&ASoundDemo9::command16,
 	// 17-23 absent
@@ -3504,10 +3692,10 @@ const ASoundDemo9::CommandPtr ASoundDemo9::_commandList[51] = {
 };
 
 ASoundDemo9::ASoundDemo9(Audio::Mixer *mixer)
-		: ASound(mixer, "asound.dr9", 0x23a0, 0x62b0) {
-	auto samplesStream = getDataStream(0x1dc);
+		: ASoundDemo(mixer, "asound.dr9", 0x23a0, 0x62b0) {
+	Common::MemoryReadStream samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
-		_samples.push_back(AdlibSample(samplesStream));
+		_samples.push_back(MADS::Phantom::Sound::AdlibSample(samplesStream));
 }
 
 int ASoundDemo9::command(int commandId, int param) {
@@ -3516,16 +3704,16 @@ int ASoundDemo9::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
-// commands 0-8: delegate to base ASound
-int ASoundDemo9::command0() { return ASound::command0(); }
-int ASoundDemo9::command1() { return ASound::command1(); }
-int ASoundDemo9::command2() { return ASound::command2(); }
-int ASoundDemo9::command3() { return ASound::command3(); }
-int ASoundDemo9::command4() { return ASound::command4(); }
-int ASoundDemo9::command5() { return ASound::command5(); }
-int ASoundDemo9::command6() { return ASound::command6(); }
-int ASoundDemo9::command7() { return ASound::command7(); }
-int ASoundDemo9::command8() { return ASound::command8(); }
+// commands 0-8: delegate to the shared demo runtime
+int ASoundDemo9::command0() { return ASoundDemo::command0(); }
+int ASoundDemo9::command1() { return ASoundDemo::command1(); }
+int ASoundDemo9::command2() { return ASoundDemo::command2(); }
+int ASoundDemo9::command3() { return ASoundDemo::command3(); }
+int ASoundDemo9::command4() { return ASoundDemo::command4(); }
+int ASoundDemo9::command5() { return ASoundDemo::command5(); }
+int ASoundDemo9::command6() { return ASoundDemo::command6(); }
+int ASoundDemo9::command7() { return ASoundDemo::command7(); }
+int ASoundDemo9::command8() { return ASoundDemo::command8(); }
 
 // ---------------------------------------------------------------------------
 // commands 16, 25-27 - sound effects via findFreeChannelFull
@@ -3550,7 +3738,7 @@ int ASoundDemo9::command27() { findFreeChannelFull(loadData(0x5F8D)); return 0;
 // (shared handler for commands 28 and 32)
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand28() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimerEx(0x62, 0x54);
 	_channels[0]->load(loadData(0x1938));
 	_channels[1]->load(loadData(0x1972));
@@ -3563,7 +3751,7 @@ void ASoundDemo9::loadCommand28() {
 
 int ASoundDemo9::command28_32() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand28));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand28));
 	else
 		loadCommand28();
 	return 0;
@@ -3574,7 +3762,7 @@ int ASoundDemo9::command28_32() {
 // (shared handler for commands 29 and 33; non-sequential channel order)
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand29() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimerEx(0x62, 0x54);
 	_channels[0]->load(loadData(0x2B10));
 	_channels[1]->load(loadData(0x2B7F));
@@ -3589,7 +3777,7 @@ void ASoundDemo9::loadCommand29() {
 
 int ASoundDemo9::command29_33() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand29));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand29));
 	else
 		loadCommand29();
 	return 0;
@@ -3599,7 +3787,7 @@ int ASoundDemo9::command29_33() {
 // command34 - isMusicChannelsActive guard, deferred callback, load ch0-2,8,4-6
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand34() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x38);
 	_channels[0]->load(loadData(0x300E));
 	_channels[1]->load(loadData(0x3204));
@@ -3612,7 +3800,7 @@ void ASoundDemo9::loadCommand34() {
 
 int ASoundDemo9::command34() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand34));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand34));
 	else
 		loadCommand34();
 	return 0;
@@ -3622,7 +3810,7 @@ int ASoundDemo9::command34() {
 // command35 - isMusicChannelsActive guard, deferred callback, load ch0-3,8,5-6
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand35() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x50);
 	_channels[0]->load(loadData(0x3924));
 	_channels[1]->load(loadData(0x396F));
@@ -3635,7 +3823,7 @@ void ASoundDemo9::loadCommand35() {
 
 int ASoundDemo9::command35() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand35));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand35));
 	else
 		loadCommand35();
 	return 0;
@@ -3645,7 +3833,7 @@ int ASoundDemo9::command35() {
 // command36 - isMusicChannelsActive guard, deferred callback, load ch0-5,8
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand36() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x28);
 	_channels[0]->load(loadData(0x3DFE));
 	_channels[1]->load(loadData(0x3E8F));
@@ -3658,7 +3846,7 @@ void ASoundDemo9::loadCommand36() {
 
 int ASoundDemo9::command36() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand36));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand36));
 	else
 		loadCommand36();
 	return 0;
@@ -3668,7 +3856,7 @@ int ASoundDemo9::command36() {
 // command37 - isMusicChannelsActive guard, deferred callback, load ch0-4,8,5
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand37() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x50);
 	_channels[0]->load(loadData(0x4334));
 	_channels[1]->load(loadData(0x43C3));
@@ -3681,7 +3869,7 @@ void ASoundDemo9::loadCommand37() {
 
 int ASoundDemo9::command37() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand37));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand37));
 	else
 		loadCommand37();
 	return 0;
@@ -3692,7 +3880,7 @@ int ASoundDemo9::command37() {
 // (ch3 reuses the same data offset as command36's ch3)
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand38() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x28);
 	_channels[0]->load(loadData(0x476C));
 	_channels[1]->load(loadData(0x47F9));
@@ -3705,7 +3893,7 @@ void ASoundDemo9::loadCommand38() {
 
 int ASoundDemo9::command38() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand38));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand38));
 	else
 		loadCommand38();
 	return 0;
@@ -3715,7 +3903,7 @@ int ASoundDemo9::command38() {
 // command39 - isMusicChannelsActive guard, deferred callback, load ch0-5,8
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand39() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x28);
 	_channels[0]->load(loadData(0x499A));
 	_channels[1]->load(loadData(0x4A2F));
@@ -3728,7 +3916,7 @@ void ASoundDemo9::loadCommand39() {
 
 int ASoundDemo9::command39() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand39));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand39));
 	else
 		loadCommand39();
 	return 0;
@@ -3739,7 +3927,7 @@ int ASoundDemo9::command39() {
 // (ch0-2 reuse the same data offsets as command34's ch0-2)
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand40() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x38);
 	_channels[0]->load(loadData(0x300E));
 	_channels[1]->load(loadData(0x3204));
@@ -3752,7 +3940,7 @@ void ASoundDemo9::loadCommand40() {
 
 int ASoundDemo9::command40() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand40));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand40));
 	else
 		loadCommand40();
 	return 0;
@@ -3762,7 +3950,7 @@ int ASoundDemo9::command40() {
 // command41 - isMusicChannelsActive guard, deferred callback, load ch0-2,8,4-6
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand41() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x54);
 	_channels[0]->load(loadData(0x1D6A));
 	_channels[1]->load(loadData(0x1E74));
@@ -3775,7 +3963,7 @@ void ASoundDemo9::loadCommand41() {
 
 int ASoundDemo9::command41() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand41));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand41));
 	else
 		loadCommand41();
 	return 0;
@@ -3785,7 +3973,7 @@ int ASoundDemo9::command41() {
 // command42 - isMusicChannelsActive guard, deferred callback, load ch0-3,8,4-6
 // ---------------------------------------------------------------------------
 void ASoundDemo9::loadCommand42() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimerEx(0xA8, 0x50);
 	_channels[0]->load(loadData(0x1DBC));
 	_channels[1]->load(loadData(0x1EBF));
@@ -3799,7 +3987,7 @@ void ASoundDemo9::loadCommand42() {
 
 int ASoundDemo9::command42() {
 	if (isMusicChannelsActive())
-		scheduleCallback(MAKE_CALLBACK(ASoundDemo9, loadCommand42));
+		scheduleCallback(MAKE_DEMO_CALLBACK(ASoundDemo9, loadCommand42));
 	else
 		loadCommand42();
 	return 0;
@@ -3818,7 +4006,7 @@ int ASoundDemo9::command44() {
 // load ch0-7 (no ch8)
 // ---------------------------------------------------------------------------
 int ASoundDemo9::command43() {
-	ASound::command1();
+	ASoundDemo::command1();
 	resetCallbackTimer(0x60);
 	_channels[0]->load(loadData(0x55EE));
 	_channels[1]->load(loadData(0x564D));
@@ -3850,7 +4038,7 @@ int ASoundDemo9::command46() {
 // command47 - no guard, no callback reset; load ch0-8 (all 9 channels)
 // ---------------------------------------------------------------------------
 int ASoundDemo9::command47() {
-	ASound::command1();
+	ASoundDemo::command1();
 	_channels[0]->load(loadData(0x59B4));
 	_channels[1]->load(loadData(0x5A3B));
 	_channels[2]->load(loadData(0x5A87));
diff --git a/engines/mads/dragonsphere/sound/asound_dragonsphere.h b/engines/mads/dragonsphere/sound/asound_dragonsphere.h
index 4aeb7846d22..3048899e28f 100644
--- a/engines/mads/dragonsphere/sound/asound_dragonsphere.h
+++ b/engines/mads/dragonsphere/sound/asound_dragonsphere.h
@@ -23,6 +23,7 @@
 #define MADS_DRAGONSPHERE_SOUND_ASOUND_DRAGONSPHERE_H
 
 #include "mads/dragonsphere/sound/asound.h"
+#include "mads/phantom/sound/asound.h"
 
 namespace MADS {
 namespace Dragonsphere {
@@ -59,6 +60,7 @@ private:
 	void loadCommand32();
 	void loadCommand33();
 	void loadCommand34();
+	void loadCallback1FBA();
 	void loadCommand35();
 	void loadCommand36();
 	void loadCommand37();
@@ -154,6 +156,8 @@ private:
 	int command100();
 	int command101();
 
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound1(Audio::Mixer *mixer);
 	~ASound1() override {}
@@ -201,6 +205,8 @@ private:
 	int command64(); int command65(); int command66(); int command67();
 	int command68(); int command69_70(); int command71(); int command72();
 
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound2(Audio::Mixer *mixer);
 	~ASound2() override {}
@@ -270,6 +276,8 @@ private:
 	int command72();
 	int command73();
 
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound3(Audio::Mixer *mixer);
 	~ASound3() override {}
@@ -359,6 +367,7 @@ private:
 
 	// Deferred loader callbacks (void, Pattern B)
 	void loadCommand16();
+	void loadCallback1B7B();
 	void loadCommand32();
 	void loadCommand33();
 	void loadCommand34();
@@ -385,6 +394,8 @@ private:
 	int command76(); int command77(); int command78();
 	int command80(); int command81();
 
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound5(Audio::Mixer *mixer);
 	~ASound5() override {}
@@ -446,29 +457,74 @@ private:
 	int command96(); int command97(); int command98();
 	int command100(); int command101();
 
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
+
 public:
 	ASound6(Audio::Mixer *mixer);
 	~ASound6() override {}
 	int command(int commandId, int param) override;
 };
 
+/**
+ * Shared runtime for the Dragonsphere demo ASOUND overlays.
+ *
+ * Both demo overlays use the older bytecode VM also used by Return of the
+ * Phantom rather than the grouped retail Dragonsphere VM. This adapter adds
+ * only the Dragonsphere demo controller callback used by their command
+ * handlers.
+ */
+class ASoundDemo : public MADS::Phantom::Sound::ASound {
+protected:
+	typedef void (ASoundDemo::*CallbackFunction)();
+
+private:
+	uint16 _callbackCounter;
+	uint16 _callbackPeriod;
+	CallbackFunction _callbackFnPtr;
+
+	void clearGameCallback();
+	void resetGameState() override;
+	void tickGameCallback() override;
+
+protected:
+	ASoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize);
+
+	int isMusicChannelsActive() const;
+	void scheduleCallback(CallbackFunction fn) {
+		_callbackFnPtr = fn;
+	}
+	void resetCallbackTimer(uint16 period) {
+		_callbackFnPtr = nullptr;
+		_callbackCounter = period;
+		_callbackPeriod = period;
+	}
+	void resetCallbackTimerEx(uint16 counter, uint16 period) {
+		_callbackFnPtr = nullptr;
+		_callbackCounter = counter;
+		_callbackPeriod = period;
+	}
+};
+
 /**
  * ASoundDemo1  (asound.dr1 [demo], _dataOffset = 0x23e0, _dataSize = 0x4900)
  *
- * Dispatch table layout (four tables collapsed to flat [93]):
+ * Dispatch table layout (five tables collapsed to flat [93]):
  *   commands0:  commands  0– 8  (base=0,    max=8)
  *   commands16: command   16    (base=0x10, max=0x10, 1 entry)
- *   commands24: commands 24–43  (base=0x18, max=0x2B; slots 28,29,44 = no-op)
+ *   commands24: commands 24–29  (base=0x18, max=0x1D; slots 28,29 = no-op)
+ *   commands32: commands 32–46  (base=0x20, max=0x2E; slot 46 = no-op)
  *   commands64: commands 64–92  (base=0x40, max=0x5C; slots 64–89 = no-op)
  */
-class ASoundDemo1 : public ASound {
+class ASoundDemo1 : public ASoundDemo {
 private:
 	typedef int (ASoundDemo1::*CommandPtr)();
 	static const CommandPtr _commandList[93];
 
+	void callFunction(uint16 offset,
+		MADS::Phantom::Sound::AdlibChannel &channel) override;
+
 	void loadCommand16();
-	void loadCommand30();
-	void loadCommand31();
 	void loadCommand32();
 	void loadCommand33();
 	void loadCommand34();
@@ -481,6 +537,8 @@ private:
 	void loadCommand41();
 	void loadCommand42();
 	void loadCommand43();
+	void loadCommand44();
+	void loadCommand45();
 	void loadCommand92();
 
 	int command0(); int command1(); int command2(); int command3();
@@ -490,10 +548,10 @@ private:
 	int command16();
 
 	int command24(); int command25(); int command26(); int command27();
-	int command30(); int command31(); int command32(); int command33();
-	int command34(); int command35(); int command36(); int command37();
-	int command38(); int command39(); int command40(); int command41();
-	int command42(); int command43();
+	int command32(); int command33(); int command34(); int command35();
+	int command36(); int command37(); int command38(); int command39();
+	int command40(); int command41(); int command42(); int command43();
+	int command44(); int command45();
 
 	int command90(); int command91(); int command92();
 
@@ -544,14 +602,9 @@ private:
 	int command57(); int command58(); int command59();
 	int command61(); int command62(); int command63();
 
-	static const CommandPtr _commandList[65];
+	bool callFunction(uint16 offset, AdlibChannel &channel) override;
 
-protected:
-	/**
-	 * Calls a function at a fixed offset within the sound driver.
-	 * @param offset		Offset of the function
-	 */
-	void callFunction(uint16 offset) override;
+	static const CommandPtr _commandList[65];
 
 public:
 	ASound9(Audio::Mixer *mixer);
@@ -573,7 +626,7 @@ public:
  *     outside the array, not via separate array slots)
  *   commands 64+ are unreachable (dispatcher upper bound is 0 for that range)
  */
-class ASoundDemo9 : public ASound {
+class ASoundDemo9 : public ASoundDemo {
 private:
 	typedef int (ASoundDemo9::*CommandPtr)();
 	static const CommandPtr _commandList[51];
diff --git a/engines/mads/phantom/sound/asound.cpp b/engines/mads/phantom/sound/asound.cpp
index 78118194aa6..30232a70450 100644
--- a/engines/mads/phantom/sound/asound.cpp
+++ b/engines/mads/phantom/sound/asound.cpp
@@ -326,6 +326,7 @@ int ASound::command0() {
 
 	/* 6. Reset callback counters */
 	resetCallback();
+	resetGameState();
 
 	return 0;
 }
@@ -847,6 +848,7 @@ void ASound::update() {
 		++_frameNumber2;
 
 		pollAllChannels();
+		tickGameCallback();
 		updateAllChannels();
 		_anySweepActive = false;
 
diff --git a/engines/mads/phantom/sound/asound.h b/engines/mads/phantom/sound/asound.h
index 0fa25776a5a..0e042c319ad 100644
--- a/engines/mads/phantom/sound/asound.h
+++ b/engines/mads/phantom/sound/asound.h
@@ -311,6 +311,18 @@ protected:
 	 */
 	void pollAllChannels();
 
+	/**
+	 * Applies game-specific state reset alongside the shared driver reset.
+	 */
+	virtual void resetGameState() {
+	}
+
+	/**
+	 * Runs game-specific controller work after the channel poll.
+	 */
+	virtual void tickGameCallback() {
+	}
+
 	/**
 	 * Per-channel update, called once per frame by pollAllChannels.
 	 * Implements a bytecode interpreter : the sound data stream is a sequence of


Commit: ab4862449307a548f389a70b7e17261b855f0160
    https://github.com/scummvm/scummvm/commit/ab4862449307a548f389a70b7e17261b855f0160
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: NEBULAR: Restore native RSOUND playback

Connect the retail and demo RSOUND overlays to MT-32 output using
the recovered DOS host cadence. Preserve command dispatch, sequence
behavior, SysEx bounds, and callbacks verified against the overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/detection_tables.h
    engines/mads/nebular/sound/rsound.cpp
    engines/mads/nebular/sound/rsound.h
    engines/mads/nebular/sound/rsound_nebular.cpp
    engines/mads/nebular/sound/rsound_nebular.h
    engines/mads/nebular/sound/sound.cpp


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 98773ee3ef9..c3f5fd9112b 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -141,7 +141,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO6(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_ORIGINAL_SAVELOAD, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 		},
 		GType_RexNebular,
 		0
diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index a319971a7c4..2c2ef48c2d8 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -66,10 +66,10 @@ void Channel::load(byte *pData) {
 
 /*-----------------------------------------------------------------------*/
 
-const uint32 RSound::UPDATE_DELTA = 1000000 / 60;
-
 RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset) : SoundDriver(mixer, filename, dataOffset, dataSize) {
+		int dataOffset, int dataSize, int sysExOffset,
+		RSoundFadeCheckMode fadeCheckMode) :
+		SoundDriver(mixer, filename, dataOffset, dataSize) {
 	_commandParam = 0;
 	_frameCounter = 0;
 	_isDisabled = false;
@@ -80,7 +80,10 @@ RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
 	_pollResult = 0;
 	_resultFlag = 0;
 	_sysExOffset = sysExOffset;
-	_updateDeltaRemainder = 0;
+	_fadeCheckMode = fadeCheckMode;
+	_fadeCheckAlternate = false;
+	_fadeCheckCounter = 0;
+	_fadeCheckPeriod = 0;
 
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
 		_channels[i]._owner = this;
@@ -123,7 +126,7 @@ RSound::~RSound() {
 	}
 }
 
-void RSound::validate() {
+void RSound::validate(bool isDemo) {
 	Common::File f;
 	static const char *const MD5[] = {
 		"6b2f2f24b54ba0177938dde17baa6231",
@@ -136,15 +139,24 @@ void RSound::validate() {
 		"40a2a8bd0d49f1acbb0569f1b22ec9b2",
 		"2ae093b2ce06f739f200ca3e9ff2af85"
 	};
+	static const char *const MD5_DEMO[] = {
+		"ad14e2a1c900287737b9f43f1d8c3fb2",
+		nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
+		"e2fafe292239be4afa2bf789bf4f496d"
+	};
+	const char *const *expectedMD5 = isDemo ? MD5_DEMO : MD5;
 
 	for (int i = 1; i <= 9; ++i) {
+		if (!expectedMD5[i - 1])
+			continue;
+
 		Common::Path filename(Common::String::format("RSOUND.00%d", i));
 		if (!f.open(filename))
 			error("Could not process - %s", filename.toString().c_str());
 		Common::String md5str = Common::computeStreamMD5AsString(f, 8192);
 		f.close();
 
-		if (md5str != MD5[i - 1])
+		if (md5str != expectedMD5[i - 1])
 			error("Invalid sound file - %s", filename.toString().c_str());
 	}
 }
@@ -220,13 +232,11 @@ int RSound::getRandomNumber() {
 void RSound::onTimer() {
 	Common::StackLock slock(_driverMutex);
 
-	// The frequency of the callbacks is dependent on the underlying driver
-	// implementation and might not be 60Hz. Adjust to make sure poll() is called
-	// with the correct frequency.
-	_updateDeltaRemainder += _driverCallbackDelta;
-	while (_updateDeltaRemainder >= UPDATE_DELTA) {
-		poll();
-		_updateDeltaRemainder -= UPDATE_DELTA;
+	uint32 serviceTicks = _hostTimer.advance(_driverCallbackDelta, 1000000);
+	while (serviceTicks--) {
+		// RSOUND export 4 is a return stub in every audited overlay.
+		if (_hostTimer.pollDue())
+			poll();
 	}
 }
 
@@ -236,29 +246,12 @@ void RSound::timerCallback(void* data) {
 }
 
 void RSound::setVolume(int volume) {
-	_masterVolume = volume;
-	if (!volume)
-		command0();
+	_masterVolume = CLIP(volume, 0, 255);
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		sendVolume(i + 1, _isDisabled ? 0 : _channels[i]._volume);
 }
 
 /*-----------------------------------------------------------------------*/
-// Low-level MIDI transmission. sendMidiByte() is the single point that
-// needs to change once the real MT-32/MIDI output interface is wired up;
-// everything else funnels through it.
-
-/*
-void RSound::sendMidiByte(byte value) {
-	warning("RSound: MIDI byte %02X", value);
-}
-
-void RSound::sendStatus(int midiChannel, byte statusNibble) {
-	byte status = statusNibble | midiChannel;
-	if (_lastMidiStatus != status) {
-		_lastMidiStatus = status;
-		sendMidiByte(status);
-	}
-}
-*/
 
 void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel, note, velocity);
@@ -269,7 +262,9 @@ void RSound::sendProgramChange(int midiChannel, int program) {
 }
 
 void RSound::sendVolume(int midiChannel, int volume) {
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel, MidiDriver::MIDI_CONTROLLER_VOLUME, volume);
+	const int scaledVolume = CLIP(volume, 0, 127) * _masterVolume / 255;
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_VOLUME, scaledVolume);
 }
 
 void RSound::sendPitchBend(int midiChannel, int value) {
@@ -289,12 +284,14 @@ void RSound::restoreChannelVolume(int midiChannel, int volume) {
 	sendVolume(midiChannel, volume);
 }
 
-byte *RSound::sendSysExData(byte *pData) {
-	// FIXME If the data is malformed, this will read out of bounds. Not sure
-	// how the original code handles this.
-	uint16 length = 0;
-	for (int i = 0; pData[i] != 0xFF; ++i) {
-		length++;
+byte *RSound::sendSysExData(byte *pData, uint maxLength) {
+	uint length = 0;
+	while (length < maxLength && pData[length] != 0xFF)
+		++length;
+
+	if (length == maxLength) {
+		warning("RSound::sendSysExData: unterminated SysEx message");
+		return nullptr;
 	}
 
 	// FIXME This call adds the necessary delay for the MT-32 to process the
@@ -309,24 +306,37 @@ byte *RSound::sendSysExData(byte *pData) {
 
 byte *RSound::sendSysEx(int offset) {
 	if (offset < 0) {
-		// _sysExOffset wasn't given a confirmed value for this driver yet
-		// (see the constructor) - deliberately not scanning for a 0xFF
-		// terminator from an unconfirmed/arbitrary offset, since that
-		// could read well past the actual command0_array table.
+		// Defensive guard for future mappings. Every validated retail and
+		// demo constructor currently supplies a nonnegative table offset.
 		warning("RSound::sendSysEx: command0_array offset not yet known for this driver");
 		return nullptr;
 	}
+	if ((uint)offset >= _soundData.size()) {
+		warning("RSound::sendSysEx: offset %d is outside the sound data", offset);
+		return nullptr;
+	}
 
-	return sendSysExData(loadData(offset));
+	return sendSysExData(loadData(offset), _soundData.size() - offset);
 }
 
 void RSound::sendSysExSequence() {
-	byte *pData = loadData(_sysExOffset);
+	byte *pData = sendSysEx(_sysExOffset);
+	if (!pData)
+		return;
+
+	byte *const dataEnd = _soundData.end();
 	for (;;) {
-		pData = sendSysExData(pData);
 		++pData;
+		if (pData == dataEnd) {
+			warning("RSound::sendSysExSequence: unterminated SysEx sequence");
+			return;
+		}
 		if (*pData == 0xFF)
 			break;
+
+		pData = sendSysExData(pData, dataEnd - pData);
+		if (!pData)
+			return;
 	}
 }
 
@@ -367,6 +377,19 @@ void RSound::Channel_checkFade(Channel *channel) {
 }
 
 void RSound::checkFadingChannels() {
+	if (_fadeCheckMode == kRSoundFadeCheckAlternating) {
+		_fadeCheckAlternate = !_fadeCheckAlternate;
+		if (_fadeCheckAlternate)
+			return;
+	} else {
+		if (!_fadeCheckPeriod)
+			return;
+		if (--_fadeCheckCounter > 0)
+			return;
+
+		_fadeCheckCounter = _fadeCheckPeriod;
+	}
+
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
 		Channel_checkFade(&_channels[i]);
 }
@@ -541,13 +564,14 @@ void RSound::Channel_pollActive(Channel *channel) {
 						channel->_innerLoopCount = 0;
 						channel->_outerLoopCount = 0;
 					} else {
-						channel->_outerLoopCount = *pSrc;
+						channel->_outerLoopCount =
+								(uint16)(int16)(int8)*pSrc;
 						channel->_pSrc = channel->_outerLoopPtr;
 						channel->_innerLoopPtr = channel->_outerLoopPtr;
 					}
 				} else if (--channel->_outerLoopCount) {
-					channel->_outerLoopPtr = channel->_pSrc;
-					channel->_innerLoopPtr = channel->_pSrc;
+					channel->_pSrc = channel->_outerLoopPtr;
+					channel->_innerLoopPtr = channel->_outerLoopPtr;
 				} else {
 					channel->_pSrc += 2;
 					channel->_outerLoopPtr = channel->_pSrc;
@@ -562,7 +586,8 @@ void RSound::Channel_pollActive(Channel *channel) {
 						channel->_innerLoopPtr = channel->_pSrc;
 						channel->_innerLoopCount = 0;
 					} else {
-						channel->_innerLoopCount = *pSrc;
+						channel->_innerLoopCount =
+								(uint16)(int16)(int8)*pSrc;
 						channel->_pSrc = channel->_innerLoopPtr;
 					}
 				} else if (--channel->_innerLoopCount) {
@@ -665,6 +690,14 @@ void RSound::resetHeldNotes() {
 			_heldNotes[i][j] = 0xFF;
 }
 
+void RSound::resetHeldNotesRange(int firstChannel, int lastChannel) {
+	assert(firstChannel >= 1 && lastChannel <= RSOUND_CHANNEL_COUNT &&
+			firstChannel <= lastChannel);
+	for (int channel = firstChannel; channel <= lastChannel; ++channel)
+		for (int slot = 0; slot < 4; ++slot)
+			_heldNotes[channel][slot] = 0xFF;
+}
+
 /**
  * Resets all 9 channels and the held-notes table.
  * Called both from the constructor (mirroring rsound_init) and from
@@ -694,6 +727,7 @@ int RSound::command0() {
 	_isDisabled = true;
 
 	resetAllChannels();
+	setFadeCheckPeriod(0);
 	sendMidiChannelReset(1, RSOUND_CHANNEL_COUNT);
 
 	// Matches the tail of the original rsound_command0.
@@ -708,6 +742,7 @@ int RSound::command0() {
 }
 
 int RSound::command1() {
+	setFadeCheckPeriod(1);
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
 		_channels[i].enable(0xFF);
 	return 0;
@@ -718,11 +753,13 @@ int RSound::command2() {
 	// table) plus the MIDI channel reset for those same channels.
 	resetChannelRange(0, 5);
 	resetHeldNotes();
+	setFadeCheckPeriod(0);
 	sendMidiChannelReset(1, 5);
 	return 0;
 }
 
 int RSound::command3() {
+	setFadeCheckPeriod(1);
 	for (int i = 0; i < 5; ++i)
 		_channels[i].enable(0xFF);
 	return 0;
@@ -732,11 +769,13 @@ int RSound::command4() {
 	// Channels 6-9 (does NOT touch the held-notes
 	// table) plus the MIDI channel reset for those same channels.
 	resetChannelRange(5, RSOUND_CHANNEL_COUNT);
+	setFadeCheckPeriod(0);
 	sendMidiChannelReset(6, RSOUND_CHANNEL_COUNT);
 	return 0;
 }
 
 int RSound::command5() {
+	setFadeCheckPeriod(1);
 	for (int i = 5; i < RSOUND_CHANNEL_COUNT; ++i)
 		_channels[i].enable(0xFF);
 	return 0;
diff --git a/engines/mads/nebular/sound/rsound.h b/engines/mads/nebular/sound/rsound.h
index afbf196f933..c62a1a4dfca 100644
--- a/engines/mads/nebular/sound/rsound.h
+++ b/engines/mads/nebular/sound/rsound.h
@@ -23,6 +23,7 @@
 #define MADS_NEBULAR_SOUND_RSOUND_H
 
 #include "mads/core/sound_manager.h"
+#include "mads/core/native_sound_timer.h"
 
 #include "audio/mt32gm.h"
 
@@ -34,6 +35,11 @@ class RSound;
 
 #define RSOUND_CHANNEL_COUNT 9
 
+enum RSoundFadeCheckMode {
+	kRSoundFadeCheckAlternating,
+	kRSoundFadeCheckProgrammable
+};
+
 /**
  * Represents the data for a channel on the Roland MT-32 / MPU-401 driver.
  * Ported from the Channel struct identified in rsound.009's disassembly;
@@ -91,8 +97,8 @@ public:
 	byte *_pSrc = nullptr;         // current read pointer into the sound-data stream
 	byte *_innerLoopPtr = nullptr; // inner-loop restart address
 	byte *_outerLoopPtr = nullptr; // outer-loop restart address
-	int _innerLoopCount = 0;
-	int _outerLoopCount = 0;
+	uint16 _innerLoopCount = 0; // signed byte stored as a 16-bit loop count
+	uint16 _outerLoopCount = 0; // signed byte stored as a 16-bit loop count
 	byte *_soundData = nullptr;    // identity pointer used by RSound::isSoundActive()
 
 public:
@@ -123,24 +129,19 @@ public:
  * Mirrors the structure of ASound (the Adlib equivalent in asound.h), but
  * for a driver family that sends real MIDI messages instead of poking
  * OPL registers.
- *
- * NOTE: The actual MIDI transmission (sendMidiByte()) currently just logs
- * via warning() - it isn't hooked up to a real ScummVM MIDI/MT-32 output
- * yet. Every other MIDI-sending helper funnels through sendMidiByte(), so
- * that's the one place that needs to change once the real interface is
- * identified.
  */
 class RSound : public SoundDriver {
 	friend class Channel;
 private:
-	// Number of microseconds between driver updates (60 Hz frequency)
-	static const uint32 UPDATE_DELTA;
-
 	uint16 _randomSeed;
 	int _masterVolume;
 	byte _lastMidiStatus;             // running-status cache, avoids resending an unchanged status byte
 	bool _noteTriggeredThisPoll;      // throttles note-on dispatch to at most one per update() tick, across all channels
 	byte _heldNotes[RSOUND_CHANNEL_COUNT + 1][4]; // per-MIDI-channel held-note slots (index 0 unused; channels are 1-9)
+	RSoundFadeCheckMode _fadeCheckMode;
+	bool _fadeCheckAlternate;
+	int _fadeCheckCounter;
+	int _fadeCheckPeriod;
 
 	/**
 	 * Data-segment offset of this driver's own "command0_array" (the
@@ -156,7 +157,7 @@ private:
 
 	MidiDriver_MT32GM *_midiDriver;
 	uint32 _driverCallbackDelta;
-	uint32 _updateDeltaRemainder;
+	NativeSoundTimer _hostTimer;
 
 	void update();
 	void pollAllChannels();
@@ -166,22 +167,16 @@ private:
 	 * Zeroes _activeCount and the three fade-step fields for channels in
 	 * [first, last).
 	 */
-	void resetChannelRange(int first, int last);
-
-	/**
-	 * Resets the per-MIDI-channel held-note tracking table to empty.
-	 */
-	void resetHeldNotes();
-
 	/**
 	 * Resets all 9 channels and the held-notes table.
 	 */
 	void resetAllChannels();
 
 	/**
-	 * Runs every other update() tick (matches the original's half-rate
-	 * toggle). Decays the volume of any pending-stop channel by 1 and
-	 * resends it, until the channel goes fully silent and is recycled.
+	 * Run pending-stop volume decay using the scheduler embedded in the
+	 * loaded overlay. Sections 1, 2, and 9 and both demo overlays use a
+	 * fixed every-other-poll toggle. Sections 3-8 use a programmable
+	 * countdown; zero disables it and the counter reloads after each pass.
 	 */
 	void checkFadingChannels();
 	void Channel_checkFade(Channel *channel);
@@ -195,6 +190,20 @@ private:
 protected:
 	int _commandParam;
 
+	void setFadeCheckPeriod(int period) {
+		if (_fadeCheckMode == kRSoundFadeCheckProgrammable)
+			_fadeCheckPeriod = period;
+	}
+
+	/** Clear the active and fade state for channel indices in [first, last). */
+	void resetChannelRange(int first, int last);
+
+	/** Reset the per-MIDI-channel held-note tracking table to empty. */
+	void resetHeldNotes();
+
+	/** Reset held-note slots for the inclusive MIDI-channel range. */
+	void resetHeldNotesRange(int firstChannel, int lastChannel);
+
 	byte *loadData(int offset) {
 		return &_soundData[offset];
 	}
@@ -241,8 +250,7 @@ protected:
 	int getRandomNumber();
 
 	// ---- Low-level MIDI send helpers -------------------------------
-	// All funnel through sendMidiByte(), the single hook point for
-	// wiring up real MT-32/MIDI output.
+	// All send through the ScummVM MT-32/MIDI driver.
 	void sendNoteOn(int midiChannel, int note, int velocity);
 	void sendProgramChange(int midiChannel, int program);
 	void sendVolume(int midiChannel, int volume);
@@ -260,15 +268,12 @@ protected:
 
 	/**
 	 * Sends a single SysEx message: bytes from pData up to (but not
-	 * including) a 0xFF terminator, via the real MT32GM MIDI driver
-	 * (_midiDriver->sysExMT32()) - this part is a work in progress and
-	 * intentionally NOT shared with the Dragonsphere/Phantom RSound
-	 * families, which still route through the sendMidiByte() warning()
-	 * stub. Returns a pointer to the terminating 0xFF byte, so callers
-	 * walking a sequence of consecutive messages can advance past it to
-	 * find the next one.
+	 * including) a 0xFF terminator, via the MT32GM MIDI driver. Returns a
+	 * pointer to the terminating byte, so callers walking a sequence of
+	 * consecutive messages can advance past it. Returns nullptr when no
+	 * terminator occurs within maxLength bytes.
 	 */
-	byte *sendSysExData(byte *pData);
+	byte *sendSysExData(byte *pData, uint maxLength);
 
 	/** sendSysExData() for a block already in this driver's own loaded sound data. */
 	byte *sendSysEx(int offset);
@@ -306,7 +311,7 @@ public:
 	int _resultFlag;
 
 public:
-	static void validate();
+	static void validate(bool isDemo);
 
 public:
 	/**
@@ -316,9 +321,11 @@ public:
 	 * @param dataOffset	Offset in the file of the data segment
 	 * @param dataSize		Size of the data segment
 	 * @param sysExOffset	Offset of this driver's own command0_array
+	 * @param fadeCheckMode Native pending-stop fade scheduler
 	 */
 	RSound(Audio::Mixer *mixer, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset);
+		int dataOffset, int dataSize, int sysExOffset,
+		RSoundFadeCheckMode fadeCheckMode);
 
 	~RSound() override;
 
diff --git a/engines/mads/nebular/sound/rsound_nebular.cpp b/engines/mads/nebular/sound/rsound_nebular.cpp
index e51b2f0da87..dd93a28af30 100644
--- a/engines/mads/nebular/sound/rsound_nebular.cpp
+++ b/engines/mads/nebular/sound/rsound_nebular.cpp
@@ -25,6 +25,79 @@ namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
+RSoundDemo::RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize, int sysExOffset,
+		int firstEffectChannel) :
+		RSound(mixer, filename, dataOffset, dataSize, sysExOffset,
+				kRSoundFadeCheckAlternating),
+		_firstEffectChannel(firstEffectChannel) {
+}
+
+void RSoundDemo::startVoice(int channelIndex, int sequenceOffset) {
+	assert(channelIndex >= 0 && channelIndex < RSOUND_CHANNEL_COUNT);
+	_channels[channelIndex].load(loadData(sequenceOffset));
+}
+
+int RSoundDemo::startVoiceInRange(int sequenceOffset, int firstChannel,
+		int lastChannel) {
+	assert(firstChannel >= 0 && firstChannel <= lastChannel && lastChannel < 8);
+
+	for (int channel = firstChannel; channel <= lastChannel; ++channel) {
+		if (!_channels[channel]._activeCount) {
+			startVoice(channel, sequenceOffset);
+			return channel;
+		}
+	}
+
+	for (int channel = lastChannel; channel >= firstChannel; --channel) {
+		if (_channels[channel]._pendingStop == 0xFF) {
+			startVoice(channel, sequenceOffset);
+			return channel;
+		}
+	}
+
+	return -1;
+}
+
+int RSoundDemo::startAnyVoice(int sequenceOffset) {
+	// Both demo overlays exclude rhythm channel 9 from their melodic pools.
+	return startVoiceInRange(sequenceOffset, 0, 7);
+}
+
+int RSoundDemo::startEffectVoice(int sequenceOffset) {
+	return startVoiceInRange(sequenceOffset, _firstEffectChannel, 7);
+}
+
+void RSoundDemo::requestStopRange(int firstChannel, int channelCount) {
+	assert(firstChannel >= 0 && channelCount >= 0 &&
+			firstChannel + channelCount <= RSOUND_CHANNEL_COUNT);
+	for (int channel = firstChannel;
+			channel < firstChannel + channelCount; ++channel)
+		_channels[channel].enable(0xFF);
+}
+
+void RSoundDemo::requestStopAll() {
+	requestStopRange(0, RSOUND_CHANNEL_COUNT);
+}
+
+void RSoundDemo::stopAndResetRange(int firstChannel, int channelCount) {
+	assert(firstChannel >= 0 && channelCount > 0 &&
+			firstChannel + channelCount <= RSOUND_CHANNEL_COUNT);
+	resetChannelRange(firstChannel, firstChannel + channelCount);
+	resetHeldNotesRange(firstChannel + 1, firstChannel + channelCount);
+	sendMidiChannelReset(firstChannel + 1, firstChannel + channelCount);
+}
+
+void RSoundDemo::setVoiceVolume(int channelIndex, byte volume) {
+	assert(channelIndex >= 0 && channelIndex < RSOUND_CHANNEL_COUNT);
+	_channels[channelIndex]._volume = volume;
+	sendVolume(channelIndex + 1, volume);
+}
+
+bool RSoundDemo::isSequenceActive(int sequenceOffset) {
+	return isSoundActive(loadData(sequenceOffset));
+}
+
 const RSound1::CommandPtr RSound1::_commandList[42] = {
 	&RSound1::command0, &RSound1::command1, &RSound1::command2, &RSound1::command3,
 	&RSound1::command4, &RSound1::command5, &RSound1::command6, &RSound1::command7,
@@ -39,7 +112,8 @@ const RSound1::CommandPtr RSound1::_commandList[42] = {
 	&RSound1::command40, &RSound1::command41
 };
 
-RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.001", 0x1350, 0x1A90, 0x67) {
+RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.001",
+		0x1350, 0x1A90, 0x67, kRSoundFadeCheckAlternating) {
 }
 
 int RSound1::command(int commandId, int param) {
@@ -302,6 +376,220 @@ int RSound1::command41() {
 
 /*-----------------------------------------------------------------------*/
 
+RSoundDemo1::RSoundDemo1(Audio::Mixer *mixer) :
+		RSoundDemo(mixer, "rsound.001", 0x12E0, 0x1D28, 0x67, 0),
+		_command23Toggle(false) {
+}
+
+byte RSoundDemo1::adjustedCommandParam() const {
+	const byte value = (byte)_commandParam;
+	return value > 0x40 ? value - 0x40 : 0;
+}
+
+void RSoundDemo1::startCommand111213() {
+	if (isSequenceActive(0x1586))
+		return;
+
+	requestStopAll();
+	startVoice(0, 0x1586);
+	startVoice(1, 0x17DC);
+	startVoice(2, 0x197C);
+	startVoice(3, 0x19F8);
+}
+
+int RSoundDemo1::executeDemoCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+		return RSound::command0();
+	case 1:
+		requestStopAll();
+		return 0;
+	case 2:
+		stopAndResetRange(0, 4);
+		return 0;
+	case 3:
+		requestStopRange(0, 4);
+		return 0;
+	case 4:
+		stopAndResetRange(4, 5);
+		return 0;
+	case 5:
+		requestStopRange(4, 5);
+		return 0;
+	case 6:
+		return RSound::command6();
+	case 7:
+		return RSound::command7();
+	case 8:
+		return RSound::command8();
+	default:
+		return 0;
+	}
+}
+
+int RSoundDemo1::command(int commandId, int param) {
+	if (commandId < 0 || commandId > 40)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	if (commandId <= 8)
+		return executeDemoCommonCommand(commandId);
+
+	switch (commandId) {
+	case 9:
+		startAnyVoice(0x0F34);
+		break;
+	case 10:
+		if (!isSequenceActive(0x1104)) {
+			requestStopAll();
+			startVoice(4, 0x1104);
+			startVoice(5, 0x1138);
+			startVoice(6, 0x12BC);
+			startVoice(7, 0x1308);
+		}
+		break;
+	case 11:
+		startCommand111213();
+		setVoiceVolume(0, 0x00);
+		setVoiceVolume(1, 0x00);
+		break;
+	case 12:
+		startCommand111213();
+		setVoiceVolume(0, 0x50);
+		setVoiceVolume(1, 0x00);
+		break;
+	case 13:
+		startCommand111213();
+		setVoiceVolume(0, 0x50);
+		setVoiceVolume(1, 0x50);
+		break;
+	case 14:
+		startAnyVoice(0x1AE2);
+		break;
+	case 15:
+		if (!isSequenceActive(0x135A)) {
+			requestStopAll();
+			startVoice(4, 0x135A);
+			startVoice(5, 0x144A);
+			startVoice(6, 0x152E);
+		}
+		break;
+	case 16:
+		startAnyVoice(0x0F3E);
+		break;
+	case 17:
+		startAnyVoice(0x0F48);
+		break;
+	case 18:
+		startAnyVoice(0x0F52);
+		break;
+	case 19:
+		requestStopAll();
+		startAnyVoice(0x0F64);
+		break;
+	case 20:
+		startAnyVoice(0x0FBE);
+		break;
+	case 21:
+		startAnyVoice(0x0FAC);
+		break;
+	case 22: {
+		byte *data = sequenceData(0x0FCE);
+		data[6] = (getRandomNumber() & 0x07) + 0x73;
+		startAnyVoice(0x0FCE);
+		break;
+	}
+	case 23:
+		_command23Toggle = !_command23Toggle;
+		startAnyVoice(_command23Toggle ? 0x0FD8 : 0x0FE0);
+		break;
+	case 24:
+		startAnyVoice(0x0FE8);
+		break;
+	case 25:
+		startAnyVoice(0x0FF2);
+		break;
+	case 26:
+	case 27: {
+		const int sequenceOffset = commandId == 26 ? 0x10F8 : 0x10EC;
+		byte *data = sequenceData(sequenceOffset);
+		data[8] = (getRandomNumber() & 0x18) + 0x2D;
+		data[5] = adjustedCommandParam() + 0x40;
+		startVoice(7, sequenceOffset);
+		break;
+	}
+	case 28:
+		startAnyVoice(0x1002);
+		break;
+	case 29: {
+		byte *data = sequenceData(0x109A);
+		data[11] = (adjustedCommandParam() >> 1) + 0x20;
+		if (!isSequenceActive(0x109A))
+			startAnyVoice(0x109A);
+		break;
+	}
+	case 30: {
+		byte *data = sequenceData(0x10AE);
+		data[11] = adjustedCommandParam() + 0x3F;
+		if (!isSequenceActive(0x10AE))
+			startAnyVoice(0x10AE);
+		break;
+	}
+	case 31:
+		startAnyVoice(0x1022);
+		break;
+	case 32: {
+		const byte value = adjustedCommandParam() >> 1;
+		byte *data = sequenceData(0x10C4);
+		data[11] = data[23] = value + 0x44;
+		data[17] = data[29] = value + 0x14;
+		if (!isSequenceActive(0x10C4))
+			startAnyVoice(0x10C4);
+		break;
+	}
+	case 33:
+		startAnyVoice(0x1034);
+		startAnyVoice(0x103E);
+		break;
+	case 34: {
+		byte *data = sequenceData(0x104C);
+		data[9] = (getRandomNumber() & 0x0C) + 0x2D;
+		data[16] = data[9] + 0x24;
+		startAnyVoice(0x104C);
+		break;
+	}
+	case 35:
+		startAnyVoice(0x1060);
+		break;
+	case 36:
+		startAnyVoice(0x1078);
+		break;
+	case 37:
+		startAnyVoice(0x1086);
+		break;
+	case 38:
+		startAnyVoice(0x1090);
+		break;
+	case 39:
+		if (!isSequenceActive(0x1C38)) {
+			startVoice(4, 0x1C38);
+			startVoice(5, 0x1C68);
+			startVoice(6, 0x1C94);
+			startVoice(7, 0x1CD4);
+			startVoice(8, 0x1CEE);
+		}
+		break;
+	case 40:
+		startAnyVoice(0x106E);
+		break;
+	}
+
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
 const uint16 RSound2::_table1[16] = {
 	0x3234, 0x3250, 0x326A, 0x3284, 0x329E, 0x32D6, 0x3304, 0x333C,
 	0x3352, 0x3378, 0x33B6, 0x33D0, 0x33EA, 0x3404, 0x341E, 0x343E
@@ -321,7 +609,8 @@ const RSound2::CommandPtr RSound2::_commandList[44] = {
 	&RSound2::command40, &RSound2::command41, &RSound2::command42, &RSound2::command43
 };
 
-RSound2::RSound2(Audio::Mixer *mixer) : RSound(mixer, "rsound.002", 0x1390, 0x42F0, 0x87) {
+RSound2::RSound2(Audio::Mixer *mixer) : RSound(mixer, "rsound.002",
+		0x1390, 0x42F0, 0x87, kRSoundFadeCheckAlternating) {
 }
 
 int RSound2::command(int commandId, int param) {
@@ -630,7 +919,8 @@ const RSound3::CommandPtr RSound3::_commandList[61] = {
 	&RSound3::command60
 };
 
-RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.003", 0x14E0, 0x4C60, 0x67) {
+RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.003",
+		0x14E0, 0x4C60, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound3::command(int commandId, int param) {
@@ -642,23 +932,14 @@ int RSound3::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
-int RSound3::notImplemented() {
-	warning("RSound3::command: not yet implemented (missing disassembly)");
-	return 0;
-}
-
 Channel *RSound3::method1(int offset, byte value) {
 	byte *pData = loadData(offset);
 	pData[5] = value;
 	return playSound(offset);
 }
 
-void RSound3::sub1074E() {
-	_byte10742 = 1;
-}
-
 void RSound3::resetUpperChannelsTail() {
-	sub1074E();
+	setFadeCheckPeriod(1);
 	_channels[4].enable(0xFF);
 	_channels[5].enable(0xFF);
 	_channels[6].enable(0xFF);
@@ -671,15 +952,20 @@ int RSound3::command1() {
 	return 0;
 }
 
+int RSound3::command3() {
+	return RSound::command3();
+}
+
 int RSound3::command5() {
-	if (!isSoundActive(loadData(0x1AE6)))
-		resetUpperChannelsTail();
+	// The native driver performs an unused active-sequence probe before
+	// unconditionally entering the shared upper-channel reset tail.
+	resetUpperChannelsTail();
 	return 0;
 }
 
 int RSound3::command9() {
 	command1();
-	_byte10742 = (byte)_commandParam;
+	setFadeCheckPeriod((byte)_commandParam);
 	return 0;
 }
 
@@ -757,7 +1043,7 @@ int RSound3::command15() {
 		_channels[5]._pendingStop = 0xFF;
 		_channels[6]._pendingStop = 0xFF;
 		_channels[7]._pendingStop = 0xFF;
-		sub1074E();
+		setFadeCheckPeriod(1);
 		return 0;
 	}
 
@@ -856,8 +1142,10 @@ int RSound3::command24() {
 }
 
 int RSound3::command25() {
-	method1(0x11A6, 42);
-	method1(0x11C4, 42);
+	// The dispatcher's preceding xor leaves ZF set, so the native jz
+	// selects 0x25 for both calls.
+	method1(0x11A6, 0x25);
+	method1(0x11C4, 0x25);
 	return 0;
 }
 
@@ -1035,7 +1323,8 @@ const RSound4::CommandPtr RSound4::_commandList[60] = {
 	&RSound4::command56, &RSound4::command57, &RSound4::command58, &RSound4::command59
 };
 
-RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.004", 0x1340, 0x2E20, 0x67) {
+RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.004",
+		0x1340, 0x2E20, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound4::command(int commandId, int param) {
@@ -1060,7 +1349,7 @@ void RSound4::tickCallback() {
 
 int RSound4::command9() {
 	command1();
-	_byte10745 = (byte)_commandParam;
+	setFadeCheckPeriod((byte)_commandParam);
 	return 0;
 }
 
@@ -1245,7 +1534,8 @@ const RSound5::CommandPtr RSound5::_commandList[42] = {
 	&RSound5::command40, &RSound5::command41
 };
 
-RSound5::RSound5(Audio::Mixer *mixer) : RSound(mixer, "rsound.005", 0x12A0, 0x1FD0, 0x67) {
+RSound5::RSound5(Audio::Mixer *mixer) : RSound(mixer, "rsound.005",
+		0x12A0, 0x1FD0, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound5::command(int commandId, int param) {
@@ -1457,7 +1747,8 @@ const RSound6::CommandPtr RSound6::_commandList[30] = {
 	&RSound6::nullCommand, &RSound6::command28
 };
 
-RSound6::RSound6(Audio::Mixer *mixer) : RSound(mixer, "rsound.006", 0x12D0, 0x1EF0, 0x67) {
+RSound6::RSound6(Audio::Mixer *mixer) : RSound(mixer, "rsound.006",
+		0x12D0, 0x1EF0, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound6::command(int commandId, int param) {
@@ -1630,7 +1921,8 @@ const RSound7::CommandPtr RSound7::_commandList[38] = {
 	&RSound7::command36, &RSound7::command37
 };
 
-RSound7::RSound7(Audio::Mixer *mixer) : RSound(mixer, "rsound.007", 0x1240, 0x1EF0, 0x67) {
+RSound7::RSound7(Audio::Mixer *mixer) : RSound(mixer, "rsound.007",
+		0x1240, 0x1EF0, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound7::command(int commandId, int param) {
@@ -1781,7 +2073,8 @@ const RSound8::CommandPtr RSound8::_commandList[38] = {
 	&RSound8::command36, &RSound8::command37
 };
 
-RSound8::RSound8(Audio::Mixer *mixer) : RSound(mixer, "rsound.008", 0x1290, 0x19A0, 0x67) {
+RSound8::RSound8(Audio::Mixer *mixer) : RSound(mixer, "rsound.008",
+		0x1290, 0x19A0, 0x67, kRSoundFadeCheckProgrammable) {
 }
 
 int RSound8::command(int commandId, int param) {
@@ -1990,7 +2283,8 @@ const RSound9::CommandPtr RSound9::_commandList[52] = {
 	&RSound9::command48, &RSound9::command49, &RSound9::command50, &RSound9::command51
 };
 
-RSound9::RSound9(Audio::Mixer *mixer) : RSound(mixer, "rsound.009", 0x1520, 0x8920, 0x6F) {
+RSound9::RSound9(Audio::Mixer *mixer) : RSound(mixer, "rsound.009",
+		0x1520, 0x8920, 0x6F, kRSoundFadeCheckAlternating) {
 	_callbackCounter = 0;
 	_callbackPeriod = 0;
 	_callbackFnPtr = nullptr;
@@ -2407,6 +2701,185 @@ int RSound9::command51() {
 	return 0;
 }
 
+/*-----------------------------------------------------------------------*/
+
+RSoundDemo9::RSoundDemo9(Audio::Mixer *mixer) :
+		RSoundDemo(mixer, "rsound.009", 0x11D0, 0x3664, 0x69, 5) {
+}
+
+int RSoundDemo9::executeDemoCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+		return RSound::command0();
+	case 1:
+		requestStopRange(0, 9);
+		return 0;
+	case 2:
+		stopAndResetRange(0, 5);
+		// The opening overlay repeats its first embedded DT1 record here.
+		sendSysEx(0x69);
+		return 0;
+	case 3:
+		requestStopRange(0, 5);
+		return 0;
+	case 4:
+		stopAndResetRange(5, 4);
+		return 0;
+	case 5:
+		requestStopRange(5, 4);
+		return 0;
+	case 6:
+		return RSound::command6();
+	case 7:
+		return RSound::command7();
+	case 8:
+		return RSound::command8();
+	default:
+		return 0;
+	}
+}
+
+int RSoundDemo9::command(int commandId, int param) {
+	if (commandId < 0 || commandId > 39)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	if (commandId <= 8)
+		return executeDemoCommonCommand(commandId);
+
+	switch (commandId) {
+	case 9:
+	case 10:
+		break;
+	case 11:
+		startVoice(7, 0x1454);
+		break;
+	case 12:
+		startVoice(7, 0x14A0);
+		break;
+	case 13:
+		startVoice(7, 0x14AC);
+		break;
+	case 14:
+		startVoice(7, 0x14B4);
+		break;
+	case 15:
+		startVoice(7, 0x14D4);
+		break;
+	case 16:
+		startVoice(7, 0x14EC);
+		break;
+	case 17:
+		startVoice(7, 0x14E2);
+		break;
+	case 18:
+		startEffectVoice(0x12BA);
+		break;
+	case 19:
+		startEffectVoice(0x12D4);
+		break;
+	case 20: {
+		byte *data = sequenceData(0x12F6);
+		data[6] = (byte)(((getRandomNumber() & 0x38) + 0x4D) & 0x7F);
+		startEffectVoice(0x12F6);
+		break;
+	}
+	case 21:
+	case 22: {
+		byte *data = sequenceData(0x130A);
+		data[9] = commandId == 21 ? 0x46 : 0x2D;
+		if (!isSequenceActive(0x130A))
+			startEffectVoice(0x130A);
+		break;
+	}
+	case 23: {
+		static const int sequences[] = { 0x1322, 0x1328, 0x133A };
+		for (uint index = 0; index < 3; ++index) {
+			const int channel = startEffectVoice(sequences[index]);
+			if (channel >= 0)
+				voice(channel)._innerLoopPtr = loadData(0x1340);
+		}
+		break;
+	}
+	case 24:
+		startEffectVoice(0x1352);
+		break;
+	case 25:
+		startEffectVoice(0x1368);
+		break;
+	case 26:
+		startEffectVoice(0x138C);
+		break;
+	case 27:
+		startEffectVoice(0x13A4);
+		break;
+	case 28: {
+		byte *data = sequenceData(0x13BC);
+		data[6] = (byte)(((getRandomNumber() & 0x1C) + 0x0F) & 0x7F);
+		startEffectVoice(0x13BC);
+		break;
+	}
+	case 29: {
+		byte *data = sequenceData(0x13D0);
+		data[6] = (byte)(((getRandomNumber() & 0x0C) + 0x21) & 0x7F);
+		startEffectVoice(0x13D0);
+		break;
+	}
+	case 30:
+		startEffectVoice(0x13F8);
+		break;
+	case 31:
+		startEffectVoice(0x1408);
+		startEffectVoice(0x1416);
+		startEffectVoice(0x1424);
+		break;
+	case 32:
+		startEffectVoice(0x1432);
+		break;
+	case 33:
+		startEffectVoice(0x143C);
+		break;
+	case 34:
+	case 39:
+		startVoice(0, 0x1522);
+		startVoice(1, 0x1700);
+		startVoice(2, 0x1892);
+		startVoice(3, 0x21F2);
+		startVoice(4, 0x2E4A);
+		break;
+	case 35:
+		startEffectVoice(0x14BE);
+		break;
+	case 36: {
+		startEffectVoice(0x13BC);
+
+		int channel = startEffectVoice(0x1334);
+		if (channel >= 0)
+			voice(channel)._innerLoopPtr = loadData(0x13EA);
+
+		channel = startEffectVoice(0x132E);
+		if (channel >= 0)
+			voice(channel)._innerLoopPtr = loadData(0x13DA);
+		break;
+	}
+	case 37: {
+		byte *data = sequenceData(0x150E);
+		data[6] = (byte)(((getRandomNumber() & 0x02) + 0x48) & 0x7F);
+		startEffectVoice(0x150E);
+		break;
+	}
+	case 38:
+		startVoice(0, 0x35C4);
+		startVoice(1, 0x35CE);
+		startVoice(2, 0x3612);
+		startVoice(3, 0x3656);
+		break;
+	}
+
+	return 0;
+}
+
 } // namespace Sound
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/sound/rsound_nebular.h b/engines/mads/nebular/sound/rsound_nebular.h
index 9a677e1a063..a7e71d0abf6 100644
--- a/engines/mads/nebular/sound/rsound_nebular.h
+++ b/engines/mads/nebular/sound/rsound_nebular.h
@@ -28,6 +28,30 @@ namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
+/** Shared mechanics of the two distinct Rex demo Roland overlays. */
+class RSoundDemo : public RSound {
+private:
+	int _firstEffectChannel;
+
+protected:
+	RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+			int dataOffset, int dataSize, int sysExOffset,
+			int firstEffectChannel);
+
+	void startVoice(int channelIndex, int sequenceOffset);
+	int startVoiceInRange(int sequenceOffset, int firstChannel,
+			int lastChannel);
+	int startAnyVoice(int sequenceOffset);
+	int startEffectVoice(int sequenceOffset);
+	void requestStopRange(int firstChannel, int channelCount);
+	void requestStopAll();
+	void stopAndResetRange(int firstChannel, int channelCount);
+	void setVoiceVolume(int channelIndex, byte volume);
+	bool isSequenceActive(int sequenceOffset);
+	byte *sequenceData(int sequenceOffset) { return loadData(sequenceOffset); }
+	Channel &voice(int channelIndex) { return _channels[channelIndex]; }
+};
+
 class RSound1 : public RSound {
 private:
 	typedef int (RSound1:: *CommandPtr)();
@@ -85,6 +109,20 @@ public:
 	int command(int commandId, int param) override;
 };
 
+/** Demo RSOUND.001: `RLND AGAdemo 6-11-92`; 41 commands. */
+class RSoundDemo1 : public RSoundDemo {
+private:
+	bool _command23Toggle;
+
+	byte adjustedCommandParam() const;
+	void startCommand111213();
+	int executeDemoCommonCommand(int commandId);
+
+public:
+	explicit RSoundDemo1(Audio::Mixer *mixer);
+	int command(int commandId, int param) override;
+};
+
 class RSound2 : public RSound {
 private:
 	typedef int (RSound2:: *CommandPtr)();
@@ -175,19 +213,8 @@ private:
 	byte _command3940Toggle = 0;
 
 	/**
-	 * Written unconditionally to 1 by
-	 * sub1074E() (called from the shared command1/command5 tail and from
-	 * command3), and separately written to the raw command parameter by
-	 * command9. No consumer of this byte showed up in the batches given
-	 * so far, so its real purpose is still unclear - kept as a plain
-	 * mirror of the original rather than guessing a meaning for it.
-	 */
-	byte _byte10742 = 0;
-
-	/**
-	 * Shared helper: pData[5] = value, then plays pData. Called once
-	 * from command25, with a truncated second call not yet
-	 * confirmed.
+	 * Shared helper: pData[5] = value, then plays pData. Command 25
+	 * calls it for both native sequence offsets.
 	 */
 	Channel *method1(int offset, byte value);
 
@@ -213,24 +240,6 @@ private:
 	 */
 	void sendDualVolume(byte volume);
 
-	/**
-	 * Just sets _byte10742 = 1. Reached
-	 * both as a genuine call (from command3, not yet given) and via the
-	 * shared command1/command5 tail below.
-	 */
-	void sub1074E();
-
-	/**
-	 * Placeholder for command slots confirmed by the dispatch table
-	 * to be real, driver-specific functions, but whose
-	 * disassembly wasn't included in this batch. Warns at runtime if
-	 * actually invoked, so a real call shows up during testing instead
-	 * of silently vanishing. Distinct from nullCommand(), which is for
-	 * slots the table confirms are genuinely no-op stubs in the original
-	 * (12, 52-56, 58).
-	 */
-	int notImplemented();
-
 	/**
 	 * Shared tail used by both command1
 	 * (falls through into it after calling command3()) and command5
@@ -241,6 +250,7 @@ private:
 	void resetUpperChannelsTail();
 
 	int command1();
+	int command3();
 	int command5();
 	int command9();
 	int command10();
@@ -306,14 +316,6 @@ private:
 	int _callbackCounter = 0;
 	int _callbackPeriod = 0;
 
-	/**
-	 * Set from the raw command parameter
-	 * by command9. No consumer showed up in this batch, so its real
-	 * purpose is unconfirmed (mirrors RSound3's equally-unconfirmed
-	 * _byte10742, set the same way by RSound3::command9).
-	 */
-	byte _byte10745 = 0;
-
 	typedef int (RSound4:: *CommandPtr)();
 	static const CommandPtr _commandList[60];
 
@@ -627,6 +629,16 @@ public:
 	int command(int commandId, int param) override;
 };
 
+/** Demo RSOUND.009: `RLND AGAdemo 6-25-92`; 40 commands. */
+class RSoundDemo9 : public RSoundDemo {
+private:
+	int executeDemoCommonCommand(int commandId);
+
+public:
+	explicit RSoundDemo9(Audio::Mixer *mixer);
+	int command(int commandId, int param) override;
+};
+
 } // namespace Sound
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/sound/sound.cpp b/engines/mads/nebular/sound/sound.cpp
index cbe5323017b..bcae5b3cb27 100644
--- a/engines/mads/nebular/sound/sound.cpp
+++ b/engines/mads/nebular/sound/sound.cpp
@@ -29,12 +29,12 @@ namespace RexNebular {
 namespace Sound {
 
 void RexSoundManager::validate() {
-	if (_isDemo)
+	if (_isDemo && _driverType != SOUND_MT32)
 		_driverType = SOUND_ADLIB;
 
 	switch (_driverType) {
 	case SOUND_MT32:
-		RSound::validate();
+		RSound::validate(_isDemo);
 		break;
 
 	case SOUND_PCSPEAKER:
@@ -50,7 +50,7 @@ void RexSoundManager::validate() {
 void RexSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_isDemo) {
+	if (_isDemo && _driverType == SOUND_ADLIB) {
 		assert(sectionNumber == 1 || sectionNumber == 9);
 		if (sectionNumber == 1)
 			_driver = new ASoundDemo1(_mixer);
@@ -62,6 +62,16 @@ void RexSoundManager::loadDriver(int sectionNumber) {
 	switch (_driverType) {
 	case SOUND_MT32:
 		// Roland MT32 drivers
+		if (_isDemo) {
+			// The demo shares RSOUND.001 across numbered gameplay sections
+			// and uses RSOUND.009 only for its opening presentation.
+			if (sectionNumber == 9)
+				_driver = new RSoundDemo9(_mixer);
+			else
+				_driver = new RSoundDemo1(_mixer);
+			break;
+		}
+
 		switch (sectionNumber) {
 		case 1:
 			_driver = new RSound1(_mixer);


Commit: 11d6f71550bd69392c47abdac0e16eeccf9b4df2
    https://github.com/scummvm/scummvm/commit/11d6f71550bd69392c47abdac0e16eeccf9b4df2
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: PHANTOM: Restore native RSOUND playback

Connect the retail and demo RSOUND overlays to MT-32 output using
the recovered DOS host cadence. Preserve command dispatch, sequence
behavior, SysEx bounds, and callbacks verified against the overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/detection_tables.h
    engines/mads/phantom/sound/rsound.cpp
    engines/mads/phantom/sound/rsound.h
    engines/mads/phantom/sound/rsound_phantom.cpp
    engines/mads/phantom/sound/rsound_phantom.h
    engines/mads/phantom/sound/sound.cpp


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index c3f5fd9112b..243a283baef 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -193,7 +193,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
@@ -208,7 +208,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
@@ -223,7 +223,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD | GF_INSTALLER,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
@@ -238,7 +238,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
diff --git a/engines/mads/phantom/sound/rsound.cpp b/engines/mads/phantom/sound/rsound.cpp
index a303d6d1238..0333b784e79 100644
--- a/engines/mads/phantom/sound/rsound.cpp
+++ b/engines/mads/phantom/sound/rsound.cpp
@@ -89,8 +89,6 @@ RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
 	_frameCounter = 0;
 	_isDisabled = false;
 	_randomSeed = 1234;
-	_lastMidiStatus = 0;
-	_sysexChecksum = 0;
 	_stateChangedFlag = 0;
 	_pollResult = 0;
 	_sysExOffset = sysExOffset;
@@ -120,32 +118,53 @@ RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
 	for (int i = 0; i < ARRAYSIZE(_scriptVariables); ++i)
 		_scriptVariables[i] = 0;
 
+	_midiDriver = new MidiDriver_MT32GM(MusicType::MT_MT32);
+	const int returnCode = _midiDriver->open();
+	if (returnCode != 0)
+		error("RSound - Failed to open MIDI music driver - error code %d.", returnCode);
+
+	_driverCallbackDelta = _midiDriver->getBaseTempo();
+
 	// Matches initDeviceOnce: command0() then sendSysExSequence(). The
 	// disassembly's _deviceInitialized guard flag is omitted - this
 	// constructor only ever runs once per driver instance, so there's
 	// nothing to guard against.
 	command0();
 	sendSysExSequence();
+
+	_midiDriver->setTimerCallback(this, &timerCallback);
+}
+
+RSound::~RSound() {
+	_isDisabled = true;
+	if (_midiDriver) {
+		_midiDriver->setTimerCallback(nullptr, nullptr);
+		_midiDriver->close();
+
+		Common::StackLock lock(_driverMutex);
+		delete _midiDriver;
+		_midiDriver = nullptr;
+	}
 }
 
 void RSound::validate() {
 	Common::File f;
 	static const char *const MD5[] = {
-		"8edcb79a8c3514eac0835496326a72ae",
-		"4b81a46440f8404d9eda1ce5ae2c5579",
-		"11d8d441e47ad1ccd8faafd6572a17d0",
-		"4cd5c4d45126e60ca701690489ab8afa",
-		"588357d711bbcdabdf7d7e5d96013ce5",
+		"d6a64de63e58d9aceadc4a0ad6bedff7",
+		"8a89af7bc6086a99c84455305e8659b3",
+		"8f68ee5787d21764d6c565f79eca2505",
+		"95de8817e92edc61dbb40a16adc0baa2",
+		"9cf8f6e744eaa43a1e1d4855da77e7c1",
 		nullptr,
 		nullptr,
 		nullptr,
-		"3d4843074c1dcbfd7919179c58aec9bc"
+		"97a4e9b701d8de4322b9ef3687497b17"
 	};
 
 	for (int i = 1; i <= 9; ++i) {
 		if (i >= 6 && i <= 8)
 			continue;
-		Common::Path filename(Common::String::format("asound.ph%d", i));
+		Common::Path filename(Common::String::format("rsound.ph%d", i));
 		if (!f.open(filename))
 			error("Could not process - %s", filename.toString().c_str());
 		Common::String md5str = Common::computeStreamMD5AsString(f, 8192);
@@ -170,8 +189,25 @@ int RSound::poll() {
 	return result;
 }
 
+void RSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+
+	uint32 serviceTicks = _hostTimer.advance(_driverCallbackDelta, 1000000);
+	while (serviceTicks--) {
+		// Export 4 is a return stub in every audited Phantom RSOUND overlay.
+		if (_hostTimer.pollDue())
+			poll();
+	}
+}
+
+void RSound::timerCallback(void *data) {
+	static_cast<RSound *>(data)->onTimer();
+}
+
 void RSound::setVolume(int volume) {
-	// TODO: no confirmed handler for this in the disassembly seen so far.
+	_masterVolume = CLIP(volume, 0, 255);
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		sendVolumeCC(i + 1, _isDisabled ? 0 : _channels[i]._volume);
 }
 
 void RSound::resultCheck() {
@@ -247,54 +283,34 @@ bool RSound::isSoundActive(byte *pData) {
 }
 
 /*-----------------------------------------------------------------------*/
-// Low-level MIDI transmission. sendMidiByte() is the single point that
-// needs to change once the real MT-32/MIDI output interface is wired up;
-// everything else funnels through it.
-
-void RSound::sendMidiByte(byte value) {
-	warning("RSound: MIDI byte %02X", value);
-}
-
-void RSound::sendStatus(int midiChannel, byte statusNibble) {
-	byte status = statusNibble | midiChannel;
-	if (_lastMidiStatus != status) {
-		_lastMidiStatus = status;
-		sendMidiByte(status);
-	}
-}
 
 void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
-	sendStatus(midiChannel, 0x90);
-	sendMidiByte(note);
-	sendMidiByte(velocity);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel,
+		note, velocity);
 }
 
 void RSound::sendProgramChange(int midiChannel, int program) {
-	sendStatus(midiChannel, 0xC0);
-	sendMidiByte(program);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | midiChannel,
+		program, 0);
 }
 
 void RSound::sendVolume(int midiChannel, int volume) {
-	sendStatus(midiChannel, 0xB0);
-	sendMidiByte(7);
-	sendMidiByte(volume);
+	const int scaledVolume = CLIP(volume, 0, 127) * _masterVolume / 255;
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+		MidiDriver::MIDI_CONTROLLER_VOLUME, scaledVolume);
 }
 
 void RSound::sendVolumeCC(int midiChannel, int volume) {
-	// Unlike sendVolume()/sendStatus(), this sends the
-	// status byte UNCONDITIONALLY (no _lastMidiStatus dedup check) -
-	// used by command7 when restoring all 9 channels' volumes in a row.
-	byte status = 0xB0 | midiChannel;
-	_lastMidiStatus = status;
-	sendMidiByte(status);
-	sendMidiByte(7);
-	sendMidiByte(volume);
+	// The original emits an unconditional status byte here. Structured MIDI
+	// messages do not retain running status, so this is equivalent to the
+	// ordinary volume helper.
+	sendVolume(midiChannel, volume);
 }
 
 void RSound::sendPitchBend(int midiChannel, int value) {
-	sendStatus(midiChannel, 0xE0);
-	sendMidiByte(0); // LSB always 0 - only coarse (MSB) control is used
-	sendMidiByte(value);
+	// The original only uses the coarse (MSB) component.
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PITCH_BEND | midiChannel,
+		0, value);
 }
 
 void RSound::resetPitchBend(int midiChannel) {
@@ -302,18 +318,12 @@ void RSound::resetPitchBend(int midiChannel) {
 }
 
 void RSound::sendPan(int midiChannel, int value) {
-	sendStatus(midiChannel, 0xB0);
-	sendMidiByte(0x0A); // CC#10: Pan
-	sendMidiByte(value);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+		MidiDriver::MIDI_CONTROLLER_PANNING, value);
 }
 
 void RSound::muteChannel(int midiChannel) {
-	// Matches muteChannel: unconditional status send, like sendVolumeCC().
-	byte status = 0xB0 | midiChannel;
-	_lastMidiStatus = status;
-	sendMidiByte(status);
-	sendMidiByte(7);
-	sendMidiByte(0);
+	sendVolume(midiChannel, 0);
 }
 
 void RSound::sendGmReset(int count) {
@@ -322,85 +332,79 @@ void RSound::sendGmReset(int count) {
 	for (int midiChannel = count; midiChannel >= 1; --midiChannel) {
 		_fadeCheckPeriod = 0; // reset at the top of every iteration
 
-		byte status = 0xB0 | midiChannel;
-		_lastMidiStatus = status;
-		sendMidiByte(status);
-		sendMidiByte(0x7B); // All Notes Off
-		sendMidiByte(0);
-		sendMidiByte(0x79); // Reset All Controllers
-		sendMidiByte(0);
-		sendMidiByte(7);    // Channel Volume
-		sendMidiByte(100);
-		sendMidiByte(0x0A); // Pan
-		sendMidiByte(0x40);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
+		sendVolume(midiChannel, 100);
+		sendPan(midiChannel, 0x40);
 	}
 }
 
-const byte *RSound::sendSysExData(const byte *pData) {
-	static const byte header[] = { 0xF0, 0x41, 0x10, 0x16, 0x12 };
-	for (int i = 0; i < ARRAYSIZE(header); ++i)
-		sendMidiByte(header[i]);
+const byte *RSound::sendSysExData(const byte *pData, uint maxLength) {
+	uint length = 0;
+	while (length < maxLength && pData[length] != 0xFF)
+		++length;
 
-	_sysexChecksum = 0;
-	int i = 0;
-	for (; pData[i] != 0xFF; ++i) {
-		sendMidiByte(pData[i]);
-		_sysexChecksum += pData[i];
+	if (length == maxLength) {
+		warning("RSound::sendSysExData: unterminated SysEx message");
+		return nullptr;
 	}
 
-	sendMidiByte((~_sysexChecksum + 1) & 0x7F);
-	sendMidiByte(0xF7);
-
-	return &pData[i];
+	_midiDriver->sysExMT32(pData, length);
+	return &pData[length];
 }
 
 const byte *RSound::sendSysEx(int offset) {
-	return sendSysExData(loadData(offset));
+	if (offset < 0 || (uint)offset >= _soundData.size()) {
+		warning("RSound::sendSysEx: offset %d is outside the sound data", offset);
+		return nullptr;
+	}
+
+	return sendSysExData(loadData(offset), _soundData.size() - offset);
 }
 
 void RSound::sendSysExSequence() {
-	const byte *pData = loadData(_sysExOffset);
+	const byte *pData = sendSysEx(_sysExOffset);
+	if (!pData)
+		return;
+
+	const byte *const dataEnd = _soundData.end();
 	for (;;) {
-		pData = sendSysExData(pData);
 		++pData;
+		if (pData == dataEnd) {
+			warning("RSound::sendSysExSequence: unterminated SysEx sequence");
+			return;
+		}
 		if (*pData == 0xFF)
 			break;
+
+		pData = sendSysExData(pData, dataEnd - pData);
+		if (!pData)
+			return;
 	}
 }
 
-void RSound::sendPatchInitSequence() {
-	// TENTATIVE - see header comment for sendPatchInitSequence(). 4 outer
-	// iterations, each sending one SysEx message built from the fixed
-	// header at loadData(0xA3) plus a computed payload; base persists
-	// and accumulates across outer iterations.
+void RSound::restorePatchMemory() {
+	// Native export 2 uses this during teardown. Four blocks restore the
+	// complete MT-32 Patch Memory table; each block contains 32 records.
 	byte base = 0;
 	for (int outer = 0; outer < 4; ++outer) {
-		byte *header = loadData(0xA3);
-		for (int i = 0; header[i] != 0xFF; ++i)
-			sendMidiByte(header[i]);
-
-		_sysexChecksum = 0;
-		byte b1 = 5;
-		_sysexChecksum += b1; sendMidiByte(b1);
-		byte b2 = (byte)(outer << 1);
-		_sysexChecksum += b2; sendMidiByte(b2);
-		byte b3 = 0;
-		_sysexChecksum += b3; sendMidiByte(b3);
+		byte message[3 + 32 * 8];
+		uint16 length = 0;
+		message[length++] = 5;
+		message[length++] = (byte)(outer << 1);
+		message[length++] = 0;
 
 		for (int inner = 0; inner < 0x20; ++inner) {
-			byte v1 = (byte)(outer >> 1);
-			_sysexChecksum += v1; sendMidiByte(v1);
-			byte v2 = (byte)(inner + base);
-			_sysexChecksum += v2; sendMidiByte(v2);
+			message[length++] = (byte)(outer >> 1);
+			message[length++] = (byte)(inner + base);
 			static const byte tail[] = { 0x18, 0x32, 0x0C, 0, 1, 0 };
-			for (int t = 0; t < ARRAYSIZE(tail); ++t) {
-				_sysexChecksum += tail[t];
-				sendMidiByte(tail[t]);
-			}
+			for (int t = 0; t < ARRAYSIZE(tail); ++t)
+				message[length++] = tail[t];
 		}
 
-		sendMidiByte((~_sysexChecksum + 1) & 0x7F);
-		sendMidiByte(0xF7);
+		_midiDriver->sysExMT32(message, length);
 
 		base = (byte)((base + 0x20) & 0x3F);
 	}
@@ -415,7 +419,7 @@ void RSound::sendReverbSysEx(int mode, int time, int level) {
 	// there's no reason to expect this address to live at the same
 	// offset in every driver's own resource file.
 	byte buffer[7] = { 0x10, 0x00, 0x01, (byte)(mode & 3), (byte)(time & 7), (byte)(level & 7), 0xFF };
-	sendSysExData(buffer);
+	sendSysExData(buffer, ARRAYSIZE(buffer));
 }
 
 /*-----------------------------------------------------------------------*/
@@ -424,9 +428,7 @@ void RSound::Channel_flushHeldNotes(Channel *channel) {
 	byte *slots = _heldNotes[channel->_midiChannel];
 	for (int i = 0; i < 4; ++i) {
 		if (slots[i] != 0xFF) {
-			sendStatus(channel->_midiChannel, 0x90);
-			sendMidiByte(slots[i]);
-			sendMidiByte(0); // velocity 0 = note off
+			sendNoteOn(channel->_midiChannel, slots[i], 0);
 			slots[i] = 0xFF;
 		}
 	}
@@ -591,10 +593,6 @@ int RSound::command8() {
 	return result;
 }
 
-void RSound::callFunction(uint16 offset) {
-	error("Unsupported call to sound driver function at offset %.4x", offset);
-}
-
 /*-----------------------------------------------------------------------*/
 
 int RSound::readScriptByte(byte *&pSrc) {
@@ -694,18 +692,16 @@ dispatch:
 
 		switch (b) {
 		case 0xBE: {
-			// TODO: purpose unconfirmed - no reader found for
-			// _clockUnknown anywhere in the disassembly seen so far.
+			// TODO: Native purpose unresolved. The full overlay corpus only
+			// stores this value; no reader was found.
 			_clockUnknown = readScriptByte(pSrc);
 			ch->_pSrc += 2;
 			goto dispatch;
 		}
 		case 0xBF: {
-			// TODO: purpose unconfirmed - no reader found for
-			// _clockCoarse/_clockEnabled1/_clockEnabled2 anywhere in the
-			// disassembly seen so far. Only takes effect (via the
-			// _tickCounter==0 gate) if executed before the very first
-			// update() tick.
+			// TODO: Native purpose unresolved. Every overlay performs these
+			// stores, but no later reader was found. The coarse value is copied
+			// only before the first update tick, and both enable words are set.
 			_clockCoarseTarget = readScriptWord(pSrc);
 			if (_tickCounter == 0)
 				_clockCoarse = _clockCoarseTarget;
@@ -715,9 +711,9 @@ dispatch:
 			goto dispatch;
 		}
 		case 0xC0: {
-			// TODO: purpose unconfirmed - no reader found for _clockMed
-			// anywhere in the disassembly seen so far. Same one-time-only
-			// gate as 0xBF above.
+			// TODO: Native purpose unresolved. This has the same first-tick
+			// copy behavior as 0xBF, with no later reader. It does not set the
+			// two enable words.
 			_clockMedTarget = readScriptByte(pSrc);
 			if (_tickCounter == 0)
 				_clockMed = _clockMedTarget;
@@ -725,10 +721,9 @@ dispatch:
 			goto dispatch;
 		}
 		case 0xC1: {
-			// TODO: purpose unconfirmed - no reader found for _clockFine
-			// anywhere in the disassembly seen so far. Unlike 0xBE/0xBF/
-			// 0xC0, this sets the value directly and unconditionally
-			// (no _tickCounter gate).
+			// TODO: Native purpose unresolved. Unlike 0xBF/0xC0, this stores
+			// the value directly and unconditionally, with no tick gate, but no
+			// later reader was found.
 			_clockFine = readScriptByte(pSrc);
 			ch->_pSrc += 2;
 			goto dispatch;
@@ -748,8 +743,10 @@ dispatch:
 			goto dispatch;
 		}
 		case 0xC4: {
-			uint16 fnOffset = readScriptWord(pSrc);
-			callFunction(fnOffset);
+			const uint16 targetOffset = readScriptWord(pSrc);
+			if (!callFunction(targetOffset))
+				error("RSound::pollActiveChannel: unsupported opcode 0xC4 target 0x%04x",
+						targetOffset);
 			ch->_pSrc += 3;
 			goto dispatch;
 		}
@@ -920,10 +917,10 @@ dispatch:
 			ch->_pSrc += 3; goto dispatch;
 		}
 		case 0xEA: {
-			// TODO: low confidence - a self-modifying op that reads
-			// table1[scriptVar[idx1]], then writes it into table2 at an
-			// offset determined by table2's own leading "size" byte.
-			// Translated as literally as possible; purpose unconfirmed.
+			// Selects a byte using a script variable as the table index,
+			// then writes it at the self-relative displacement stored after
+			// the table. The operation is confirmed, but its higher-level
+			// purpose remains unknown.
 			int idx1 = readScriptByte(pSrc);
 			int len2 = readScriptByte(pSrc);
 			byte *table1Base = pSrc + 1;
@@ -966,6 +963,50 @@ dispatch:
 			ch->_pSrc += len1 + 3;
 			goto dispatch;
 		}
+		case 0xFE: {
+			// End an outer loop. A zero count establishes the next outer
+			// and inner loop anchors without repeating. The signed byte is
+			// stored in the native 16-bit counter, so 0xFF means 65535.
+			if (!ch->_outerLoopCount) {
+				ch->_outerLoopCount = (uint16)(int16)(int8)
+						readScriptByte(pSrc);
+				if (!ch->_outerLoopCount) {
+					ch->_pSrc += 2;
+					ch->_outerLoopPtr = ch->_pSrc;
+					ch->_innerLoopCount = 0;
+					ch->_outerLoopCount = 0;
+				} else {
+					ch->_pSrc = ch->_outerLoopPtr;
+				}
+			} else if (--ch->_outerLoopCount == 0) {
+				ch->_pSrc += 2;
+				ch->_outerLoopPtr = ch->_pSrc;
+			} else {
+				ch->_pSrc = ch->_outerLoopPtr;
+			}
+			ch->_innerLoopPtr = ch->_pSrc;
+			goto post_keyon;
+		}
+		case 0xFF: {
+			// End an inner loop. The restart pointer is the beginning of
+			// the current inner-loop region and advances after completion.
+			if (!ch->_innerLoopCount) {
+				ch->_innerLoopCount = (uint16)(int16)(int8)
+						readScriptByte(pSrc);
+				if (!ch->_innerLoopCount) {
+					ch->_pSrc += 2;
+					ch->_innerLoopPtr = ch->_pSrc;
+				} else {
+					ch->_pSrc = ch->_innerLoopPtr;
+				}
+			} else if (--ch->_innerLoopCount == 0) {
+				ch->_pSrc += 2;
+				ch->_innerLoopPtr = ch->_pSrc;
+			} else {
+				ch->_pSrc = ch->_innerLoopPtr;
+			}
+			goto post_keyon;
+		}
 
 		// ---- Loop / restart-pointer opcodes ----
 		case 0xFD: {
diff --git a/engines/mads/phantom/sound/rsound.h b/engines/mads/phantom/sound/rsound.h
index 6bbc3b8516d..6dcb4331b41 100644
--- a/engines/mads/phantom/sound/rsound.h
+++ b/engines/mads/phantom/sound/rsound.h
@@ -22,6 +22,8 @@
 #ifndef MADS_PHANTOM_SOUND_RSOUND_H
 #define MADS_PHANTOM_SOUND_RSOUND_H
 
+#include "audio/mt32gm.h"
+#include "mads/core/native_sound_timer.h"
 #include "mads/core/sound_manager.h"
 
 namespace MADS {
@@ -78,8 +80,8 @@ public:
 	byte *_pSrc = nullptr;         // current read pointer into the sound-data stream
 	byte *_innerLoopPtr = nullptr; // inner-loop restart address
 	byte *_outerLoopPtr = nullptr; // outer-loop restart address
-	int _innerLoopCount = 0;
-	int _outerLoopCount = 0;
+	uint16 _innerLoopCount = 0; // signed byte stored as a 16-bit loop count
+	uint16 _outerLoopCount = 0; // signed byte stored as a 16-bit loop count
 	byte *_soundData = nullptr;    // identity pointer used by RSound::isSoundActive()
 	byte *_branchTarget = nullptr; // resume-after-branch pointer, used by the call/return opcode pair (new to Phantom)
 	int _transpose = 0;            // added to note bytes before comparison/storage (new to Phantom)
@@ -118,11 +120,6 @@ public:
  * conditional branches, and a call/return pair, in addition to the shared
  * note/fade/loop mechanics.
  *
- * NOTE: The actual MIDI transmission (sendMidiByte()) currently just logs
- * via warning() - it isn't hooked up to a real ScummVM MIDI/MT-32 output
- * yet, matching the Rex Nebular RSound family. Every other MIDI-sending
- * helper funnels through sendMidiByte().
- *
  * NOTE: DOS-specific driver ceremony from the original (timer IRQ hooking,
  * MPU-401 hardware detection/reset, PIT-based SysEx delay calibration, the
  * system-clock save/restore around it) has no ScummVM equivalent and is not
@@ -133,32 +130,29 @@ class RSound : public SoundDriver {
 	friend class Channel;
 private:
 	uint16 _randomSeed;
-	byte _lastMidiStatus;         // running-status cache, avoids resending an unchanged status byte
-	byte _sysexChecksum;
+	int _masterVolume = 255;
 	int _stateChangedFlag;        // latches _pollResult=0xFFFF once per state change
+	MidiDriver_MT32GM *_midiDriver;
+	uint32 _driverCallbackDelta;
+	NativeSoundTimer _hostTimer;
 
 	/**
 	 * Per-MIDI-channel held-note slots (index 0 unused; channels are
-	 * 1-9; 4 = max chord polyphony). TODO/unconfirmed: the disassembly
-	 * shows TWO seemingly-parallel tables using the identical
-	 * "channel*4+slot" indexing and 0xFF-empty-slot convention - one
-	 * table (read/written directly by the chord-note-storing logic
-	 * in Channel_pollActive) and a second, unnamed array (used by
-	 * resetAllChannels's initialization and by the flush-held-notes
-	 * helper). The disassembly never resolves the second array to a named
-	 * symbol, so it's not confirmed whether these are the same underlying
-	 * memory (most likely, and what's implemented here) or two genuinely
-	 * separate tables - worth double-checking.
+	 * 1-9; 4 = max chord polyphony). The apparent second table in the
+	 * reset/flush helpers is this same storage expressed relative to the
+	 * overlay's data segment. Converting it using independently named
+	 * data-segment fields resolves to the absolute table read and written by
+	 * the chord interpreter, reset, and held-note flush paths.
 	 */
 	byte _heldNotes[RSOUND_CHANNEL_COUNT + 1][4];
 
 	/**
 	 * Data-segment offset of this driver's own "command0_array" (the
 	 * table sent by command0() via sendSysEx). Each driver has its own
-	 * copy of this table at its own offset within its own resource file -
-	 * matches the Rex Nebular RSound family's identical need. Not yet
-	 * confirmed for rsound.ph1 - the disassembly shows only the symbolic
-	 * "command0_array" label, not its numeric offset.
+	 * copy of this table at a verified offset within its resource file.
+	 *
+	 * The retail and demo subclasses supply that offset to the base
+	 * constructor.
 	 */
 	int _sysExOffset;
 
@@ -178,16 +172,18 @@ private:
 	int _fadeCheckCounter;
 
 	/**
-	 * Cluster of globals written by opcodes 0xBE-0xC1 but with no
-	 * confirmed reader anywhere in the disassembly seen so far (all
-	 * TENTATIVE names - see individual comments). _clockFine/_clockMed/
-	 * _clockCoarse default to 7/28/112, a clean 4x progression,
-	 * suggesting a coarse/medium/fine clock-division hierarchy (a common
-	 * shape for a MIDI-clock-like timing subdivision) rather than three
-	 * unrelated parameters. _tickCounter gates a one-time-only override:
-	 * opcodes 0xBF/0xC0 only take effect if executed before the first
-	 * update() tick ever runs, since _tickCounter increments
-	 * unconditionally every tick thereafter and the gate checks "== 0".
+	 * Cluster of globals written by opcodes 0xBE-0xC1. All supported retail
+	 * and demo overlays were checked: the values are stored, and 0xBF/0xC0
+	 * conditionally copy their targets before the first update tick, but no
+	 * later reader exists. Their native purpose therefore remains unresolved,
+	 * and the names below are descriptive rather than semantic.
+	 *
+	 * _clockFine/_clockMed/_clockCoarse default to 7/28/112, a clean 4x
+	 * progression that suggests a coarse/medium/fine clock-division hierarchy.
+	 * This remains an inference rather than implemented behavior because the
+	 * checked overlays never read those values. _tickCounter gates the only
+	 * observed copies: opcodes 0xBF/0xC0 can replace the coarse/medium values
+	 * before the first update tick, after which the counter is nonzero.
 	 */
 	int _tickCounter;
 	int _clockMedTarget;          // pending value for _clockMed, set by opcode 0xC0
@@ -295,6 +291,16 @@ protected:
 
 	void resultCheck();
 
+	/**
+	 * Handles a native C4 callback target embedded in a sequence. Controllers
+	 * override this only for statically identified native targets; the default
+	 * remains a fatal rejection in the bytecode interpreter.
+	 */
+	virtual bool callFunction(uint16 targetOffset) {
+		(void)targetOffset;
+		return false;
+	}
+
 	/**
 	 * Plays the specified sound, using any free channel from 6 to 8.
 	 * Matches the disassembly's playSound exactly (rsound_channel6-8).
@@ -350,10 +356,6 @@ protected:
 	int getRandomNumber();
 
 	// ---- Low-level MIDI send helpers -------------------------------
-	// All funnel through sendMidiByte(), the single hook point for
-	// wiring up real MT-32/MIDI output.
-	void sendMidiByte(byte value);
-	void sendStatus(int midiChannel, byte statusNibble);
 	void sendNoteOn(int midiChannel, int note, int velocity);
 	void sendProgramChange(int midiChannel, int program);
 	void sendVolume(int midiChannel, int volume);
@@ -381,8 +383,9 @@ protected:
 	 * the terminating 0xFF byte (matching the disassembly's own si
 	 * register value on return), so callers walking a sequence of
 	 * consecutive messages can advance past it to find the next one.
+	 * Returns nullptr when no terminator occurs within maxLength bytes.
 	 */
-	const byte *sendSysExData(const byte *pData);
+	const byte *sendSysExData(const byte *pData, uint maxLength);
 
 	/** sendSysExData() for a block already in this driver's own loaded sound data. */
 	const byte *sendSysEx(int offset);
@@ -399,14 +402,17 @@ protected:
 	void sendSysExSequence();
 
 	/**
-	 * TENTATIVE: a nested loop (4 outer x 32 inner
-	 * iterations) building and sending a SysEx message each inner pass.
-	 * The overall shape (loop counters, accumulating base value, fixed
-	 * bytes 0x18/0x32/0x0C) is clear from the disassembly, but the exact
-	 * purpose (a bulk patch/rhythm-setup initialization sequence is the
-	 * working hypothesis) is not confirmed.
+	 * Restore all 128 MT-32 Patch Memory records to the standard A/B
+	 * timbre mapping. Native export 2 calls this during driver teardown,
+	 * after stopping playback. All checked Phantom retail and demo
+	 * overlays use the same four-block, 32-record structure.
+	 *
+	 * ScummVM does not call the DOS hardware teardown path: opening each
+	 * MidiDriver_MT32GM resets the selected device, while closing follows
+	 * the shared MIDI-driver lifecycle. Keep the exact native translation
+	 * available without imposing its synchronous teardown sequence.
 	 */
-	void sendPatchInitSequence();
+	void restorePatchMemory();
 
 	/**
 	 * CONFIRMED: masks the 3 caller-supplied values
@@ -416,6 +422,8 @@ protected:
 	 * hardware protocol address, not driver-specific sound data.
 	 */
 	void sendReverbSysEx(int mode, int time, int level);
+	void onTimer();
+	static void timerCallback(void *data);
 
 	/**
 	 * A confirmed no-op (reads one operand, does
@@ -460,12 +468,6 @@ protected:
 	int command7();
 	int command8();
 
-	/**
-	 * Calls a function at a fixed offset within the sound driver.
-	 * @param offset		Offset of the function
-	 */
-	virtual void callFunction(uint16 offset);
-
 	int nullCommand() {
 		return 0;
 	}
@@ -494,8 +496,7 @@ public:
 	RSound(Audio::Mixer *mixer, const Common::Path &filename,
 		int dataOffset, int dataSize, int sysExOffset);
 
-	~RSound() override {
-	}
+	~RSound() override;
 
 	int stop() override;
 	int poll() override;
diff --git a/engines/mads/phantom/sound/rsound_phantom.cpp b/engines/mads/phantom/sound/rsound_phantom.cpp
index 32470579bc4..9c0f39c36b6 100644
--- a/engines/mads/phantom/sound/rsound_phantom.cpp
+++ b/engines/mads/phantom/sound/rsound_phantom.cpp
@@ -19,6 +19,8 @@
  *
  */
 
+#include "common/file.h"
+#include "common/md5.h"
 #include "common/util.h"
 #include "mads/phantom/sound/rsound_phantom.h"
 
@@ -53,6 +55,7 @@ int RSound1::command(int commandId, int param) {
 	case 34: return command34();
 	case 35: return command35();
 	case 36: return command36();
+	case 37: return command37();
 	case 38: return command38();
 	case 39: return command39();
 	case 64: return command64();
@@ -69,8 +72,7 @@ int RSound1::command(int commandId, int param) {
 	case 75: return command75();
 	case 76: return command76();
 	default:
-		// TODO: command 37 not yet implemented - disassembly not yet
-		// provided. There is no command 17 (see the class comment).
+		// There is no command 17 (see the class comment).
 		return 0;
 	}
 }
@@ -875,6 +877,18 @@ const RSound4::CommandPtr RSound4::_commandList[72] = {
 RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph4", 0x2A40, 0xE00, 0xEC) {
 }
 
+bool RSound4::callFunction(uint16 targetOffset) {
+	if (targetOffset != 0x229c)
+		return false;
+
+	const uint tableOffset = 0x0984 + (getRandomNumber() & 0x0f) * 2;
+	const byte *source = loadData(tableOffset);
+	byte *destination = loadData(0x0666);
+	destination[0] = source[0];
+	destination[1] = source[1];
+	return true;
+}
+
 int RSound4::command(int commandId, int param) {
 	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
 		return 0;
@@ -1469,6 +1483,160 @@ int RSound9::command71() {
 	return 0;
 }
 
+namespace {
+
+enum {
+	kDemoFileSize = 35413,
+	kDemoDataOffset = 0x2cd0,
+	kDemoInitializedDataSize = 0x5d85,
+	kDemoDeclaredDataSize = 0x5f60,
+	kDemoSysExOffset = 0x00e2
+};
+
+const char *const kDemoFilename = "rsound.pha";
+const char *const kDemoFirst8192Md5 = "8fca9087bfe5897dd8079d0513eed377";
+
+} // namespace
+
+RSoundDemo::RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+					   int dataOffset, int dataSize, int sysExOffset) :
+		RSound(mixer, filename, dataOffset, dataSize, sysExOffset) {
+}
+
+int RSoundDemo::dispatchCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0: return command0();
+	case 1: return command1();
+	case 2: return command2();
+	case 3: return command3();
+	case 4: return command4();
+	case 5: return command5();
+	case 6: return command6();
+	case 7: return command7();
+	case 8: return command8();
+	default: return 0;
+	}
+}
+
+int RSoundDemo::command4() {
+	// PHA's command 4 has no retail-style sound-active guard.
+	resetAndGmResetUpperChannels();
+	return 0;
+}
+
+int RSoundDemo::command5() {
+	// PHA's command 5 enters the shared upper-channel enable tail directly.
+	enableUpperChannels();
+	return 0;
+}
+
+RSoundDemoPHA::RSoundDemoPHA(Audio::Mixer *mixer) :
+		RSoundDemo(mixer, kDemoFilename, kDemoDataOffset,
+				   kDemoDeclaredDataSize, kDemoSysExOffset) {
+	// The MZ file omits the zero-initialized tail declared by the overlay
+	// descriptor. Supply it explicitly rather than depending on container
+	// growth semantics in the common file loader.
+	for (uint offset = kDemoInitializedDataSize; offset < kDemoDeclaredDataSize; ++offset)
+		_soundData[offset] = 0;
+}
+
+bool RSoundDemoPHA::validate(Common::String *reason) {
+	Common::File file;
+	if (!file.open(kDemoFilename)) {
+		if (reason)
+			*reason = "file is missing";
+		return false;
+	}
+	if (file.size() != kDemoFileSize) {
+		if (reason)
+			*reason = "file size does not match";
+		return false;
+	}
+	if (kDemoDataOffset + kDemoInitializedDataSize != file.size() ||
+		kDemoInitializedDataSize > kDemoDeclaredDataSize) {
+		if (reason)
+			*reason = "declared data segment is inconsistent";
+		return false;
+	}
+
+	file.seek(0);
+	if (Common::computeStreamMD5AsString(file, 8192) != kDemoFirst8192Md5) {
+		if (reason)
+			*reason = "first-8192-byte signature does not match";
+		return false;
+	}
+	return true;
+}
+
+int RSoundDemoPHA::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	_commandParam = param;
+	_frameCounter = 0;
+
+	if (commandId >= 0 && commandId <= 8)
+		return dispatchCommonCommand(commandId);
+	if (commandId < 9 || commandId > 27)
+		return 0;
+
+	// Each presentation handler calls command 1 and then invokes the native
+	// channels-1-to-8 allocator once for every root in its row.
+	static const uint16 roots[19][8] = {
+		{0x0602, 0x0745, 0x0793, 0x086f, 0x0911, 0x09b3, 0x0a01, 0x0a89},
+		{0x01e0, 0x023a, 0x0282, 0x0554},
+		{0x0560, 0x0583, 0x05a4, 0x05c0, 0x05e1},
+		{0x1c20, 0x1e77, 0x1f88, 0x20a7, 0x2198, 0x225a},
+		{0x135c, 0x1494, 0x154e, 0x15fa, 0x1683},
+		{0x2f0e, 0x2fc8, 0x3080, 0x3331},
+		{0x338c, 0x355c, 0x3821, 0x39ab, 0x3b6d, 0x3e6c, 0x3f14},
+		{0x2340, 0x25fd, 0x27f1, 0x2a29, 0x2bb3, 0x2d21},
+		{0x3f6e, 0x3fcd, 0x40b0, 0x40f5, 0x4115},
+		{0x4136, 0x418c, 0x41f1},
+		{0x4248, 0x42cb, 0x434e, 0x43be, 0x4446, 0x44e2, 0x4514},
+		{0x455e, 0x45e5, 0x461d, 0x4671},
+		{0x46dc, 0x477d, 0x4845, 0x48f7, 0x493b, 0x4983, 0x49a9},
+		{0x0aea, 0x0b38, 0x0c38, 0x0cb6, 0x0d0e, 0x0f61, 0x1272},
+		{0x17b2, 0x17e6, 0x1807, 0x1828, 0x1880, 0x18db, 0x1916},
+		{0x1942, 0x1973, 0x199f, 0x19d3, 0x1a07, 0x1a2d},
+		{0x1a62, 0x1a9e, 0x1afd, 0x1b40, 0x1ba5, 0x1bde},
+		{0x49d0, 0x4ac2, 0x4b35, 0x4bea},
+		{0x4c94, 0x4e00, 0x4f0d, 0x4fb2}
+	};
+
+	command1();
+	const uint16 *commandRoots = roots[commandId - 9];
+	for (uint index = 0; index < ARRAYSIZE(roots[0]) && commandRoots[index]; ++index)
+		playSoundAny(commandRoots[index]);
+	return 0;
+}
+
+void RSoundDemoPHA::writeRandomizedPair(uint16 firstLowOffset,
+		uint16 secondLowOffset, uint16 firstHighOffset, uint16 secondHighOffset,
+		byte firstLow, byte secondLow, byte firstHigh, byte secondHigh) {
+	if ((getRandomNumber() >> 8) <= 0x80) {
+		SWAP(firstLow, secondLow);
+		SWAP(firstHigh, secondHigh);
+	}
+	*loadData(firstLowOffset) = firstLow;
+	*loadData(secondLowOffset) = secondLow;
+	*loadData(firstHighOffset) = firstHigh;
+	*loadData(secondHighOffset) = secondHigh;
+}
+
+bool RSoundDemoPHA::callFunction(uint16 targetOffset) {
+	switch (targetOffset) {
+	case 0x240e:
+		writeRandomizedPair(0x3fd5, 0x404a, 0x3fd7, 0x404c,
+							0x14, 0x03, 0x32, 0x64);
+		return true;
+	case 0x243b:
+		writeRandomizedPair(0x3ffb, 0x4065, 0x3ffd, 0x4067,
+							0x0d, 0x0b, 0x4b, 0x5a);
+		return true;
+	default:
+		return false;
+	}
+}
+
 } // namespace Sound
 } // namespace Phantom
 } // namespace MADS
diff --git a/engines/mads/phantom/sound/rsound_phantom.h b/engines/mads/phantom/sound/rsound_phantom.h
index 097722654f1..86146e3497e 100644
--- a/engines/mads/phantom/sound/rsound_phantom.h
+++ b/engines/mads/phantom/sound/rsound_phantom.h
@@ -122,9 +122,10 @@ public:
  *                    random picker like RSound1's command16)
  *   commands 24-27  (this class)
  *   commands 32-35  (this class)
- *   commands 64-72  (this class); 73 is confirmed nullsub_1
- * All other indices in [0, 73] are confirmed or inferred unreachable
- * (nullCommand).
+ *   commands 64-72  (this class)
+ * The native bucket limits are 8, 16, 27, 35, and 72. A trailing table
+ * word points to nullsub_1, but command 73 is above the native limit and
+ * is rejected before dispatch. Its C++ slot remains an equivalent no-op.
  */
 class RSound2 : public RSound {
 private:
@@ -275,6 +276,7 @@ class RSound4 : public RSound {
 private:
 	typedef int (RSound4:: *CommandPtr)();
 	static const CommandPtr _commandList[72];
+	bool callFunction(uint16 targetOffset) override;
 
 	int command4() override;
 	int command5() override;
@@ -450,6 +452,32 @@ public:
 	int command(int commandId, int param) override;
 };
 
+/** Shared control-command behavior of the Phantom demo RSOUND overlay. */
+class RSoundDemo : public RSound {
+protected:
+	RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+			   int dataOffset, int dataSize, int sysExOffset);
+
+	int dispatchCommonCommand(int commandId);
+	int command4() override;
+	int command5() override;
+};
+
+/** Controller for the exact RSOUND.PHA overlay shipped with the demo. */
+class RSoundDemoPHA final : public RSoundDemo {
+private:
+	bool callFunction(uint16 targetOffset) override;
+	void writeRandomizedPair(uint16 firstLowOffset, uint16 secondLowOffset,
+							 uint16 firstHighOffset, uint16 secondHighOffset,
+							 byte firstLow, byte secondLow, byte firstHigh, byte secondHigh);
+
+public:
+	explicit RSoundDemoPHA(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+	static bool validate(Common::String *reason = nullptr);
+};
+
 } // namespace Sound
 } // namespace Phantom
 } // namespace MADS
diff --git a/engines/mads/phantom/sound/sound.cpp b/engines/mads/phantom/sound/sound.cpp
index 0f77f2b4c6e..9345ee55a1c 100644
--- a/engines/mads/phantom/sound/sound.cpp
+++ b/engines/mads/phantom/sound/sound.cpp
@@ -28,9 +28,15 @@ namespace Phantom {
 namespace Sound {
 
 void PhantomSoundManager::validate() {
-	if (_driverType == SOUND_MT32 && !_isDemo) {
-		// MT32
-		RSound::validate();
+	if (_driverType == SOUND_MT32) {
+		if (_isDemo) {
+			Common::String reason;
+			if (!RSoundDemoPHA::validate(&reason))
+				error("Cannot use Phantom demo RSOUND.PHA: %s",
+						reason.c_str());
+		} else {
+			RSound::validate();
+		}
 	} else {
 		// Adlib
 		ASound::validate(_isDemo);
@@ -40,12 +46,11 @@ void PhantomSoundManager::validate() {
 void PhantomSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_isDemo) {
-		_driver = new ASoundDemo(_mixer);
-
-	} else if (_driverType == SOUND_MT32) {
+	if (_driverType == SOUND_MT32) {
 		// MT32
-		switch (sectionNumber) {
+		if (_isDemo) {
+			_driver = new RSoundDemoPHA(_mixer);
+		} else switch (sectionNumber) {
 		case 1:
 			_driver = new RSound1(_mixer);
 			break;
@@ -68,6 +73,8 @@ void PhantomSoundManager::loadDriver(int sectionNumber) {
 			_driver = nullptr;
 			break;
 		}
+	} else if (_isDemo) {
+		_driver = new ASoundDemo(_mixer);
 	} else {
 		// Adlib
 		switch (sectionNumber) {


Commit: 7291754f59aea0e6d97eb8b99532fe0ac24235a7
    https://github.com/scummvm/scummvm/commit/7291754f59aea0e6d97eb8b99532fe0ac24235a7
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: DRAGONSPHERE: Restore native RSOUND playback

Connect the retail and demo RSOUND overlays to MT-32 output using
the recovered DOS host cadence. Preserve command dispatch, sequence
behavior, SysEx bounds, and callbacks verified against the overlays.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/detection_tables.h
    engines/mads/dragonsphere/sound/rsound.cpp
    engines/mads/dragonsphere/sound/rsound.h
    engines/mads/dragonsphere/sound/rsound_dragonsphere.cpp
    engines/mads/dragonsphere/sound/rsound_dragonsphere.h
    engines/mads/dragonsphere/sound/sound.cpp


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 243a283baef..65dd7c5160a 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -253,7 +253,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Dragonsphere,
 		0
@@ -268,7 +268,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Dragonsphere,
 		0
@@ -284,7 +284,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD | GF_INSTALLER,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Dragonsphere,
 		0
@@ -299,7 +299,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Dragonsphere,
 		0
diff --git a/engines/mads/dragonsphere/sound/rsound.cpp b/engines/mads/dragonsphere/sound/rsound.cpp
index 4fa5c7f4397..b0e2df322d0 100644
--- a/engines/mads/dragonsphere/sound/rsound.cpp
+++ b/engines/mads/dragonsphere/sound/rsound.cpp
@@ -81,28 +81,23 @@ void Channel::load(byte *pData) {
 byte RSound::_silenceStream[2] = { 0, 0 };
 
 RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset) : SoundDriver(mixer, filename, dataOffset, dataSize) {
+		int dataOffset, int dataSize, int sysExOffset,
+		bool usesDemoOpcodeSemantics) :
+		SoundDriver(mixer, filename, dataOffset, dataSize) {
 	_commandParam = 0;
 	_frameCounter = 0;
-	_tickCounter = 0;
 	_isDisabled = false;
 	_randomSeed = 1234;
-	_lastMidiStatus = 0;
-	_sysexChecksum = 0;
 	_stateChangedFlag = 0;
 	_pollResult = 0;
 	_sysExOffset = sysExOffset;
 	_fadeCheckCounter = 0;
 	_fadeCheckPeriod = 0;
-
-	_clockMedTarget = 0;
-	_clockCoarseTarget = 0;
-	_clockUnknown = 0;
-	_clockCoarse = 112;
-	_clockMed = 28;
-	_clockFine = 7;
-	_clockEnabled1 = 0;
-	_clockEnabled2 = 0;
+	_opcodeBeBfValue = 0;
+	_opcodeC0Value = 0;
+	_opcodeC1Value = 0;
+	_usesImmediateArithmeticOperands = false;
+	_usesDemoOpcodeSemantics = usesDemoOpcodeSemantics;
 
 	_callbackCounter = 0;
 	_callbackPeriod = 0;
@@ -121,12 +116,33 @@ RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
 	for (int i = 0; i < ARRAYSIZE(_scriptVariables); ++i)
 		_scriptVariables[i] = 0;
 
+	_midiDriver = new MidiDriver_MT32GM(MusicType::MT_MT32);
+	const int returnCode = _midiDriver->open();
+	if (returnCode != 0)
+		error("RSound - Failed to open MIDI music driver - error code %d.", returnCode);
+
+	_driverCallbackDelta = _midiDriver->getBaseTempo();
+
 	// Matches initDeviceOnce: command0() then sendSysExSequence(). The
 	// disassembly's _deviceInitialized guard flag is omitted - this
 	// constructor only ever runs once per driver instance, so there's
 	// nothing to guard against.
 	command0();
 	sendSysExSequence();
+
+	_midiDriver->setTimerCallback(this, &timerCallback);
+}
+
+RSound::~RSound() {
+	_isDisabled = true;
+	if (_midiDriver) {
+		_midiDriver->setTimerCallback(nullptr, nullptr);
+		_midiDriver->close();
+
+		Common::StackLock lock(_driverMutex);
+		delete _midiDriver;
+		_midiDriver = nullptr;
+	}
 }
 
 void RSound::validate() {
@@ -172,8 +188,25 @@ int RSound::poll() {
 	return result;
 }
 
+void RSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+
+	uint32 serviceTicks = _hostTimer.advance(_driverCallbackDelta, 1000000);
+	while (serviceTicks--) {
+		// Export 4 is a return stub in every audited Dragonsphere RSOUND overlay.
+		if (_hostTimer.pollDue())
+			poll();
+	}
+}
+
+void RSound::timerCallback(void *data) {
+	static_cast<RSound *>(data)->onTimer();
+}
+
 void RSound::setVolume(int volume) {
-	// TODO: no confirmed handler for this in the disassembly seen so far.
+	_masterVolume = CLIP(volume, 0, 255);
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		sendVolumeCC(i + 1, _isDisabled ? 0 : _channels[i]._volume);
 }
 
 void RSound::resultCheck() {
@@ -249,21 +282,6 @@ int RSound::isMusicChannelsActive() {
 }
 
 /*-----------------------------------------------------------------------*/
-// Low-level MIDI transmission. sendMidiByte() is the single point that
-// needs to change once the real MT-32/MIDI output interface is wired up;
-// everything else funnels through it.
-
-void RSound::sendMidiByte(byte value) {
-	warning("RSound: MIDI byte %02X", value);
-}
-
-void RSound::sendStatus(int midiChannel, byte statusNibble) {
-	byte status = statusNibble | midiChannel;
-	if (_lastMidiStatus != status) {
-		_lastMidiStatus = status;
-		sendMidiByte(status);
-	}
-}
 
 void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
 	// The disassembly derives midiChannel/velocity from
@@ -271,14 +289,13 @@ void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
 	// explicit call-site parameters, but the transmitted bytes are
 	// identical either way - kept parameterized here for API consistency
 	// with the rest of the class (and with the Phantom RSound family).
-	sendStatus(midiChannel, 0x90);
-	sendMidiByte(note);
-	sendMidiByte(velocity);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel,
+			note, velocity);
 }
 
 void RSound::sendProgramChange(int midiChannel, int program) {
-	sendStatus(midiChannel, 0xC0);
-	sendMidiByte(program);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | midiChannel,
+			program, 0);
 }
 
 void RSound::sendVolume(Channel *ch) {
@@ -288,26 +305,23 @@ void RSound::sendVolume(Channel *ch) {
 	// own fade-out mechanism takes over otherwise.
 	if (ch->_pendingStop)
 		return;
-	sendStatus(ch->_midiChannel, 0xB0);
-	sendMidiByte(7);
-	sendMidiByte(ch->_volume);
+	const int volume = CLIP(ch->_volume, 0, 127) * _masterVolume / 255;
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE |
+			ch->_midiChannel, MidiDriver::MIDI_CONTROLLER_VOLUME, volume);
 }
 
 void RSound::sendVolumeCC(int midiChannel, int volume) {
-	// Unlike sendVolume(), this sends the status byte UNCONDITIONALLY (no
-	// _lastMidiStatus dedup check) - used by command7 when restoring all
-	// 9 channels' volumes in a row.
-	byte status = 0xB0 | midiChannel;
-	_lastMidiStatus = status;
-	sendMidiByte(status);
-	sendMidiByte(7);
-	sendMidiByte(volume);
+	// Structured messages do not retain MIDI running status, so the native
+	// unconditional status write is equivalent to an ordinary volume event.
+	volume = CLIP(volume, 0, 127) * _masterVolume / 255;
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_VOLUME, volume);
 }
 
 void RSound::sendPitchBend(int midiChannel, int value) {
-	sendStatus(midiChannel, 0xE0);
-	sendMidiByte(0); // LSB always 0 - only coarse (MSB) control is used
-	sendMidiByte(value);
+	// The original only uses the coarse (MSB) component.
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PITCH_BEND | midiChannel,
+			0, value);
 }
 
 void RSound::resetPitchBend(int midiChannel) {
@@ -317,17 +331,13 @@ void RSound::resetPitchBend(int midiChannel) {
 void RSound::sendPan(int midiChannel, int value) {
 	// CORRECTED naming - see rsound.h class comment: this is the function
 	// the disassembly auto-named "sendVolume", which actually sends CC#10.
-	sendStatus(midiChannel, 0xB0);
-	sendMidiByte(0x0A); // CC#10: Pan
-	sendMidiByte(value);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_PANNING, value);
 }
 
 void RSound::muteChannel(int midiChannel) {
-	byte status = 0xB0 | midiChannel;
-	_lastMidiStatus = status;
-	sendMidiByte(status);
-	sendMidiByte(7);
-	sendMidiByte(0);
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+			MidiDriver::MIDI_CONTROLLER_VOLUME, 0);
 }
 
 void RSound::sendGmReset(int count) {
@@ -340,92 +350,88 @@ void RSound::sendGmResetRange(int high, int low) {
 	for (int midiChannel = high; midiChannel >= low; --midiChannel) {
 		_fadeCheckPeriod = 0;
 
-		byte status = 0xB0 | midiChannel;
-		_lastMidiStatus = status;
-		sendMidiByte(status);
-		sendMidiByte(0x7B); // All Notes Off
-		sendMidiByte(0);
-		sendMidiByte(0x79); // Reset All Controllers
-		sendMidiByte(0);
-		sendMidiByte(7);    // Channel Volume
-		sendMidiByte(100);
-		sendMidiByte(0x0A); // Pan
-		sendMidiByte(0x40);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+				MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+				MidiDriver::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
+		sendVolumeCC(midiChannel, 100);
+		sendPan(midiChannel, 0x40);
 	}
 }
 
-const byte *RSound::sendSysExData(const byte *pData) {
-	static const byte header[] = { 0xF0, 0x41, 0x10, 0x16, 0x12 };
-	for (int i = 0; i < ARRAYSIZE(header); ++i)
-		sendMidiByte(header[i]);
+const byte *RSound::sendSysExData(const byte *pData, uint maxLength) {
+	uint length = 0;
+	while (length < maxLength && pData[length] != 0xFF)
+		++length;
 
-	_sysexChecksum = 0;
-	int i = 0;
-	for (; pData[i] != 0xFF; ++i) {
-		sendMidiByte(pData[i]);
-		_sysexChecksum += pData[i];
+	if (length == maxLength) {
+		warning("RSound::sendSysExData: unterminated SysEx message");
+		return nullptr;
 	}
 
-	sendMidiByte((~_sysexChecksum + 1) & 0x7F);
-	sendMidiByte(0xF7);
-
-	return &pData[i];
+	_midiDriver->sysExMT32(pData, length);
+	return &pData[length];
 }
 
 const byte *RSound::sendSysEx(int offset) {
-	return sendSysExData(loadData(offset));
+	if (offset < 0 || (uint)offset >= _soundData.size()) {
+		warning("RSound::sendSysEx: offset %d is outside the sound data", offset);
+		return nullptr;
+	}
+
+	return sendSysExData(loadData(offset), _soundData.size() - offset);
 }
 
 void RSound::sendSysExSequence() {
-	const byte *pData = loadData(_sysExOffset);
+	const byte *pData = sendSysEx(_sysExOffset);
+	if (!pData)
+		return;
+
+	const byte *const dataEnd = _soundData.end();
 	for (;;) {
-		pData = sendSysExData(pData);
 		++pData;
+		if (pData == dataEnd) {
+			warning("RSound::sendSysExSequence: unterminated SysEx sequence");
+			return;
+		}
 		if (*pData == 0xFF)
 			break;
+
+		pData = sendSysExData(pData, dataEnd - pData);
+		if (!pData)
+			return;
 	}
 }
 
-void RSound::sendPatchInitSequence() {
-	// Matches sendPatchInitSequence exactly: 4 outer iterations, each
-	// sending one SysEx message built from the fixed header at
-	// loadData(0x61) plus a computed payload.
+void RSound::restorePatchMemory() {
+	// Native export 2 uses this during teardown. Four blocks restore the
+	// complete MT-32 Patch Memory table; each block contains 32 records.
 	byte base = 0;
 	for (int outer = 0; outer < 4; ++outer) {
-		byte *header = loadData(0x61);
-		for (int i = 0; header[i] != 0xFF; ++i)
-			sendMidiByte(header[i]);
-
-		_sysexChecksum = 0;
-		byte b1 = 5;
-		_sysexChecksum += b1; sendMidiByte(b1);
-		byte b2 = (byte)(outer << 1);
-		_sysexChecksum += b2; sendMidiByte(b2);
-		byte b3 = 0;
-		_sysexChecksum += b3; sendMidiByte(b3);
+		byte message[3 + 32 * 8];
+		uint16 length = 0;
+		message[length++] = 5;
+		message[length++] = (byte)(outer << 1);
+		message[length++] = 0;
 
 		for (int inner = 0; inner < 0x20; ++inner) {
-			byte v1 = (byte)(outer >> 1);
-			_sysexChecksum += v1; sendMidiByte(v1);
-			byte v2 = (byte)(inner + base);
-			_sysexChecksum += v2; sendMidiByte(v2);
+			message[length++] = (byte)(outer >> 1);
+			message[length++] = (byte)(inner + base);
 			static const byte tail[] = { 0x18, 0x32, 0x0C, 0, 1, 0 };
-			for (int t = 0; t < ARRAYSIZE(tail); ++t) {
-				_sysexChecksum += tail[t];
-				sendMidiByte(tail[t]);
-			}
+			for (int t = 0; t < ARRAYSIZE(tail); ++t)
+				message[length++] = tail[t];
 		}
 
-		sendMidiByte((~_sysexChecksum + 1) & 0x7F);
-		sendMidiByte(0xF7);
+		_midiDriver->sysExMT32(message, length);
 
 		base = (byte)((base + 0x20) & 0x3F);
 	}
 }
 
 void RSound::sendReverbSysEx(int mode, int time, int level) {
+	// Native retail and demo helpers mutate this same fixed template.
 	byte buffer[7] = { 0x10, 0x00, 0x01, (byte)(mode & 3), (byte)(time & 7), (byte)(level & 7), 0xFF };
-	sendSysExData(buffer);
+	sendSysExData(buffer, ARRAYSIZE(buffer));
 }
 
 /*-----------------------------------------------------------------------*/
@@ -434,9 +440,7 @@ void RSound::Channel_flushHeldNotes(Channel *channel) {
 	byte *slots = _heldNotes[channel->_midiChannel];
 	for (int i = 0; i < 4; ++i) {
 		if (slots[i] != 0xFF) {
-			sendStatus(channel->_midiChannel, 0x90);
-			sendMidiByte(slots[i]);
-			sendMidiByte(0); // velocity 0 = note off
+			sendNoteOn(channel->_midiChannel, slots[i], 0);
 			slots[i] = 0xFF;
 		}
 	}
@@ -614,10 +618,6 @@ int RSound::command8() {
 	return result;
 }
 
-void RSound::callFunction(uint16 offset) {
-	error("Unsupported call to sound driver function at offset %.4x", offset);
-}
-
 /*-----------------------------------------------------------------------*/
 
 int RSound::readScriptByte(byte *&pSrc) {
@@ -652,7 +652,6 @@ void RSound::update() {
 		return;
 
 	++_frameCounter;
-	++_tickCounter;
 	pollAllChannels();
 	tickCallback();
 	checkFadingChannels();
@@ -664,11 +663,9 @@ void RSound::pollAllChannels() {
 }
 
 /*-----------------------------------------------------------------------*/
-// Per-channel opcode interpreter. Ported by structural analogy to the
-// already-confirmed Phantom RSound::pollActiveChannel() - see the class
-// comment in rsound.h for exactly which parts were independently
-// re-checked against THIS game's disassembly (the dispatch range and the
-// loop/restart/branch opcode cluster) versus carried over unchanged.
+// Per-channel opcode interpreter. Every opcode was rechecked against the
+// retail and demo overlays. The explicit demo switches below preserve the
+// differences found in DR1/DR9 instead of relying on Phantom by analogy.
 
 void RSound::pollActiveChannel(Channel *ch) {
 	int midiChannel = ch->_midiChannel;
@@ -689,7 +686,10 @@ dispatch:
 		byte *pSrc = ch->_pSrc;
 		byte b = *pSrc;
 
-		if (!(b & 0x80)) {
+		// The native signed comparison dispatches every value through 0xBD
+		// to the two-byte note path. Values 0x80-0xBD are therefore signed
+		// note values, not unknown control opcodes.
+		if (b <= 0xBD) {
 			// ---- Simple note event: [note][duration] ----
 			int note = (int8)pSrc[0] + ch->_transpose;
 			int duration = pSrc[1];
@@ -718,33 +718,25 @@ dispatch:
 			goto post_keyon;
 		}
 
-		if (b <= 0xBD)
-			goto post_keyon;
-
 		switch (b) {
 		case 0xBE: {
-			_clockUnknown = readScriptByte(pSrc);
+			_opcodeBeBfValue = readScriptByte(pSrc);
 			ch->_pSrc += 2;
 			goto dispatch;
 		}
 		case 0xBF: {
-			_clockCoarseTarget = readScriptWord(pSrc);
-			if (_tickCounter == 0)
-				_clockCoarse = _clockCoarseTarget;
-			_clockEnabled1 = 1;
-			_clockEnabled2 = 1;
+			_opcodeBeBfValue = readScriptByte(pSrc);
+			readScriptByte(pSrc); // Native helper skips this byte and returns.
 			ch->_pSrc += 3;
 			goto dispatch;
 		}
 		case 0xC0: {
-			_clockMedTarget = readScriptByte(pSrc);
-			if (_tickCounter == 0)
-				_clockMed = _clockMedTarget;
+			_opcodeC0Value = readScriptByte(pSrc);
 			ch->_pSrc += 2;
 			goto dispatch;
 		}
 		case 0xC1: {
-			_clockFine = readScriptByte(pSrc);
+			_opcodeC1Value = readScriptByte(pSrc);
 			ch->_pSrc += 2;
 			goto dispatch;
 		}
@@ -762,8 +754,9 @@ dispatch:
 			goto dispatch;
 		}
 		case 0xC4: {
-			uint16 fnOffset = readScriptWord(pSrc);
-			callFunction(fnOffset);
+			const uint16 targetOffset = readScriptWord(pSrc);
+			if (!callFunction(targetOffset, *ch))
+				error("Unknown Dragonsphere RSOUND callback 0x%04x", targetOffset);
 			ch->_pSrc += 3;
 			goto dispatch;
 		}
@@ -840,10 +833,12 @@ dispatch:
 		}
 		case 0xDC: {
 			int idx1 = readScriptByte(pSrc);
-			readScriptByte(pSrc); // operand read but unused, matching Phantom's confirmed same-shaped bug
-			byte self = _scriptVariables[idx1 & 0xFF];
-			if (self)
-				_scriptVariables[idx1 & 0xFF] = self % self;
+			int operand = readScriptByte(pSrc);
+			byte value = _scriptVariables[idx1 & 0xFF];
+			uint16 divisor = _usesImmediateArithmeticOperands ?
+					(uint16)(int16)operand : value;
+			if (divisor)
+				_scriptVariables[idx1 & 0xFF] = value % divisor;
 			ch->_pSrc += 3; goto dispatch;
 		}
 		case 0xDD: { // DIV, variable
@@ -855,10 +850,12 @@ dispatch:
 		}
 		case 0xDE: {
 			int idx1 = readScriptByte(pSrc);
-			readScriptByte(pSrc); // operand read but unused, matching Phantom's confirmed same-shaped bug
-			byte self = _scriptVariables[idx1 & 0xFF];
-			if (self)
-				_scriptVariables[idx1 & 0xFF] = self / self;
+			int operand = readScriptByte(pSrc);
+			byte value = _scriptVariables[idx1 & 0xFF];
+			uint16 divisor = _usesImmediateArithmeticOperands ?
+					(uint16)(int16)operand : value;
+			if (divisor)
+				_scriptVariables[idx1 & 0xFF] = value / divisor;
 			ch->_pSrc += 3; goto dispatch;
 		}
 		case 0xDF: { // MUL, variable
@@ -920,6 +917,10 @@ dispatch:
 			ch->_pSrc += 3; goto dispatch;
 		}
 		case 0xEA: {
+			// Selects a byte using a script variable as the table index,
+			// then writes it at the self-relative displacement stored after
+			// the table. The operation is confirmed, but its higher-level
+			// purpose remains unknown.
 			int idx1 = readScriptByte(pSrc);
 			int len2 = readScriptByte(pSrc);
 			byte *table1Base = pSrc + 1;
@@ -934,7 +935,10 @@ dispatch:
 			int rangeLow = readScriptByte(pSrc);
 			int rangeHigh = readScriptByte(pSrc);
 			int range = rangeHigh - rangeLow + 1;
-			int r = range ? (getRandomNumber() % range) : 0;
+			uint16 random = getRandomNumber();
+			if (!_usesDemoOpcodeSemantics)
+				random &= 0x7fff;
+			int r = range ? (random % range) : 0;
 			byte value = (byte)(rangeLow + r);
 			int tableByte = (int8)*(pSrc + 1);
 			(pSrc + 2 + tableByte)[0] = value;
@@ -943,7 +947,10 @@ dispatch:
 		}
 		case 0xEC: {
 			int len1 = readScriptByte(pSrc);
-			int r = len1 ? (getRandomNumber() % len1) : 0;
+			uint16 random = getRandomNumber();
+			if (!_usesDemoOpcodeSemantics)
+				random &= 0x7fff;
+			int r = len1 ? (random % len1) : 0;
 			byte *table1 = pSrc + 1;
 			byte v1 = table1[r];
 			byte *table2 = pSrc + 1 + len1 + 1;
@@ -952,6 +959,50 @@ dispatch:
 			ch->_pSrc += len1 + 3;
 			goto dispatch;
 		}
+		case 0xFE: {
+			// End an outer loop. A zero count establishes the next outer
+			// and inner loop anchors without repeating. The signed byte is
+			// stored in the native 16-bit counter, so 0xFF means 65535.
+			if (!ch->_outerLoopCount) {
+				ch->_outerLoopCount = (uint16)(int16)(int8)
+						readScriptByte(pSrc);
+				if (!ch->_outerLoopCount) {
+					ch->_pSrc += 2;
+					ch->_outerLoopPtr = ch->_pSrc;
+					ch->_innerLoopCount = 0;
+					ch->_outerLoopCount = 0;
+				} else {
+					ch->_pSrc = ch->_outerLoopPtr;
+				}
+			} else if (--ch->_outerLoopCount == 0) {
+				ch->_pSrc += 2;
+				ch->_outerLoopPtr = ch->_pSrc;
+			} else {
+				ch->_pSrc = ch->_outerLoopPtr;
+			}
+			ch->_innerLoopPtr = ch->_pSrc;
+			goto post_keyon;
+		}
+		case 0xFF: {
+			// End an inner loop. The restart pointer is the beginning of
+			// the current inner-loop region and advances after completion.
+			if (!ch->_innerLoopCount) {
+				ch->_innerLoopCount = (uint16)(int16)(int8)
+						readScriptByte(pSrc);
+				if (!ch->_innerLoopCount) {
+					ch->_pSrc += 2;
+					ch->_innerLoopPtr = ch->_pSrc;
+				} else {
+					ch->_pSrc = ch->_innerLoopPtr;
+				}
+			} else if (--ch->_innerLoopCount == 0) {
+				ch->_pSrc += 2;
+				ch->_innerLoopPtr = ch->_pSrc;
+			} else {
+				ch->_pSrc = ch->_innerLoopPtr;
+			}
+			goto post_keyon;
+		}
 
 		// ---- Loop / restart-pointer opcodes ----
 		case 0xFD: {
@@ -1038,7 +1089,9 @@ dispatch:
 			goto post_keyon;
 		}
 		case 0xF1: {
-			ch->_pitchBend = readScriptByte(pSrc);
+			int pitchBend = readScriptByte(pSrc);
+			if (_usesDemoOpcodeSemantics || !ch->_pendingStop)
+				ch->_pitchBend = pitchBend;
 			ch->_pSrc += 2;
 			sendPitchBend(midiChannel, ch->_pitchBend);
 			goto post_keyon;
diff --git a/engines/mads/dragonsphere/sound/rsound.h b/engines/mads/dragonsphere/sound/rsound.h
index ef9d5925d5b..4df9b2ac635 100644
--- a/engines/mads/dragonsphere/sound/rsound.h
+++ b/engines/mads/dragonsphere/sound/rsound.h
@@ -22,6 +22,8 @@
 #ifndef MADS_DRAGONSPHERE_SOUND_RSOUND_H
 #define MADS_DRAGONSPHERE_SOUND_RSOUND_H
 
+#include "audio/mt32gm.h"
+#include "mads/core/native_sound_timer.h"
 #include "mads/core/sound_manager.h"
 
 namespace MADS {
@@ -36,9 +38,10 @@ class RSound;
  * Represents the data for a channel on the Dragonsphere MT-32 / MPU-401
  * driver. Confirmed identical in layout (sizeof 0x27, same field offsets/
  * order/roles) to the equivalent Return of the Phantom RSound Channel
- * struct (engines/mads/phantom/rsound.h) - spot-checked directly against
- * this game's own rsound.dr1 disassembly at every anchor point that has an
- * IDA-resolved name or a distinctive access pattern: _activeCount(0x00),
+ * struct (engines/mads/phantom/rsound.h). The interpreter and callback
+ * audits checked every retail and demo overlay, while the structure itself
+ * was checked against this game's rsound.dr1 at every anchor point that has
+ * an IDA-resolved name or a distinctive access pattern: _activeCount(0x00),
  * the 0x00-0x01/0x02-0x03 word-pair zeroing in resetAllChannels, _program
  * (0x05, sendProgramChange), _velocity (0x06, the note-on helper),
  * field_9/_keyOnDelay (0x09, the countdown+flush at the top of
@@ -79,8 +82,8 @@ public:
 	byte *_pSrc = nullptr;         // current read pointer into the sound-data stream
 	byte *_innerLoopPtr = nullptr; // inner-loop restart address
 	byte *_outerLoopPtr = nullptr; // outer-loop restart address
-	int _innerLoopCount = 0;
-	int _outerLoopCount = 0;
+	uint16 _innerLoopCount = 0; // signed byte stored as a 16-bit loop count
+	uint16 _outerLoopCount = 0; // signed byte stored as a 16-bit loop count
 	byte *_soundData = nullptr;    // identity pointer used by RSound::isSoundActive()
 	byte *_branchTarget = nullptr; // resume-after-branch pointer, used by the call/return opcode pair
 	int _transpose = 0;            // added to note bytes before comparison/storage
@@ -111,13 +114,11 @@ public:
 
 /**
  * Base class for the Dragonsphere MT-32 / MPU-401 sound player resource
- * files (rsound.dr1-.dr6, .dr9). Ported from rsound.dr1's disassembly,
- * cross-checked wherever possible against the already-confirmed Return of
- * the Phantom RSound family (engines/mads/phantom/rsound.h/.cpp) - the two
- * games' RSound engines share an essentially identical Channel struct and
- * Channel_pollActive opcode VM (same 0xBE-0xFF opcode range; every
- * spot-checked opcode - the loop/restart/branch cluster, the dispatch
- * range itself - matched Phantom's confirmed implementation exactly).
+ * files (rsound.dr1-.dr6, .dr9). Ported from rsound.dr1's disassembly and
+ * audited across every retail and demo overlay. The family uses the same
+ * 0xBE-0xFF opcode range as Return of the Phantom, but the direct audit also
+ * found and preserves demo-specific arithmetic, random, and pitch-bend
+ * behavior instead of assuming that the two families are identical.
  * Genuine, CONFIRMED differences from Phantom's RSound base:
  *
  *  - command1/command3/command5 use a 6-channel "lower" group (1-5 AND 9)
@@ -151,22 +152,16 @@ public:
  *    _pendingStop is zero - Channel_checkFade's own separate fade-out
  *    mechanism otherwise takes precedence. No equivalent gate exists on
  *    Phantom's sendVolume().
- *  - sendReverbSysEx()'s exact byte layout (fixed 10 00 01h Roland System
- *    Area Reverb address) is INFERRED by strong structural analogy to
- *    Phantom's confirmed implementation (same 2/3/3-bit parameter
- *    masking, same "mutate 3 bytes then sendSysEx" shape found in
- *    Phantom's driver) - NOT independently confirmed by inspecting the literal
- *    bytes at rsound.dr1's offset 0x67 sysex template.
+ *  - sendReverbSysEx()'s exact byte layout is confirmed in every retail
+ *    and demo overlay: each native helper masks the parameters to 2/3/3
+ *    bits, writes them after the fixed 10 00 01h Roland System Area
+ *    Reverb address, and sends that SysEx template.
  *  - null_sound_data (see _silenceStream) is a fixed 2-byte (0, 0)
  *    silence marker referenced by BOTH Channel::enable() and
  *    Channel_checkFade() - same intent as Phantom's fixed 3-byte
  *    silence stream (2 bytes here, unlike Phantom's 3 - confirmed
  *    directly from this disassembly).
  *
- * NOTE: The actual MIDI transmission (sendMidiByte()) currently just logs
- * via warning() - it isn't hooked up to a real ScummVM MIDI/MT-32 output
- * yet, matching every other RSound/ASound driver in this codebase.
- *
  * NOTE: DOS-specific driver ceremony from the original (timer IRQ hooking,
  * MPU-401 hardware detection/reset, PIT-based SysEx delay calibration, the
  * system-clock save/restore around it) has no ScummVM equivalent and is
@@ -174,24 +169,27 @@ public:
  */
 class RSound : public SoundDriver {
 	friend class Channel;
+private:
+	int _masterVolume = 255;
 public:
 	/**
 	 * Member-function pointer type for deferred sound-loader callbacks.
 	 * Returns int (the return value is discarded by tickCallback()) so
 	 * that MAKE_CALLBACK's reinterpret_cast only ever crosses the
-	 * enclosing-class boundary, never the return type as well - every
-	 * driver-specific callback target is required to return int to match.
+	 * enclosing-class boundary, never the return type as well. Every
+	 * driver-specific callback target returns int to match.
 	 * Public so driver subclasses can build a MAKE_CALLBACK-style cast
 	 * (reinterpret_cast<RSound::CallbackFunction>(&DerivedClass::fn)) to
 	 * pass to scheduleCallback().
 	 */
 	typedef int (RSound::*CallbackFunction)();
 
-private:
+	private:
 	uint16 _randomSeed;
-	byte _lastMidiStatus;         // running-status cache, avoids resending an unchanged status byte
-	byte _sysexChecksum;
 	int _stateChangedFlag;        // latches _pollResult=0xFFFF once per state change
+	MidiDriver_MT32GM *_midiDriver;
+	uint32 _driverCallbackDelta;
+	NativeSoundTimer _hostTimer;
 
 	/**
 	 * Per-MIDI-channel held-note slots (index 0 unused; channels are
@@ -244,21 +242,14 @@ private:
 	int _fadeCheckCounter;
 
 	/**
-	 * Cluster of globals written by opcodes 0xBE-0xC1, matching the
-	 * identically-shaped (and identically unconfirmed-purpose) cluster in
-	 * Phantom's RSound - see that class's comment for details. Ported by
-	 * structural analogy: the opcode dispatch range confirmed these same
-	 * four opcodes exist here, but their bodies were not independently
-	 * re-read for Dragonsphere.
+	 * Bytes written by opcodes 0xBE-0xC1. Their purpose remains unknown
+	 * because no reader exists in the checked overlays. Every retail and
+	 * demo overlay writes 0xBE and 0xBF to the same byte, and 0xC0 and
+	 * 0xC1 to separate bytes.
 	 */
-	int _clockMedTarget;
-	int _clockCoarseTarget;
-	int _clockUnknown;
-	int _clockCoarse;
-	int _clockMed;
-	int _clockFine;
-	int _clockEnabled1;
-	int _clockEnabled2;
+	byte _opcodeBeBfValue;
+	byte _opcodeC0Value;
+	byte _opcodeC1Value;
 
 	// ---- Deferred-callback subsystem - NEW vs. the
 	// Phantom RSound family, mirrors the sibling ASound driver's
@@ -275,9 +266,8 @@ private:
 
 	/**
 	 * Per-channel opcode interpreter (Channel_pollActive in the
-	 * disassembly). Implements the same bytecode VM as the Phantom
-	 * RSound family - see the class comment for what's been independently
-	 * re-confirmed vs. carried over by structural analogy.
+	 * disassembly). The common and demo-specific paths are based on the
+	 * direct retail/demo overlay audit described in the class comment.
 	 */
 	void pollActiveChannel(Channel *channel);
 
@@ -297,6 +287,19 @@ private:
 protected:
 	int _commandParam;
 
+	/**
+	 * The DR1 demo's 0xDC/0xDE handlers use their immediate divisor.
+	 * Retail drivers and the DR9 demo instead reproduce the Phantom
+	 * handlers' self-divisor bug.
+	 */
+	bool _usesImmediateArithmeticOperands;
+
+	/**
+	 * The demo VM omits the retail random mask and pending-stop pitch-bend
+	 * guard. Both demo overlays share those two differences.
+	 */
+	bool _usesDemoOpcodeSemantics;
+
 	byte *loadData(int offset) {
 		return &_soundData[offset];
 	}
@@ -443,9 +446,12 @@ protected:
 
 	int getRandomNumber();
 
+	/** Resolve native near callbacks embedded in section-specific streams. */
+	virtual bool callFunction(uint16, Channel &) {
+		return false;
+	}
+
 	// ---- Low-level MIDI send helpers -------------------------------
-	void sendMidiByte(byte value);
-	void sendStatus(int midiChannel, byte statusNibble);
 	void sendNoteOn(int midiChannel, int note, int velocity);
 	void sendProgramChange(int midiChannel, int program);
 
@@ -491,8 +497,9 @@ protected:
 	 * pointer to the terminating 0xFF byte (matching the disassembly's
 	 * own si register value on return), so callers walking a sequence
 	 * of consecutive messages can advance past it to find the next one.
+	 * Returns nullptr when no terminator occurs within maxLength bytes.
 	 */
-	const byte *sendSysExData(const byte *pData);
+	const byte *sendSysExData(const byte *pData, uint maxLength);
 
 	/** sendSysExData() for a block already in this driver's own loaded sound data. */
 	const byte *sendSysEx(int offset);
@@ -508,16 +515,28 @@ protected:
 	 */
 	void sendSysExSequence();
 
-	void sendPatchInitSequence();
+	/**
+	 * Restore all 128 MT-32 Patch Memory records to the standard A/B
+	 * timbre mapping. Native export 2 calls this during driver teardown,
+	 * after stopping playback. All checked Dragonsphere retail and demo
+	 * overlays use the same four-block, 32-record structure.
+	 *
+	 * ScummVM does not call the DOS hardware teardown path: opening each
+	 * MidiDriver_MT32GM resets the selected device, while closing follows
+	 * the shared MIDI-driver lifecycle. Keep the exact native translation
+	 * available without imposing its synchronous teardown sequence.
+	 */
+	void restorePatchMemory();
 
 	/**
 	 * Masks the 3 caller-supplied values to 2/3/3 bits (mode 0-3, time
 	 * 0-7, level 0-7) and sends them via the Roland MT-32 System Area
-	 * Reverb SysEx address (10 00 01h) - see class comment re: this
-	 * being inferred by analogy rather than independently confirmed for
-	 * Dragonsphere.
+	 * Reverb SysEx address (10 00 01h), matching the native retail and
+	 * demo templates.
 	 */
 	void sendReverbSysEx(int mode, int time, int level);
+	void onTimer();
+	static void timerCallback(void *data);
 
 	int command0();
 	int command1();
@@ -529,12 +548,6 @@ protected:
 	int command7();
 	int command8();
 
-	/**
-	 * Calls a function at a fixed offset within the sound driver.
-	 * @param offset		Offset of the function
-	 */
-	virtual void callFunction(uint16 offset);
-
 	int nullCommand() {
 		return 0;
 	}
@@ -542,7 +555,6 @@ protected:
 public:
 	Channel _channels[RSOUND_CHANNEL_COUNT];
 	int _frameCounter;
-	int _tickCounter; // incremented alongside _frameCounter every update() tick
 	bool _isDisabled;
 	int _pollResult;
 
@@ -562,10 +574,10 @@ public:
 	 * @param sysExOffset	Offset of this driver's own command0_array
 	 */
 	RSound(Audio::Mixer *mixer, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset);
+		int dataOffset, int dataSize, int sysExOffset,
+		bool usesDemoOpcodeSemantics = false);
 
-	~RSound() override {
-	}
+	~RSound() override;
 
 	int stop() override;
 	int poll() override;
diff --git a/engines/mads/dragonsphere/sound/rsound_dragonsphere.cpp b/engines/mads/dragonsphere/sound/rsound_dragonsphere.cpp
index de96a2255e5..c7ef5805c8c 100644
--- a/engines/mads/dragonsphere/sound/rsound_dragonsphere.cpp
+++ b/engines/mads/dragonsphere/sound/rsound_dragonsphere.cpp
@@ -19,6 +19,9 @@
  *
  */
 
+#include "common/file.h"
+#include "common/md5.h"
+#include "common/textconsole.h"
 #include "common/util.h"
 #include "mads/dragonsphere/sound/rsound_dragonsphere.h"
 
@@ -31,6 +34,39 @@ namespace Sound {
 RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr1", 0x2C00, 0x3840, 0x9C) {
 }
 
+bool RSound1::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	switch (targetOffset) {
+	case 0x0886:
+		command1();
+		return true;
+	case 0x23A6:
+		command16();
+		return true;
+	case 0x23EE:
+		command32();
+		return true;
+	case 0x2478:
+		if (isSoundActive(loadData(0xC96)))
+			return true;
+		if (isMusicChannelsActive())
+			scheduleCallback(MAKE_CALLBACK(RSound1, loadCallback2478));
+		else {
+			resetCallbackTimer(0x50);
+			loadCallback2478();
+		}
+		return true;
+	case 0x262C:
+		command40();
+		return true;
+	case 0x2674:
+		command41();
+		return true;
+	default:
+		return false;
+	}
+}
+
 const RSound1::CommandPtr RSound1::_commandList[102] = {
 	&RSound1::command0, &RSound1::command1, &RSound1::command2, &RSound1::command3,
 	&RSound1::command4, &RSound1::command5, &RSound1::command6, &RSound1::command7,
@@ -95,6 +131,14 @@ int RSound1::loadCommand16() {
 	return 0;
 }
 
+int RSound1::loadCallback2478() {
+	command3();
+	_channels[0].load(loadData(0xC96));
+	_channels[1].load(loadData(0xCD2));
+	_channels[2].load(loadData(0xD41));
+	return 0;
+}
+
 int RSound1::command17() {
 	// Ungated scheduling (Pattern A): no isMusicChannelsActive() check,
 	// always loads immediately once the isSoundActive() gate passes.
@@ -422,7 +466,7 @@ int RSound1::command48() {
 	return 0;
 }
 
-int RSound1::loadCommand43_48() {
+void RSound1::loadCommand43_48() {
 	setMusicIndex(0x28);
 	command3();
 	_channels[0].load(loadData(0x24EC));
@@ -430,7 +474,6 @@ int RSound1::loadCommand43_48() {
 	_channels[2].load(loadData(0x257D));
 	_channels[3].load(loadData(0x25A4));
 	_channels[4].load(loadData(0x25CD));
-	return 0;
 }
 
 int RSound1::command44() {
@@ -1028,6 +1071,25 @@ int RSound2::command(int commandId, int param) {
 RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr3", 0x2750, 0x1780, 0xAC) {
 }
 
+bool RSound3::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2523)
+		return false;
+
+	Channel *source = &_channels[0];
+	if (source->_innerLoopCount) {
+		source = &_channels[3];
+		if (source->_innerLoopCount)
+			source = &_channels[4];
+	}
+
+	byte note = source->_note;
+	while (note < 0x58)
+		note += 12;
+	*loadData(0x1496) = note;
+	return true;
+}
+
 int RSound3::command1() {
 	// Must call THIS driver's own command3() (not virtual in the base -
 	// see class comment).
@@ -1062,9 +1124,8 @@ int RSound3::command5() {
 }
 
 int RSound3::command16() {
-	// Gate uses the disassembly's own "isSoundPlaying" name - treated as
-	// equivalent to isSoundActive() (confirmed identical for Phantom's
-	// RSound4; not independently re-confirmed here).
+	// The native helper scans the active channels for this sequence pointer,
+	// which is equivalent to isSoundActive().
 	byte *pData = loadData(0x7BF);
 	if (isSoundActive(pData))
 		return 0;
@@ -1281,6 +1342,18 @@ int RSound3::command(int commandId, int param) {
 RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr4", 0x2930, 0x2370, 0xAC) {
 }
 
+bool RSound4::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x270E)
+		return false;
+
+	byte note = _channels[0]._note;
+	while (note > 0x2B)
+		note -= 12;
+	*loadData(0x2158) = note;
+	return true;
+}
+
 int RSound4::command1() {
 	// Must call THIS driver's own command3() (not virtual in the base -
 	// see class comment).
@@ -1733,6 +1806,14 @@ int RSound4::command78() {
 RSound5::RSound5(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr5", 0x2910, 0x2530, 0x9C) {
 }
 
+bool RSound5::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x23AD)
+		return false;
+	dispatchCommand16B();
+	return true;
+}
+
 int RSound5::command1() {
 	// Must call THIS driver's own command5() (not virtual in the base -
 	// see class comment).
@@ -1835,6 +1916,18 @@ int RSound5::loadCommand16B() {
 	return 0;
 }
 
+void RSound5::dispatchCommand16B() {
+	if (isSoundActive(loadData(0x7E9)) ||
+			isSoundActive(loadData(0x7DC)))
+		return;
+	if (isMusicChannelsActive())
+		scheduleCallback(MAKE_CALLBACK(RSound5, loadCommand16B));
+	else {
+		resetCallbackTimer(0xC0);
+		loadCommand16B();
+	}
+}
+
 int RSound5::command17() {
 	// Ungated scheduling (Pattern A), matching every other driver's
 	// command17 shape.
@@ -2080,10 +2173,12 @@ int RSound5::command69() {
 }
 
 int RSound5::command70() {
-	// Confirmed bug in the original (missing retn/jmp after the second,
-	// redundant call) - treated as a single play, matching the intended
-	// final action.
+	// The native handler deliberately makes four allocation requests and ends
+	// with a tail jump, so the repeated first sequence is not fallthrough.
 	playSoundChannels6to8(0x21C4);
+	playSoundChannels6to8(0x21C4);
+	playSoundChannels6to8(0x21D4);
+	playSoundChannels6to8(0x21B8);
 	return 0;
 }
 
@@ -2149,6 +2244,15 @@ int RSound5::command78() {
 RSound6::RSound6(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr6", 0x2B60, 0x2840, 0xAC) {
 }
 
+bool RSound6::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2588)
+		return false;
+
+	command37();
+	return true;
+}
+
 int RSound6::command1() {
 	// Must call THIS driver's own command3() (not virtual in the base -
 	// see class comment).
@@ -2454,7 +2558,12 @@ int RSound6::command35() {
 		command3();
 		_channels[0].load(loadData(0xA1A));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command35));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand35));
+	return 0;
+}
+
+int RSound6::retryCommand35() {
+	command35();
 	return 0;
 }
 
@@ -2472,7 +2581,12 @@ int RSound6::command36() {
 		_channels[2].load(loadData(0xC42));
 		_channels[3].load(loadData(0xCB3));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command36));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand36));
+	return 0;
+}
+
+int RSound6::retryCommand36() {
+	command36();
 	return 0;
 }
 
@@ -2488,7 +2602,12 @@ int RSound6::command37() {
 		_channels[3].load(loadData(0xE7E));
 		_channels[4].load(loadData(0xF7B));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command37));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand37));
+	return 0;
+}
+
+int RSound6::retryCommand37() {
+	command37();
 	return 0;
 }
 
@@ -2505,7 +2624,12 @@ int RSound6::command38() {
 		_channels[4].load(loadData(0x11B3));
 		_channels[5].load(loadData(0x139C));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command38));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand38));
+	return 0;
+}
+
+int RSound6::retryCommand38() {
+	command38();
 	return 0;
 }
 
@@ -2522,7 +2646,12 @@ int RSound6::command39() {
 		_channels[4].load(loadData(0x17C3));
 		_channels[8].load(loadData(0x18DD));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command39));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand39));
+	return 0;
+}
+
+int RSound6::retryCommand39() {
+	command39();
 	return 0;
 }
 
@@ -2539,7 +2668,12 @@ int RSound6::command40() {
 		_channels[4].load(loadData(0x1D12));
 		_channels[5].load(loadData(0x1E0E));
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command40));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand40));
+	return 0;
+}
+
+int RSound6::retryCommand40() {
+	command40();
 	return 0;
 }
 
@@ -2574,7 +2708,12 @@ int RSound6::command45() {
 		command3();
 		playSoundChannels1To6(0x20AA);
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command45));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand45));
+	return 0;
+}
+
+int RSound6::retryCommand45() {
+	command45();
 	return 0;
 }
 
@@ -2777,7 +2916,12 @@ int RSound6::command96() {
 		playSoundChannels1To6(0x1E70);
 		playSoundChannels1To6(0x1E70);
 	} else
-		scheduleCallback(MAKE_CALLBACK(RSound6, command96));
+		scheduleCallback(MAKE_CALLBACK(RSound6, retryCommand96));
+	return 0;
+}
+
+int RSound6::retryCommand96() {
+	command96();
 	return 0;
 }
 
@@ -2802,11 +2946,12 @@ int RSound6::command98() {
 RSound9::RSound9(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr9", 0x2BC0, 0x52D0, 0x9A) {
 }
 
-void RSound9::callFunction(uint16 offset) {
-	if (offset == 0x23a0)
-		command32();
-	else
-		RSound::callFunction(offset);
+bool RSound9::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x23A0)
+		return false;
+	command32();
+	return true;
 }
 
 int RSound9::command1() {
@@ -3077,25 +3222,15 @@ int RSound9::loadCommand41() {
 }
 
 int RSound9::command53() {
-	// See class comment: this is the unlabeled function directly
-	// following command41() in the disassembly, assigned to index 53 by
-	// elimination.
-	if (!isMusicChannelsActive()) {
-		resetCallbackTimer(84);
-		loadCommand53();
-	} else
-		scheduleCallback(MAKE_CALLBACK(RSound9, loadCommand53));
+	// The native dispatch table maps slot 53 to 0x22DA. It arms a
+	// 1200-poll timer whose callback is command1 at 0x0880.
+	resetCallbackTimer(1200);
+	scheduleCallback(MAKE_CALLBACK(RSound9, command53Callback));
 	return 0;
 }
 
-int RSound9::loadCommand53() {
+int RSound9::command53Callback() {
 	command1();
-	_channels[1].load(loadData(0x33C0));
-	_channels[2].load(loadData(0x3575));
-	_channels[8].load(loadData(0x3686));
-	_channels[4].load(loadData(0x36FF));
-	_channels[5].load(loadData(0x3966));
-	_channels[6].load(loadData(0x3B89));
 	return 0;
 }
 
@@ -3293,7 +3428,7 @@ int RSound9::command63() {
 	return 0;
 }
 
-const RSound9::CommandPtr RSound9::_commandList[96] = {
+const RSound9::CommandPtr RSound9::_commandList[64] = {
 	&RSound9::command0, &RSound9::command1, &RSound9::command2, &RSound9::command3,
 	&RSound9::command4, &RSound9::command5, &RSound9::command6, &RSound9::command7,
 	&RSound9::command8, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
@@ -3309,14 +3444,6 @@ const RSound9::CommandPtr RSound9::_commandList[96] = {
 	&RSound9::command48, &RSound9::command49, &RSound9::command50, &RSound9::command51,
 	&RSound9::command52, &RSound9::command53, &RSound9::command34Or54, &RSound9::command55,
 	&RSound9::nullCommand, &RSound9::command57, &RSound9::command58, &RSound9::command59,
-	&RSound9::command60, &RSound9::command61, &RSound9::command62, &RSound9::command63,
-	&RSound9::command32, &RSound9::command33Or47, &RSound9::command34Or54, &RSound9::command35,
-	&RSound9::command36, &RSound9::command37, &RSound9::command38, &RSound9::command39,
-	&RSound9::command40, &RSound9::command41, &RSound9::command42, &RSound9::command43,
-	&RSound9::nullCommand, &RSound9::command45, &RSound9::command46, &RSound9::command33Or47,
-	&RSound9::command48, &RSound9::command49, &RSound9::command50, &RSound9::command51,
-	&RSound9::command52, &RSound9::command53, &RSound9::command34Or54, &RSound9::command55,
-	&RSound9::nullCommand, &RSound9::command57, &RSound9::command58, &RSound9::command59,
 	&RSound9::command60, &RSound9::command61, &RSound9::command62, &RSound9::command63
 };
 
@@ -3328,6 +3455,772 @@ int RSound9::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
+/*-----------------------------------------------------------------------*/
+/* Dragonsphere demo RSOUND overlays                                     */
+/*-----------------------------------------------------------------------*/
+
+namespace {
+
+enum {
+	kDemo1FileSize = 24095,
+	kDemo1DataOffset = 0x2790,
+	kDemo1InitializedDataSize = 0x368F,
+	kDemo1DeclaredDataSize = 0x3860,
+	kDemo1SysExOffset = 0x00D8,
+	kDemo9FileSize = 29443,
+	kDemo9DataOffset = 0x2EE0,
+	kDemo9InitializedDataSize = 0x4423,
+	kDemo9DeclaredDataSize = 0x4600,
+	kDemo9SysExOffset = 0x00DC
+};
+
+const char *const kDemo1Filename = "rsound.dr1";
+const char *const kDemo1First8192Md5 = "0d1e47f5ffdb1fe21c90c4b9d62f6ac9";
+const char *const kDemo9Filename = "rsound.dr9";
+const char *const kDemo9First8192Md5 = "22bd9dc82ff180b46f02d1f8a94a18c1";
+
+} // namespace
+
+RSoundDemo::RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize, int sysExOffset) :
+		RSound(mixer, filename, dataOffset, dataSize, sysExOffset, true) {
+}
+
+void RSoundDemo::validate() {
+	struct DemoFile {
+		const char *filename;
+		uint32 size;
+		uint32 dataOffset;
+		uint32 initializedDataSize;
+		uint32 declaredDataSize;
+		const char *md5;
+	};
+	const DemoFile files[] = {
+		{ kDemo1Filename, kDemo1FileSize, kDemo1DataOffset,
+			kDemo1InitializedDataSize, kDemo1DeclaredDataSize,
+			kDemo1First8192Md5 },
+		{ kDemo9Filename, kDemo9FileSize, kDemo9DataOffset,
+			kDemo9InitializedDataSize, kDemo9DeclaredDataSize,
+			kDemo9First8192Md5 }
+	};
+
+	for (uint index = 0; index < ARRAYSIZE(files); ++index) {
+		Common::File file;
+		if (!file.open(files[index].filename))
+			error("Could not process - %s", files[index].filename);
+		if ((uint32)file.size() != files[index].size ||
+				files[index].dataOffset + files[index].initializedDataSize != files[index].size ||
+				files[index].initializedDataSize > files[index].declaredDataSize)
+			error("Invalid sound file - %s", files[index].filename);
+
+		file.seek(0);
+		const Common::String md5 = Common::computeStreamMD5AsString(file, 8192);
+		if (md5 != files[index].md5)
+			error("Invalid sound file - %s", files[index].filename);
+	}
+}
+
+int RSoundDemo::executeDemoCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+		return RSound::command0();
+	case 1:
+		return RSound::command1();
+	case 2:
+		return RSound::command2();
+	case 3:
+		return RSound::command3();
+	case 4:
+		return RSound::command4();
+	case 5:
+		return RSound::command5();
+	case 6:
+		return RSound::command6();
+	case 7:
+		return RSound::command7();
+	case 8:
+		return RSound::command8();
+	default:
+		return 0;
+	}
+}
+
+int RSoundDemo::queueDemoMusic(CallbackFunction callback, uint16 counter,
+		uint16 period) {
+	if (isMusicChannelsActive())
+		scheduleCallback(callback);
+	else {
+		resetCallbackTimerEx(counter, period);
+		(this->*callback)();
+	}
+	return 0;
+}
+
+Channel *RSoundDemo::playDemoSoundAny(int offset) {
+	return playSoundData(loadData(offset), 0, 8, 8);
+}
+
+/*-----------------------------------------------------------------------*/
+/* RSoundDemo1                                                            */
+/*-----------------------------------------------------------------------*/
+
+RSoundDemo1::RSoundDemo1(Audio::Mixer *mixer) :
+		RSoundDemo(mixer, kDemo1Filename, kDemo1DataOffset,
+				kDemo1DeclaredDataSize, kDemo1SysExOffset) {
+	_usesImmediateArithmeticOperands = true;
+
+	// The MZ image declares a zero-initialized tail which is absent from
+	// the file. Supply it explicitly rather than relying on array growth.
+	for (uint offset = kDemo1InitializedDataSize; offset < kDemo1DeclaredDataSize; ++offset)
+		_soundData[offset] = 0;
+}
+
+bool RSoundDemo1::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	switch (targetOffset) {
+	case 0x1B50:
+		command16();
+		return true;
+	case 0x1B90:
+		command32();
+		return true;
+	case 0x1C10:
+		command34();
+		return true;
+	case 0x1D76:
+		command40();
+		return true;
+	case 0x1DBE:
+		command41();
+		return true;
+	default:
+		return false;
+	}
+}
+
+int RSoundDemo1::command16() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand16), 0x90, 0x90);
+}
+
+int RSoundDemo1::loadCommand16() {
+	command1();
+	_channels[0].load(loadData(0x0AC3));
+	_channels[1].load(loadData(0x0B3A));
+	_channels[2].load(loadData(0x0BAD));
+	return 0;
+}
+
+int RSoundDemo1::command32() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand32), 0xB0, 0xB0);
+}
+
+int RSoundDemo1::loadCommand32() {
+	command1();
+	_channels[0].load(loadData(0x0C00));
+	_channels[1].load(loadData(0x0C46));
+	_channels[2].load(loadData(0x0CA5));
+	_channels[3].load(loadData(0x0CC2));
+	return 0;
+}
+
+int RSoundDemo1::command33() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand33), 0xB0, 0xB0);
+}
+
+int RSoundDemo1::loadCommand33() {
+	command1();
+	_channels[0].load(loadData(0x0CE8));
+	_channels[1].load(loadData(0x0D8A));
+	_channels[2].load(loadData(0x0DE9));
+	_channels[3].load(loadData(0x0E0C));
+	return 0;
+}
+
+int RSoundDemo1::command34() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand34), 0x50, 0x50);
+}
+
+int RSoundDemo1::loadCommand34() {
+	command1();
+	_channels[0].load(loadData(0x0E82));
+	_channels[1].load(loadData(0x0EB8));
+	_channels[2].load(loadData(0x0F27));
+	return 0;
+}
+
+int RSoundDemo1::command35() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand35), 0x60, 0x60);
+}
+
+int RSoundDemo1::loadCommand35() {
+	command1();
+	_channels[0].load(loadData(0x0FC0));
+	_channels[1].load(loadData(0x1055));
+	_channels[2].load(loadData(0x10DA));
+	_channels[3].load(loadData(0x1125));
+	return 0;
+}
+
+int RSoundDemo1::command36() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand36), 0x80, 0x80);
+}
+
+int RSoundDemo1::loadCommand36() {
+	command1();
+	_channels[0].load(loadData(0x114C));
+	_channels[1].load(loadData(0x120E));
+	_channels[2].load(loadData(0x12F9));
+	_channels[3].load(loadData(0x138C));
+	return 0;
+}
+
+int RSoundDemo1::command37() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand37), 0xC0, 0xC0);
+}
+
+int RSoundDemo1::loadCommand37() {
+	command1();
+	_channels[0].load(loadData(0x13F2));
+	_channels[1].load(loadData(0x144D));
+	return 0;
+}
+
+int RSoundDemo1::command38() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand38), 0x60, 0x60);
+}
+
+int RSoundDemo1::loadCommand38() {
+	command1();
+	_channels[0].load(loadData(0x14B4));
+	_channels[1].load(loadData(0x1553));
+	_channels[2].load(loadData(0x1627));
+	_channels[3].load(loadData(0x1712));
+	return 0;
+}
+
+int RSoundDemo1::command39() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand39), 0xB0, 0xB0);
+}
+
+int RSoundDemo1::loadCommand39() {
+	command1();
+	_channels[0].load(loadData(0x17FA));
+	_channels[1].load(loadData(0x1850));
+	_channels[2].load(loadData(0x18AF));
+	return 0;
+}
+
+int RSoundDemo1::command40() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand40), 0xA8, 0xA8);
+}
+
+int RSoundDemo1::loadCommand40() {
+	command1();
+	_channels[0].load(loadData(0x1906));
+	_channels[1].load(loadData(0x1A74));
+	_channels[2].load(loadData(0x1C1F));
+	_channels[3].load(loadData(0x1EE2));
+	_channels[4].load(loadData(0x20DD));
+	return 0;
+}
+
+int RSoundDemo1::command41() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand41), 0x90, 0x90);
+}
+
+int RSoundDemo1::loadCommand41() {
+	command1();
+	_channels[0].load(loadData(0x2376));
+	_channels[1].load(loadData(0x23E4));
+	_channels[8].load(loadData(0x245B));
+	_channels[3].load(loadData(0x24B0));
+	_channels[4].load(loadData(0x2589));
+	return 0;
+}
+
+int RSoundDemo1::command42() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand42), 0x90, 0x90);
+}
+
+int RSoundDemo1::loadCommand42() {
+	command1();
+	_channels[0].load(loadData(0x25D8));
+	_channels[1].load(loadData(0x2603));
+	_channels[2].load(loadData(0x2634));
+	_channels[3].load(loadData(0x2671));
+	_channels[4].load(loadData(0x26A8));
+	return 0;
+}
+
+int RSoundDemo1::command43() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand43), 0x50, 0x50);
+}
+
+int RSoundDemo1::loadCommand43() {
+	command1();
+	_channels[0].load(loadData(0x27CA));
+	_channels[1].load(loadData(0x2838));
+	_channels[2].load(loadData(0x284E));
+	return 0;
+}
+
+int RSoundDemo1::command44() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand44), 0x60, 0xE0);
+}
+
+int RSoundDemo1::loadCommand44() {
+	command1();
+	_channels[0].load(loadData(0x2864));
+	_channels[1].load(loadData(0x28A2));
+	_channels[2].load(loadData(0x28E7));
+	_channels[3].load(loadData(0x290F));
+	_channels[4].load(loadData(0x2A8B));
+	_channels[5].load(loadData(0x2B4B));
+	return 0;
+}
+
+int RSoundDemo1::command45() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand45), 0x60, 0x60);
+}
+
+int RSoundDemo1::loadCommand45() {
+	command1();
+	_channels[0].load(loadData(0x2BBC));
+	_channels[1].load(loadData(0x2D26));
+	_channels[2].load(loadData(0x2E0C));
+	_channels[3].load(loadData(0x2F07));
+	_channels[4].load(loadData(0x2FF5));
+	return 0;
+}
+
+int RSoundDemo1::command92() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo1, loadCommand92), 0x54, 0x54);
+}
+
+int RSoundDemo1::loadCommand92() {
+	command1();
+	_channels[0].load(loadData(0x26C4));
+	_channels[1].load(loadData(0x2710));
+	_channels[2].load(loadData(0x2755));
+	_channels[3].load(loadData(0x277C));
+	_channels[4].load(loadData(0x27A5));
+	return 0;
+}
+
+int RSoundDemo1::command(int commandId, int param) {
+	_commandParam = param;
+	if (commandId >= 0 && commandId <= 8)
+		return executeDemoCommonCommand(commandId);
+
+	switch (commandId) {
+	case 16:
+		return command16();
+	case 24:
+		playSoundChannels6to8(0x3434);
+		playSoundChannels6to8(0x344B);
+		break;
+	case 25:
+		playSoundChannels6to8(0x345F);
+		playSoundChannels6to8(0x3476);
+		break;
+	case 26:
+		playSoundChannels6to8(0x348A);
+		break;
+	case 27:
+		playSoundChannels6to8(0x3494);
+		break;
+	case 28:
+		playSoundChannels6to8(0x34B0);
+		break;
+	case 29:
+		playSoundChannels6to8(0x34CA);
+		break;
+	case 32:
+		return command32();
+	case 33:
+		return command33();
+	case 34:
+		return command34();
+	case 35:
+		return command35();
+	case 36:
+		return command36();
+	case 37:
+		return command37();
+	case 38:
+		return command38();
+	case 39:
+		return command39();
+	case 40:
+		return command40();
+	case 41:
+		return command41();
+	case 42:
+		return command42();
+	case 43:
+		return command43();
+	case 44:
+		return command44();
+	case 45:
+		return command45();
+	case 64:
+		playSoundChannels6to8(0x318C);
+		playSoundChannels6to8(0x3192);
+		playSoundChannels6to8(0x3198);
+		break;
+	case 65:
+		playSoundChannels6to8(0x31BD);
+		break;
+	case 66:
+		playSoundChannels6to8(0x31CD);
+		break;
+	case 67:
+		playSoundChannels6to8(0x31F1);
+		break;
+	case 68:
+		playSoundChannels6to8(0x321F);
+		break;
+	case 69:
+		playSoundChannels6to8(0x322B);
+		break;
+	case 70:
+		playSoundChannels6to8(0x324D);
+		break;
+	case 71:
+		playSoundChannels6to8(0x3263);
+		playSoundChannels6to8(0x3263);
+		playSoundChannels6to8(0x3263);
+		break;
+	case 72:
+		playSoundChannels6to8(0x3265);
+		break;
+	case 73:
+		playSoundChannels6to8(0x3267);
+		break;
+	case 74:
+		playSoundChannels6to8(0x3277);
+		break;
+	case 75:
+		playSoundChannels6to8(0x329B);
+		break;
+	case 76:
+		playSoundChannels6to8(0x32B7);
+		break;
+	case 77:
+		playSoundChannels6to8(0x32C3);
+		playSoundChannels6to8(0x32CF);
+		playSoundChannels6to8(0x32DB);
+		break;
+	case 78:
+		playSoundChannels6to8(0x32E7);
+		break;
+	case 79:
+		playSoundChannels6to8(0x3303);
+		break;
+	case 80:
+		playSoundChannels6to8(0x331C);
+		break;
+	case 81:
+		playSoundChannels6to8(0x331E);
+		break;
+	case 82:
+		playSoundChannels6to8(0x3320);
+		break;
+	case 83:
+		playSoundChannels6to8(0x3322);
+		break;
+	case 84:
+		playSoundChannels6to8(0x333E);
+		break;
+	case 85:
+		playSoundChannels6to8(0x3348);
+		break;
+	case 86:
+		playSoundChannels6to8(0x336A);
+		break;
+	case 87:
+		playSoundChannels6to8(0x338F);
+		break;
+	case 88:
+		playSoundChannels6to8(0x33A9);
+		break;
+	case 89:
+		playSoundChannels6to8(0x33C2);
+		break;
+	case 90:
+		playSoundChannels6to8(0x33D4);
+		break;
+	case 91:
+		playSoundChannels6to8(0x33FE);
+		break;
+	case 92:
+		return command92();
+	default:
+		break;
+	}
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+/* RSoundDemo9                                                            */
+/*-----------------------------------------------------------------------*/
+
+RSoundDemo9::RSoundDemo9(Audio::Mixer *mixer) :
+		RSoundDemo(mixer, kDemo9Filename, kDemo9DataOffset,
+				kDemo9DeclaredDataSize, kDemo9SysExOffset) {
+	// See RSoundDemo1: this range is the overlay's declared BSS tail.
+	for (uint offset = kDemo9InitializedDataSize; offset < kDemo9DeclaredDataSize; ++offset)
+		_soundData[offset] = 0;
+}
+
+int RSoundDemo9::command32() {
+	resetCallbackTimerEx(0x62, 0x54);
+	command1();
+	_channels[0].load(loadData(0x05B4));
+	_channels[1].load(loadData(0x05DA));
+	_channels[2].load(loadData(0x0694));
+	_channels[3].load(loadData(0x06CC));
+	_channels[4].load(loadData(0x0800));
+	_channels[8].load(loadData(0x08EA));
+	return 0;
+}
+
+int RSoundDemo9::command33Or47() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand33Or47),
+			0x62, 0x54);
+}
+
+int RSoundDemo9::loadCommand33Or47() {
+	command1();
+	_channels[0].load(loadData(0x093C));
+	_channels[1].load(loadData(0x099A));
+	_channels[2].load(loadData(0x09D0));
+	_channels[8].load(loadData(0x0A8E));
+	_channels[4].load(loadData(0x0ADA));
+	_channels[5].load(loadData(0x0B38));
+	_channels[6].load(loadData(0x0C9A));
+	return 0;
+}
+
+int RSoundDemo9::command34() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand34), 0x38, 0x38);
+}
+
+int RSoundDemo9::loadCommand34() {
+	command1();
+	_channels[0].load(loadData(0x0D74));
+	_channels[1].load(loadData(0x0F63));
+	_channels[2].load(loadData(0x1162));
+	_channels[4].load(loadData(0x14C6));
+	_channels[5].load(loadData(0x15FB));
+	_channels[6].load(loadData(0x1646));
+	_channels[8].load(loadData(0x1361));
+	return 0;
+}
+
+int RSoundDemo9::command35() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand35), 0x50, 0x50);
+}
+
+int RSoundDemo9::loadCommand35() {
+	command1();
+	_channels[0].load(loadData(0x169A));
+	_channels[1].load(loadData(0x1706));
+	_channels[2].load(loadData(0x1732));
+	_channels[3].load(loadData(0x17F6));
+	_channels[8].load(loadData(0x1864));
+	_channels[5].load(loadData(0x1886));
+	_channels[6].load(loadData(0x1902));
+	return 0;
+}
+
+int RSoundDemo9::command36() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand36), 0x28, 0x28);
+}
+
+int RSoundDemo9::loadCommand36() {
+	command1();
+	_channels[0].load(loadData(0x1A64));
+	_channels[1].load(loadData(0x1AD8));
+	_channels[2].load(loadData(0x1B52));
+	_channels[3].load(loadData(0x1B84));
+	_channels[4].load(loadData(0x1E3A));
+	_channels[5].load(loadData(0x1EA4));
+	_channels[6].load(loadData(0x1F0E));
+	return 0;
+}
+
+int RSoundDemo9::command37() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand37), 0x50, 0x50);
+}
+
+int RSoundDemo9::loadCommand37() {
+	command1();
+	_channels[0].load(loadData(0x1F4E));
+	_channels[1].load(loadData(0x1FDE));
+	_channels[2].load(loadData(0x2072));
+	_channels[3].load(loadData(0x20B4));
+	_channels[8].load(loadData(0x2186));
+	_channels[5].load(loadData(0x21E8));
+	return 0;
+}
+
+int RSoundDemo9::command38() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand38), 0x28, 0x28);
+}
+
+int RSoundDemo9::loadCommand38() {
+	command1();
+	_channels[0].load(loadData(0x231C));
+	_channels[1].load(loadData(0x2394));
+	_channels[2].load(loadData(0x2412));
+	_channels[3].load(loadData(0x1B84));
+	_channels[4].load(loadData(0x2444));
+	_channels[8].load(loadData(0x24A6));
+	_channels[6].load(loadData(0x2506));
+	return 0;
+}
+
+int RSoundDemo9::command39() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand39), 0x28, 0x28);
+}
+
+int RSoundDemo9::loadCommand39() {
+	command1();
+	_channels[0].load(loadData(0x2542));
+	_channels[1].load(loadData(0x25CE));
+	_channels[2].load(loadData(0x2660));
+	_channels[3].load(loadData(0x275A));
+	_channels[4].load(loadData(0x2BD4));
+	_channels[8].load(loadData(0x2C6C));
+	_channels[6].load(loadData(0x2CB0));
+	return 0;
+}
+
+int RSoundDemo9::command40() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand40), 0x38, 0x38);
+}
+
+int RSoundDemo9::loadCommand40() {
+	command1();
+	_channels[0].load(loadData(0x0D74));
+	_channels[1].load(loadData(0x0F63));
+	_channels[2].load(loadData(0x1162));
+	_channels[8].load(loadData(0x2DAA));
+	_channels[4].load(loadData(0x2E9E));
+	_channels[5].load(loadData(0x2F7D));
+	_channels[6].load(loadData(0x2FB5));
+	return 0;
+}
+
+int RSoundDemo9::command41() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand41), 0x54, 0x54);
+}
+
+int RSoundDemo9::loadCommand41() {
+	command1();
+	_channels[0].load(loadData(0x2FDE));
+	_channels[2].load(loadData(0x34AA));
+	_channels[8].load(loadData(0x364F));
+	_channels[4].load(loadData(0x36E4));
+	_channels[5].load(loadData(0x37BD));
+	_channels[6].load(loadData(0x3A40));
+	return 0;
+}
+
+int RSoundDemo9::command42() {
+	return queueDemoMusic(MAKE_CALLBACK(RSoundDemo9, loadCommand42), 0xA8, 0x50);
+}
+
+int RSoundDemo9::loadCommand42() {
+	command1();
+	_channels[0].load(loadData(0x3027));
+	_channels[1].load(loadData(0x307B));
+	_channels[2].load(loadData(0x3523));
+	_channels[8].load(loadData(0x367E));
+	_channels[4].load(loadData(0x371B));
+	_channels[5].load(loadData(0x38A0));
+	_channels[6].load(loadData(0x3ADD));
+	return 0;
+}
+
+int RSoundDemo9::command43() {
+	resetCallbackTimer(0x14);
+	command1();
+	_channels[0].load(loadData(0x3C3A));
+	_channels[1].load(loadData(0x3C94));
+	_channels[2].load(loadData(0x3CDC));
+	_channels[3].load(loadData(0x3FAE));
+	return 0;
+}
+
+int RSoundDemo9::command44() {
+	command1();
+	_channels[0].load(loadData(0x4110));
+	_channels[1].load(loadData(0x4148));
+	_channels[2].load(loadData(0x417E));
+	_channels[3].load(loadData(0x41AA));
+	_channels[8].load(loadData(0x41C6));
+	_channels[5].load(loadData(0x41EE));
+	return 0;
+}
+
+int RSoundDemo9::command(int commandId, int param) {
+	_commandParam = param;
+	if (commandId >= 0 && commandId <= 8)
+		return executeDemoCommonCommand(commandId);
+
+	switch (commandId) {
+	case 32:
+		return command32();
+	case 33:
+	case 47:
+		return command33Or47();
+	case 34:
+		return command34();
+	case 35:
+		return command35();
+	case 36:
+		return command36();
+	case 37:
+		return command37();
+	case 38:
+		return command38();
+	case 39:
+		return command39();
+	case 40:
+		return command40();
+	case 41:
+		return command41();
+	case 42:
+		return command42();
+	case 43:
+		return command43();
+	case 44:
+		return command44();
+	case 45:
+		playDemoSoundAny(0x3FBA);
+		playDemoSoundAny(0x401A);
+		break;
+	case 46:
+		playDemoSoundAny(0x4065);
+		playDemoSoundAny(0x40BA);
+		break;
+	case 48:
+		playSoundChannels6to8(0x424A);
+		break;
+	case 49:
+		playSoundChannels6to8(0x4280);
+		break;
+	case 50:
+		playSoundChannels6to8(0x4274);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
 } // namespace Sound
 } // namespace Dragonsphere
 } // namespace MADS
diff --git a/engines/mads/dragonsphere/sound/rsound_dragonsphere.h b/engines/mads/dragonsphere/sound/rsound_dragonsphere.h
index 15fec9756dc..eaf32ed93fe 100644
--- a/engines/mads/dragonsphere/sound/rsound_dragonsphere.h
+++ b/engines/mads/dragonsphere/sound/rsound_dragonsphere.h
@@ -58,6 +58,7 @@ private:
 
 	int command16();
 	int loadCommand16();
+	int loadCallback2478();
 
 	int command17();
 
@@ -103,7 +104,7 @@ private:
 	void command43_48Tail(byte variant);
 	int command43();
 	int command48();
-	int loadCommand43_48();
+	void loadCommand43_48();
 
 	int command44();
 	int loadCommand44();
@@ -149,6 +150,7 @@ private:
 	int command99();
 	int command100();
 	int command101();
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
 
 public:
 	RSound1(Audio::Mixer *mixer);
@@ -310,6 +312,7 @@ private:
 	int command71();
 	int command72();
 	int command73();
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
 
 public:
 	RSound3(Audio::Mixer *mixer);
@@ -421,6 +424,7 @@ private:
 	int command76();
 	int command77();
 	int command78();
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
 
 public:
 	RSound4(Audio::Mixer *mixer);
@@ -466,6 +470,7 @@ private:
 	int command16();
 	int loadCommand16A();
 	int loadCommand16B();
+	void dispatchCommand16B();
 
 	int command17();
 	int command18();
@@ -520,6 +525,7 @@ private:
 	/** Uses _commandParam: if 0, conditionally redirects channel 8's inner loop pointer; otherwise writes a clamped 7-bit value into the sound data at offset 0x20D6 (11 bytes into the block about to be played) and gate-loads channel 8. */
 	int command77();
 	int command78();
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
 
 public:
 	RSound5(Audio::Mixer *mixer);
@@ -561,6 +567,8 @@ public:
  */
 class RSound6 : public RSound {
 private:
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+
 	int command1();
 	int command2();
 	int command3();
@@ -631,16 +639,23 @@ private:
 	void command34LoadRestOnly();
 
 	int command35();
+	int retryCommand35();
 
 	int command36();
+	int retryCommand36();
 	int command37();
+	int retryCommand37();
 	int command38();
+	int retryCommand38();
 	int command39();
+	int retryCommand39();
 	int command40();
+	int retryCommand40();
 
 	int command44();
 	int loadCommand44();
 	int command45();
+	int retryCommand45();
 
 	int command64();
 	int command65();
@@ -675,6 +690,7 @@ private:
 	int command94();
 	int command95();
 	int command96();
+	int retryCommand96();
 	int command97();
 	int command98();
 
@@ -712,17 +728,13 @@ public:
  *   commands 32-63  (this class); 44,56 confirmed nullsub_2. Two shared
  *                    handlers: commands 33 and 47 point to the exact
  *                    same function (command33Or47()); commands 34 and
- *                    54 likewise (command34Or54()). command53's body is
- *                    an unlabeled function in the disassembly (no proc
- *                    name/index shown) - assigned to index 53 by
- *                    elimination (the only index in this range with no
- *                    other confirmed body), not from an explicit label;
- *                    flag if that's wrong.
- *   commands 64-95  CONFIRMED by the user to be an exact duplicate of
- *                    the 32-63 table (index 64+N behaves identically to
- *                    index 32+N) - the flat _commandList[] below just
- *                    reuses the same function pointers for that range,
- *                    no separate implementation needed.
+ *                    54 likewise (command34Or54()). Command 53 maps
+ *                    directly to 0x22DA, which schedules command1 after
+ *                    1200 polls.
+ *   commands 64+    rejected. The native dispatcher enters a fifth
+ *                    bucket at command 64, but its maximum-command word
+ *                    is zero, so every command in that bucket is above
+ *                    the accepted maximum.
  */
 class RSound9 : public RSound {
 private:
@@ -769,13 +781,8 @@ private:
 	int command41();
 	int loadCommand41();
 
-	/**
-	 * Matches an unlabeled function immediately following command41() in
-	 * the disassembly - see class comment re: its index being inferred
-	 * by elimination.
-	 */
 	int command53();
-	int loadCommand53();
+	int command53Callback();
 
 	int command42();
 	int loadCommand42();
@@ -819,18 +826,106 @@ private:
 	int command63();
 
 	typedef int (RSound9:: *CommandPtr)();
-	static const CommandPtr _commandList[96];
+	static const CommandPtr _commandList[64];
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+
+public:
+	RSound9(Audio::Mixer *mixer);
 
+	int command(int commandId, int param) override;
+};
+
+/**
+ * Shared support for the Dragonsphere demo RSOUND overlays.
+ *
+ * The demos use the retail game's bytecode interpreter and host cadence, but
+ * have their own overlay layouts, dispatch tables and embedded sequence roots.
+ */
+class RSoundDemo : public RSound {
 protected:
-	/**
-	 * Calls a function at a fixed offset within the sound driver.
-	 * @param offset		Offset of the function
-	 */
-	void callFunction(uint16 offset) override;
+	RSoundDemo(Audio::Mixer *mixer, const Common::Path &filename,
+			int dataOffset, int dataSize, int sysExOffset);
+
+	int executeDemoCommonCommand(int commandId);
+	int queueDemoMusic(CallbackFunction callback, uint16 counter,
+			uint16 period);
+	Channel *playDemoSoundAny(int offset);
 
 public:
-	RSound9(Audio::Mixer *mixer);
+	static void validate();
+};
+
+/** Demo RSOUND.DR1: `RLND DragonS07/21/93`. */
+class RSoundDemo1 : public RSoundDemo {
+private:
+	int command16();
+	int loadCommand16();
+	int command32();
+	int loadCommand32();
+	int command33();
+	int loadCommand33();
+	int command34();
+	int loadCommand34();
+	int command35();
+	int loadCommand35();
+	int command36();
+	int loadCommand36();
+	int command37();
+	int loadCommand37();
+	int command38();
+	int loadCommand38();
+	int command39();
+	int loadCommand39();
+	int command40();
+	int loadCommand40();
+	int command41();
+	int loadCommand41();
+	int command42();
+	int loadCommand42();
+	int command43();
+	int loadCommand43();
+	int command44();
+	int loadCommand44();
+	int command45();
+	int loadCommand45();
+	int command92();
+	int loadCommand92();
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
 
+public:
+	explicit RSoundDemo1(Audio::Mixer *mixer);
+	int command(int commandId, int param) override;
+};
+
+/** Demo RSOUND.DR9: `RLND DragonS07/30/93`. */
+class RSoundDemo9 : public RSoundDemo {
+private:
+	int command32();
+	int command33Or47();
+	int loadCommand33Or47();
+	int command34();
+	int loadCommand34();
+	int command35();
+	int loadCommand35();
+	int command36();
+	int loadCommand36();
+	int command37();
+	int loadCommand37();
+	int command38();
+	int loadCommand38();
+	int command39();
+	int loadCommand39();
+	int command40();
+	int loadCommand40();
+	int command41();
+	int loadCommand41();
+	int command42();
+	int loadCommand42();
+	int command43();
+	int command44();
+
+public:
+	explicit RSoundDemo9(Audio::Mixer *mixer);
 	int command(int commandId, int param) override;
 };
 
diff --git a/engines/mads/dragonsphere/sound/sound.cpp b/engines/mads/dragonsphere/sound/sound.cpp
index 64242e1fa9a..3d466842372 100644
--- a/engines/mads/dragonsphere/sound/sound.cpp
+++ b/engines/mads/dragonsphere/sound/sound.cpp
@@ -28,8 +28,11 @@ namespace Dragonsphere {
 namespace Sound {
 
 void DragonSoundManager::validate() {
-	if (_driverType == SOUND_MT32 && !_isDemo) {
-		RSound::validate();
+	if (_driverType == SOUND_MT32) {
+		if (_isDemo)
+			RSoundDemo::validate();
+		else
+			RSound::validate();
 	} else {
 		ASound::validate(_isDemo);
 	}
@@ -38,8 +41,23 @@ void DragonSoundManager::validate() {
 void DragonSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_driverType == SOUND_MT32 && !_isDemo) {
+	if (_driverType == SOUND_MT32) {
 		// Roland MT32 drivers
+		if (_isDemo) {
+			switch (sectionNumber) {
+			case 1:
+				_driver = new RSoundDemo1(_mixer);
+				break;
+			case 9:
+				_driver = new RSoundDemo9(_mixer);
+				break;
+			default:
+				_driver = nullptr;
+				break;
+			}
+			return;
+		}
+
 		switch (sectionNumber) {
 		case 1:
 			_driver = new RSound1(_mixer);


Commit: cecf1a0be4c5c6324bbb4b34c0168f1e53c90d6a
    https://github.com/scummvm/scummvm/commit/cecf1a0be4c5c6324bbb4b34c0168f1e53c90d6a
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: PHANTOM: Add ISOUND PC speaker support

Reimplement the retail PC-speaker overlays with Phantom-specific command
tables and the shared PIT renderer used by Nebular.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/phantom/sound/isound.cpp
  A engines/mads/phantom/sound/isound.h
  A engines/mads/phantom/sound/isound_phantom.cpp
  A engines/mads/phantom/sound/isound_phantom.h
    engines/mads/detection_tables.h
    engines/mads/module.mk
    engines/mads/phantom/sound/sound.cpp


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 65dd7c5160a..5ec9c768ddf 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -193,7 +193,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
@@ -208,7 +208,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 3a9881231d7..73ccdaa48a5 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -278,6 +278,8 @@ MODULE_OBJS := \
 	phantom/rooms/room506.o \
 	phantom/sound/asound.o \
 	phantom/sound/asound_phantom.o \
+	phantom/sound/isound.o \
+	phantom/sound/isound_phantom.o \
 	phantom/sound/rsound.o \
 	phantom/sound/rsound_phantom.o \
 	phantom/sound/sound.o \
diff --git a/engines/mads/phantom/sound/isound.cpp b/engines/mads/phantom/sound/isound.cpp
new file mode 100644
index 00000000000..dd6faffa239
--- /dev/null
+++ b/engines/mads/phantom/sound/isound.cpp
@@ -0,0 +1,883 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "common/endian.h"
+#include "common/file.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/phantom/sound/isound.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+namespace {
+
+void setReason(Common::String *reason, const Common::String &value) {
+	if (reason)
+		*reason = value;
+}
+
+} // namespace
+
+bool ISound::readOverlayLayout(const Common::Path &filename,
+		OverlayLayout &layout, Common::String *reason) {
+	Common::File file;
+	if (!file.open(filename)) {
+		setReason(reason, Common::String::format("could not open %s",
+			filename.toString().c_str()));
+		return false;
+	}
+
+	const int64 fileSize = file.size();
+	if (fileSize < 0x200) {
+		setReason(reason, "overlay is smaller than its DOS MZ header");
+		return false;
+	}
+
+	file.seek(0);
+	if (file.readUint16LE() != 0x5a4d) {
+		setReason(reason, "overlay does not have an MZ signature");
+		return false;
+	}
+
+	const uint16 bytesOnLastPage = file.readUint16LE();
+	const uint16 pageCount = file.readUint16LE();
+	if (!pageCount || bytesOnLastPage >= 512) {
+		setReason(reason, "overlay has invalid MZ size fields");
+		return false;
+	}
+	const uint32 declaredSize = (pageCount - 1) * 512U +
+		(bytesOnLastPage ? bytesOnLastPage : 512U);
+	if (declaredSize != (uint64)fileSize) {
+		setReason(reason, "overlay MZ size does not match the file");
+		return false;
+	}
+
+	file.seek(8);
+	const uint32 imageOffset = file.readUint16LE() * 16U;
+	if (imageOffset < 0x1c || imageOffset + 0x48 > (uint64)fileSize) {
+		setReason(reason, "overlay has an invalid MZ header size");
+		return false;
+	}
+
+	file.seek(imageOffset + 0x2a);
+	const uint32 dataSegmentOffset = file.readUint16LE() * 16U;
+	const uint16 dataSegmentSize = file.readUint16LE();
+	const uint16 nominalTimerHz = file.readUint16LE();
+	const uint16 exportCount = file.readUint16LE();
+	const uint32 dataOffset = imageOffset + dataSegmentOffset;
+	if (dataOffset >= (uint64)fileSize) {
+		setReason(reason, "overlay data segment is outside the file");
+		return false;
+	}
+
+	layout.dataOffset = dataOffset;
+	layout.initializedDataSize = (uint32)(fileSize - dataOffset);
+	layout.dataSegmentSize = dataSegmentSize;
+	if (dataSegmentSize < layout.initializedDataSize ||
+		dataSegmentSize < 0x1a0 || nominalTimerHz != 100 ||
+		exportCount != 11) {
+		setReason(reason, "overlay descriptor is not the supported Phantom ISOUND ABI");
+		return false;
+	}
+
+	return true;
+}
+
+bool ISound::isOverlaySupported(const Common::Path &filename,
+		Common::String *reason) {
+	OverlayLayout layout;
+	return readOverlayLayout(filename, layout, reason);
+}
+
+ISound::ISound(Audio::Mixer *mixer, const Common::Path &filename,
+		const OverlaySpec &spec) :
+		SoundDriver(mixer),
+	_spec(spec),
+	_noiseEnabled(false),
+	_updatesEnabled(false),
+	_streamInvalid(false),
+	_masterVolume(255),
+	_outputRate(kPCSpeakerSampleRate),
+	_hostTimerAccumulator(0),
+	_sequenceServiceCountdown(1),
+	_pitRenderer(_outputRate, kPitClockHz),
+	_frameCounter(0),
+	_randomSeed(0),
+	_pollResult(0),
+	_resultState(0),
+	_priority(0),
+	_sequenceStart(0),
+	_position(0),
+	_innerLoopStart(0),
+	_outerLoopStart(0),
+	_restartOverride(0),
+	_branchReturn(0),
+	_innerLoopCount(0),
+	_outerLoopCount(0),
+	_note(0),
+	_activeTicks(0),
+	_releaseCounter(0),
+	_releaseOverride(0),
+	_gateOffset(0),
+	_transpose(0),
+	_fineOffset(0),
+	_noiseMask(0),
+	_currentDivisor(0),
+	_pitchStep(0),
+	_directDivisor(0),
+	_alternationReload(0),
+	_alternationOffset(0),
+	_alternationCounter(0),
+	_alternationToggle(false),
+	_sweepInitialized(false),
+	_sweepUpper(0),
+	_sweepLower(0),
+	_sweepDirection(1),
+	_tempoShift(0),
+	_tempoTarget(0),
+	_tempoReload(0),
+	_tempoScale(0) {
+	OverlayLayout layout;
+	Common::String reason;
+	if (!readOverlayLayout(filename, layout, &reason))
+		error("Unsupported Phantom ISOUND overlay %s: %s",
+			filename.toString().c_str(), reason.c_str());
+
+	Common::File soundFile;
+	if (!soundFile.open(filename))
+		error("Could not open file - %s", filename.toString().c_str());
+	_soundData.resize(layout.dataSegmentSize);
+	soundFile.seek(layout.dataOffset);
+	soundFile.read(&_soundData[0], layout.initializedDataSize);
+	if (layout.dataSegmentSize > layout.initializedDataSize) {
+		memset(&_soundData[layout.initializedDataSize], 0,
+			layout.dataSegmentSize - layout.initializedDataSize);
+	}
+	memset(_scriptVariables, 0, sizeof(_scriptVariables));
+	if (!readWord(_spec.randomSeedOffset, _randomSeed))
+		_randomSeed = 0x4d2;
+
+	initializeDriver();
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_speakerHandle,
+		this, -1, Audio::Mixer::kMaxChannelVolume, 0,
+		DisposeAfterUse::NO, true);
+}
+
+ISound::~ISound() {
+	_mixer->stopHandle(_speakerHandle);
+}
+
+bool ISound::readByte(uint16 offset, byte &value) {
+	if (offset >= _soundData.size()) {
+		invalidateStream("byte read outside the mutable data segment", offset);
+		return false;
+	}
+	value = _soundData[offset];
+	return true;
+}
+
+bool ISound::readWord(uint16 offset, uint16 &value) {
+	if ((uint32)offset + 1 >= _soundData.size()) {
+		invalidateStream("word read outside the mutable data segment", offset);
+		return false;
+	}
+	value = READ_LE_UINT16(&_soundData[offset]);
+	return true;
+}
+
+bool ISound::writeByte(uint16 offset, byte value) {
+	if (offset >= _soundData.size()) {
+		invalidateStream("byte write outside the mutable data segment", offset);
+		return false;
+	}
+	_soundData[offset] = value;
+	return true;
+}
+
+void ISound::invalidateStream(const char *reason, uint16 offset) {
+	if (!_streamInvalid)
+		warning("Phantom ISOUND stopped invalid stream at 0x%04x: %s",
+			offset, reason);
+	_streamInvalid = true;
+	_activeTicks = 0;
+	_priority = 0;
+	_noiseMask = 0;
+	_pitchStep = 0;
+	_alternationCounter = 0;
+	setResultState(-1);
+	stopSpeaker();
+}
+
+void ISound::initializeDriver() {
+	resetDriver();
+	_position = _spec.nullSequenceOffset;
+	_updatesEnabled = true;
+}
+
+void ISound::resetDriver() {
+	_streamInvalid = false;
+	_sweepInitialized = false;
+	_alternationCounter = 0;
+	_priority = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_noiseMask = 0;
+	_currentDivisor = 0;
+	_pitchStep = 0;
+	_gateOffset = 0;
+	_fineOffset = 0;
+	_activeTicks = 0;
+	_alternationReload = 0;
+	_alternationOffset = 0;
+	stopSpeaker();
+}
+
+int ISound::executeCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+	case 1:
+	case 3:
+	case 4:
+	case 5:
+		resetDriver();
+		return 0;
+	case 2:
+		_position = _spec.nullSequenceOffset;
+		return 0;
+	case 6:
+		_updatesEnabled = false;
+		stopSpeaker();
+		return 0;
+	case 7:
+		_updatesEnabled = true;
+		return 0;
+	case 8:
+		return _activeTicks;
+	default:
+		return 0;
+	}
+}
+
+void ISound::playSequence(uint16 sequenceOffset, byte priority) {
+	const bool wasEnabled = _updatesEnabled;
+	_updatesEnabled = false;
+	if (_priority > priority) {
+		_updatesEnabled = wasEnabled;
+		return;
+	}
+
+	if (sequenceOffset >= _soundData.size()) {
+		invalidateStream("command selected an out-of-range sequence", sequenceOffset);
+		_updatesEnabled = wasEnabled;
+		return;
+	}
+
+	_streamInvalid = false;
+	_priority = priority;
+	_sequenceStart = sequenceOffset;
+	_position = sequenceOffset;
+	_innerLoopStart = sequenceOffset;
+	_outerLoopStart = sequenceOffset;
+	_restartOverride = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_noiseMask = 0;
+	_currentDivisor = 0;
+	_pitchStep = 0;
+	_gateOffset = 0;
+	_fineOffset = 0;
+	_transpose = 0;
+	_alternationReload = 0;
+	_alternationOffset = 0;
+	_activeTicks = 1;
+	_updatesEnabled = wasEnabled;
+}
+
+uint16 ISound::nextRandom() {
+	const uint16 value = (uint16)(0x9248 + _randomSeed);
+	_randomSeed = (uint16)((value >> 3) | (value << 13));
+	return _randomSeed;
+}
+
+void ISound::setResultState(int8 state) {
+	if (_resultState == state)
+		return;
+	_resultState = state;
+	_pollResult = state;
+}
+
+void ISound::processInnerLoop() {
+	byte count;
+	if (!readByte((uint16)(_position + 1), count))
+		return;
+	if (!_innerLoopCount) {
+		if (!count) {
+			_position = (uint16)(_position + 2);
+			_innerLoopStart = _position;
+		} else {
+			_innerLoopCount = count;
+			_position = _innerLoopStart;
+		}
+	} else if (--_innerLoopCount) {
+		_position = _innerLoopStart;
+	} else {
+		_position = (uint16)(_position + 2);
+		_innerLoopStart = _position;
+	}
+}
+
+void ISound::processOuterLoop() {
+	byte count;
+	if (!readByte((uint16)(_position + 1), count))
+		return;
+	if (!_outerLoopCount) {
+		if (!count) {
+			_position = (uint16)(_position + 2);
+			_innerLoopStart = _position;
+			_outerLoopStart = _position;
+			_innerLoopCount = 0;
+		} else {
+			_outerLoopCount = count;
+			_position = _outerLoopStart;
+			_innerLoopStart = _outerLoopStart;
+		}
+	} else if (--_outerLoopCount) {
+		_position = _outerLoopStart;
+		_innerLoopStart = _outerLoopStart;
+	} else {
+		_position = (uint16)(_position + 2);
+		_outerLoopStart = _position;
+		_innerLoopStart = _position;
+	}
+}
+
+bool ISound::calculateNoteDivisor(byte note, uint16 &divisor) {
+	const byte index = (byte)(note + _transpose);
+	uint16 tableValue;
+	if (!readWord((uint16)(_spec.noteTableOffset + (uint16)index * 2),
+			tableValue))
+		return false;
+	divisor = (uint16)(tableValue + _fineOffset);
+	return true;
+}
+
+byte ISound::outputVolume() const {
+	return (byte)((kDefaultOutputVolume * _masterVolume) / 255);
+}
+
+void ISound::outputDivisor(uint16 divisor) {
+	_pitRenderer.writeMode3Count(divisor);
+}
+
+void ISound::startSpeaker() {
+	if (_directDivisor) {
+		_currentDivisor = _directDivisor;
+	} else if (!calculateNoteDivisor(_note, _currentDivisor)) {
+		return;
+	}
+	_directDivisor = 0;
+	_sweepInitialized = false;
+	_alternationToggle = false;
+	outputDivisor(_currentDivisor);
+	_pitRenderer.setControl(true, true);
+}
+
+void ISound::stopSpeaker() {
+	_pitRenderer.setControl(false, false);
+	_sweepInitialized = false;
+}
+
+bool ISound::readControlByte(uint16 delta, byte &value) {
+	return readByte((uint16)(_position + delta), value);
+}
+
+bool ISound::readControlWord(uint16 delta, uint16 &value) {
+	return readWord((uint16)(_position + delta), value);
+}
+
+bool ISound::isScriptVariableValid(byte index) {
+	if (index < kScriptVariableCount)
+		return true;
+	invalidateStream("script variable index is out of range", _position);
+	return false;
+}
+
+bool ISound::transferControl(bool take, bool saveReturn) {
+	uint16 target;
+	if (!readControlWord(3, target))
+		return false;
+	if (take) {
+		if (saveReturn)
+			_branchReturn = (uint16)(_position + 5);
+		_position = target;
+	} else {
+		_position = (uint16)(_position + 5);
+	}
+	return true;
+}
+
+void ISound::processOrdinaryEvent() {
+	if (!readByte(_position, _note) ||
+		!readByte((uint16)(_position + 1), _activeTicks))
+		return;
+	_position = (uint16)(_position + 2);
+	if (!_note || !_activeTicks)
+		stopSpeaker();
+	if (!_activeTicks) {
+		_priority = 0;
+		_pitchStep = 0;
+		_sweepInitialized = false;
+		_alternationCounter = 0;
+		setResultState(-1);
+		return;
+	}
+	if (!_note)
+		return;
+	_releaseCounter = _releaseOverride ? _releaseOverride :
+		(byte)(_activeTicks - _gateOffset);
+	startSpeaker();
+}
+
+bool ISound::processControl(byte opcode) {
+	byte a = 0, b = 0;
+	uint16 w = 0;
+
+	switch (opcode) {
+	case 0xff:
+		processInnerLoop();
+		break;
+	case 0xfe:
+		processOuterLoop();
+		break;
+	case 0xfd:
+		if (_restartOverride) {
+			_sequenceStart = _restartOverride;
+			_position = _restartOverride;
+			_innerLoopStart = _restartOverride;
+			_outerLoopStart = _restartOverride;
+			_restartOverride = 0;
+			_innerLoopCount = 0;
+			_outerLoopCount = 0;
+			_fineOffset = 0;
+			_gateOffset = 0;
+		} else {
+			_position = _sequenceStart;
+		}
+		break;
+	case 0xfc:
+		if (!readControlWord(1, w)) return false;
+		_sequenceStart = _position = _innerLoopStart = _outerLoopStart =
+			_restartOverride = w;
+		break;
+	case 0xfb:
+		if (!readControlWord(1, w)) return false;
+		_position = w;
+		break;
+	case 0xfa:
+		if (!readControlWord(1, w)) return false;
+		_branchReturn = (uint16)(_position + 3);
+		_position = w;
+		break;
+	case 0xf9:
+		if (_branchReturn) {
+			_position = _branchReturn;
+			_branchReturn = 0;
+		} else {
+			++_position;
+		}
+		break;
+	case 0xf8:
+		if (!readControlWord(1, w)) return false;
+		_noiseMask = w;
+		setResultState(_noiseMask ? 1 : -1);
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xf7:
+		if (!readControlByte(1, a)) return false;
+		_gateOffset = a;
+		_releaseOverride = 0;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xf6:
+		if (!readControlByte(1, a)) return false;
+		_releaseOverride = a;
+		_gateOffset = 0;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xf5:
+		if (!readControlWord(1, w)) return false;
+		_pitchStep = w;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xf4:
+	case 0xf1:
+	case 0xf0:
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xf3:
+	case 0xef:
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xf2:
+		if (!readControlByte(1, a)) return false;
+		_fineOffset = (int8)a;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xee:
+		if (!readControlByte(1, a)) return false;
+		_transpose = a;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xed:
+		if (!readControlByte(1, a)) return false;
+		_position = (uint16)((int8)a + 3);
+		break;
+	case 0xec: {
+		if (!readControlByte(1, a) || !a) {
+			invalidateStream("random table has a zero size", _position);
+			return false;
+		}
+		const uint16 base = (uint16)(_position + 2);
+		const uint16 selectedOffset = (uint16)(base +
+			(nextRandom() & 0x7fff) % a);
+		byte selected, target;
+		if (!readByte(selectedOffset, selected) ||
+			!readByte((uint16)(base + a), target) ||
+			!writeByte((uint16)(base + a + target + 1), selected))
+			return false;
+		_position = (uint16)(_position + a + 3);
+		break;
+	}
+	case 0xeb: {
+		if (!readControlByte(1, a) || !readControlByte(2, b)) return false;
+		const int16 range = (int8)b - (int8)a + 1;
+		byte target;
+		if (range <= 0 || !readByte((uint16)(_position + 3), target)) {
+			invalidateStream("random range is invalid", _position);
+			return false;
+		}
+		const byte result = (byte)((nextRandom() & 0x7fff) % range + (int8)a);
+		if (!writeByte((uint16)(_position + 4 + target), result)) return false;
+		_position = (uint16)(_position + 4);
+		break;
+	}
+	case 0xea: {
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+				!isScriptVariableValid(a)) return false;
+		const uint16 base = (uint16)(_position + 3);
+		byte selected, target;
+		if (!readByte((uint16)(base + _scriptVariables[a]), selected) ||
+			!readByte((uint16)(base + b), target) ||
+			!writeByte((uint16)(base + target + 1), selected)) return false;
+		_position = (uint16)(_position + b + 4);
+		break;
+	}
+	case 0xe9:
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+				!isScriptVariableValid(a)) return false;
+		_scriptVariables[a] = b;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xe8:
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+				!isScriptVariableValid(a) || !isScriptVariableValid(b))
+			return false;
+		_scriptVariables[a] = _scriptVariables[b];
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xe7:
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+			!isScriptVariableValid(a) ||
+			!writeByte((uint16)(_position + 3 + b), _scriptVariables[a])) return false;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xe6:
+	case 0xe5:
+		if (!readControlByte(1, a) || !isScriptVariableValid(a)) return false;
+		_scriptVariables[a] += opcode == 0xe6 ? 1 : (byte)-1;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xe4: case 0xe3: case 0xe2: case 0xe1:
+	case 0xe0: case 0xdf: case 0xde: case 0xdd:
+	case 0xdc: case 0xdb: case 0xda: case 0xd9:
+	case 0xd8: case 0xd7: case 0xd6: case 0xd5: {
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+				!isScriptVariableValid(a)) return false;
+		const bool usesVariable = (opcode & 1) != 0;
+		if (usesVariable && !isScriptVariableValid(b)) return false;
+		const byte operand = usesVariable ? _scriptVariables[b] : b;
+		byte &destination = _scriptVariables[a];
+		switch (opcode) {
+		case 0xe4: destination += operand; break;
+		case 0xe3: destination += operand; break;
+		case 0xe2: destination -= operand; break;
+		case 0xe1: destination -= operand; break;
+		case 0xe0: destination = (byte)(destination * operand); break;
+		case 0xdf: destination = (byte)(destination * operand); break;
+		case 0xde: case 0xdd: case 0xdc: case 0xdb:
+			if (!destination) {
+				invalidateStream("division by zero", _position);
+				return false;
+			}
+			if (opcode == 0xde || opcode == 0xdd)
+				destination = (byte)((int8)operand / (int8)destination);
+			else
+				destination = (byte)((int8)operand % (int8)destination);
+			break;
+		case 0xda: destination &= operand; break;
+		case 0xd9: destination &= operand; break;
+		case 0xd8: destination |= operand; break;
+		case 0xd7: destination |= operand; break;
+		case 0xd6: destination ^= operand; break;
+		case 0xd5: destination ^= operand; break;
+		default: break;
+		}
+		_position = (uint16)(_position + 3);
+		break;
+	}
+	case 0xd4: case 0xd3: case 0xd2: case 0xd1:
+	case 0xd0: case 0xcf: case 0xce: case 0xcd:
+	case 0xcc: case 0xcb: case 0xca: case 0xc9:
+	case 0xc8: case 0xc7: case 0xc6: case 0xc5: {
+		if (!readControlByte(1, a) || !readControlByte(2, b) ||
+				!isScriptVariableValid(a)) return false;
+		const bool variablePair =
+			(opcode <= 0xd0 && opcode >= 0xcd) || opcode <= 0xc8;
+		if (variablePair && !isScriptVariableValid(b)) return false;
+		const byte left = variablePair ? _scriptVariables[b] : _scriptVariables[a];
+		const byte right = variablePair ? _scriptVariables[a] : b;
+		bool take = false;
+		switch (opcode) {
+		case 0xd4: case 0xd0: case 0xcc: case 0xc8: take = left == right; break;
+		case 0xd3: case 0xcf: case 0xcb: case 0xc7: take = left != right; break;
+		case 0xd2: case 0xca: take = (int8)left < (int8)right; break;
+		case 0xd1: case 0xc9: take = (int8)left > (int8)right; break;
+		case 0xce: case 0xc6: take = left > right; break;
+		case 0xcd: case 0xc5: take = left < right; break;
+		default: break;
+		}
+		if (!transferControl(take, opcode <= 0xcc)) return false;
+		break;
+	}
+	case 0xc4:
+		if (!readControlWord(1, w)) return false;
+		warning("Phantom ISOUND ignored native callback 0x%04x", w);
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xc3:
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xc2:
+		_position = (uint16)(_position + 4);
+		break;
+	case 0xc1:
+		if (!readControlWord(1, w)) return false;
+		_tempoScale = w;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xc0:
+		if (!readControlByte(1, a)) return false;
+		_tempoReload = a;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xbf:
+		if (!readControlWord(1, w)) return false;
+		_tempoTarget = w;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xbe:
+		if (!readControlByte(1, a)) return false;
+		_tempoShift = a;
+		_position = (uint16)(_position + 2);
+		break;
+	case 0xbd:
+		if (!readControlByte(1, a) || !readControlByte(2, b)) return false;
+		_alternationReload = a;
+		_alternationCounter = a;
+		_alternationToggle = false;
+		_alternationOffset = b;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xbc:
+		if (!readControlWord(1, w)) return false;
+		_directDivisor = w;
+		_position = (uint16)(_position + 3);
+		break;
+	case 0xbb:
+		warning("Phantom ISOUND ignored unreachable malformed opcode 0xBB");
+		invalidateStream("malformed native 0xBB jump-table entry", _position);
+		return false;
+	default:
+		invalidateStream("unknown control opcode", _position);
+		return false;
+	}
+
+	return !_streamInvalid;
+}
+
+void ISound::processSequenceTick() {
+	if (!_activeTicks || _streamInvalid)
+		return;
+	if (_releaseCounter && !--_releaseCounter)
+		stopSpeaker();
+	if (--_activeTicks)
+		return;
+
+	for (uint operation = 0; operation < kMaxOperationsPerTick; ++operation) {
+		byte opcode;
+		if (!readByte(_position, opcode))
+			return;
+		if (opcode <= 0xba) {
+			processOrdinaryEvent();
+			return;
+		}
+		if (!processControl(opcode))
+			return;
+	}
+
+	invalidateStream("operation limit exceeded", _position);
+}
+
+void ISound::updateAlternation() {
+	_alternationCounter = _alternationReload;
+	byte offset = 0;
+	if (!_alternationToggle) {
+		_alternationToggle = true;
+		offset = _alternationOffset;
+	} else {
+		_alternationToggle = false;
+	}
+	calculateNoteDivisor((byte)(_note + offset), _currentDivisor);
+}
+
+void ISound::updatePitch() {
+	bool alternationChanged = false;
+	if (_alternationCounter && !--_alternationCounter) {
+		updateAlternation();
+		alternationChanged = true;
+	}
+	if (!_pitchStep) {
+		if (alternationChanged)
+			outputDivisor(_currentDivisor);
+		return;
+	}
+
+	if ((_pitchStep & 0xf000) == 0x8000) {
+		const uint16 range = _pitchStep & 0xff;
+		const uint16 step = (_pitchStep >> 8) & 0xf;
+		if (!_sweepInitialized) {
+			_sweepUpper = (uint16)(_currentDivisor + range);
+			_sweepLower = (uint16)(_currentDivisor - range);
+			_sweepInitialized = true;
+		}
+		if ((int16)_currentDivisor > (int16)_sweepUpper)
+			_sweepDirection = -1;
+		else if ((int16)_currentDivisor < (int16)_sweepLower)
+			_sweepDirection = 1;
+		_currentDivisor = (uint16)(_currentDivisor + step * _sweepDirection);
+	} else {
+		_currentDivisor = (uint16)(_currentDivisor - _pitchStep);
+	}
+	outputDivisor(_currentDivisor);
+}
+
+void ISound::update() {
+	if (!_updatesEnabled)
+		return;
+	nextRandom();
+	++_frameCounter;
+	processSequenceTick();
+	updatePitch();
+}
+
+void ISound::timerTick() {
+	update();
+	if (_pollResult) {
+		_noiseEnabled = _pollResult > 0;
+		_pollResult = 0;
+	}
+}
+
+void ISound::noiseTick() {
+	if (!_noiseMask)
+		return;
+	outputDivisor((uint16)((nextRandom() & _noiseMask) + _currentDivisor));
+}
+
+int ISound::poll() {
+	return 0;
+}
+
+void ISound::noise() {
+	Common::StackLock lock(_driverMutex);
+	noiseTick();
+}
+
+int ISound::stop() {
+	Common::StackLock lock(_driverMutex);
+	resetDriver();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void ISound::setVolume(int volume) {
+	Common::StackLock lock(_driverMutex);
+	_masterVolume = CLIP(volume, 0, 255);
+}
+
+int ISound::readBuffer(int16 *buffer, int numSamples) {
+	Common::StackLock lock(_driverMutex);
+	const uint64 serviceThreshold = (uint64)_outputRate *
+		kHostTimerDivisor * kHostServiceDivider;
+	assert(serviceThreshold > kPitClockHz);
+
+	for (int sample = 0; sample < numSamples; ++sample) {
+		const uint64 previousHostTimerAccumulator = _hostTimerAccumulator;
+		_hostTimerAccumulator += kPitClockHz;
+		if (_hostTimerAccumulator >= serviceThreshold) {
+			// Apply host writes at their fractional position inside this output
+			// sample so the shared PIT renderer preserves transition timing.
+			const uint64 servicePosition = serviceThreshold -
+				previousHostTimerAccumulator;
+			assert(servicePosition <= kPitClockHz);
+			_pitRenderer.advanceToSampleFraction(
+				(uint32)servicePosition, kPitClockHz);
+			_hostTimerAccumulator -= serviceThreshold;
+
+			// The native host services noise before polling the sequence VM.
+			// A poll result therefore changes noise on the following service.
+			if (_noiseEnabled)
+				noiseTick();
+			if (!--_sequenceServiceCountdown) {
+				_sequenceServiceCountdown = kSequenceServiceDivider;
+				timerTick();
+			}
+		}
+		buffer[sample] = _pitRenderer.generateSample(outputVolume());
+	}
+	return numSamples;
+}
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/sound/isound.h b/engines/mads/phantom/sound/isound.h
new file mode 100644
index 00000000000..e850c702462
--- /dev/null
+++ b/engines/mads/phantom/sound/isound.h
@@ -0,0 +1,176 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef MADS_PHANTOM_SOUND_ISOUND_H
+#define MADS_PHANTOM_SOUND_ISOUND_H
+
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "mads/core/pcspk_pit.h"
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+/** Native implementation of the Return of the Phantom IBM PC speaker VM. */
+class ISound : public SoundDriver, public Audio::AudioStream {
+public:
+	enum {
+		kPitClockHz = 1193182,
+		kHostTimerDivisor = 0x07a8,
+		kHostServiceDivider = 2,
+		kSequenceServiceDivider = 5,
+		kPCSpeakerSampleRate = 48000,
+		kDefaultOutputVolume = 20,
+		kMaxOperationsPerTick = 1024,
+		kScriptVariableCount = 34
+	};
+
+	struct OverlaySpec {
+		uint16 noteTableOffset;
+		uint16 nullSequenceOffset;
+		uint16 randomSeedOffset;
+	};
+
+protected:
+	struct OverlayLayout {
+		uint32 dataOffset;
+		uint32 initializedDataSize;
+		uint16 dataSegmentSize;
+	};
+
+	Audio::SoundHandle _speakerHandle;
+	OverlaySpec _spec;
+	bool _noiseEnabled;
+	bool _updatesEnabled;
+	bool _streamInvalid;
+	int _masterVolume;
+	int _outputRate;
+	uint64 _hostTimerAccumulator;
+	byte _sequenceServiceCountdown;
+	PCSpeakerPITRenderer _pitRenderer;
+
+	uint16 _frameCounter;
+	uint16 _randomSeed;
+	int16 _pollResult;
+	int8 _resultState;
+
+	byte _priority;
+	uint16 _sequenceStart;
+	uint16 _position;
+	uint16 _innerLoopStart;
+	uint16 _outerLoopStart;
+	uint16 _restartOverride;
+	uint16 _branchReturn;
+	byte _innerLoopCount;
+	byte _outerLoopCount;
+
+	byte _note;
+	byte _activeTicks;
+	byte _releaseCounter;
+	byte _releaseOverride;
+	byte _gateOffset;
+	byte _transpose;
+	int8 _fineOffset;
+
+	uint16 _noiseMask;
+	uint16 _currentDivisor;
+	uint16 _pitchStep;
+	uint16 _directDivisor;
+
+	byte _alternationReload;
+	byte _alternationOffset;
+	byte _alternationCounter;
+	bool _alternationToggle;
+
+	bool _sweepInitialized;
+	uint16 _sweepUpper;
+	uint16 _sweepLower;
+	int16 _sweepDirection;
+
+	byte _scriptVariables[kScriptVariableCount];
+	uint16 _tempoShift;
+	uint16 _tempoTarget;
+	uint16 _tempoReload;
+	uint16 _tempoScale;
+
+	static bool readOverlayLayout(const Common::Path &filename,
+		OverlayLayout &layout, Common::String *reason = nullptr);
+
+	bool readByte(uint16 offset, byte &value);
+	bool readWord(uint16 offset, uint16 &value);
+	bool writeByte(uint16 offset, byte value);
+	void invalidateStream(const char *reason, uint16 offset);
+	bool readControlByte(uint16 delta, byte &value);
+	bool readControlWord(uint16 delta, uint16 &value);
+	bool isScriptVariableValid(byte index);
+	bool transferControl(bool take, bool saveReturn);
+
+	void resetDriver();
+	void initializeDriver();
+	int executeCommonCommand(int commandId);
+	void playSequence(uint16 sequenceOffset, byte priority);
+
+	void update();
+	void timerTick();
+	void noiseTick();
+	void processSequenceTick();
+	bool processControl(byte opcode);
+	void processOrdinaryEvent();
+	void processInnerLoop();
+	void processOuterLoop();
+
+	void updatePitch();
+	void updateAlternation();
+	bool calculateNoteDivisor(byte note, uint16 &divisor);
+	uint16 nextRandom();
+	void setResultState(int8 state);
+
+	void outputDivisor(uint16 divisor);
+	void startSpeaker();
+	void stopSpeaker();
+	byte outputVolume() const;
+
+public:
+	ISound(Audio::Mixer *mixer, const Common::Path &filename,
+		const OverlaySpec &spec);
+	~ISound() override;
+
+	static bool isOverlaySupported(const Common::Path &filename,
+		Common::String *reason = nullptr);
+
+	int stop() override;
+	int poll() override;
+	void noise() override;
+	void setVolume(int volume) override;
+
+	int readBuffer(int16 *buffer, int numSamples) override;
+	bool isStereo() const override { return false; }
+	bool endOfData() const override { return false; }
+	bool endOfStream() const override { return false; }
+	int getRate() const override { return _outputRate; }
+};
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/phantom/sound/isound_phantom.cpp b/engines/mads/phantom/sound/isound_phantom.cpp
new file mode 100644
index 00000000000..c169e6af67e
--- /dev/null
+++ b/engines/mads/phantom/sound/isound_phantom.cpp
@@ -0,0 +1,263 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "common/util.h"
+#include "mads/phantom/sound/isound_phantom.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+namespace {
+
+const ISound::OverlaySpec kEarlyLayout = { 0x00f4, 0x00ce, 0x00dc };
+const ISound::OverlaySpec kLateLayout = { 0x00e0, 0x00ba, 0x00c8 };
+
+struct ISoundSectionDefinition {
+	int sectionNumber;
+	const char *filename;
+	const ISound::OverlaySpec *layout;
+};
+
+const ISoundSectionDefinition kSectionDefinitions[] = {
+	{ 1, "ISOUND.PH1", &kEarlyLayout },
+	{ 2, "ISOUND.PH2", &kEarlyLayout },
+	{ 3, "ISOUND.PH3", &kLateLayout },
+	{ 4, "ISOUND.PH4", &kLateLayout },
+	{ 5, "ISOUND.PH5", &kLateLayout },
+	{ 9, "ISOUND.PH9", &kEarlyLayout }
+};
+
+struct ISoundCommandSequence {
+	byte commandId;
+	uint16 sequenceOffset;
+	byte priority;
+};
+
+struct ISoundCommandTable {
+	int sectionNumber;
+	const ISoundCommandSequence *commands;
+	uint commandCount;
+	const byte *malformedCommands;
+	uint malformedCommandCount;
+};
+
+const ISoundCommandSequence kSection1Commands[] = {
+	{ 24, 0x01d0, 0 }, { 25, 0x0201, 2 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 64, 0x0240, 0 }, { 65, 0x0256, 0 },
+	{ 66, 0x0269, 0 }, { 67, 0x027e, 0 }, { 69, 0x0288, 0 },
+	{ 70, 0x0292, 0 }, { 73, 0x02a3, 0 }, { 74, 0x02d4, 0 },
+	{ 75, 0x02ba, 0 }
+};
+
+const ISoundCommandSequence kSection2Commands[] = {
+	{ 24, 0x01d0, 0 }, { 25, 0x0201, 2 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 36, 0x0240, 0 }, { 37, 0x0255, 0 },
+	{ 38, 0x025f, 0 }, { 39, 0x026f, 0 }, { 64, 0x0240, 0 },
+	{ 65, 0x0255, 0 }, { 66, 0x025f, 0 }, { 67, 0x026f, 0 },
+	{ 70, 0x0279, 0 }, { 71, 0x0283, 0 }, { 72, 0x0291, 0 }
+};
+const byte kSection2MalformedCommands[] = { 76 };
+
+const ISoundCommandSequence kSection3Commands[] = {
+	{ 24, 0x01d0, 0 }, { 25, 0x0201, 2 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 34, 0x02b2, 0 }, { 37, 0x02b2, 0 },
+	{ 38, 0x0240, 0 }, { 39, 0x0272, 0 }, { 64, 0x0240, 0 },
+	{ 65, 0x0272, 0 }, { 66, 0x027c, 0 }, { 67, 0x02a3, 0 },
+	{ 68, 0x02c0, 0 }, { 69, 0x02ce, 0 }, { 72, 0x02e6, 0 },
+	{ 73, 0x02dc, 0 }, { 74, 0x02f5, 0 }, { 75, 0x02ff, 0 }
+};
+
+const ISoundCommandSequence kSection4Commands[] = {
+	{ 24, 0x0240, 0 }, { 25, 0x0240, 0 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 32, 0x0250, 0 }, { 33, 0x025e, 0 },
+	{ 34, 0x026e, 0 }, { 35, 0x0277, 0 }, { 36, 0x0281, 0 },
+	{ 37, 0x028b, 0 }, { 38, 0x0295, 0 }, { 64, 0x0250, 0 },
+	{ 65, 0x025e, 0 }, { 66, 0x026e, 0 }, { 67, 0x0277, 0 },
+	{ 68, 0x0281, 0 }, { 69, 0x028b, 0 }, { 70, 0x0295, 0 }
+};
+const byte kSection4MalformedCommands[] = { 74, 75, 76 };
+
+const ISoundCommandSequence kSection5Commands[] = {
+	{ 24, 0x01d0, 0 }, { 25, 0x0201, 2 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 64, 0x02c4, 0 }, { 65, 0x0240, 0 },
+	{ 66, 0x0242, 0 }, { 67, 0x024c, 0 }, { 68, 0x0256, 0 },
+	{ 69, 0x026c, 0 }, { 70, 0x02a0, 0 }, { 71, 0x02ae, 0 },
+	{ 72, 0x02b8, 0 }, { 73, 0x02c2, 0 }, { 74, 0x02d3, 0 },
+	{ 78, 0x02e4, 0 }
+};
+
+const ISoundCommandSequence kSection9Commands[] = {
+	{ 24, 0x01d0, 0 }, { 25, 0x0201, 2 }, { 26, 0x022b, 4 },
+	{ 27, 0x0235, 6 }, { 64, 0x0240, 0 }, { 65, 0x024e, 2 },
+	{ 67, 0x0258, 6 }, { 68, 0x027f, 8 }, { 70, 0x0289, 12 },
+	{ 71, 0x02a5, 14 }
+};
+
+const ISoundCommandTable kCommandTables[] = {
+	{ 1, kSection1Commands, ARRAYSIZE(kSection1Commands), nullptr, 0 },
+	{ 2, kSection2Commands, ARRAYSIZE(kSection2Commands),
+		kSection2MalformedCommands, ARRAYSIZE(kSection2MalformedCommands) },
+	{ 3, kSection3Commands, ARRAYSIZE(kSection3Commands), nullptr, 0 },
+	{ 4, kSection4Commands, ARRAYSIZE(kSection4Commands),
+		kSection4MalformedCommands, ARRAYSIZE(kSection4MalformedCommands) },
+	{ 5, kSection5Commands, ARRAYSIZE(kSection5Commands), nullptr, 0 },
+	{ 9, kSection9Commands, ARRAYSIZE(kSection9Commands), nullptr, 0 }
+};
+
+const ISoundCommandTable *findCommandTable(int sectionNumber) {
+	for (uint index = 0; index < ARRAYSIZE(kCommandTables); ++index) {
+		if (kCommandTables[index].sectionNumber == sectionNumber)
+			return &kCommandTables[index];
+	}
+	return nullptr;
+}
+
+const ISoundSectionDefinition *findSectionDefinition(int sectionNumber) {
+	for (uint index = 0; index < ARRAYSIZE(kSectionDefinitions); ++index) {
+		if (kSectionDefinitions[index].sectionNumber == sectionNumber)
+			return &kSectionDefinitions[index];
+	}
+	return nullptr;
+}
+
+const ISoundSectionDefinition &getSectionDefinition(int sectionNumber) {
+	const ISoundSectionDefinition *definition = findSectionDefinition(sectionNumber);
+	assert(definition);
+	return *definition;
+}
+
+} // namespace
+
+ISoundSection::CommandDisposition ISoundSection::lookupCommand(
+		int sectionNumber, byte commandId, uint16 &sequenceOffset,
+		byte &priority) {
+	const ISoundCommandTable *table = findCommandTable(sectionNumber);
+	if (table) {
+		for (uint index = 0; index < table->commandCount; ++index) {
+			if (table->commands[index].commandId == commandId) {
+				sequenceOffset = table->commands[index].sequenceOffset;
+				priority = table->commands[index].priority;
+				return kCommandPlaySequence;
+			}
+		}
+		for (uint index = 0; index < table->malformedCommandCount; ++index) {
+			if (table->malformedCommands[index] == commandId)
+				return kCommandMalformed;
+		}
+	}
+	return kCommandUnhandled;
+}
+
+bool ISoundSection::validateSectionLayout(int sectionNumber,
+		const OverlayLayout &layout, Common::String *reason) {
+	const ISoundCommandTable *table = findCommandTable(sectionNumber);
+	if (!table) {
+		if (reason)
+			*reason = "unsupported Phantom ISOUND section";
+		return false;
+	}
+
+	const bool lateLayout = sectionNumber >= 3 && sectionNumber <= 5;
+	const uint16 noteTableOffset = lateLayout ? 0x00e0 : 0x00f4;
+	const uint16 nullSequenceOffset = lateLayout ? 0x00ba : 0x00ce;
+	const uint16 randomSeedOffset = lateLayout ? 0x00c8 : 0x00dc;
+	const uint32 noteTableEnd = noteTableOffset + (0x00ba + 1) * 2U;
+	if (noteTableEnd > layout.initializedDataSize ||
+		(uint32)nullSequenceOffset + 1 >= layout.initializedDataSize ||
+		(uint32)randomSeedOffset + 1 >= layout.initializedDataSize) {
+		if (reason)
+			*reason = "overlay data does not contain the selected section layout";
+		return false;
+	}
+
+	for (uint index = 0; index < table->commandCount; ++index) {
+		if ((uint32)table->commands[index].sequenceOffset + 1 >=
+				layout.initializedDataSize) {
+			if (reason)
+				*reason = "overlay data does not contain a mapped command stream";
+			return false;
+		}
+	}
+	return true;
+}
+
+ISoundSection::ISoundSection(Audio::Mixer *mixer, int sectionNumber) :
+	ISound(mixer, getSectionDefinition(sectionNumber).filename,
+		*getSectionDefinition(sectionNumber).layout),
+	_sectionNumber(sectionNumber) {
+}
+
+bool ISoundSection::isOverlaySupported(int sectionNumber,
+		Common::String *reason) {
+	const ISoundSectionDefinition *definition = findSectionDefinition(sectionNumber);
+	if (!definition) {
+		if (reason)
+			*reason = "unsupported Phantom ISOUND section";
+		return false;
+	}
+
+	OverlayLayout layout;
+	if (!readOverlayLayout(definition->filename, layout, reason))
+		return false;
+	return validateSectionLayout(sectionNumber, layout, reason);
+}
+
+int ISoundSection::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	(void)param;
+	if (commandId >= 0 && commandId <= 8)
+		return executeCommonCommand(commandId);
+
+	uint16 sequenceOffset = 0;
+	byte priority = 0;
+	switch (lookupCommand(_sectionNumber, commandId, sequenceOffset, priority)) {
+	case kCommandPlaySequence:
+		playSequence(sequenceOffset, priority);
+		break;
+	case kCommandMalformed:
+		warning("Phantom ISOUND ignored command %d with a malformed native handler",
+			commandId);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+ISound1::ISound1(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 1) {
+}
+
+ISound2::ISound2(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 2) {
+}
+
+ISound3::ISound3(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 3) {
+}
+
+ISound4::ISound4(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 4) {
+}
+
+ISound5::ISound5(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 5) {
+}
+
+ISound9::ISound9(Audio::Mixer *mixer) :
+	ISoundSection(mixer, 9) {
+}
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/sound/isound_phantom.h b/engines/mads/phantom/sound/isound_phantom.h
new file mode 100644
index 00000000000..27f2c019686
--- /dev/null
+++ b/engines/mads/phantom/sound/isound_phantom.h
@@ -0,0 +1,87 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef MADS_PHANTOM_SOUND_ISOUND_PHANTOM_H
+#define MADS_PHANTOM_SOUND_ISOUND_PHANTOM_H
+
+#include "mads/phantom/sound/isound.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+class ISoundSection : public ISound {
+private:
+	enum CommandDisposition {
+		kCommandUnhandled,
+		kCommandPlaySequence,
+		kCommandMalformed
+	};
+
+	int _sectionNumber;
+
+	static CommandDisposition lookupCommand(int sectionNumber,
+		byte commandId, uint16 &sequenceOffset, byte &priority);
+	static bool validateSectionLayout(int sectionNumber,
+		const OverlayLayout &layout, Common::String *reason);
+
+protected:
+	ISoundSection(Audio::Mixer *mixer, int sectionNumber);
+
+public:
+	static bool isOverlaySupported(int sectionNumber,
+		Common::String *reason = nullptr);
+
+	int command(int commandId, int param) override;
+};
+
+/** ISOUND.PH1: "Phantom  00105-26-93". */
+class ISound1 : public ISoundSection {
+public:
+	ISound1(Audio::Mixer *mixer);
+};
+
+/** ISOUND.PH2: "Phantom  00205-26-93". */
+class ISound2 : public ISoundSection {
+public:
+	ISound2(Audio::Mixer *mixer);
+};
+
+/** ISOUND.PH3: "Phantom  00406-02-93". */
+class ISound3 : public ISoundSection {
+public:
+	ISound3(Audio::Mixer *mixer);
+};
+
+/** ISOUND.PH4: "Phantom  00405-26-93". */
+class ISound4 : public ISoundSection {
+public:
+	ISound4(Audio::Mixer *mixer);
+};
+
+/** ISOUND.PH5: "Phantom  00505-26-93". */
+class ISound5 : public ISoundSection {
+public:
+	ISound5(Audio::Mixer *mixer);
+};
+
+/** ISOUND.PH9: "Phantom  00905-26-93". */
+class ISound9 : public ISoundSection {
+public:
+	ISound9(Audio::Mixer *mixer);
+};
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/phantom/sound/sound.cpp b/engines/mads/phantom/sound/sound.cpp
index 9345ee55a1c..49bb1661dc2 100644
--- a/engines/mads/phantom/sound/sound.cpp
+++ b/engines/mads/phantom/sound/sound.cpp
@@ -21,12 +21,61 @@
 
 #include "mads/phantom/sound/sound.h"
 #include "mads/phantom/sound/asound_phantom.h"
+#include "mads/phantom/sound/isound_phantom.h"
 #include "mads/phantom/sound/rsound_phantom.h"
 
 namespace MADS {
 namespace Phantom {
 namespace Sound {
 
+namespace {
+
+const int kRetailSections[] = { 1, 2, 3, 4, 5, 9 };
+
+Common::Path getISoundFilename(int sectionNumber) {
+	return Common::Path(Common::String::format("ISOUND.PH%d", sectionNumber));
+}
+
+SoundDriver *createASound(Audio::Mixer *mixer, int sectionNumber) {
+	switch (sectionNumber) {
+	case 1:
+		return new ASound1(mixer);
+	case 2:
+		return new ASound2(mixer);
+	case 3:
+		return new ASound3(mixer);
+	case 4:
+		return new ASound4(mixer);
+	case 5:
+		return new ASound5(mixer);
+	case 9:
+		return new ASound9(mixer);
+	default:
+		return nullptr;
+	}
+}
+
+SoundDriver *createISound(Audio::Mixer *mixer, int sectionNumber) {
+	switch (sectionNumber) {
+	case 1:
+		return new ISound1(mixer);
+	case 2:
+		return new ISound2(mixer);
+	case 3:
+		return new ISound3(mixer);
+	case 4:
+		return new ISound4(mixer);
+	case 5:
+		return new ISound5(mixer);
+	case 9:
+		return new ISound9(mixer);
+	default:
+		return nullptr;
+	}
+}
+
+} // namespace
+
 void PhantomSoundManager::validate() {
 	if (_driverType == SOUND_MT32) {
 		if (_isDemo) {
@@ -37,6 +86,19 @@ void PhantomSoundManager::validate() {
 		} else {
 			RSound::validate();
 		}
+	} else if (_driverType == SOUND_PCSPEAKER && !_isDemo) {
+		bool needsAdlibFallback = false;
+		for (uint index = 0; index < ARRAYSIZE(kRetailSections); ++index) {
+			Common::String reason;
+			if (!ISoundSection::isOverlaySupported(kRetailSections[index], &reason)) {
+				const Common::Path filename = getISoundFilename(kRetailSections[index]);
+				warning("Cannot use %s: %s; section %d will use AdLib",
+					filename.toString().c_str(), reason.c_str(), kRetailSections[index]);
+				needsAdlibFallback = true;
+			}
+		}
+		if (needsAdlibFallback)
+			ASound::validate(false);
 	} else {
 		// Adlib
 		ASound::validate(_isDemo);
@@ -75,31 +137,19 @@ void PhantomSoundManager::loadDriver(int sectionNumber) {
 		}
 	} else if (_isDemo) {
 		_driver = new ASoundDemo(_mixer);
+	} else if (_driverType == SOUND_PCSPEAKER) {
+		const Common::Path filename = getISoundFilename(sectionNumber);
+		Common::String reason;
+		if (ISoundSection::isOverlaySupported(sectionNumber, &reason)) {
+			_driver = createISound(_mixer, sectionNumber);
+		} else {
+			warning("Cannot use %s: %s; using AdLib for section %d",
+				filename.toString().c_str(), reason.c_str(), sectionNumber);
+			_driver = createASound(_mixer, sectionNumber);
+		}
 	} else {
 		// Adlib
-		switch (sectionNumber) {
-		case 1:
-			_driver = new ASound1(_mixer);
-			break;
-		case 2:
-			_driver = new ASound2(_mixer);
-			break;
-		case 3:
-			_driver = new ASound3(_mixer);
-			break;
-		case 4:
-			_driver = new ASound4(_mixer);
-			break;
-		case 5:
-			_driver = new ASound5(_mixer);
-			break;
-		case 9:
-			_driver = new ASound9(_mixer);
-			break;
-		default:
-			_driver = nullptr;
-			break;
-		}
+		_driver = createASound(_mixer, sectionNumber);
 	}
 }
 


Commit: 4affc417b9aa323256ad67632e8f7275ed7edf76
    https://github.com/scummvm/scummvm/commit/4affc417b9aa323256ad67632e8f7275ed7edf76
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: NEBULAR: Add PAS16 PSOUND support

Reimplement the verified retail and demo overlays through the maintained
PAS16 OPL3 path, preserving native timing and driver selection.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/nebular/sound/psound.cpp
  A engines/mads/nebular/sound/psound.h
  A engines/mads/nebular/sound/psound_nebular.cpp
  A engines/mads/nebular/sound/psound_nebular.h
    engines/mads/core/sound_manager.h
    engines/mads/detection.h
    engines/mads/detection_tables.h
    engines/mads/metaengine.cpp
    engines/mads/module.mk
    engines/mads/nebular/bonus/bonus.cpp
    engines/mads/nebular/nebular.cpp
    engines/mads/nebular/sound/sound.cpp
    engines/mads/nebular/sound/sound.h


diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index d2da049369f..a6e0a85eab1 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -93,7 +93,7 @@ public:
 
 class SoundManager {
 protected:
-	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_PCSPEAKER };
+	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_PCSPEAKER, SOUND_PAS };
 	Audio::Mixer *_mixer;
 	DriverType _driverType;
 	bool &_soundFlag;
diff --git a/engines/mads/detection.h b/engines/mads/detection.h
index f729e9cb2f4..221995de1aa 100644
--- a/engines/mads/detection.h
+++ b/engines/mads/detection.h
@@ -62,6 +62,7 @@ struct MADSGameDescription {
 #define GAMEOPTION_ORIGINAL_SAVELOAD   GUIO_GAMEOPTIONS7
 
 #define GAMEOPTION_ORIGINAL_MAC_MENUS  GUIO_GAMEOPTIONS8
+#define GAMEOPTION_PAS                 GUIO_GAMEOPTIONS9
 
 } // namespace MADS
 
diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 5ec9c768ddf..69a3b65359c 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -32,9 +32,9 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | GF_INSTALLER,
 #ifdef USE_TTS
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #else
-			GUIO7(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #endif
 		},
 		GType_RexNebular,
@@ -51,9 +51,9 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | GF_INSTALLER,
 #ifdef USE_TTS
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #else
-			GUIO7(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #endif
 		},
 		GType_RexNebular,
@@ -70,9 +70,9 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | GF_INSTALLER,
 #ifdef USE_TTS
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #else
-			GUIO7(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #endif
 		},
 		GType_RexNebular,
@@ -89,9 +89,9 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
 #ifdef USE_TTS
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #else
-			GUIO7(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #endif
 		},
 		GType_RexNebular,
@@ -108,9 +108,9 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
 #ifdef USE_TTS
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_TTS_NARRATOR, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #else
-			GUIO7(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_COPY_PROTECTION, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 #endif
 		},
 		GType_RexNebular,
@@ -141,7 +141,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO8(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_ORIGINAL_SAVELOAD, GUIO_MIDIADLIB, GUIO_MIDIMT32)
+			GUIO9(GUIO_NOSPEECH, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ANIMATED_INVENTORY, GAMEOPTION_ANIMATED_INTERFACE, GAMEOPTION_NAUGHTY_MODE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS, GUIO_MIDIADLIB, GUIO_MIDIMT32)
 		},
 		GType_RexNebular,
 		0
diff --git a/engines/mads/metaengine.cpp b/engines/mads/metaengine.cpp
index b0f6ad9de83..77d447c47cf 100644
--- a/engines/mads/metaengine.cpp
+++ b/engines/mads/metaengine.cpp
@@ -128,6 +128,19 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		}
 	},
 
+	{
+		GAMEOPTION_PAS,
+		{
+			_s("Use Pro Audio Spectrum 16 instead of AdLib"),
+			_s("Use the Pro Audio Spectrum 16 driver for music and sound effects "
+				"instead of the AdLib driver."),
+			"use_pas",
+			false,
+			0,
+			0
+		}
+	},
+
 #ifdef USE_TTS
 	{
 		GAMEOPTION_TTS_NARRATOR,
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 73ccdaa48a5..f0236135403 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -212,6 +212,8 @@ MODULE_OBJS := \
 	nebular/sound/isound.o \
 	nebular/sound/isound_nebular.o \
 	nebular/sound/mac_sound.o \
+	nebular/sound/psound.o \
+	nebular/sound/psound_nebular.o \
 	nebular/sound/rsound.o \
 	nebular/sound/rsound_nebular.o \
 	nebular/sound/sound.o \
diff --git a/engines/mads/nebular/bonus/bonus.cpp b/engines/mads/nebular/bonus/bonus.cpp
index 8bd3f12cef0..0564cdb8ee7 100644
--- a/engines/mads/nebular/bonus/bonus.cpp
+++ b/engines/mads/nebular/bonus/bonus.cpp
@@ -304,7 +304,7 @@ Common::Error BonusEngine::run() {
 	art_hags_are_on_hd = true;
 
 	Sound::RexSoundManager *soundManager =
-			new Sound::RexSoundManager(_mixer, _soundFlag, false);
+			new Sound::RexSoundManager(_mixer, _soundFlag, false, false);
 	_soundManager = soundManager;
 	soundManager->validate();
 
diff --git a/engines/mads/nebular/nebular.cpp b/engines/mads/nebular/nebular.cpp
index 989595a731b..72b42b4307e 100644
--- a/engines/mads/nebular/nebular.cpp
+++ b/engines/mads/nebular/nebular.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "engines/util.h"
+#include "common/config-manager.h"
 #include "mads/core/mps_installer.h"
 #include "mads/core/attr.h"
 #include "mads/core/config.h"
@@ -99,7 +100,9 @@ Common::Error RexNebularEngine::run() {
 			return Common::Error(Common::kNoGameDataFoundError,
 				"Could not open the Macintosh Rex resource files");
 	} else {
-		_soundManager = new Sound::RexSoundManager(_mixer, _soundFlag, isDemo());
+		const bool usePas = ConfMan.getBool("use_pas");
+		_soundManager = new Sound::RexSoundManager(_mixer, _soundFlag,
+			usePas, isDemo());
 	}
 	_soundManager->validate();
 
diff --git a/engines/mads/nebular/sound/psound.cpp b/engines/mads/nebular/sound/psound.cpp
new file mode 100644
index 00000000000..a4818fe6471
--- /dev/null
+++ b/engines/mads/nebular/sound/psound.cpp
@@ -0,0 +1,1174 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "audio/fmopl.h"
+#include "common/endian.h"
+#include "common/file.h"
+#include "common/func.h"
+#include "common/md5.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/nebular/sound/psound.h"
+
+namespace MADS {
+namespace RexNebular {
+namespace Sound {
+
+namespace {
+
+/*
+ * Rex PSOUND is rendered as PAS16/OPL3. The two native logical destinations
+ * map directly to the low and high OPL3 register banks. A write to both
+ * destinations is still emitted twice because OPL3 has no broadcast register.
+ */
+
+/*
+ * The executable, not the PSOUND descriptor's nominal value 100, owns the
+ * driver cadence. Its PIT channel-0 handler retains every second raw interrupt,
+ * calls export 4 on each retained service while enabled, and calls export 3
+ * every fifth service. The OPL callback is only a time source close to that raw
+ * interrupt grid; NativeSoundTimer reconstructs the exact long-term rates with
+ * rational arithmetic.
+ */
+constexpr int kHostCallbackRateHz =
+		NativeSoundTimer::kPitClockHz / NativeSoundTimer::kHostTimerDivisor;
+
+struct PSoundFileSignature {
+	int section;
+	uint32 fileSize;
+	const char *md5;
+};
+
+/*
+ * These signatures cover the first 8192 bytes of each verified PSOUND module,
+ * matching the validation used by the other native sound overlays. Keep the
+ * retail and demo tables separate because they reuse filenames while containing
+ * different binaries and, for demo PSOUND.009, a different table layout.
+ */
+static const PSoundFileSignature kRetailSignatures[] = {
+	{ 1, 0x3786, "49045141642eddc7aa8652554c87d7ca" },
+	{ 2, 0x61a0, "a48086be285c23abd64a73cffb6de9a1" },
+	{ 3, 0x6942, "0c0907533a6dfe5e49973eba845e006b" },
+	{ 4, 0x404f, "988b96242bdd29cb8d52d15a97a5a05c" },
+	{ 5, 0x38e2, "64cfeab1fce320f8941ee5e2808656c4" },
+	{ 6, 0x3a18, "4675c3f172fa7c8c73e1ff3b903f9af6" },
+	{ 7, 0x4339, "3e9c54dccc93c01524ab3ec38b17ae0c" },
+	{ 8, 0x325a, "4721aee44bd56857a8fb01d1245f5e23" },
+	{ 9, 0x936c, "5bfac799abc1149cdfcf73a4a1583a39" }
+};
+
+static const PSoundFileSignature kDemoSignatures[] = {
+	{ 1, 0x449a, "92890dee6887466ee0bbb835934fcc4e" },
+	{ 9, 0x6676, "fa366b53123001e29ba9be9537c44ae2" }
+};
+
+static void validateInitializedRange(uint32 initializedSize, uint32 offset,
+		uint32 length, const char *description) {
+	if (offset > initializedSize || length > initializedSize - offset)
+		error("PSOUND %s outside initialized data: offset 0x%04x, length %u, size 0x%04x",
+				description, (uint)offset, (uint)length, (uint)initializedSize);
+}
+
+} // namespace
+
+static int clampLevel(int value) {
+	return CLIP(value, 0, 63);
+}
+
+static byte panningBits(byte panning) {
+	if (panning < 0x2b)
+		return 0x10;
+	if (panning < 0x55)
+		return 0x30;
+	return 0x20;
+}
+
+void PSound::validate(bool isDemo) {
+	const PSoundFileSignature *signatures = isDemo ?
+			kDemoSignatures : kRetailSignatures;
+	const uint count = isDemo ? ARRAYSIZE(kDemoSignatures) :
+			ARRAYSIZE(kRetailSignatures);
+
+	for (uint index = 0; index < count; ++index) {
+		const PSoundFileSignature &signature = signatures[index];
+		const Common::Path filename(Common::String::format("PSOUND.00%d",
+				signature.section));
+		Common::File file;
+		if (!file.open(filename))
+			error("Could not process - %s", filename.toString().c_str());
+		if ((uint32)file.size() != signature.fileSize)
+			error("Invalid sound file size - %s", filename.toString().c_str());
+
+		file.seek(0);
+		const Common::String md5 = Common::computeStreamMD5AsString(file,
+				8192);
+		file.close();
+		if (md5 != signature.md5)
+			error("Invalid sound file - %s", filename.toString().c_str());
+	}
+}
+
+void PSound::Channel::reset() {
+	activeCount = 0;
+	pitchBend = 0;
+	volumeFadeStep = 0;
+	panningFadeStep = 0;
+	note = 0;
+	patch = 0;
+	volume = 0;
+	noteOffset = 0;
+	keyOnDelay = 0;
+	volumeFadeCounter = 0;
+	volumeFadeReload = 0xff;
+	panningFadeCounter = 0;
+	panningFadeReload = 0;
+	panning = 0x40;
+	volumeOffset = 0;
+	mode = 0;
+	memset(operatorTotalLevel, 0, sizeof(operatorTotalLevel));
+	sequenceStart = 0;
+	position = 0;
+	innerLoopStart = 0;
+	outerLoopStart = 0;
+	innerLoopCount = 0;
+	outerLoopCount = 0;
+	originalSequence = 0;
+	transpose = 0;
+	noteTranspose = 0;
+	pendingStop = 0;
+	patchAttenuation = 0;
+}
+
+void PSound::Channel::load(uint16 sequenceOffset) {
+	sequenceStart = sequenceOffset;
+	position = sequenceOffset;
+	innerLoopStart = sequenceOffset;
+	outerLoopStart = sequenceOffset;
+	originalSequence = sequenceOffset;
+	volumeFadeReload = 0xff;
+	pitchBend = 0;
+	volumeFadeStep = 0;
+	panningFadeStep = 0;
+	panning = 0x40;
+	transpose = 0;
+	volumeOffset = 0;
+	volume = 0;
+	volumeFadeCounter = 0;
+	panningFadeCounter = 0;
+	pendingStop = 0;
+	noteTranspose = 0;
+	innerLoopCount = 0;
+	outerLoopCount = 0;
+	noteOffset = 0;
+	activeCount = 1;
+}
+
+void PSound::ChannelData::reset() {
+	noiseMode = 0;
+	frequencyMask = 0;
+	frequencyBase = 0;
+	frequencyStep = 0;
+}
+
+PSound::PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData) :
+		SoundDriver(mixer, driverData.filename, driverData.dataOffset,
+				driverData.initializedDataSize), _opl(nullptr) {
+	_masterVolume = 255;
+	_randomSeed = 0;
+	_frameCounter = 0;
+	_pollResult = 0;
+	_resultFlag = 0;
+	_nullSequenceOffset = driverData.nullSequenceOffset;
+	_patchTableOffset = driverData.patchTableOffset;
+	_tableLayout = driverData.tables;
+	_patchCount = driverData.patchCount;
+	_musicChannelCount = driverData.musicChannelCount;
+	_commandParam = 0;
+	_updatesEnabled = false;
+	_noiseServiceEnabled = false;
+	_noiseState = false;
+
+	if (_soundData.size() != (uint32)driverData.initializedDataSize)
+		error("PSOUND initialized data has unexpected size %u (expected %d)",
+				(uint)_soundData.size(), driverData.initializedDataSize);
+	if (driverData.totalDataSize < driverData.initializedDataSize ||
+			driverData.totalDataSize > 0xffff)
+		error("PSOUND has invalid mutable data size %d", driverData.totalDataSize);
+
+	const uint32 initializedSize = _soundData.size();
+	_soundData.resize(driverData.totalDataSize);
+	if (_soundData.size() > initializedSize)
+		memset(&_soundData[initializedSize], 0, _soundData.size() - initializedSize);
+
+	validateDataLayout(driverData);
+
+	memset(_noiseTicks, 0, sizeof(_noiseTicks));
+	memset(_savedNoiseTicks, 0, sizeof(_savedNoiseTicks));
+	memset(_noiseChannel, 0, sizeof(_noiseChannel));
+	memset(_noiseMask, 0, sizeof(_noiseMask));
+	memset(_noiseBase, 0, sizeof(_noiseBase));
+	memset(_noiseStep, 0, sizeof(_noiseStep));
+	for (uint i = 0; i < kChannelCount; ++i) {
+		_channels[i].reset();
+		_channelData[i].reset();
+	}
+
+	_randomSeed = readDataUint16(0x58);
+	memset(_registerCache, 0, sizeof(_registerCache));
+
+	_opl = OPL::Config::create(OPL::Config::kOpl3);
+	if (!_opl || !_opl->init()) {
+		delete _opl;
+		_opl = nullptr;
+		return;
+	}
+	resetDriver();
+	_updatesEnabled = true;
+	/*
+	 * The original host calls PSOUND export 4 at the retained service rate
+	 * (about 304.383 Hz) and export 3 every fifth service (about 60.877 Hz).
+	 * Starting at 608 Hz mirrors the source grid used by the recovered host;
+	 * NativeSoundTimer corrects the fractional difference from the physical
+	 * PIT rate.
+	 *
+	 * Emulated OPL backends schedule these callbacks from generated audio
+	 * samples and are the timing reference. Audio::RealChip deliberately caps
+	 * operating-system timer requests at 100 Hz, then invokes a requested
+	 * high-rate callback several times per timer event. Counts, ordering and
+	 * long-term phase therefore remain correct, but export-4 modulation writes
+	 * reach physical OPL backends in roughly 10 ms bursts instead of being
+	 * spaced about 3.3 ms apart. That can subtly change noise-like effects.
+	 * Lowering this request to 100 Hz would also degrade emulated playback, so
+	 * fixing physical-chip spacing belongs in the shared RealChip scheduler.
+	 */
+	_opl->start(new Common::Functor0Mem<void, PSound>(this, &PSound::onTimer),
+			kHostCallbackRateHz);
+}
+
+PSound::~PSound() {
+	if (_opl) {
+		_opl->stop();
+		delete _opl;
+	}
+}
+
+bool PSound::isDataRangeValid(uint32 offset, uint32 length) const {
+	return offset <= _soundData.size() && length <= _soundData.size() - offset;
+}
+
+void PSound::requireDataRange(uint32 offset, uint32 length,
+		const char *operation) const {
+	if (!isDataRangeValid(offset, length))
+		error("PSOUND %s outside data image: offset 0x%04x, length %u, size 0x%04x",
+				operation, (uint)offset, (uint)length, (uint)_soundData.size());
+}
+
+const byte *PSound::getDataPointer(uint32 offset, uint32 length,
+		const char *operation) const {
+	requireDataRange(offset, length, operation);
+	return &_soundData[offset];
+}
+
+byte *PSound::getDataPointer(uint32 offset, uint32 length,
+		const char *operation) {
+	requireDataRange(offset, length, operation);
+	return &_soundData[offset];
+}
+
+void PSound::validateDataLayout(const PSoundDriverData &driverData) const {
+	const uint32 initializedSize = driverData.initializedDataSize;
+
+	if (!driverData.patchCount || driverData.musicChannelCount > kChannelCount)
+		error("PSOUND has invalid patch/channel counts");
+	validateInitializedRange(initializedSize, 0x58, 2, "random seed");
+	validateInitializedRange(initializedSize, driverData.nullSequenceOffset, 1,
+			"null sequence");
+	validateInitializedRange(initializedSize, driverData.patchTableOffset,
+			(uint32)driverData.patchCount * kPatchSize, "patch table");
+	validateInitializedRange(initializedSize, driverData.tables.panning, 0x80,
+			"panning table");
+	validateInitializedRange(initializedSize, driverData.tables.frequency, 12 * 2,
+			"frequency table");
+	validateInitializedRange(initializedSize, driverData.tables.bank, kChannelCount,
+			"bank table");
+	validateInitializedRange(initializedSize, driverData.tables.channel, kChannelCount,
+			"channel table");
+	validateInitializedRange(initializedSize, driverData.tables.operators,
+			kChannelCount * 4, "operator table");
+
+	for (uint channel = 0; channel < kChannelCount; ++channel) {
+		const byte bank = readDataByte(driverData.tables.bank + channel);
+		const byte oplChannel = readDataByte(driverData.tables.channel + channel);
+		if (!bank || (bank & ~kBothBanks) || oplChannel > 8)
+			error("PSOUND has invalid OPL routing for channel %u", channel);
+		const uint operatorCount = channel < 6 ? 4 : 2;
+		for (uint op = 0; op < operatorCount; ++op) {
+			if (readDataByte(driverData.tables.operators + channel * 4 + op) > 0x15)
+				error("PSOUND has invalid operator routing for channel %u", channel);
+		}
+	}
+}
+
+byte PSound::readDataByte(uint32 offset) const {
+	return *getDataPointer(offset, 1, "byte read");
+}
+
+uint16 PSound::readDataUint16(uint32 offset) const {
+	return READ_LE_UINT16(getDataPointer(offset, 2, "word read"));
+}
+
+void PSound::writeDataByte(uint32 offset, byte value) {
+	*getDataPointer(offset, 1, "byte write") = value;
+}
+
+void PSound::writeDataUint16(uint32 offset, uint16 value) {
+	WRITE_LE_UINT16(getDataPointer(offset, 2, "word write"), value);
+}
+
+byte PSound::getBankMask(uint channel) const {
+	assert(channel < kChannelCount);
+	return readDataByte(_tableLayout.bank + channel);
+}
+
+byte PSound::getOplChannel(uint channel) const {
+	assert(channel < kChannelCount);
+	return readDataByte(_tableLayout.channel + channel);
+}
+
+byte PSound::getOperatorOffset(uint channel, uint operatorIndex) const {
+	assert(channel < kChannelCount && operatorIndex < 4);
+	return readDataByte(_tableLayout.operators + channel * 4 + operatorIndex);
+}
+
+const byte *PSound::getPatch(uint patchIndex) const {
+	if (patchIndex >= _patchCount)
+		patchIndex = 0;
+	return getDataPointer(_patchTableOffset + (uint32)patchIndex * kPatchSize,
+			kPatchSize, "patch");
+}
+
+byte PSound::getPanningAttenuation(byte panning) const {
+	return readDataByte(_tableLayout.panning + (panning & 0x7f));
+}
+
+uint16 PSound::getFrequencyNumber(byte semitone) const {
+	return readDataUint16(_tableLayout.frequency + (semitone % 12) * 2);
+}
+
+void PSound::writeRegister(byte banks, byte reg, byte value) {
+	assert(_opl);
+	assert(banks && !(banks & ~kBothBanks));
+	if (banks & kFirstBank) {
+		_registerCache[0][reg] = value;
+		_opl->writeReg(reg, value);
+	}
+	if (banks & kSecondBank) {
+		_registerCache[1][reg] = value;
+		_opl->writeReg(0x100 | reg, value);
+	}
+}
+
+byte PSound::getCachedRegister(byte banks, byte reg) const {
+	assert(banks && !(banks & ~kBothBanks));
+	return _registerCache[(banks & kFirstBank) ? 0 : 1][reg];
+}
+
+void PSound::resetDriver() {
+	const bool wasEnabled = _updatesEnabled;
+	_updatesEnabled = false;
+	for (uint i = 0; i < kChannelCount; ++i) {
+		_channels[i].activeCount = 0;
+		_channels[i].pitchBend = 0;
+		_channels[i].volumeFadeStep = 0;
+		_channels[i].panningFadeStep = 0;
+		_channels[i].pendingStop = 0;
+		_channelData[i].reset();
+	}
+	memset(_noiseTicks, 0, sizeof(_noiseTicks));
+	memset(_savedNoiseTicks, 0, sizeof(_savedNoiseTicks));
+	memset(_noiseMask, 0, sizeof(_noiseMask));
+	memset(_noiseBase, 0, sizeof(_noiseBase));
+	memset(_noiseStep, 0, sizeof(_noiseStep));
+	_resultFlag = 0;
+	_pollResult = 0;
+
+	/*
+	 * Preserve the PAS16 initialization literally: secondary-bank registers
+	 * 0x05 and 0x04 are OPL3 registers 0x105 (New mode) and 0x104 (enable all
+	 * six four-operator pairs). These writes must precede ordinary bank setup.
+	 */
+	writeRegister(kSecondBank, 0x05, 0x01);
+	writeRegister(kSecondBank, 0x04, 0x3f);
+
+	for (int reg = 0x56; reg >= 0x40; --reg)
+		writeRegister(kBothBanks, reg, 0x3f);
+	for (int reg = 0xff; reg >= 0x60; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	for (int reg = 0x3f; reg >= 0x20; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	writeRegister(kBothBanks, 0x01, 0x20);
+	writeRegister(kBothBanks, 0xbd, 0xc0);
+
+	_updatesEnabled = wasEnabled;
+}
+
+void PSound::requestStop(uint firstChannel, uint endChannel) {
+	assert(firstChannel <= endChannel && endChannel <= kChannelCount);
+	for (uint i = firstChannel; i < endChannel; ++i) {
+		if (_channels[i].activeCount) {
+			_channels[i].pendingStop = 0xff;
+			_channels[i].originalSequence = 0xffff;
+		}
+	}
+}
+
+void PSound::setCurrentSequence(uint firstChannel, uint endChannel, uint16 sequenceOffset) {
+	assert(firstChannel <= endChannel && endChannel <= kChannelCount);
+	requireDataRange(sequenceOffset, 1, "sequence position");
+	for (uint i = firstChannel; i < endChannel; ++i)
+		_channels[i].position = sequenceOffset;
+}
+
+void PSound::loadChannel(uint channel, uint16 sequenceOffset) {
+	assert(channel < kChannelCount);
+	requireDataRange(sequenceOffset, 1, "sequence start");
+	_channels[channel].load(sequenceOffset);
+}
+
+void PSound::playSound(uint16 sequenceOffset) {
+	for (uint i = _musicChannelCount; i < kChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (uint i = _musicChannelCount; i < kChannelCount; ++i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+}
+
+void PSound::playSoundAny(uint16 sequenceOffset) {
+	for (uint i = 0; i < _musicChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (uint i = 0; i < _musicChannelCount; ++i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+}
+
+bool PSound::isSoundActive(uint16 sequenceOffset) const {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount && _channels[i].originalSequence == sequenceOffset)
+			return true;
+	}
+	return false;
+}
+
+uint16 PSound::nextRandom() {
+	const uint16 value = 0x9248 + _randomSeed;
+	_randomSeed = (value >> 3) | (value << 13);
+	return _randomSeed;
+}
+
+byte PSound::scaledCommandParameter(int param) const {
+	const byte value = param;
+	return value > 0x1e ? value - 0x1e : 0;
+}
+
+int PSound::command0() {
+	resetDriver();
+	return 0;
+}
+
+int PSound::command1() {
+	requestStop(0, kChannelCount);
+	return 0;
+}
+
+int PSound::command2() {
+	setCurrentSequence(0, _musicChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command3() {
+	requestStop(0, _musicChannelCount);
+	return 0;
+}
+
+int PSound::command4() {
+	setCurrentSequence(_musicChannelCount, kChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command5() {
+	requestStop(_musicChannelCount, kChannelCount);
+	return 0;
+}
+
+int PSound::command6() {
+	_savedNoiseTicks[0] = _noiseTicks[0];
+	_savedNoiseTicks[1] = _noiseTicks[1];
+	_noiseTicks[0] = _noiseTicks[1] = 0;
+	for (uint i = 0; i < kChannelCount; ++i)
+		keyOff(i);
+	_updatesEnabled = false;
+	return 0;
+}
+
+int PSound::command7() {
+	_noiseTicks[0] = _savedNoiseTicks[0];
+	_noiseTicks[1] = _savedNoiseTicks[1];
+	_updatesEnabled = true;
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount) {
+			updateChannelLevels(i);
+			updateChannelFrequency(i, true);
+		}
+	}
+	if (_noiseTicks[0] != _noiseTicks[1])
+		resultCheck();
+	return _savedNoiseTicks[1];
+}
+
+int PSound::command8() {
+	int result = 0;
+	for (uint i = 0; i < kChannelCount; ++i)
+		result |= _channels[i].activeCount;
+	return result;
+}
+
+void PSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+
+	uint32 serviceTicks = _hostTimer.advance(1, kHostCallbackRateHz);
+	while (serviceTicks--) {
+		/*
+		 * The executable calls export 4 before export 3. A nonzero result
+		 * from export 3 consequently changes the export-4 gate beginning on
+		 * the following retained service tick.
+		 */
+		if (_noiseServiceEnabled)
+			serviceNoise();
+
+		if (_hostTimer.pollDue()) {
+			const int result = serviceUpdate();
+			if (result)
+				_noiseServiceEnabled = result > 0;
+		}
+	}
+}
+
+int PSound::serviceUpdate() {
+	update();
+
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void PSound::serviceNoise() {
+	const uint16 random = nextRandom();
+	if (_noiseTicks[0])
+		setNoiseFrequency(_noiseChannel[0],
+				((~random) & _noiseMask[0]) + _noiseBase[0]);
+	if (_noiseTicks[1])
+		setNoiseFrequency(_noiseChannel[1],
+				(random & _noiseMask[1]) + _noiseBase[1]);
+}
+
+void PSound::update() {
+	/*
+	 * Native export 3 advances the shared random state before testing the
+	 * driver's disabled sentinel. Export 4 uses the same seed, so moving this
+	 * call after the guard changes later noise modulation.
+	 */
+	nextRandom();
+	if (!_updatesEnabled)
+		return;
+
+	tickCallback();
+	++_frameCounter;
+	for (uint i = 0; i < kChannelCount; ++i)
+		updateChannel(i);
+	checkPendingStops();
+	updateNoise();
+
+}
+
+void PSound::updateChannel(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	if (!channel.activeCount)
+		return;
+
+	if (channel.keyOnDelay && --channel.keyOnDelay == 0)
+		keyOff(channelIndex);
+
+	if (--channel.activeCount == 0) {
+		bool levelsDirty = false;
+		int budget = kOpcodeBudgetPerTick;
+		while (budget-- > 0) {
+			if (!isDataRangeValid(channel.position, 1)) {
+				finishChannel(channelIndex);
+				break;
+			}
+
+			const byte value = readDataByte(channel.position);
+			if (value <= 0xf0) {
+				if (!isDataRangeValid(channel.position, 2)) {
+					finishChannel(channelIndex);
+					break;
+				}
+				if (levelsDirty)
+					updateChannelLevels(channelIndex);
+
+				channel.note = value;
+				channel.activeCount = readDataByte(channel.position + 1);
+				channel.position += 2;
+				if (!channel.note || !channel.activeCount) {
+					keyOff(channelIndex);
+					if (!channel.activeCount)
+						finishChannel(channelIndex);
+				} else {
+					channel.keyOnDelay = (byte)(channel.activeCount - channel.noteOffset);
+					updateChannelFrequency(channelIndex, true);
+				}
+				break;
+			}
+
+			levelsDirty = false;
+			if (!executeOpcode(channelIndex, value, levelsDirty)) {
+				finishChannel(channelIndex);
+				break;
+			}
+		}
+
+		if (budget < 0 && channel.activeCount == 0)
+			finishChannel(channelIndex);
+	}
+
+	if (channel.pitchBend)
+		updatePitchBend(channelIndex);
+
+	bool levelsDirty = false;
+	if (channel.volumeFadeCounter && --channel.volumeFadeCounter == 0) {
+		channel.volumeFadeCounter = channel.volumeFadeReload;
+		if (channel.volumeFadeStep) {
+			channel.volumeOffset += channel.volumeFadeStep;
+			levelsDirty = true;
+		}
+	}
+
+	if (channel.panningFadeCounter && --channel.panningFadeCounter == 0) {
+		channel.panningFadeCounter = channel.panningFadeReload;
+		if (channel.panningFadeStep) {
+			channel.panning += channel.panningFadeStep;
+			updatePanning(channelIndex);
+			levelsDirty = true;
+		}
+	}
+
+	if (levelsDirty)
+		updateChannelLevels(channelIndex);
+}
+
+bool PSound::executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty) {
+	Channel &channel = _channels[channelIndex];
+	const uint32 position = channel.position;
+	if (opcode < 0xf1)
+		return false;
+
+	switch (opcode) {
+	case 0xff: {
+		if (!isDataRangeValid(position, 2))
+			return false;
+		const byte count = readDataByte(position + 1);
+		if (!channel.innerLoopCount) {
+			if (!count) {
+				channel.position = (uint16)(position + 2);
+				channel.innerLoopStart = channel.position;
+				channel.innerLoopCount = 0;
+			} else {
+				if (!isDataRangeValid(channel.innerLoopStart, 1))
+					return false;
+				channel.innerLoopCount = count;
+				channel.position = channel.innerLoopStart;
+			}
+		} else if (--channel.innerLoopCount) {
+			if (!isDataRangeValid(channel.innerLoopStart, 1))
+				return false;
+			channel.position = channel.innerLoopStart;
+		} else {
+			channel.position = (uint16)(position + 2);
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+
+	case 0xfe: {
+		if (!isDataRangeValid(position, 2))
+			return false;
+		const byte count = readDataByte(position + 1);
+		if (!channel.outerLoopCount) {
+			if (!count) {
+				channel.position = (uint16)(position + 2);
+				channel.outerLoopStart = channel.position;
+				channel.innerLoopStart = channel.position;
+				channel.innerLoopCount = 0;
+				channel.outerLoopCount = 0;
+			} else {
+				if (!isDataRangeValid(channel.outerLoopStart, 1))
+					return false;
+				channel.outerLoopCount = count;
+				channel.position = channel.outerLoopStart;
+				channel.innerLoopStart = channel.outerLoopStart;
+			}
+		} else if (--channel.outerLoopCount) {
+			if (!isDataRangeValid(channel.outerLoopStart, 1))
+				return false;
+			channel.position = channel.outerLoopStart;
+			channel.innerLoopStart = channel.outerLoopStart;
+		} else {
+			channel.position = (uint16)(position + 2);
+			channel.outerLoopStart = channel.position;
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+
+	case 0xfd:
+		if (!isDataRangeValid(channel.originalSequence, 1))
+			return false;
+		channel.sequenceStart = channel.originalSequence;
+		channel.position = channel.originalSequence;
+		channel.innerLoopStart = channel.originalSequence;
+		channel.outerLoopStart = channel.originalSequence;
+		channel.pitchBend = 0;
+		channel.volumeFadeStep = 0;
+		channel.panningFadeStep = 0;
+		channel.transpose = 0;
+		channel.volumeOffset = 0;
+		channel.volume = 0;
+		channel.volumeFadeCounter = 0;
+		channel.panningFadeCounter = 0;
+		channel.innerLoopCount = 0;
+		channel.outerLoopCount = 0;
+		channel.noteOffset = 0;
+		break;
+
+	case 0xfc:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.patch = readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		loadPatch(channelIndex, channel.patch);
+		break;
+
+	case 0xfb:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.noteOffset = readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		break;
+
+	case 0xfa:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.pitchBend = (int8)readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		break;
+
+	case 0xf9:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.volume = (byte)((int8)readDataByte(position + 1) >> 1);
+		channel.position = (uint16)(position + 2);
+		levelsDirty = true;
+		break;
+
+	case 0xf8:
+		if (!isDataRangeValid(position, 3))
+			return false;
+		if (!channel.pendingStop) {
+			channel.volumeFadeReload = readDataByte(position + 1);
+			channel.volumeFadeStep = (int8)readDataByte(position + 2);
+			channel.volumeFadeCounter = 1;
+		}
+		channel.position = (uint16)(position + 3);
+		break;
+
+	case 0xf7:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.transpose = (int8)readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		break;
+
+	case 0xf6: {
+		if (!isDataRangeValid(position, 2))
+			return false;
+		const byte count = readDataByte(position + 1);
+		if (!count || !isDataRangeValid(position, (uint32)count + 3))
+			return false;
+		const uint32 table = position + 2;
+		const byte selected = readDataByte(table + ((count - 1) & nextRandom()));
+		const byte destination = readDataByte(table + count);
+		const uint32 target = table + count + 1 + destination;
+		if (!isDataRangeValid(target, 1))
+			return false;
+		writeDataByte(target, selected);
+		channel.position = (uint16)(position + count + 3);
+		break;
+	}
+
+	case 0xf5: {
+		if (!isDataRangeValid(position, 2))
+			return false;
+		const int8 value = (int8)(((int8)readDataByte(position + 1) >> 1) - 50);
+		if (!channel.pendingStop || value < channel.volumeOffset) {
+			channel.volumeOffset = value;
+			levelsDirty = true;
+		}
+		channel.position = (uint16)(position + 2);
+		break;
+	}
+
+	case 0xf4:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.panning = readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		updatePanning(channelIndex);
+		levelsDirty = true;
+		break;
+
+	case 0xf3:
+		if (!isDataRangeValid(position, 3))
+			return false;
+		channel.panningFadeReload = readDataByte(position + 1);
+		channel.panningFadeStep = (int8)readDataByte(position + 2);
+		channel.panningFadeCounter = 1;
+		channel.position = (uint16)(position + 3);
+		break;
+
+	case 0xf2:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.noteTranspose = (int8)readDataByte(position + 1);
+		channel.position = (uint16)(position + 2);
+		break;
+
+	case 0xf1:
+		if (!isDataRangeValid(position, 2))
+			return false;
+		channel.position = (uint16)(position + 2);
+		break;
+
+	default:
+		return false;
+	}
+
+	return true;
+}
+
+void PSound::finishChannel(uint channelIndex) {
+	keyOff(channelIndex);
+	_channels[channelIndex].activeCount = 0;
+	_channels[channelIndex].keyOnDelay = 0;
+}
+
+void PSound::checkPendingStops() {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		Channel &channel = _channels[i];
+		if (!channel.activeCount || !channel.pendingStop)
+			continue;
+		if ((byte)channel.volumeOffset == 0xd8) {
+			channel.position = _nullSequenceOffset;
+			channel.pendingStop = 0;
+		} else {
+			channel.volumeFadeStep = -1;
+			channel.volumeFadeReload = getStopFadeReload();
+			if (!channel.volumeFadeCounter)
+				channel.volumeFadeCounter = 1;
+		}
+	}
+}
+
+void PSound::programOperator(byte banks, uint channelIndex,
+		uint operatorIndex, const byte *operatorData) {
+	const byte op = getOperatorOffset(channelIndex, operatorIndex);
+	const byte characteristics = (operatorData[9] & 0x0f) |
+			((operatorData[5] & 1) << 4) | ((operatorData[4] & 1) << 5) |
+			((operatorData[12] & 1) << 6) | ((operatorData[11] & 1) << 7);
+	const byte totalLevel = ((operatorData[7] & 3) << 6) |
+			clampLevel(0x3f - (operatorData[6] & 0x3f));
+
+	writeRegister(banks, 0x40 + op, 0x3f);
+	writeRegister(banks, 0x20 + op, characteristics);
+	writeRegister(banks, 0x60 + op,
+			(operatorData[0] << 4) | (operatorData[1] & 0x0f));
+	writeRegister(banks, 0x80 + op,
+			(operatorData[2] << 4) | (operatorData[3] & 0x0f));
+	writeRegister(banks, 0xe0 + op, operatorData[8] & 3);
+	writeRegister(banks, 0x40 + op, totalLevel);
+}
+
+void PSound::loadPatch(uint channelIndex, byte patchIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(patchIndex);
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const uint operatorCount = channelIndex < 6 ? 4 : 2;
+
+	keyOff(channelIndex);
+	channel.mode = patch[0x0d];
+	for (uint i = 0; i < 4; ++i)
+		channel.operatorTotalLevel[i] = patch[i * 14 + 6];
+
+	for (uint i = 0; i < operatorCount; ++i)
+		programOperator(banks, channelIndex, i, patch + i * 14);
+
+	if (channelIndex < 6) {
+		const byte stereo = panningBits(channel.panning);
+		const byte firstValue = stereo | ((patch[0x0a] & 7) << 1) |
+				((patch[0x0d] & 1) ^ 1);
+		const byte secondValue = stereo | ((patch[0x26] & 7) << 1) |
+				((patch[0x29] & 1) ^ 1);
+		writeRegister(banks, 0xc0 + oplChannel, firstValue);
+		writeRegister(banks, 0xc3 + oplChannel, secondValue);
+	} else {
+		const byte value = ((patch[0x0a] & 7) << 1) |
+				((channel.mode & 1) ^ 1);
+		writeRegister(kFirstBank, 0xc0 + oplChannel, value | 0x20);
+		writeRegister(kSecondBank, 0xc0 + oplChannel, value | 0x10);
+	}
+
+	_channelData[channelIndex].noiseMode = patch[0x38];
+	_channelData[channelIndex].frequencyMask = READ_LE_UINT16(patch + 0x3a);
+	_channelData[channelIndex].frequencyBase = READ_LE_UINT16(patch + 0x3c);
+	_channelData[channelIndex].frequencyStep =
+			(int16)READ_LE_UINT16(patch + 0x3e);
+
+	updatePanning(channelIndex);
+	updateChannelLevels(channelIndex);
+}
+
+void PSound::updatePanning(uint channelIndex) {
+	const byte oplChannel = getOplChannel(channelIndex);
+	if (channelIndex < 6) {
+		const byte banks = getBankMask(channelIndex);
+		const byte stereo = panningBits(_channels[channelIndex].panning);
+		const byte reg1 = 0xc0 + oplChannel;
+		const byte reg2 = 0xc3 + oplChannel;
+		writeRegister(banks, reg1,
+				(getCachedRegister(banks, reg1) & 0x0f) | stereo);
+		writeRegister(banks, reg2,
+				(getCachedRegister(banks, reg2) & 0x0f) | stereo);
+	} else {
+		const byte reg = 0xc0 + oplChannel;
+		writeRegister(kFirstBank, reg,
+				(getCachedRegister(kFirstBank, reg) & 0x0f) | 0x20);
+		writeRegister(kSecondBank, reg,
+				(getCachedRegister(kSecondBank, reg) & 0x0f) | 0x10);
+	}
+}
+
+void PSound::updateChannelLevels(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(channel.patch);
+	int base = 0x7e - channel.volume - channel.volumeOffset -
+			channel.patchAttenuation;
+	base += (255 - _masterVolume) * 63 / 255;
+
+	if (channelIndex >= 6) {
+		const uint carriers[2] = { 1, 0 };
+		const bool enabled[2] = { true, channel.mode == 0 };
+		for (uint i = 0; i < 2; ++i) {
+			if (!enabled[i])
+				continue;
+			const uint opIndex = carriers[i];
+			const byte op = getOperatorOffset(channelIndex, opIndex);
+			const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+			const int left = clampLevel(base -
+					channel.operatorTotalLevel[opIndex] +
+					getPanningAttenuation(channel.panning));
+			const int right = clampLevel(base -
+					channel.operatorTotalLevel[opIndex] +
+					getPanningAttenuation(0x7f - channel.panning));
+			writeRegister(kFirstBank, 0x40 + op, scaling | left);
+			writeRegister(kSecondBank, 0x40 + op, scaling | right);
+		}
+		return;
+	}
+
+	if (channel.panning > 0x2a && channel.panning < 0x55)
+		base += 6;
+
+	const byte banks = getBankMask(channelIndex);
+	const uint operators[4] = { 3, 1, 0, 2 };
+	const bool enabled[4] = {
+		true,
+		channel.mode == 1,
+		(channel.mode & 2) != 0,
+		channel.mode == 3
+	};
+	for (uint i = 0; i < 4; ++i) {
+		if (!enabled[i])
+			continue;
+		const uint opIndex = operators[i];
+		const byte op = getOperatorOffset(channelIndex, opIndex);
+		const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+		const int level = clampLevel(base -
+				channel.operatorTotalLevel[opIndex]);
+		writeRegister(banks, 0x40 + op, scaling | level);
+	}
+}
+
+void PSound::updateChannelFrequency(uint channelIndex, bool keyOn) {
+	Channel &channel = _channels[channelIndex];
+	updateChannelLevels(channelIndex);
+	if (_channelData[channelIndex].noiseMode) {
+		startNoise(channelIndex);
+		return;
+	}
+
+	const byte effectiveNote = (byte)(channel.note + channel.noteTranspose);
+	const byte semitone = effectiveNote % 12;
+	const byte octave = effectiveNote / 12;
+	const int frequency = getFrequencyNumber(semitone) + channel.transpose;
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const byte lowReg = 0xa0 + oplChannel;
+	const byte highReg = 0xb0 + oplChannel;
+	writeRegister(banks, lowReg, frequency & 0xff);
+	byte high = ((octave & 7) << 2) | ((frequency >> 8) & 3);
+	if (keyOn)
+		high |= 0x20;
+	writeRegister(banks, highReg, high);
+}
+
+void PSound::updatePitchBend(uint channelIndex) {
+	const Channel &channel = _channels[channelIndex];
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const byte lowReg = 0xa0 + oplChannel;
+	const byte highReg = 0xb0 + oplChannel;
+	int frequency = ((getCachedRegister(banks, highReg) & 0x1f) << 8) |
+			getCachedRegister(banks, lowReg);
+	frequency += channel.pitchBend;
+	writeRegister(banks, lowReg, frequency & 0xff);
+	writeRegister(banks, highReg,
+			(getCachedRegister(banks, highReg) & 0x20) | ((frequency >> 8) & 0x1f));
+}
+
+void PSound::keyOff(uint channelIndex) {
+	const byte banks = getBankMask(channelIndex);
+	const byte highReg = 0xb0 + getOplChannel(channelIndex);
+	writeRegister(banks, highReg, getCachedRegister(banks, highReg) & 0xdf);
+}
+
+void PSound::startNoise(uint channelIndex) {
+	if (_noiseChannel[0] == channelIndex)
+		_noiseState = false;
+	if (_noiseChannel[1] == channelIndex)
+		_noiseState = true;
+	const uint slot = _noiseState ? 1 : 0;
+	_noiseState = !_noiseState;
+	if (_noiseTicks[slot])
+		keyOff(_noiseChannel[slot]);
+	_noiseChannel[slot] = channelIndex;
+	_noiseTicks[slot] = _channelData[channelIndex].noiseMode;
+	_noiseMask[slot] = _channelData[channelIndex].frequencyMask;
+	_noiseBase[slot] = _channelData[channelIndex].frequencyBase;
+	_noiseStep[slot] = _channelData[channelIndex].frequencyStep;
+	resultCheck();
+}
+
+void PSound::setNoiseFrequency(uint channelIndex, int frequency) {
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	writeRegister(banks, 0xa0 + oplChannel, frequency & 0xff);
+	writeRegister(banks, 0xb0 + oplChannel, ((frequency >> 8) & 0x1f) | 0x20);
+}
+
+void PSound::updateNoise() {
+	for (uint slot = 0; slot < 2; ++slot) {
+		if (!_noiseTicks[slot])
+			continue;
+		_noiseBase[slot] += _noiseStep[slot];
+		if (!--_noiseTicks[slot]) {
+			const uint other = slot ^ 1;
+			if (!_noiseTicks[other] || _noiseChannel[slot] != _noiseChannel[other])
+				keyOff(_noiseChannel[slot]);
+		}
+	}
+
+	if (_noiseTicks[0] == _noiseTicks[1] && _resultFlag != -1) {
+		_resultFlag = -1;
+		_pollResult = -1;
+	}
+}
+
+void PSound::resultCheck() {
+	if (_resultFlag != 1) {
+		_resultFlag = 1;
+		_pollResult = 1;
+	}
+}
+
+int PSound::stop() {
+	Common::StackLock lock(_driverMutex);
+	command0();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+int PSound::poll() {
+	Common::StackLock lock(_driverMutex);
+	return serviceUpdate();
+}
+
+void PSound::noise() {
+	Common::StackLock lock(_driverMutex);
+	serviceNoise();
+}
+
+void PSound::setVolume(int volume) {
+	Common::StackLock lock(_driverMutex);
+	_masterVolume = CLIP(volume, 0, 255);
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount)
+			updateChannelLevels(i);
+	}
+}
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/sound/psound.h b/engines/mads/nebular/sound/psound.h
new file mode 100644
index 00000000000..525bd30b15c
--- /dev/null
+++ b/engines/mads/nebular/sound/psound.h
@@ -0,0 +1,238 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_NEBULAR_SOUND_PSOUND_H
+#define MADS_NEBULAR_SOUND_PSOUND_H
+
+#include "mads/core/native_sound_timer.h"
+#include "mads/core/sound_manager.h"
+
+namespace OPL {
+class OPL;
+}
+
+namespace MADS {
+namespace RexNebular {
+namespace Sound {
+
+/** Offsets of the lookup tables embedded in one PSOUND data image. */
+struct PSoundTableLayout {
+	uint16 panning;
+	uint16 frequency;
+	uint16 bank;
+	uint16 channel;
+	uint16 operators;
+};
+
+/** Layout information for one exact PSOUND driver variant. */
+struct PSoundDriverData {
+	const char *filename;
+	int dataOffset;
+	int initializedDataSize;
+	int totalDataSize;
+	uint16 nullSequenceOffset;
+	uint16 patchTableOffset;
+	byte patchCount;
+	byte musicChannelCount;
+	PSoundTableLayout tables;
+};
+
+/** Common PSOUND driver implementation. */
+class PSound : public SoundDriver {
+public:
+	enum {
+		kChannelCount = 9,
+		kMusicChannelCount = 6,
+		kPatchSize = 64,
+		kOpcodeBudgetPerTick = 256
+	};
+
+	enum RegisterBank {
+		kFirstBank = 1,
+		kSecondBank = 2,
+		kBothBanks = kFirstBank | kSecondBank
+	};
+
+protected:
+	/** Logical layout of the original 0x26-byte channel record. */
+	struct Channel {
+		byte activeCount;              // +00
+		int8 pitchBend;                // +01
+		int8 volumeFadeStep;           // +02
+		int8 panningFadeStep;           // +03
+		byte note;                     // +04
+		byte patch;                    // +05
+		byte volume;                   // +06
+		byte noteOffset;               // +07
+		byte keyOnDelay;               // +08
+		byte volumeFadeCounter;        // +09
+		byte volumeFadeReload;         // +0a
+		byte panningFadeCounter;        // +0b
+		byte panningFadeReload;         // +0c
+		byte panning;                  // +0d
+		int8 volumeOffset;             // +0e
+		byte mode;                     // +0f
+		byte operatorTotalLevel[4];    // +10..+13
+		uint16 sequenceStart;          // +14
+		uint16 position;               // +16
+		uint16 innerLoopStart;         // +18
+		uint16 outerLoopStart;         // +1a
+		uint16 innerLoopCount;         // +1c
+		uint16 outerLoopCount;         // +1e
+		uint16 originalSequence;       // +20
+		int8 transpose;                // +22
+		int8 noteTranspose;            // +23
+		byte pendingStop;              // +24
+		int8 patchAttenuation;         // +25
+
+		void reset();
+		void load(uint16 sequenceOffset);
+	};
+
+	struct ChannelData {
+		byte noiseMode;
+		uint16 frequencyMask;
+		uint16 frequencyBase;
+		int16 frequencyStep;
+
+		void reset();
+	};
+
+	OPL::OPL *_opl;
+	byte _registerCache[2][256];
+	NativeSoundTimer _hostTimer;
+	Channel _channels[kChannelCount];
+	ChannelData _channelData[kChannelCount];
+
+	int _masterVolume;
+	uint16 _randomSeed;
+	uint16 _frameCounter;
+	int16 _pollResult;
+	int8 _resultFlag;
+	uint16 _nullSequenceOffset;
+	uint16 _patchTableOffset;
+	PSoundTableLayout _tableLayout;
+	byte _patchCount;
+	byte _musicChannelCount;
+	int _commandParam;
+	bool _updatesEnabled;
+	// Host-owned gate for native export 4; changed only by export-3 results.
+	bool _noiseServiceEnabled;
+
+	byte _noiseTicks[2];
+	byte _savedNoiseTicks[2];
+	byte _noiseChannel[2];
+	uint16 _noiseMask[2];
+	int32 _noiseBase[2];
+	int16 _noiseStep[2];
+	bool _noiseState;
+
+	PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData);
+	~PSound() override;
+
+	bool isDataRangeValid(uint32 offset, uint32 length) const;
+	void requireDataRange(uint32 offset, uint32 length, const char *operation) const;
+	const byte *getDataPointer(uint32 offset, uint32 length,
+			const char *operation) const;
+	byte *getDataPointer(uint32 offset, uint32 length, const char *operation);
+	void validateDataLayout(const PSoundDriverData &driverData) const;
+
+	byte readDataByte(uint32 offset) const;
+	uint16 readDataUint16(uint32 offset) const;
+	void writeDataByte(uint32 offset, byte value);
+	void writeDataUint16(uint32 offset, uint16 value);
+
+	byte getBankMask(uint channel) const;
+	byte getOplChannel(uint channel) const;
+	byte getOperatorOffset(uint channel, uint operatorIndex) const;
+	const byte *getPatch(uint patchIndex) const;
+	byte getPanningAttenuation(byte panning) const;
+	uint16 getFrequencyNumber(byte semitone) const;
+
+	void writeRegister(byte banks, byte reg, byte value);
+	byte getCachedRegister(byte banks, byte reg) const;
+	void resetDriver();
+	virtual int command0();
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+	int command5();
+	int command6();
+	int command7();
+	int command8();
+	int nullCommand() {
+		return 0;
+	}
+
+	void loadChannel(uint channel, uint16 sequenceOffset);
+	void playSound(uint16 sequenceOffset);
+	void playSoundAny(uint16 sequenceOffset);
+	bool isSoundActive(uint16 sequenceOffset) const;
+	void requestStop(uint firstChannel, uint endChannel);
+	void setCurrentSequence(uint firstChannel, uint endChannel,
+			uint16 sequenceOffset);
+
+	void onTimer();
+	int serviceUpdate();
+	void serviceNoise();
+	void update();
+	virtual void tickCallback() {
+	}
+	virtual byte getStopFadeReload() const {
+		return 4;
+	}
+	void updateChannel(uint channelIndex);
+	bool executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty);
+	void checkPendingStops();
+	void finishChannel(uint channelIndex);
+
+	void loadPatch(uint channelIndex, byte patchIndex);
+	void programOperator(byte banks, uint channelIndex, uint operatorIndex,
+			const byte *operatorData);
+	void updatePanning(uint channelIndex);
+	void updateChannelLevels(uint channelIndex);
+	void updateChannelFrequency(uint channelIndex, bool keyOn);
+	void updatePitchBend(uint channelIndex);
+	void keyOff(uint channelIndex);
+	void startNoise(uint channelIndex);
+	void updateNoise();
+	void setNoiseFrequency(uint channelIndex, int frequency);
+	void resultCheck();
+
+	uint16 nextRandom();
+	byte scaledCommandParameter(int param) const;
+
+public:
+	static void validate(bool isDemo);
+	bool isReady() const { return _opl != nullptr; }
+
+	int stop() override;
+	int poll() override;
+	void noise() override;
+	void setVolume(int volume) override;
+};
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
+
+#endif // MADS_NEBULAR_SOUND_PSOUND_H
diff --git a/engines/mads/nebular/sound/psound_nebular.cpp b/engines/mads/nebular/sound/psound_nebular.cpp
new file mode 100644
index 00000000000..3e68c1b5d3b
--- /dev/null
+++ b/engines/mads/nebular/sound/psound_nebular.cpp
@@ -0,0 +1,2702 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/util.h"
+#include "mads/nebular/sound/psound_nebular.h"
+
+namespace MADS {
+namespace RexNebular {
+namespace Sound {
+
+// -------------------------------------------------------------------------
+// Retail section 1
+// -------------------------------------------------------------------------
+
+const PSound1::CommandPtr PSound1::_commandList[42] = {
+	&PSound1::command0, &PSound1::command1, &PSound1::command2, &PSound1::command3,
+	&PSound1::command4, &PSound1::command5, &PSound1::command6, &PSound1::command7,
+	&PSound1::command8, &PSound1::command9, &PSound1::command10, &PSound1::command11,
+	&PSound1::command12, &PSound1::command13, &PSound1::command14, &PSound1::command15,
+	&PSound1::command16, &PSound1::command17, &PSound1::command18, &PSound1::command19,
+	&PSound1::command20, &PSound1::command21, &PSound1::command22, &PSound1::command23,
+	&PSound1::command24, &PSound1::command25, &PSound1::command26, &PSound1::command27,
+	&PSound1::command28, &PSound1::command29, &PSound1::command30, &PSound1::command31,
+	&PSound1::command32, &PSound1::command33, &PSound1::command34, &PSound1::command35,
+	&PSound1::command36, &PSound1::command37, &PSound1::command38, &PSound1::command39,
+	&PSound1::command40, &PSound1::command41
+};
+
+static const PSoundDriverData kPSound1Data = {
+	"PSOUND.001",
+	0x23c0, 0x13c6, 0x17a0,
+	0x0934, 0x0134, 32, PSound::kMusicChannelCount,
+	{ 0x0060, 0x00e0, 0x00fc, 0x0106, 0x0110 }
+};
+
+PSound1::PSound1(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound1Data),
+		_command23Toggle(false) {
+}
+
+void PSound1::loadCommand11Music() {
+	if (isSoundActive(0x0b68))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0b68);
+	loadChannel(1, 0x0c7a);
+	loadChannel(2, 0x0d8a);
+	loadChannel(3, 0x0dd8);
+	loadChannel(4, 0x0d86);
+
+	// These values are set by the command handler/shared loader, not encoded
+	// in the streams themselves.
+	_channels[2].panning = 0;
+	_channels[4].panning = 0x7f;
+}
+
+int PSound1::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound1::command0() {
+	_command23Toggle = false;
+	return PSound::command0();
+}
+
+int PSound1::command9() {
+	if (!isSoundActive(0x11a4))
+		playSound(0x11a4);
+	return 0;
+}
+
+int PSound1::command10() {
+	if (!isSoundActive(0x093e)) {
+		requestStop(0, kChannelCount);
+		loadChannel(0, 0x093e);
+		loadChannel(1, 0x097e);
+		loadChannel(2, 0x0b04);
+		loadChannel(3, 0x0b36);
+	}
+	return 0;
+}
+
+int PSound1::command11() {
+	loadCommand11Music();
+	_channels[0].volumeOffset = (int8)0xc1;
+	_channels[1].volumeOffset = (int8)0xc1;
+	return 0;
+}
+
+int PSound1::command12() {
+	loadCommand11Music();
+	_channels[0].volumeOffset = 0;
+	_channels[1].volumeOffset = (int8)0xc1;
+	return 0;
+}
+
+int PSound1::command13() {
+	loadCommand11Music();
+	_channels[0].volumeOffset = 0;
+	_channels[1].volumeOffset = 0;
+	return 0;
+}
+
+int PSound1::command14() {
+	playSoundAny(0x0e18);
+	return 0;
+}
+
+int PSound1::command15() {
+	if (!isSoundActive(0x0e9c)) {
+		requestStop(0, kChannelCount);
+		loadChannel(0, 0x0e9c);
+		loadChannel(1, 0x0f66);
+		loadChannel(2, 0x0fca);
+		loadChannel(3, 0x102e);
+		loadChannel(4, 0x105c);
+	}
+	return 0;
+}
+
+int PSound1::command16() {
+	playSound(0x11ac);
+	return 0;
+}
+
+int PSound1::command17() {
+	playSound(0x133e);
+	return 0;
+}
+
+int PSound1::command18() {
+	playSound(0x11e2);
+	return 0;
+}
+
+int PSound1::command19() {
+	requestStop(0, kChannelCount);
+	playSound(0x11f4);
+	return 0;
+}
+
+int PSound1::command20() {
+	playSound(0x1250);
+	return 0;
+}
+
+int PSound1::command21() {
+	playSound(0x123e);
+	return 0;
+}
+
+int PSound1::command22() {
+	writeDataByte(0x126e, (nextRandom() & 7) + 0x55);
+	playSound(0x1268);
+	return 0;
+}
+
+int PSound1::command23() {
+	_command23Toggle = !_command23Toggle;
+	playSound(_command23Toggle ? 0x1270 : 0x1278);
+	return 0;
+}
+
+int PSound1::command24() {
+	playSound(0x1280);
+	playSound(0x1290);
+	playSound(0x12a2);
+	return 0;
+}
+
+int PSound1::command25() {
+	playSound(0x12ac);
+	return 0;
+}
+
+int PSound1::command26() {
+	const byte scaledParam = scaledCommandParameter(_commandParam);
+	writeDataByte(0x1399, (scaledParam >> 1) + 0x28);
+	if (!isSoundActive(0x1394))
+		loadChannel(8, 0x1394);
+	return 0;
+}
+
+int PSound1::command27() {
+	const byte scaledParam = scaledCommandParameter(_commandParam);
+	writeDataByte(0x138d, (scaledParam >> 1) + 0x32);
+	if (!isSoundActive(0x1388))
+		loadChannel(8, 0x1388);
+	return 0;
+}
+
+int PSound1::command28() {
+	playSound(0x12bc);
+	return 0;
+}
+
+int PSound1::command29() {
+	const byte scaledParam = scaledCommandParameter(_commandParam);
+	const byte value = (scaledParam >> 1) + 0x2d;
+	writeDataByte(0x11c5, value);
+	writeDataByte(0x11cb, value);
+	writeDataByte(0x11d3, value);
+	writeDataByte(0x11d9, value);
+	if (!isSoundActive(0x11be))
+		playSoundAny(0x11be);
+	return 0;
+}
+
+int PSound1::command30() {
+	const byte scaledParam = scaledCommandParameter(_commandParam);
+	writeDataByte(0x134f, (scaledParam >> 1) + 0x23);
+	if (!isSoundActive(0x1346))
+		playSoundAny(0x1346);
+	return 0;
+}
+
+int PSound1::command31() {
+	playSound(0x131a);
+	loadChannel(5, 0x12d6);
+	return 0;
+}
+
+int PSound1::command32() {
+	const byte scaledParam = scaledCommandParameter(_commandParam);
+	writeDataByte(0x1367, (scaledParam >> 1) + 0x41);
+	if (!isSoundActive(0x1360))
+		playSoundAny(0x1360);
+	return 0;
+}
+
+int PSound1::command33() {
+	playSound(0x12e2);
+	playSound(0x12ea);
+	return 0;
+}
+
+int PSound1::command34() {
+	const byte value = (nextRandom() & 0x0c) + 0x23;
+	writeDataByte(0x13a9, value);
+	writeDataByte(0x13ae, value + 0x24);
+	playSound(0x13a0);
+	return 0;
+}
+
+int PSound1::command35() {
+	playSound(0x12f2);
+	return 0;
+}
+
+int PSound1::command36() {
+	playSound(0x1310);
+	return 0;
+}
+
+int PSound1::command37() {
+	playSound(0x1328);
+	return 0;
+}
+
+int PSound1::command38() {
+	playSound(0x1330);
+	return 0;
+}
+
+int PSound1::command39() {
+	if (!isSoundActive(0x108a)) {
+		loadChannel(0, 0x108a);
+		loadChannel(1, 0x1108);
+		loadChannel(2, 0x1124);
+		loadChannel(3, 0x1152);
+		loadChannel(4, 0x10da);
+	}
+	return 0;
+}
+
+int PSound1::command40() {
+	playSound(0x1300);
+	return 0;
+}
+
+int PSound1::command41() {
+	playSoundAny(0x13b4);
+	return 0;
+}
+
+
+// -------------------------------------------------------------------------
+// Retail section 2
+// -------------------------------------------------------------------------
+
+const PSound2::CommandPtr PSound2::_commandList[44] = {
+	&PSound2::command0, &PSound2::command1, &PSound2::command2, &PSound2::command3,
+	&PSound2::command4, &PSound2::command5, &PSound2::command6, &PSound2::command7,
+	&PSound2::command8, &PSound2::command9, &PSound2::command10, &PSound2::command11,
+	&PSound2::command12, &PSound2::command13, &PSound2::command14, &PSound2::command15,
+	&PSound2::command16, &PSound2::command17, &PSound2::command18, &PSound2::command19,
+	&PSound2::command20, &PSound2::command21, &PSound2::command22, &PSound2::command23,
+	&PSound2::command24, &PSound2::command25, &PSound2::command26, &PSound2::command27,
+	&PSound2::command28, &PSound2::command29, &PSound2::command30, &PSound2::command31,
+	&PSound2::command32, &PSound2::command33, &PSound2::command34, &PSound2::command35,
+	&PSound2::command36, &PSound2::command37, &PSound2::command38, &PSound2::command39,
+	&PSound2::command40, &PSound2::command41, &PSound2::command42, &PSound2::command43
+};
+
+static const PSoundDriverData kPSound2Data = {
+	"PSOUND.002",
+	0x2470, 0x3d30, 0x4100,
+	0x0fc4, 0x3530, 32, PSound::kMusicChannelCount,
+	{ 0x0082, 0x0102, 0x011e, 0x0128, 0x0132 }
+};
+
+PSound2::PSound2(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound2Data),
+		_command12Phase(0x50) {
+}
+
+void PSound2::mutateCommand9Sequence() {
+	// Generate two interleaved ten-byte ramps.
+	uint16 value;
+	do {
+		value = nextRandom() & 0x3f;
+	} while (value > 0x24);
+
+	byte ramp = (byte)(value + 0x14);
+	for (uint i = 0; i < 10; ++i)
+		writeDataByte(0x0207 + i * 2, ramp - i);
+
+	writeDataByte(0x01ff, nextRandom() & 0xff);
+
+	ramp = (byte)(10 - ((value + 1) / 6));
+	for (uint i = 0; i < 10; ++i)
+		writeDataByte(0x0208 + i * 2, ramp + i);
+}
+
+void PSound2::loadCommand9Music() {
+	if (isSoundActive(0x0156))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0156);
+	loadChannel(2, 0x0222);
+	mutateCommand9Sequence();
+	loadChannel(1, 0x01fc);
+}
+
+void PSound2::loadCommand10Music() {
+	if (isSoundActive(0x02ca))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x02ca);
+	loadChannel(1, 0x031e);
+	loadChannel(2, 0x0584);
+}
+
+void PSound2::loadCommand11Music() {
+	if (isSoundActive(0x169c))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x169c);
+	loadChannel(1, 0x1748);
+	loadChannel(2, 0x1ece);
+	loadChannel(3, 0x20c0);
+}
+
+void PSound2::loadCommand15Music() {
+	if (isSoundActive(0x211e))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x211e);
+	loadChannel(1, 0x2550);
+	loadChannel(2, 0x2968);
+	loadChannel(3, 0x2af2);
+	loadChannel(4, 0x2d58);
+}
+
+void PSound2::loadCommand16Music() {
+	if (isSoundActive(0x11ee))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x11ee);
+	loadChannel(1, 0x1298);
+	loadChannel(2, 0x1354);
+	loadChannel(3, 0x1438);
+	loadChannel(4, 0x159e);
+	loadChannel(5, 0x161c);
+}
+
+void PSound2::loadCommand17Music() {
+	if (isSoundActive(0x2e76))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x2e76);
+	loadChannel(1, 0x302e);
+	loadChannel(2, 0x31da);
+	loadChannel(3, 0x3388);
+}
+
+void PSound2::loadCommand19Music() {
+	if (isSoundActive(0x063a))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x063a);
+	loadChannel(1, 0x07ae);
+	loadChannel(2, 0x0976);
+	loadChannel(3, 0x0a8c);
+	loadChannel(4, 0x0bda);
+	loadChannel(5, 0x0c7e);
+}
+
+void PSound2::loadCommand38Music() {
+	if (isSoundActive(0x06e0))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x06e0);
+	loadChannel(1, 0x089a);
+	loadChannel(2, 0x0a0c);
+	loadChannel(3, 0x0b36);
+	loadChannel(4, 0x0c2e);
+	loadChannel(5, 0x0d3e);
+}
+
+int PSound2::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound2::command0() {
+	_command12Phase = 0x50;
+	return PSound::command0();
+}
+
+int PSound2::command9() {
+	loadCommand9Music();
+	return 0;
+}
+
+int PSound2::command10() {
+	loadCommand10Music();
+	return 0;
+}
+
+int PSound2::command11() {
+	loadCommand11Music();
+	return 0;
+}
+
+int PSound2::command12() {
+	_command12Phase = (_command12Phase + 8) & 0x7f;
+	writeDataByte(0x0fcd, _command12Phase);
+	playSound(0x0fcc);
+	writeDataByte(0x0fe3, _command12Phase);
+	playSound(0x0fe2);
+	return 0;
+}
+
+int PSound2::command13() {
+	playSound(0x0ff0);
+	playSound(0x1004);
+	return 0;
+}
+
+int PSound2::command14() {
+	playSound(0x1026);
+	playSound(0x1050);
+	return 0;
+}
+
+int PSound2::command15() {
+	loadCommand15Music();
+	return 0;
+}
+
+int PSound2::command16() {
+	loadCommand16Music();
+	return 0;
+}
+
+int PSound2::command17() {
+	loadCommand17Music();
+	return 0;
+}
+
+int PSound2::command18() {
+	if (!_channels[5].activeCount) {
+		const uint16 tableByteOffset = nextRandom() & 0x1e;
+		loadChannel(5, readDataUint16(0x005e + tableByteOffset));
+	}
+	return 0;
+}
+
+int PSound2::command19() {
+	loadCommand19Music();
+	return 0;
+}
+
+int PSound2::command20() {
+	playSound(0x1086);
+	return 0;
+}
+
+int PSound2::command21() {
+	playSound(0x10a0);
+	return 0;
+}
+
+int PSound2::command22() {
+	playSound(0x107a);
+	return 0;
+}
+
+int PSound2::command23() {
+	playSound(0x10aa);
+	return 0;
+}
+
+int PSound2::command24() {
+	playSound(0x10b4);
+	return 0;
+}
+
+int PSound2::command25() {
+	playSound(0x10bc);
+	return 0;
+}
+
+int PSound2::command26() {
+	playSound(0x10c8);
+	return 0;
+}
+
+int PSound2::command27() {
+	playSound(0x10d4);
+	return 0;
+}
+
+int PSound2::command28() {
+	const uint16 random = nextRandom();
+	writeDataByte(0x10f7, random & 0x7f);
+	const byte base = (random & 0x0f) + 0x2c;
+	writeDataByte(0x10f8, base);
+	writeDataByte(0x10fa, base + 0x0c);
+	playSound(0x10f0);
+	return 0;
+}
+
+int PSound2::command29() {
+	playSound(0x10fe);
+	return 0;
+}
+
+int PSound2::command30() {
+	playSound(0x1136);
+	return 0;
+}
+
+int PSound2::command31() {
+	playSound(0x113e);
+	return 0;
+}
+
+int PSound2::command32() {
+	playSound(0x1152);
+	return 0;
+}
+
+int PSound2::command33() {
+	playSound(0x115c);
+	return 0;
+}
+
+int PSound2::command34() {
+	playSound(0x1164);
+	return 0;
+}
+
+int PSound2::command35() {
+	playSound(0x1174);
+	return 0;
+}
+
+int PSound2::command36() {
+	playSound(0x11ca);
+	return 0;
+}
+
+int PSound2::command37() {
+	playSound(0x1188);
+	return 0;
+}
+
+int PSound2::command38() {
+	loadCommand38Music();
+	return 0;
+}
+
+int PSound2::command39() {
+	writeDataByte(0x119c, (nextRandom() & 7) + 0x55);
+	playSound(0x1196);
+	return 0;
+}
+
+int PSound2::command40() {
+	playSound(0x119e);
+	return 0;
+}
+
+int PSound2::command41() {
+	playSound(0x1120);
+	return 0;
+}
+
+int PSound2::command42() {
+	playSound(0x1146);
+	return 0;
+}
+
+int PSound2::command43() {
+	playSound(0x11b0);
+	return 0;
+}
+
+
+// -------------------------------------------------------------------------
+// Retail section 3
+// -------------------------------------------------------------------------
+
+const PSound3::CommandPtr PSound3::_commandList[61] = {
+	&PSound3::command0, &PSound3::command1, &PSound3::command2, &PSound3::command3,
+	&PSound3::command4, &PSound3::command5, &PSound3::command6, &PSound3::command7,
+	&PSound3::command8, &PSound3::command9, &PSound3::command10, &PSound3::command11,
+	&PSound3::nullCommand, &PSound3::command13, &PSound3::command14, &PSound3::command15,
+	&PSound3::command16, &PSound3::command17, &PSound3::command18, &PSound3::command19,
+	&PSound3::command20, &PSound3::command21, &PSound3::command22, &PSound3::command23,
+	&PSound3::command24, &PSound3::command25, &PSound3::command26, &PSound3::command27,
+	&PSound3::command28, &PSound3::command29, &PSound3::command30, &PSound3::command31,
+	&PSound3::command32, &PSound3::command33, &PSound3::command34, &PSound3::command35,
+	&PSound3::command36, &PSound3::command37, &PSound3::command38, &PSound3::command39,
+	&PSound3::command40, &PSound3::command41, &PSound3::command42, &PSound3::command43,
+	&PSound3::command44, &PSound3::command45, &PSound3::nullCommand, &PSound3::nullCommand,
+	&PSound3::nullCommand, &PSound3::nullCommand, &PSound3::nullCommand, &PSound3::command51,
+	&PSound3::nullCommand, &PSound3::nullCommand, &PSound3::nullCommand, &PSound3::nullCommand,
+	&PSound3::nullCommand, &PSound3::command57, &PSound3::nullCommand, &PSound3::command59,
+	&PSound3::command60
+};
+
+static const PSoundDriverData kPSound3Data = {
+	"PSOUND.003",
+	0x2450, 0x44f2, 0x48d0,
+	0x2336, 0x3972, 32, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound3::PSound3(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound3Data),
+		_command39Toggle(false),
+		_stopFadeReload(4) {
+}
+
+void PSound3::loadCommand10Music() {
+	if (isSoundActive(0x181e))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x181e);
+	loadChannel(1, 0x191c);
+	loadChannel(2, 0x1ae0);
+	loadChannel(3, 0x1c6c);
+	loadChannel(4, 0x1ca6);
+	loadChannel(5, 0x1ce2);
+}
+
+void PSound3::loadCommand11Music() {
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0136);
+	loadChannel(1, 0x038a);
+	loadChannel(2, 0x05aa);
+	loadChannel(3, 0x0c48);
+	loadChannel(4, 0x1090);
+	loadChannel(5, 0x1234);
+}
+
+void PSound3::loadCommand13Music() {
+	if (isSoundActive(0x34e4))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x34e4);
+	loadChannel(1, 0x3524);
+	loadChannel(2, 0x3564);
+	loadChannel(3, 0x35a4);
+	loadChannel(4, 0x35e4);
+	loadChannel(5, 0x3624);
+}
+
+void PSound3::loadCommand14Music() {
+	if (isSoundActive(0x3664))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x3664);
+	loadChannel(1, 0x3688);
+	loadChannel(2, 0x36ac);
+	loadChannel(3, 0x36d0);
+	loadChannel(4, 0x36f0);
+	loadChannel(5, 0x373c);
+}
+
+void PSound3::loadCommand16Music() {
+	requestStop(0, kChannelCount);
+	playSoundAny(0x1d2c);
+	playSoundAny(0x1eb6);
+	playSoundAny(0x2014);
+	playSoundAny(0x21ae);
+}
+
+void PSound3::loadCommand17Music() {
+	if (isSoundActive(0x2514))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x2514);
+	loadChannel(1, 0x26a6);
+	loadChannel(2, 0x28f4);
+	loadChannel(3, 0x2aa0);
+	loadChannel(4, 0x2df6);
+	loadChannel(5, 0x31cc);
+}
+
+void PSound3::loadCommand18Music() {
+	if (isSoundActive(0x3786))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x3786);
+	loadChannel(1, 0x3814);
+	loadChannel(2, 0x38c0);
+	loadChannel(3, 0x3918);
+}
+
+int PSound3::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound3::command1() {
+	_stopFadeReload = 1;
+	return PSound::command1();
+}
+
+int PSound3::command3() {
+	_stopFadeReload = 1;
+	return PSound::command3();
+}
+
+int PSound3::command9() {
+	// Section 3 adds a parameterized global stop. It marks every channel
+	// pending and stores twice the low command byte as the fade cadence.
+	requestStop(0, kChannelCount);
+	_stopFadeReload = (byte)((byte)_commandParam << 1);
+	return 0;
+}
+
+int PSound3::command10() {
+	loadCommand10Music();
+	return 0;
+}
+
+int PSound3::command11() {
+	loadCommand11Music();
+	return 0;
+}
+
+int PSound3::command13() {
+	loadCommand13Music();
+	return 0;
+}
+
+int PSound3::command14() {
+	loadCommand14Music();
+	return 0;
+}
+
+int PSound3::command15() {
+	loadCommand11Music();
+	for (uint i = 0; i < kMusicChannelCount; ++i)
+		_channels[i].patchAttenuation = (int8)0xf4;
+	return 0;
+}
+
+int PSound3::command16() {
+	loadCommand16Music();
+	return 0;
+}
+
+int PSound3::command17() {
+	loadCommand17Music();
+	return 0;
+}
+
+int PSound3::command18() {
+	loadCommand18Music();
+	return 0;
+}
+
+int PSound3::command19() {
+	playSound(0x2398);
+	return 0;
+}
+
+int PSound3::command20() {
+	playSound(0x238e);
+	return 0;
+}
+
+int PSound3::command21() {
+	loadChannel(8, 0x23a0);
+	return 0;
+}
+
+int PSound3::command22() {
+	loadChannel(8, 0x23a8);
+	return 0;
+}
+
+int PSound3::command23() {
+	loadChannel(7, 0x23c2);
+	loadChannel(8, 0x23b8);
+	return 0;
+}
+
+int PSound3::command24() {
+	playSound(0x2350);
+	return 0;
+}
+
+int PSound3::command25() {
+	playSound(0x2338);
+	return 0;
+}
+
+int PSound3::command26() {
+	playSound(0x23cc);
+	return 0;
+}
+
+int PSound3::command27() {
+	playSound(0x248c);
+	return 0;
+}
+
+int PSound3::command28() {
+	playSound(0x23dc);
+	return 0;
+}
+
+int PSound3::command29() {
+	playSound(0x23e4);
+	return 0;
+}
+
+int PSound3::command30() {
+	playSound(0x2378);
+	return 0;
+}
+
+int PSound3::command31() {
+	playSound(0x23ee);
+	return 0;
+}
+
+int PSound3::command32() {
+	playSound(0x249e);
+	return 0;
+}
+
+int PSound3::command33() {
+	playSound(0x24a8);
+	return 0;
+}
+
+int PSound3::command34() {
+	playSound(0x24b0);
+	return 0;
+}
+
+int PSound3::command35() {
+	playSound(0x24c0);
+	return 0;
+}
+
+int PSound3::command36() {
+	playSound(0x24f6);
+	return 0;
+}
+
+int PSound3::command37() {
+	playSound(0x24d4);
+	return 0;
+}
+
+int PSound3::command38() {
+	playSound(0x241e);
+	return 0;
+}
+
+int PSound3::command39() {
+	_command39Toggle = !_command39Toggle;
+	playSound(_command39Toggle ? 0x2446 : 0x243e);
+	return 0;
+}
+
+int PSound3::command40() {
+	_command39Toggle = !_command39Toggle;
+	playSound(_command39Toggle ? 0x2456 : 0x244e);
+	return 0;
+}
+
+int PSound3::command41() {
+	playSound(0x2380);
+	return 0;
+}
+
+int PSound3::command42() {
+	playSound(0x24e2);
+	return 0;
+}
+
+int PSound3::command43() {
+	playSound(0x2428);
+	playSound(0x2432);
+	return 0;
+}
+
+int PSound3::command44() {
+	playSound(0x2468);
+	return 0;
+}
+
+int PSound3::command45() {
+	playSound(0x2476);
+	return 0;
+}
+
+int PSound3::command51() {
+	playSound(0x245e);
+	return 0;
+}
+
+int PSound3::command57() {
+	writeDataByte(0x248a, (nextRandom() & 7) + 0x55);
+	playSound(0x2484);
+	return 0;
+}
+
+int PSound3::command59() {
+	playSound(0x23d4);
+	return 0;
+}
+
+int PSound3::command60() {
+	playSound(0x2416);
+	return 0;
+}
+
+
+
+// -------------------------------------------------------------------------
+// Retail section 4
+// -------------------------------------------------------------------------
+
+const PSound4::CommandPtr PSound4::_commandList[60] = {
+	&PSound4::command0, &PSound4::command1, &PSound4::command2, &PSound4::command3,
+	&PSound4::command4, &PSound4::command5, &PSound4::command6, &PSound4::command7,
+	&PSound4::command8, &PSound4::command9, &PSound4::command10, &PSound4::nullCommand,
+	&PSound4::command12, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::command19,
+	&PSound4::command20, &PSound4::command21, &PSound4::command22, &PSound4::command23,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::command27,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::command30, &PSound4::nullCommand,
+	&PSound4::command32, &PSound4::command33, &PSound4::command34, &PSound4::command35,
+	&PSound4::command36, &PSound4::command37, &PSound4::command38, &PSound4::nullCommand,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand,
+	&PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand, &PSound4::nullCommand,
+	&PSound4::command52, &PSound4::command53, &PSound4::command54, &PSound4::command55,
+	&PSound4::command56, &PSound4::command57, &PSound4::command58, &PSound4::command59
+};
+
+static const PSoundDriverData kPSound4Data = {
+	"PSOUND.004",
+	0x23a0, 0x1caf, 0x2080,
+	0x08b6, 0x0136, 30, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound4::PSound4(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound4Data),
+		_stopFadeReload(4) {
+}
+
+void PSound4::loadCommand10Music() {
+	if (isSoundActive(0x175a))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x175a);
+	loadChannel(1, 0x1876);
+	loadChannel(2, 0x1a5a);
+	loadChannel(3, 0x1be4);
+	loadChannel(4, 0x1c24);
+	loadChannel(5, 0x1c66);
+}
+
+void PSound4::loadCommand12Music(int param) {
+	// The executable has a fast path when channel 0 already owns the primary
+	// stream. If the same cue is active elsewhere, the handler returns without
+	// changing attenuation.
+	const bool primaryOnChannel0 = _channels[0].activeCount &&
+			_channels[0].sequenceStart == 0x0be2;
+	if (!primaryOnChannel0) {
+		if (isSoundActive(0x0be2))
+			return;
+
+		requestStop(0, kChannelCount);
+		loadChannel(0, 0x0be2);
+		loadChannel(1, 0x0e08);
+		loadChannel(2, 0x0f98);
+		loadChannel(3, 0x1072);
+		loadChannel(4, 0x11cc);
+		loadChannel(5, 0x1402);
+	}
+
+	const uint16 value = (uint16)param;
+	const int8 attenuation = (int8)((value >> 3) + (value >> 5) - 0x12);
+	for (uint i = 0; i < kMusicChannelCount; ++i)
+		_channels[i].patchAttenuation = attenuation;
+}
+
+bool PSound4::loadCommand53Music() {
+	if (isSoundActive(0x1632))
+		return false;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x1632);
+	loadChannel(1, 0x166c);
+	loadChannel(2, 0x169c);
+	loadChannel(3, 0x16ce);
+	loadChannel(4, 0x16f6);
+	loadChannel(5, 0x1712);
+
+	for (uint i = 0; i < kMusicChannelCount; ++i)
+		_channels[i].patchAttenuation = (int8)0xd8;
+	return true;
+}
+
+int PSound4::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound4::command1() {
+	_stopFadeReload = 1;
+	return PSound::command1();
+}
+
+int PSound4::command3() {
+	_stopFadeReload = 1;
+	return PSound::command3();
+}
+
+int PSound4::command9() {
+	requestStop(0, kChannelCount);
+	_stopFadeReload = (byte)_commandParam;
+	return 0;
+}
+
+int PSound4::command10() {
+	loadCommand10Music();
+	return 0;
+}
+
+int PSound4::command12() {
+	loadCommand12Music(_commandParam);
+	return 0;
+}
+
+int PSound4::command19() {
+	playSound(0x08d0);
+	return 0;
+}
+
+int PSound4::command20() {
+	playSound(0x08d8);
+	return 0;
+}
+
+int PSound4::command21() {
+	loadChannel(8, 0x0978);
+	return 0;
+}
+
+int PSound4::command22() {
+	loadChannel(8, 0x0980);
+	return 0;
+}
+
+int PSound4::command23() {
+	loadChannel(7, 0x099a);
+	loadChannel(8, 0x0990);
+	return 0;
+}
+
+int PSound4::command27() {
+	playSound(0x0904);
+	return 0;
+}
+
+int PSound4::command30() {
+	playSound(0x08c8);
+	return 0;
+}
+
+int PSound4::command32() {
+	playSound(0x0916);
+	return 0;
+}
+
+int PSound4::command33() {
+	playSound(0x0920);
+	return 0;
+}
+
+int PSound4::command34() {
+	playSound(0x0928);
+	return 0;
+}
+
+int PSound4::command35() {
+	playSound(0x0938);
+	return 0;
+}
+
+int PSound4::command36() {
+	playSound(0x095a);
+	return 0;
+}
+
+int PSound4::command37() {
+	playSound(0x094c);
+	return 0;
+}
+
+int PSound4::command38() {
+	playSound(0x08f2);
+	return 0;
+}
+
+int PSound4::command52() {
+	if (_channels[1].sequenceStart == 0x1876 &&
+			!isSoundActive(0x09a4)) {
+		loadChannel(0, 0x09a4);
+		_channels[1].patchAttenuation = (int8)0xd8;
+		_channels[2].patchAttenuation = (int8)0xd8;
+	}
+	return 0;
+}
+
+int PSound4::command53() {
+	if (loadCommand53Music())
+		_channels[0].patchAttenuation = 0;
+	return 0;
+}
+
+int PSound4::command54() {
+	if (loadCommand53Music()) {
+		_channels[1].patchAttenuation = 0;
+		_channels[2].patchAttenuation = 0;
+	}
+	return 0;
+}
+
+int PSound4::command55() {
+	if (loadCommand53Music()) {
+		_channels[3].patchAttenuation = 0;
+		_channels[4].patchAttenuation = 0;
+	}
+	return 0;
+}
+
+int PSound4::command56() {
+	if (loadCommand53Music())
+		_channels[5].patchAttenuation = 0;
+	return 0;
+}
+
+int PSound4::command57() {
+	writeDataByte(0x0902, (nextRandom() & 7) + 0x55);
+	playSound(0x08fc);
+	return 0;
+}
+
+int PSound4::command58() {
+	if (_channels[0].sequenceStart == 0x09a4) {
+		loadChannel(0, 0x175a);
+		_channels[1].patchAttenuation = 0;
+		_channels[2].patchAttenuation = 0;
+	}
+	return 0;
+}
+
+int PSound4::command59() {
+	playSound(0x08e2);
+	return 0;
+}
+
+
+
+// -------------------------------------------------------------------------
+// Retail section 5
+// -------------------------------------------------------------------------
+
+const PSound5::CommandPtr PSound5::_commandList[42] = {
+	&PSound5::command0, &PSound5::command1, &PSound5::command2, &PSound5::command3,
+	&PSound5::command4, &PSound5::command5, &PSound5::command6, &PSound5::command7,
+	&PSound5::command8, &PSound5::command9, &PSound5::command10, &PSound5::command11122425,
+	&PSound5::command11122425, &PSound5::command13, &PSound5::command14, &PSound5::command15,
+	&PSound5::command16, &PSound5::command17, &PSound5::command18, &PSound5::command1921,
+	&PSound5::command20, &PSound5::command1921, &PSound5::command22, &PSound5::command23,
+	&PSound5::command11122425, &PSound5::command11122425, &PSound5::command26, &PSound5::command27,
+	&PSound5::command28, &PSound5::command29, &PSound5::command30, &PSound5::command31,
+	&PSound5::command32, &PSound5::command33, &PSound5::command34, &PSound5::command35,
+	&PSound5::command36, &PSound5::command37, &PSound5::command38, &PSound5::command39,
+	&PSound5::command40, &PSound5::command41
+};
+
+static const PSoundDriverData kPSound5Data = {
+	"PSOUND.005",
+	0x22f0, 0x15f2, 0x19d0,
+	0x09b6, 0x0136, 34, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound5::PSound5(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound5Data) {
+}
+
+void PSound5::loadCommand29Music() {
+	if (isSoundActive(0x0d8a))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0d8a);
+	loadChannel(1, 0x0eaa);
+	loadChannel(2, 0x0fce);
+	loadChannel(3, 0x10a2);
+	loadChannel(4, 0x12a6);
+}
+
+int PSound5::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound5::command9() {
+	writeDataByte(0x09f0, (nextRandom() & 7) + 0x55);
+	playSound(0x09ea);
+	return 0;
+}
+
+int PSound5::command10() {
+	playSound(0x0a48);
+	return 0;
+}
+
+int PSound5::command11122425() {
+	playSound(0x09d0);
+	return 0;
+}
+
+int PSound5::command13() {
+	playSound(0x0a60);
+	return 0;
+}
+
+int PSound5::command14() {
+	loadChannel(8, 0x0af6);
+	return 0;
+}
+
+int PSound5::command15() {
+	if (_channels[8].sequenceStart == 0x0af6) {
+		// Change the restart target without reloading the channel.
+		_channels[8].originalSequence = 0x0b0c;
+		_channels[8].innerLoopCount = 1;
+		_channels[8].outerLoopCount = 1;
+	}
+	return 0;
+}
+
+int PSound5::command16() {
+	playSound(0x0a58);
+	return 0;
+}
+
+int PSound5::command17() {
+	playSound(0x0a50);
+	return 0;
+}
+
+int PSound5::command18() {
+	playSound(0x0ad4);
+	return 0;
+}
+
+int PSound5::command1921() {
+	playSound(0x0a8c);
+	return 0;
+}
+
+int PSound5::command20() {
+	playSound(0x0a7c);
+	return 0;
+}
+
+int PSound5::command22() {
+	playSound(0x0a74);
+	return 0;
+}
+
+int PSound5::command23() {
+	playSound(0x0a6a);
+	return 0;
+}
+
+int PSound5::command26() {
+	playSound(0x0a9e);
+	return 0;
+}
+
+int PSound5::command27() {
+	playSound(0x0ab0);
+	return 0;
+}
+
+int PSound5::command28() {
+	playSound(0x09f2);
+	return 0;
+}
+
+int PSound5::command29() {
+	loadCommand29Music();
+	return 0;
+}
+
+int PSound5::command30() {
+	playSound(0x09c8);
+	return 0;
+}
+
+int PSound5::command31() {
+	playSound(0x0aea);
+	return 0;
+}
+
+int PSound5::command32() {
+	playSound(0x0a04);
+	return 0;
+}
+
+int PSound5::command33() {
+	playSound(0x0a0e);
+	return 0;
+}
+
+int PSound5::command34() {
+	playSound(0x0a16);
+	return 0;
+}
+
+int PSound5::command35() {
+	playSound(0x0a26);
+	return 0;
+}
+
+int PSound5::command36() {
+	playSound(0x0b18);
+	return 0;
+}
+
+int PSound5::command37() {
+	playSound(0x0a3a);
+	return 0;
+}
+
+int PSound5::command38() {
+	if (_channels[3].sequenceStart == 0x0b4c) {
+		loadChannel(3, 0x10a2);
+		loadChannel(4, 0x12a6);
+	}
+	return 0;
+}
+
+int PSound5::command39() {
+	loadChannel(6, 0x0b38);
+	return 0;
+}
+
+int PSound5::command40() {
+	loadChannel(6, 0x0b40);
+	return 0;
+}
+
+int PSound5::command41() {
+	if (!isSoundActive(0x0b4c) &&
+			_channels[3].sequenceStart == 0x10a2) {
+		loadChannel(3, 0x0b4c);
+		loadChannel(4, _nullSequenceOffset);
+	}
+	return 0;
+}
+
+
+// -------------------------------------------------------------------------
+// Retail section 6
+// -------------------------------------------------------------------------
+
+const PSound6::CommandPtr PSound6::_commandList[30] = {
+	&PSound6::command0, &PSound6::command1, &PSound6::command2, &PSound6::command3,
+	&PSound6::command4, &PSound6::command5, &PSound6::command6, &PSound6::command7,
+	&PSound6::command8, &PSound6::command9, &PSound6::command10, &PSound6::command11,
+	&PSound6::command12, &PSound6::command13, &PSound6::command14, &PSound6::command15,
+	&PSound6::command16, &PSound6::command17, &PSound6::command18, &PSound6::command19,
+	&PSound6::command20, &PSound6::command21, &PSound6::command22, &PSound6::command23,
+	&PSound6::command24, &PSound6::command25, &PSound6::nullCommand, &PSound6::nullCommand,
+	&PSound6::nullCommand, &PSound6::command29
+};
+
+static const PSoundDriverData kPSound6Data = {
+	"PSOUND.006",
+	0x2250, 0x17c8, 0x1ba0,
+	0x0936, 0x0136, 32, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound6::PSound6(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound6Data) {
+}
+
+void PSound6::loadCommand24Music() {
+	if (isSoundActive(0x0abe))
+		return;
+
+	requestStop(0, kChannelCount);
+	playSoundAny(0x0abe);
+	playSoundAny(0x0d8e);
+	playSoundAny(0x0f28);
+}
+
+void PSound6::loadCommand29Music() {
+	if (isSoundActive(0x0f60))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0f60);
+	loadChannel(1, 0x1080);
+	loadChannel(2, 0x11a4);
+	loadChannel(3, 0x1278);
+	loadChannel(4, 0x147c);
+}
+
+int PSound6::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound6::command9() {
+	writeDataByte(0x0946, (nextRandom() & 7) + 0x55);
+	playSound(0x0940);
+	return 0;
+}
+
+int PSound6::command10() {
+	playSound(0x09e2);
+	return 0;
+}
+
+int PSound6::command11() {
+	playSound(0x09c0);
+	return 0;
+}
+
+int PSound6::command12() {
+	playSound(0x0a0e);
+	return 0;
+}
+
+int PSound6::command13() {
+	playSound(0x0a74);
+	return 0;
+}
+
+int PSound6::command14() {
+	playSound(0x0a90);
+	return 0;
+}
+
+int PSound6::command15() {
+	playSound(0x0968);
+	return 0;
+}
+
+int PSound6::command16() {
+	playSound(0x0974);
+	playSound(0x0968);
+	return 0;
+}
+
+int PSound6::command17() {
+	playSound(0x098a);
+	return 0;
+}
+
+int PSound6::command18() {
+	playSound(0x0a42);
+	return 0;
+}
+
+int PSound6::command19() {
+	playSound(0x0a52);
+	return 0;
+}
+
+int PSound6::command20() {
+	playSound(0x09fa);
+	return 0;
+}
+
+int PSound6::command21() {
+	playSound(0x0a20);
+	return 0;
+}
+
+int PSound6::command22() {
+	playSound(0x0a62);
+	return 0;
+}
+
+int PSound6::command23() {
+	playSound(0x0948);
+	return 0;
+}
+
+int PSound6::command24() {
+	loadCommand24Music();
+	return 0;
+}
+
+int PSound6::command25() {
+	playSound(0x0aac);
+	return 0;
+}
+
+int PSound6::command29() {
+	loadCommand29Music();
+	return 0;
+}
+
+
+
+// -------------------------------------------------------------------------
+// Retail section 7
+// -------------------------------------------------------------------------
+
+const PSound7::CommandPtr PSound7::_commandList[38] = {
+	&PSound7::command0, &PSound7::command1, &PSound7::command2, &PSound7::command3,
+	&PSound7::command4, &PSound7::command5, &PSound7::command6, &PSound7::command7,
+	&PSound7::command8, &PSound7::command9, &PSound7::nullCommand, &PSound7::nullCommand,
+	&PSound7::nullCommand, &PSound7::nullCommand, &PSound7::nullCommand, &PSound7::command15,
+	&PSound7::command1617, &PSound7::command1617, &PSound7::command18, &PSound7::command19,
+	&PSound7::command20, &PSound7::command21, &PSound7::command22, &PSound7::command23,
+	&PSound7::command24, &PSound7::command25, &PSound7::command26, &PSound7::command27,
+	&PSound7::command28, &PSound7::nullCommand, &PSound7::command30, &PSound7::nullCommand,
+	&PSound7::command32, &PSound7::command33, &PSound7::command34, &PSound7::command35,
+	&PSound7::nullCommand, &PSound7::command37
+};
+
+static const PSoundDriverData kPSound7Data = {
+	"PSOUND.007",
+	0x2300, 0x2039, 0x2410,
+	0x09f6, 0x0136, 35, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound7::PSound7(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound7Data) {
+}
+
+void PSound7::loadCommand9Music() {
+	if (isSoundActive(0x1ef4))
+		return;
+
+	requestStop(0, kChannelCount);
+	playSoundAny(0x1ef4);
+	playSoundAny(0x1f4e);
+	playSoundAny(0x1fa6);
+	playSoundAny(0x200c);
+}
+
+void PSound7::loadCommand24Music() {
+	if (isSoundActive(0x1658))
+		return;
+
+	requestStop(0, kChannelCount);
+	playSoundAny(0x1658);
+	playSoundAny(0x171e);
+	playSoundAny(0x17cc);
+	playSoundAny(0x1872);
+	playSoundAny(0x190a);
+}
+
+void PSound7::loadCommand25Music() {
+	if (isSoundActive(0x0ac6))
+		return;
+
+	requestStop(0, kChannelCount);
+	playSoundAny(0x0ac6);
+	playSoundAny(0x0b7a);
+	playSoundAny(0x0c30);
+	playSoundAny(0x0cec);
+}
+
+void PSound7::loadCommand26Music() {
+	if (isSoundActive(0x0dda))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0dda);
+	loadChannel(1, 0x0efa);
+	loadChannel(2, 0x101e);
+	loadChannel(3, 0x10f2);
+	loadChannel(4, 0x1302);
+}
+
+void PSound7::loadCommand27Music() {
+	if (isSoundActive(0x195e))
+		return;
+
+	requestStop(0, kChannelCount);
+	playSoundAny(0x195e);
+	playSoundAny(0x1a32);
+	playSoundAny(0x1b3e);
+	playSoundAny(0x1c60);
+	playSoundAny(0x1dac);
+	playSoundAny(0x1ef2);
+}
+
+int PSound7::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound7::command9() {
+	loadCommand9Music();
+	return 0;
+}
+
+int PSound7::command15() {
+	writeDataByte(0x0a06, (nextRandom() & 7) + 0x55);
+	playSound(0x0a00);
+	return 0;
+}
+
+int PSound7::command1617() {
+	playSound(0x0abe);
+	return 0;
+}
+
+int PSound7::command18() {
+	loadChannel(8, 0x0a70);
+	return 0;
+}
+
+int PSound7::command19() {
+	if (_channels[8].sequenceStart == 0x0a70) {
+		// The handler changes the eventual restart target and both nested
+		// loop counters without reloading or repositioning channel 8.
+		_channels[8].originalSequence = 0x0a86;
+		_channels[8].innerLoopCount = 1;
+		_channels[8].outerLoopCount = 1;
+	}
+	return 0;
+}
+
+int PSound7::command20() {
+	playSound(0x0aac);
+	return 0;
+}
+
+int PSound7::command21() {
+	playSound(0x0aa2);
+	return 0;
+}
+
+int PSound7::command22() {
+	playSound(0x0a64);
+	return 0;
+}
+
+int PSound7::command23() {
+	playSound(0x0a08);
+	return 0;
+}
+
+int PSound7::command24() {
+	loadCommand24Music();
+	return 0;
+}
+
+int PSound7::command25() {
+	loadCommand25Music();
+	return 0;
+}
+
+int PSound7::command26() {
+	loadCommand26Music();
+	return 0;
+}
+
+int PSound7::command27() {
+	loadCommand27Music();
+	return 0;
+}
+
+int PSound7::command28() {
+	loadChannel(8, 0x0a92);
+	return 0;
+}
+
+int PSound7::command30() {
+	playSound(0x0a28);
+	return 0;
+}
+
+int PSound7::command32() {
+	playSound(0x0a30);
+	return 0;
+}
+
+int PSound7::command33() {
+	playSound(0x0a3a);
+	return 0;
+}
+
+int PSound7::command34() {
+	playSound(0x0a42);
+	return 0;
+}
+
+int PSound7::command35() {
+	playSound(0x0a52);
+	return 0;
+}
+
+int PSound7::command37() {
+	playSound(0x0a1a);
+	return 0;
+}
+
+
+
+// -------------------------------------------------------------------------
+// Retail section 8
+// -------------------------------------------------------------------------
+
+const PSound8::CommandPtr PSound8::_commandList[38] = {
+	&PSound8::command0, &PSound8::command1, &PSound8::command2, &PSound8::command3,
+	&PSound8::command4, &PSound8::command5, &PSound8::command6, &PSound8::command7,
+	&PSound8::command8, &PSound8::command9, &PSound8::nullCommand, &PSound8::command11,
+	&PSound8::command12, &PSound8::command13, &PSound8::command14, &PSound8::command15,
+	&PSound8::command16, &PSound8::command17, &PSound8::command18, &PSound8::command19,
+	&PSound8::command20, &PSound8::command21, &PSound8::command22, &PSound8::nullCommand,
+	&PSound8::command24, &PSound8::command25, &PSound8::command26, &PSound8::command27,
+	&PSound8::command28, &PSound8::command29, &PSound8::command30, &PSound8::command31,
+	&PSound8::command32, &PSound8::command33, &PSound8::command34, &PSound8::command35,
+	&PSound8::nullCommand, &PSound8::command37
+};
+
+static const PSoundDriverData kPSound8Data = {
+	"PSOUND.008",
+	0x22e0, 0x0f7a, 0x1350,
+	0x061a, 0x077a, 32, PSound::kMusicChannelCount,
+	{ 0x0062, 0x00e2, 0x00fe, 0x0108, 0x0112 }
+};
+
+PSound8::PSound8(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound8Data) {
+}
+
+void PSound8::mutateCommand28Sequence() {
+	uint16 random;
+	do {
+		random = nextRandom() & 0x3f;
+	} while (random > 0x24);
+
+	// The executable generates ten descending note bytes in the odd byte
+	// positions of the mutable stream body.
+	for (uint i = 0; i < 10; ++i)
+		writeDataByte(0x01e7 + i * 2, (byte)(random + 0x14 - i));
+
+	writeDataByte(0x01df, (byte)nextRandom());
+
+	// The matching duration/control bytes rise from a value derived from the
+	// accepted random note range. The 8086 DIV is an unsigned quotient.
+	const byte firstValue = 10 - (byte)((random + 1) / 6);
+	for (uint i = 0; i < 10; ++i)
+		writeDataByte(0x01e8 + i * 2, firstValue + i);
+}
+
+void PSound8::loadCommand28Music() {
+	if (isSoundActive(0x0136))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x0136);
+	loadChannel(1, 0x0202);
+	mutateCommand28Sequence();
+	loadChannel(2, 0x01dc);
+}
+
+void PSound8::loadCommand29Music() {
+	if (isSoundActive(0x02aa))
+		return;
+
+	requestStop(0, kChannelCount);
+	loadChannel(0, 0x02aa);
+	loadChannel(1, 0x02fe);
+	loadChannel(2, 0x0564);
+}
+
+int PSound8::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound8::command9() {
+	writeDataByte(0x069a, (nextRandom() & 7) + 0x55);
+	playSound(0x0694);
+	return 0;
+}
+
+int PSound8::command11() {
+	playSound(0x0742);
+	return 0;
+}
+
+int PSound8::command12() {
+	playSound(0x074e);
+	return 0;
+}
+
+int PSound8::command13() {
+	playSound(0x0636);
+	return 0;
+}
+
+int PSound8::command14() {
+	loadChannel(8, 0x06b4);
+	return 0;
+}
+
+int PSound8::command15() {
+	loadChannel(8, 0x06cc);
+	return 0;
+}
+
+int PSound8::command16() {
+	playSound(0x062a);
+	return 0;
+}
+
+int PSound8::command17() {
+	playSound(0x0762);
+	return 0;
+}
+
+int PSound8::command18() {
+	playSound(0x076e);
+	return 0;
+}
+
+int PSound8::command19() {
+	playSound(0x06f4);
+	return 0;
+}
+
+int PSound8::command20() {
+	playSound(0x06fc);
+	return 0;
+}
+
+int PSound8::command21() {
+	playSound(0x075a);
+	return 0;
+}
+
+int PSound8::command22() {
+	loadChannel(6, 0x0704);
+	loadChannel(7, 0x0712);
+	loadChannel(8, 0x0720);
+	return 0;
+}
+
+int PSound8::command24() {
+	playSound(0x072e);
+	return 0;
+}
+
+int PSound8::command25() {
+	playSound(0x0736);
+	return 0;
+}
+
+int PSound8::command26() {
+	playSound(0x06de);
+	return 0;
+}
+
+int PSound8::command27() {
+	playSound(0x069c);
+	return 0;
+}
+
+int PSound8::command28() {
+	loadCommand28Music();
+	return 0;
+}
+
+int PSound8::command29() {
+	loadCommand29Music();
+	return 0;
+}
+
+int PSound8::command30() {
+	playSound(0x0640);
+	return 0;
+}
+
+int PSound8::command31() {
+	playSound(0x0622);
+	return 0;
+}
+
+int PSound8::command32() {
+	playSound(0x0650);
+	return 0;
+}
+
+int PSound8::command33() {
+	playSound(0x065a);
+	return 0;
+}
+
+int PSound8::command34() {
+	playSound(0x0662);
+	return 0;
+}
+
+int PSound8::command35() {
+	playSound(0x0672);
+	return 0;
+}
+
+int PSound8::command37() {
+	playSound(0x0686);
+	return 0;
+}
+
+
+
+// -------------------------------------------------------------------------
+// Retail section 9
+// -------------------------------------------------------------------------
+
+const PSound9::CommandPtr PSound9::_commandList[52] = {
+	&PSound9::command0, &PSound9::command1, &PSound9::command2, &PSound9::command3,
+	&PSound9::command4, &PSound9::command5, &PSound9::command6, &PSound9::command7,
+	&PSound9::command8, &PSound9::command9, &PSound9::command10, &PSound9::command11,
+	&PSound9::command12, &PSound9::command13, &PSound9::command14, &PSound9::command15,
+	&PSound9::command16, &PSound9::command17, &PSound9::command18, &PSound9::command19,
+	&PSound9::command20, &PSound9::command21, &PSound9::command22, &PSound9::command23,
+	&PSound9::command24, &PSound9::command25, &PSound9::command26, &PSound9::command27,
+	&PSound9::command28, &PSound9::command29, &PSound9::command30, &PSound9::command31,
+	&PSound9::command32, &PSound9::command33, &PSound9::command34, &PSound9::command35,
+	&PSound9::command36, &PSound9::command37, &PSound9::command38, &PSound9::command39,
+	&PSound9::command40, &PSound9::command41, &PSound9::command42, &PSound9::command43,
+	&PSound9::command4446, &PSound9::command45, &PSound9::command4446, &PSound9::command47,
+	&PSound9::command48, &PSound9::command49, &PSound9::command50, &PSound9::command51
+};
+
+static const PSoundDriverData kPSound9Data = {
+	"PSOUND.009",
+	0x2630, 0x6d3c, 0x7110,
+	0x5f3c, 0x62bc, 42, 7,
+	{ 0x0068, 0x00e8, 0x0104, 0x010e, 0x0118 }
+};
+
+PSound9::PSound9(Audio::Mixer *mixer) :
+		PSound(mixer, kPSound9Data),
+		_scheduledCallbackOffset(0) {
+}
+
+void PSound9::scheduleCallback(uint16 callbackOffset) {
+	// Store the callback offset used by the section timer.
+	_scheduledCallbackOffset = callbackOffset;
+	writeDataUint16(0x0064, callbackOffset);
+}
+
+void PSound9::tickCallback() {
+	uint16 period = readDataUint16(0x0062);
+	if (!period)
+		return;
+
+	uint16 counter = readDataUint16(0x0060);
+	if (!counter)
+		counter = period;
+	if (--counter) {
+		writeDataUint16(0x0060, counter);
+		return;
+	}
+
+	writeDataUint16(0x0060, period);
+	if (_scheduledCallbackOffset)
+		runScheduledCallback();
+}
+
+void PSound9::runScheduledCallback() {
+	const uint16 callbackOffset = _scheduledCallbackOffset;
+	_scheduledCallbackOffset = 0;
+	writeDataUint16(0x0064, 0);
+
+	switch (callbackOffset) {
+	case 0x0851:
+		loadChannel(0, 0x0198);
+		loadChannel(1, 0x081a);
+		loadChannel(2, 0x0e64);
+		loadChannel(3, 0x13b0);
+		break;
+	case 0x087c:
+		loadChannel(0, 0x041e);
+		loadChannel(1, 0x0a54);
+		loadChannel(2, 0x0ffa);
+		loadChannel(3, 0x155c);
+		break;
+	case 0x08a7:
+		loadChannel(0, 0x06a8);
+		loadChannel(1, 0x0c8a);
+		loadChannel(2, 0x1190);
+		loadChannel(3, 0x170a);
+		break;
+	case 0x096e:
+		loadChannel(0, 0x19ee);
+		loadChannel(1, 0x2012);
+		loadChannel(2, 0x298e);
+		loadChannel(3, 0x34d4);
+		loadChannel(4, 0x3fb6);
+		loadChannel(5, 0x49dc);
+		break;
+	case 0x09a7:
+		loadChannel(0, 0x1a84);
+		loadChannel(1, 0x20d8);
+		loadChannel(2, 0x2a56);
+		loadChannel(3, 0x3668);
+		loadChannel(4, 0x4312);
+		loadChannel(5, 0x4c46);
+		break;
+	case 0x09e0:
+		loadChannel(0, 0x1bda);
+		loadChannel(1, 0x2438);
+		loadChannel(2, 0x2dbe);
+		loadChannel(3, 0x39e0);
+		loadChannel(4, 0x43b6);
+		loadChannel(5, 0x4d5c);
+		break;
+	case 0x0a19:
+		writeDataByte(0x3c2b, 0);
+		writeDataByte(0x45fd, 0);
+		writeDataByte(0x4f19, 0);
+		loadChannel(0, 0x1cf0);
+		loadChannel(1, 0x252c);
+		loadChannel(2, 0x2ec8);
+		loadChannel(3, 0x3bb0);
+		loadChannel(4, 0x4552);
+		loadChannel(5, 0x4e40);
+		break;
+	case 0x0b16:
+		loadChannel(0, 0x5368);
+		loadChannel(1, 0x56e2);
+		loadChannel(2, 0x5580);
+		loadChannel(3, 0x5bdc);
+		loadChannel(4, 0x58ca);
+		loadChannel(5, 0x5a7a);
+		loadChannel(6, 0x5d52);
+		break;
+	case 0x0b56:
+		loadChannel(0, 0x53e0);
+		loadChannel(1, 0x5778);
+		loadChannel(2, 0x55cc);
+		loadChannel(3, 0x5c06);
+		loadChannel(4, 0x5976);
+		loadChannel(5, 0x5b20);
+		loadChannel(6, 0x5e4a);
+		break;
+	default:
+		break;
+	}
+}
+
+void PSound9::loadCommand9Music() {
+	if (isSoundActive(0x528c))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	writeDataUint16(0x0060, 0x0738);
+	writeDataUint16(0x0062, 0x0054);
+	loadChannel(0, 0x528c);
+	loadChannel(1, 0x543e);
+	loadChannel(2, 0x5622);
+	loadChannel(3, 0x5b22);
+	loadChannel(4, 0x57d6);
+	loadChannel(5, 0x5978);
+	loadChannel(6, 0x5c08);
+}
+
+void PSound9::loadCommand10Music() {
+	if (isSoundActive(0x5eaa))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	loadChannel(0, 0x5eaa);
+	loadChannel(1, 0x5ec6);
+	loadChannel(2, 0x5ee4);
+	loadChannel(3, 0x5efa);
+	loadChannel(4, 0x5f10);
+	loadChannel(5, 0x5f26);
+}
+
+void PSound9::loadCommand34Music() {
+	writeDataByte(0x3c2b, 2);
+	writeDataByte(0x45fd, 2);
+	writeDataByte(0x4f19, 2);
+	writeDataUint16(0x0060, 0x0060);
+	writeDataUint16(0x0062, 0x0060);
+	loadChannel(0, 0x184c);
+	loadChannel(1, 0x1d6e);
+	loadChannel(2, 0x25ac);
+	loadChannel(3, 0x2f6e);
+	loadChannel(4, 0x3c32);
+	loadChannel(5, 0x4604);
+}
+
+void PSound9::loadCommand43Music() {
+	if (isSoundActive(0x013c))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	playSoundAny(0x013c);
+	playSoundAny(0x07bc);
+	playSoundAny(0x0d9e);
+	playSoundAny(0x12e4);
+}
+
+void PSound9::loadCommand49Music() {
+	loadChannel(0, 0x4f20);
+	loadChannel(1, 0x4f88);
+	loadChannel(2, 0x4fec);
+	loadChannel(3, 0x525e);
+	loadChannel(4, 0x5210);
+	loadChannel(5, 0x5230);
+	loadChannel(6, 0x51f2);
+}
+
+void PSound9::loadCommand51Music() {
+	writeDataByte(0x3c2b, 2);
+	writeDataByte(0x45fd, 2);
+	writeDataByte(0x4f19, 2);
+	writeDataUint16(0x0060, 0x0060);
+	writeDataUint16(0x0062, 0x0060);
+	loadChannel(0, 0x1880);
+	loadChannel(1, 0x1db2);
+	loadChannel(2, 0x27f8);
+	loadChannel(3, 0x31c6);
+	loadChannel(4, 0x3c98);
+	loadChannel(5, 0x4698);
+}
+
+int PSound9::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSound9::command0() {
+	_scheduledCallbackOffset = 0;
+	writeDataUint16(0x0060, 0);
+	writeDataUint16(0x0062, 0);
+	writeDataUint16(0x0064, 0);
+	return PSound::command0();
+}
+
+int PSound9::command9() {
+	loadCommand9Music();
+	return 0;
+}
+
+int PSound9::command10() {
+	loadCommand10Music();
+	return 0;
+}
+
+int PSound9::command11() {
+	loadChannel(7, 0x600e);
+	loadChannel(8, 0x60a2);
+	return 0;
+}
+
+int PSound9::command12() {
+	loadChannel(8, 0x6184);
+	return 0;
+}
+
+int PSound9::command13() {
+	loadChannel(7, 0x61a0);
+	return 0;
+}
+
+int PSound9::command14() {
+	loadChannel(7, 0x5fd2);
+	return 0;
+}
+
+int PSound9::command15() {
+	loadChannel(7, 0x61ba);
+	return 0;
+}
+
+int PSound9::command16() {
+	loadChannel(7, 0x61d8);
+	return 0;
+}
+
+int PSound9::command17() {
+	loadChannel(7, 0x614a);
+	return 0;
+}
+
+int PSound9::command18() {
+	playSound(0x6292);
+	return 0;
+}
+
+int PSound9::command19() {
+	playSound(0x6260);
+	return 0;
+}
+
+int PSound9::command20() {
+	writeDataByte(0x5f60, (nextRandom() & 0x10) | 0x4d);
+	loadChannel(7, 0x5f5c);
+	return 0;
+}
+
+int PSound9::command21() {
+	loadChannel(8, 0x5f74);
+	return 0;
+}
+
+int PSound9::command22() {
+	loadChannel(8, 0x5f84);
+	return 0;
+}
+
+int PSound9::command23() {
+	loadChannel(8, 0x5f64);
+	return 0;
+}
+
+int PSound9::command24() {
+	loadChannel(8, 0x6224);
+	return 0;
+}
+
+int PSound9::command25() {
+	loadChannel(7, 0x6248);
+	return 0;
+}
+
+int PSound9::command26() {
+	loadChannel(8, 0x6138);
+	return 0;
+}
+
+int PSound9::command27() {
+	loadChannel(8, 0x61f0);
+	return 0;
+}
+
+int PSound9::command28() {
+	loadChannel(7, 0x5fa2);
+	return 0;
+}
+
+int PSound9::command29() {
+	loadChannel(7, 0x5fae);
+	return 0;
+}
+
+int PSound9::command30() {
+	loadChannel(8, 0x5f94);
+	return 0;
+}
+
+int PSound9::command31() {
+	loadChannel(7, 0x5fe8);
+	loadChannel(8, 0x5ff2);
+	return 0;
+}
+
+int PSound9::command32() {
+	loadChannel(7, 0x6202);
+	return 0;
+}
+
+int PSound9::command33() {
+	loadChannel(8, 0x620a);
+	return 0;
+}
+
+int PSound9::command34() {
+	loadCommand34Music();
+	return 0;
+}
+
+int PSound9::command35() {
+	loadChannel(8, 0x615c);
+	return 0;
+}
+
+int PSound9::command36() {
+	loadChannel(7, 0x5fba);
+	loadChannel(8, 0x5fc2);
+	return 0;
+}
+
+int PSound9::command37() {
+	loadChannel(7, 0x621a);
+	return 0;
+}
+
+int PSound9::command38() {
+	scheduleCallback(0x0b16);
+	return 0;
+}
+
+int PSound9::command39() {
+	scheduleCallback(0x0b56);
+	return 0;
+}
+
+int PSound9::command40() {
+	scheduleCallback(0x096e);
+	return 0;
+}
+
+int PSound9::command41() {
+	scheduleCallback(0x09a7);
+	return 0;
+}
+
+int PSound9::command42() {
+	scheduleCallback(0x09e0);
+	return 0;
+}
+
+int PSound9::command43() {
+	loadCommand43Music();
+	return 0;
+}
+
+int PSound9::command4446() {
+	scheduleCallback(0x0851);
+	return 0;
+}
+
+int PSound9::command45() {
+	scheduleCallback(0x087c);
+	return 0;
+}
+
+int PSound9::command47() {
+	scheduleCallback(0x08a7);
+	return 0;
+}
+
+int PSound9::command48() {
+	loadChannel(7, 0x62b4);
+	return 0;
+}
+
+int PSound9::command49() {
+	loadCommand49Music();
+	return 0;
+}
+
+int PSound9::command50() {
+	scheduleCallback(0x0a19);
+	return 0;
+}
+
+int PSound9::command51() {
+	loadCommand51Music();
+	return 0;
+}
+
+
+// -------------------------------------------------------------------------
+// Demo section 1
+// -------------------------------------------------------------------------
+
+const PSoundDemo1::CommandPtr PSoundDemo1::_commandList[39] = {
+	&PSoundDemo1::command0, &PSoundDemo1::command1, &PSoundDemo1::command2, &PSoundDemo1::command3,
+	&PSoundDemo1::command4, &PSoundDemo1::command5, &PSoundDemo1::command6, &PSoundDemo1::command7,
+	&PSoundDemo1::command8, &PSoundDemo1::nullCommand, &PSoundDemo1::command101112, &PSoundDemo1::command101112,
+	&PSoundDemo1::command101112, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand,
+	&PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand, &PSoundDemo1::nullCommand
+};
+
+static const PSoundDriverData kPSoundDemo1Data = {
+	"PSOUND.001",
+	0x2130, 0x236a, 0x2740,
+	0x2134, 0x0134, 32, PSound::kMusicChannelCount,
+	{ 0x0060, 0x00e0, 0x00fc, 0x0106, 0x0110 }
+};
+
+PSoundDemo1::PSoundDemo1(Audio::Mixer *mixer) :
+		PSound(mixer, kPSoundDemo1Data) {
+}
+
+void PSoundDemo1::loadDemoMusic() {
+	if (isSoundActive(0x2140))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	playSoundAny(0x2140);
+	playSoundAny(0x2180);
+	playSoundAny(0x2306);
+	playSoundAny(0x2338);
+}
+
+int PSoundDemo1::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSoundDemo1::command101112() {
+	loadDemoMusic();
+	return 0;
+}
+
+
+// -------------------------------------------------------------------------
+// Demo section 9
+// -------------------------------------------------------------------------
+
+const PSoundDemo9::CommandPtr PSoundDemo9::_commandList[39] = {
+	&PSoundDemo9::command0, &PSoundDemo9::command1, &PSoundDemo9::command2, &PSoundDemo9::command3,
+	&PSoundDemo9::command4, &PSoundDemo9::command5, &PSoundDemo9::command6, &PSoundDemo9::command7,
+	&PSoundDemo9::command8, &PSoundDemo9::nullCommand, &PSoundDemo9::nullCommand, &PSoundDemo9::command11,
+	&PSoundDemo9::nullCommand, &PSoundDemo9::nullCommand, &PSoundDemo9::command14, &PSoundDemo9::nullCommand,
+	&PSoundDemo9::nullCommand, &PSoundDemo9::command17, &PSoundDemo9::nullCommand, &PSoundDemo9::nullCommand,
+	&PSoundDemo9::command20, &PSoundDemo9::command21, &PSoundDemo9::command22, &PSoundDemo9::command23,
+	&PSoundDemo9::nullCommand, &PSoundDemo9::nullCommand, &PSoundDemo9::command26, &PSoundDemo9::nullCommand,
+	&PSoundDemo9::command28, &PSoundDemo9::command29, &PSoundDemo9::command30, &PSoundDemo9::command31,
+	&PSoundDemo9::nullCommand, &PSoundDemo9::nullCommand, &PSoundDemo9::command34, &PSoundDemo9::command35,
+	&PSoundDemo9::command36, &PSoundDemo9::nullCommand, &PSoundDemo9::command38
+};
+
+static const PSoundDriverData kPSoundDemo9Data = {
+	"PSOUND.009",
+	0x21f0, 0x4486, 0x4860,
+	0x2356, 0x0134, 32, PSound::kMusicChannelCount,
+	{ 0x0060, 0x00e0, 0x00fc, 0x0106, 0x0110 }
+};
+
+PSoundDemo9::PSoundDemo9(Audio::Mixer *mixer) :
+		PSound(mixer, kPSoundDemo9Data) {
+}
+
+void PSoundDemo9::loadCommand34Music() {
+	if (isSoundActive(0x2428))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	playSoundAny(0x2428);
+	playSoundAny(0x263a);
+	playSoundAny(0x2952);
+	playSoundAny(0x2ef8);
+	playSoundAny(0x385c);
+	playSoundAny(0x3e48);
+}
+
+void PSoundDemo9::loadCommand38Music() {
+	if (isSoundActive(0x2362))
+		return;
+
+	requestStop(0, _musicChannelCount);
+	playSoundAny(0x2362);
+	playSoundAny(0x2374);
+	playSoundAny(0x23a4);
+	playSoundAny(0x23d6);
+	playSoundAny(0x2402);
+	playSoundAny(0x2416);
+}
+
+int PSoundDemo9::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || static_cast<uint>(commandId) >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int PSoundDemo9::command11() {
+	loadChannel(7, 0x21e2);
+	loadChannel(8, 0x2276);
+	return 0;
+}
+
+int PSoundDemo9::command14() {
+	loadChannel(6, 0x21a6);
+	return 0;
+}
+
+int PSoundDemo9::command17() {
+	loadChannel(7, 0x231c);
+	return 0;
+}
+
+int PSoundDemo9::command20() {
+	writeDataByte(0x2138, (nextRandom() & 0x10) | 0x4d);
+	loadChannel(6, 0x2134);
+	return 0;
+}
+
+int PSoundDemo9::command21() {
+	loadChannel(6, 0x214c);
+	return 0;
+}
+
+int PSoundDemo9::command22() {
+	loadChannel(6, 0x215c);
+	return 0;
+}
+
+int PSoundDemo9::command23() {
+	loadChannel(6, 0x213c);
+	return 0;
+}
+
+int PSoundDemo9::command26() {
+	loadChannel(6, 0x230c);
+	return 0;
+}
+
+int PSoundDemo9::command28() {
+	loadChannel(8, 0x217a);
+	return 0;
+}
+
+int PSoundDemo9::command29() {
+	loadChannel(8, 0x2184);
+	return 0;
+}
+
+int PSoundDemo9::command30() {
+	loadChannel(7, 0x216c);
+	return 0;
+}
+
+int PSoundDemo9::command31() {
+	loadChannel(7, 0x21bc);
+	loadChannel(8, 0x21c6);
+	return 0;
+}
+
+int PSoundDemo9::command34() {
+	loadCommand34Music();
+	return 0;
+}
+
+int PSoundDemo9::command35() {
+	loadChannel(6, 0x232e);
+	return 0;
+}
+
+int PSoundDemo9::command36() {
+	playSoundAny(0x218e);
+	playSoundAny(0x2196);
+	return 0;
+}
+
+int PSoundDemo9::command38() {
+	loadCommand38Music();
+	return 0;
+}
+
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/sound/psound_nebular.h b/engines/mads/nebular/sound/psound_nebular.h
new file mode 100644
index 00000000000..4e0358e9593
--- /dev/null
+++ b/engines/mads/nebular/sound/psound_nebular.h
@@ -0,0 +1,515 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_NEBULAR_SOUND_PSOUND_NEBULAR_H
+#define MADS_NEBULAR_SOUND_PSOUND_NEBULAR_H
+
+#include "mads/nebular/sound/psound.h"
+
+namespace MADS {
+namespace RexNebular {
+namespace Sound {
+
+class PSound1 : public PSound {
+private:
+	typedef int (PSound1:: *CommandPtr)();
+	static const CommandPtr _commandList[42];
+	int command0() override;
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
+	int command41();
+
+	bool _command23Toggle;
+
+	void loadCommand11Music();
+
+public:
+	explicit PSound1(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound2 : public PSound {
+private:
+	typedef int (PSound2:: *CommandPtr)();
+	static const CommandPtr _commandList[44];
+	int command0() override;
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
+	int command41();
+	int command42();
+	int command43();
+
+	byte _command12Phase;
+
+	void mutateCommand9Sequence();
+	void loadCommand9Music();
+	void loadCommand10Music();
+	void loadCommand11Music();
+	void loadCommand15Music();
+	void loadCommand16Music();
+	void loadCommand17Music();
+	void loadCommand19Music();
+	void loadCommand38Music();
+
+public:
+	explicit PSound2(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound3 : public PSound {
+private:
+	typedef int (PSound3:: *CommandPtr)();
+	static const CommandPtr _commandList[61];
+	int command1();
+	int command3();
+	int command9();
+	int command10();
+	int command11();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
+	int command41();
+	int command42();
+	int command43();
+	int command44();
+	int command45();
+	int command51();
+	int command57();
+	int command59();
+	int command60();
+
+	bool _command39Toggle;
+	byte _stopFadeReload;
+	byte getStopFadeReload() const override { return _stopFadeReload; }
+
+	void loadCommand10Music();
+	void loadCommand11Music();
+	void loadCommand13Music();
+	void loadCommand14Music();
+	void loadCommand16Music();
+	void loadCommand17Music();
+	void loadCommand18Music();
+
+public:
+	explicit PSound3(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound4 : public PSound {
+private:
+	typedef int (PSound4:: *CommandPtr)();
+	static const CommandPtr _commandList[60];
+	int command1();
+	int command3();
+	int command9();
+	int command10();
+	int command12();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command27();
+	int command30();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command52();
+	int command53();
+	int command54();
+	int command55();
+	int command56();
+	int command57();
+	int command58();
+	int command59();
+
+	byte _stopFadeReload;
+	byte getStopFadeReload() const override { return _stopFadeReload; }
+
+	void loadCommand10Music();
+	void loadCommand12Music(int param);
+	bool loadCommand53Music();
+
+public:
+	explicit PSound4(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound5 : public PSound {
+private:
+	typedef int (PSound5:: *CommandPtr)();
+	static const CommandPtr _commandList[42];
+	int command9();
+	int command10();
+	int command11122425();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command1921();
+	int command20();
+	int command22();
+	int command23();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
+	int command41();
+
+	void loadCommand29Music();
+
+public:
+	explicit PSound5(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound6 : public PSound {
+private:
+	typedef int (PSound6:: *CommandPtr)();
+	static const CommandPtr _commandList[30];
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command29();
+
+	void loadCommand24Music();
+	void loadCommand29Music();
+
+public:
+	explicit PSound6(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound7 : public PSound {
+private:
+	typedef int (PSound7:: *CommandPtr)();
+	static const CommandPtr _commandList[38];
+	int command9();
+	int command15();
+	int command1617();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command30();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command37();
+
+	void loadCommand9Music();
+	void loadCommand24Music();
+	void loadCommand25Music();
+	void loadCommand26Music();
+	void loadCommand27Music();
+
+public:
+	explicit PSound7(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound8 : public PSound {
+private:
+	typedef int (PSound8:: *CommandPtr)();
+	static const CommandPtr _commandList[38];
+	int command9();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command37();
+
+	void mutateCommand28Sequence();
+	void loadCommand28Music();
+	void loadCommand29Music();
+
+public:
+	explicit PSound8(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSound9 : public PSound {
+private:
+	typedef int (PSound9:: *CommandPtr)();
+	static const CommandPtr _commandList[52];
+	int command0() override;
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
+	int command41();
+	int command42();
+	int command43();
+	int command4446();
+	int command45();
+	int command47();
+	int command48();
+	int command49();
+	int command50();
+	int command51();
+
+	uint16 _scheduledCallbackOffset;
+
+	void scheduleCallback(uint16 callbackOffset);
+	void runScheduledCallback();
+	void tickCallback() override;
+	void loadCommand9Music();
+	void loadCommand10Music();
+	void loadCommand34Music();
+	void loadCommand43Music();
+	void loadCommand49Music();
+	void loadCommand51Music();
+
+public:
+	explicit PSound9(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSoundDemo1 : public PSound {
+private:
+	typedef int (PSoundDemo1:: *CommandPtr)();
+	static const CommandPtr _commandList[39];
+	int command101112();
+
+	void loadDemoMusic();
+
+public:
+	explicit PSoundDemo1(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class PSoundDemo9 : public PSound {
+private:
+	typedef int (PSoundDemo9:: *CommandPtr)();
+	static const CommandPtr _commandList[39];
+	int command11();
+	int command14();
+	int command17();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command26();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command34();
+	int command35();
+	int command36();
+	int command38();
+
+	void loadCommand34Music();
+	void loadCommand38Music();
+
+public:
+	explicit PSoundDemo9(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+} // namespace Sound
+} // namespace RexNebular
+} // namespace MADS
+
+#endif // MADS_NEBULAR_SOUND_PSOUND_NEBULAR_H
diff --git a/engines/mads/nebular/sound/sound.cpp b/engines/mads/nebular/sound/sound.cpp
index bcae5b3cb27..e42073cd865 100644
--- a/engines/mads/nebular/sound/sound.cpp
+++ b/engines/mads/nebular/sound/sound.cpp
@@ -20,16 +20,34 @@
  */
 
 #include "mads/nebular/sound/sound.h"
+
+#include "audio/fmopl.h"
+#include "common/textconsole.h"
 #include "mads/nebular/sound/asound_nebular.h"
 #include "mads/nebular/sound/isound_nebular.h"
+#include "mads/nebular/sound/psound_nebular.h"
 #include "mads/nebular/sound/rsound_nebular.h"
 
 namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
+RexSoundManager::RexSoundManager(Audio::Mixer *mixer, bool &soundFlag,
+		bool usePas, bool isDemo) :
+		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+	if (usePas && _driverType == SOUND_ADLIB) {
+		if (OPL::Config::detect(OPL::Config::kOpl3) >= 0) {
+			_driverType = SOUND_PAS;
+		} else {
+			warning("Pro Audio Spectrum 16 requires OPL3 output; "
+					"falling back to AdLib");
+		}
+	}
+}
+
 void RexSoundManager::validate() {
-	if (_isDemo && _driverType != SOUND_MT32)
+	// The demo has distinct AdLib, MT-32 and PAS overlays, but no ISOUND set.
+	if (_isDemo && _driverType == SOUND_PCSPEAKER)
 		_driverType = SOUND_ADLIB;
 
 	switch (_driverType) {
@@ -41,6 +59,10 @@ void RexSoundManager::validate() {
 		ISound::validate();
 		break;
 
+	case SOUND_PAS:
+		PSound::validate(_isDemo);
+		break;
+
 	default:
 		ASound::validate(_isDemo);
 		break;
@@ -51,11 +73,16 @@ void RexSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
 	if (_isDemo && _driverType == SOUND_ADLIB) {
-		assert(sectionNumber == 1 || sectionNumber == 9);
-		if (sectionNumber == 1)
+		switch (sectionNumber) {
+		case 1:
 			_driver = new ASoundDemo1(_mixer);
-		else
+			break;
+		case 9:
 			_driver = new ASoundDemo9(_mixer);
+			break;
+		default:
+			return;
+		}
 		return;
 	}
 
@@ -139,6 +166,63 @@ void RexSoundManager::loadDriver(int sectionNumber) {
 		}
 		break;
 
+	case SOUND_PAS:
+		// Pro Audio Spectrum drivers
+		if (_isDemo) {
+			switch (sectionNumber) {
+			case 1:
+				_driver = new PSoundDemo1(_mixer);
+				break;
+			case 9:
+				_driver = new PSoundDemo9(_mixer);
+				break;
+			default:
+				return;
+			}
+		} else {
+			switch (sectionNumber) {
+			case 1:
+				_driver = new PSound1(_mixer);
+				break;
+			case 2:
+				_driver = new PSound2(_mixer);
+				break;
+			case 3:
+				_driver = new PSound3(_mixer);
+				break;
+			case 4:
+				_driver = new PSound4(_mixer);
+				break;
+			case 5:
+				_driver = new PSound5(_mixer);
+				break;
+			case 6:
+				_driver = new PSound6(_mixer);
+				break;
+			case 7:
+				_driver = new PSound7(_mixer);
+				break;
+			case 8:
+				_driver = new PSound8(_mixer);
+				break;
+			case 9:
+				_driver = new PSound9(_mixer);
+				break;
+			default:
+				return;
+			}
+		}
+
+		if (_driver && !static_cast<PSound *>(_driver)->isReady()) {
+			warning("Could not initialize Pro Audio Spectrum 16 OPL3 output; "
+					"falling back to AdLib");
+			removeDriver();
+			_driverType = SOUND_ADLIB;
+			loadDriver(sectionNumber);
+			return;
+		}
+		break;
+
 	default:
 		// Adlib drivers
 		switch (sectionNumber) {
diff --git a/engines/mads/nebular/sound/sound.h b/engines/mads/nebular/sound/sound.h
index 2854e29b7cf..79403d24c58 100644
--- a/engines/mads/nebular/sound/sound.h
+++ b/engines/mads/nebular/sound/sound.h
@@ -40,9 +40,7 @@ protected:
 	void loadDriver(int sectionNum) override;
 
 public:
-	RexSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
-		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
-	}
+	RexSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool usePas, bool isDemo);
 	~RexSoundManager() override {
 	}
 


Commit: 0809b132d4d7937724315b7141f588e0165588c4
    https://github.com/scummvm/scummvm/commit/0809b132d4d7937724315b7141f588e0165588c4
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: PHANTOM: Add PAS16 PSOUND support

Reimplement the verified retail and demo overlays through the maintained
PAS16 OPL3 path, preserving native timing and driver selection.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/phantom/sound/psound.cpp
  A engines/mads/phantom/sound/psound.h
  A engines/mads/phantom/sound/psound_phantom.cpp
  A engines/mads/phantom/sound/psound_phantom.h
    engines/mads/detection_tables.h
    engines/mads/module.mk
    engines/mads/phantom/phantom.cpp
    engines/mads/phantom/sound/sound.cpp
    engines/mads/phantom/sound/sound.h


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 69a3b65359c..74035f1341c 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -193,7 +193,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO6(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Phantom,
 		0
@@ -208,7 +208,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO6(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIPCSPK, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Phantom,
 		0
@@ -223,7 +223,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD | GF_INSTALLER,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Phantom,
 		0
@@ -238,7 +238,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Phantom,
 		0
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index f0236135403..750a96a01cf 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -282,6 +282,8 @@ MODULE_OBJS := \
 	phantom/sound/asound_phantom.o \
 	phantom/sound/isound.o \
 	phantom/sound/isound_phantom.o \
+	phantom/sound/psound.o \
+	phantom/sound/psound_phantom.o \
 	phantom/sound/rsound.o \
 	phantom/sound/rsound_phantom.o \
 	phantom/sound/sound.o \
diff --git a/engines/mads/phantom/phantom.cpp b/engines/mads/phantom/phantom.cpp
index 1c3b50647e0..b135bc87548 100644
--- a/engines/mads/phantom/phantom.cpp
+++ b/engines/mads/phantom/phantom.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "engines/util.h"
+#include "common/config-manager.h"
 #include "mads/console.h"
 #include "mads/core/conv.h"
 #include "mads/core/env.h"
@@ -63,7 +64,8 @@ Common::Error PhantomEngine::run() {
 	}
 
 	// Set up sound manager
-	_soundManager = new Sound::PhantomSoundManager(_mixer, _soundFlag, isDemo());
+	_soundManager = new Sound::PhantomSoundManager(_mixer, _soundFlag,
+			ConfMan.getBool("use_pas"), isDemo());
 	_soundManager->validate();
 
 	// Run the game
diff --git a/engines/mads/phantom/sound/psound.cpp b/engines/mads/phantom/sound/psound.cpp
new file mode 100644
index 00000000000..bd87be8695a
--- /dev/null
+++ b/engines/mads/phantom/sound/psound.cpp
@@ -0,0 +1,1271 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "audio/fmopl.h"
+#include "common/endian.h"
+#include "common/file.h"
+#include "common/func.h"
+#include "common/md5.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/phantom/sound/psound.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+namespace {
+
+const int kHostCallbackRateHz =
+		NativeSoundTimer::kPitClockHz / NativeSoundTimer::kHostTimerDivisor;
+
+int clampLevel(int value) {
+	return CLIP(value, 0, 63);
+}
+
+byte panningBits(byte panning) {
+	if (panning < 0x2b)
+		return 0x10;
+	if (panning < 0x55)
+		return 0x30;
+	return 0x20;
+}
+
+} // namespace
+
+void PSound::Channel::reset() {
+	memset(this, 0, sizeof(*this));
+	volumeFadeReload = 0xff;
+	panning = 0x40;
+}
+
+void PSound::Channel::load(uint16 sequenceOffset) {
+	reset();
+	loopStart = sequenceOffset;
+	position = sequenceOffset;
+	innerLoopStart = sequenceOffset;
+	outerLoopStart = sequenceOffset;
+	originalSequence = sequenceOffset;
+	activeCount = 1;
+}
+
+bool PSound::validateFile(const PSoundDriverData &driverData,
+						  const char *first8192Md5, Common::String *reason) {
+	Common::File file;
+	if (!file.open(driverData.filename)) {
+		if (reason)
+			*reason = "file is missing";
+		return false;
+	}
+	const uint32 minimumSize = driverData.dataOffset +
+			driverData.initializedDataSize;
+	if ((uint32)file.size() < minimumSize) {
+		if (reason)
+			*reason = "initialized data is truncated";
+		return false;
+	}
+
+	file.seek(0);
+	const Common::String md5 = Common::computeStreamMD5AsString(file, 8192);
+	if (md5 != first8192Md5) {
+		if (reason)
+			*reason = "first-8192-byte signature does not match";
+		return false;
+	}
+	return true;
+}
+
+PSound::PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData) :
+		SoundDriver(mixer, driverData.filename, driverData.dataOffset,
+				driverData.initializedDataSize), _opl(nullptr) {
+	_masterVolume = 255;
+	_randomSeed = 0;
+	_frameCounter = 0;
+	_pollResult = 0;
+	_resultFlag = 0;
+	_nullSequenceOffset = driverData.nullSequenceOffset;
+	_patchTableOffset = driverData.patchTableOffset;
+	_tableLayout = driverData.tables;
+	_patchCount = driverData.patchCount;
+	_updatesEnabled = false;
+	_noiseServiceEnabled = false;
+	_tickEnabled = 1;
+	_tickCounter = 0;
+	_tempoReload = 0;
+	_tempoTarget = 0;
+	_tempoShift = 0;
+	_tempoBase = 0xa0;
+	_tempoCurrent = 0x28;
+	_tempoScale = 0x0a;
+	_frameNumber2 = 0;
+	if (_soundData.size() != (uint32)driverData.initializedDataSize)
+		error("Phantom PSOUND initialized data has unexpected size %u (expected %d)",
+			  (uint)_soundData.size(), driverData.initializedDataSize);
+	if (driverData.totalDataSize < driverData.initializedDataSize ||
+		driverData.totalDataSize > 0xffff)
+		error("Phantom PSOUND has invalid mutable data size %d",
+			  driverData.totalDataSize);
+
+	const uint32 initializedSize = _soundData.size();
+	_soundData.resize(driverData.totalDataSize);
+	if (_soundData.size() > initializedSize)
+		memset(&_soundData[initializedSize], 0,
+			   _soundData.size() - initializedSize);
+
+	if (!_patchCount ||
+		_patchTableOffset + (uint32)_patchCount * kPatchSize > initializedSize)
+		error("Phantom PSOUND patch table is outside initialized data");
+	if ((uint32)driverData.randomSeedOffset + 2 > initializedSize)
+		error("Phantom PSOUND random seed is outside initialized data");
+	if ((uint32)_tableLayout.panning + 0x80 > initializedSize ||
+		(uint32)_tableLayout.frequency + 24 > initializedSize ||
+		(uint32)_tableLayout.bank + kChannelCount > initializedSize ||
+		(uint32)_tableLayout.channel + kChannelCount > initializedSize ||
+		(uint32)_tableLayout.operators + kChannelCount * 4 > initializedSize)
+		error("Phantom PSOUND lookup table is outside initialized data");
+	if (_nullSequenceOffset >= initializedSize)
+		error("Phantom PSOUND null sequence is outside initialized data");
+
+	for (uint channel = 0; channel < kChannelCount; ++channel) {
+		const byte banks = getBankMask(channel);
+		if (!banks || (banks & ~kBothBanks))
+			error("Phantom PSOUND has invalid bank routing for channel %u", channel);
+		if (getOplChannel(channel) > 8)
+			error("Phantom PSOUND has invalid OPL routing for channel %u", channel);
+		for (uint op = 0; op < 4; ++op) {
+			if (getOperatorOffset(channel, op) > 0x15)
+				error("Phantom PSOUND has invalid operator routing for channel %u",
+					  channel);
+		}
+		_channels[channel].reset();
+	}
+	memset(_scriptVars, 0, sizeof(_scriptVars));
+	memset(_registerCache, 0, sizeof(_registerCache));
+	_randomSeed = readDataUint16(driverData.randomSeedOffset);
+
+	// The original overlays select this path only for PAS16 card types
+	// 20h-22h. ScummVM intentionally implements that native branch and not the
+	// older dual-OPL2 branch in the same binaries.
+	_opl = OPL::Config::create(OPL::Config::kOpl3);
+	if (!_opl || !_opl->init()) {
+		delete _opl;
+		_opl = nullptr;
+		return;
+	}
+	initializePas16();
+	resetDriver();
+	_updatesEnabled = true;
+	_opl->start(new Common::Functor0Mem<void, PSound>(this,
+													  &PSound::onTimer),
+				kHostCallbackRateHz);
+}
+
+PSound::~PSound() {
+	if (_opl) {
+		_opl->stop();
+		shutdownPas16();
+		delete _opl;
+	}
+}
+
+bool PSound::isDataRangeValid(uint32 offset, uint32 length) const {
+	return offset <= _soundData.size() && length <= _soundData.size() - offset;
+}
+
+const byte *PSound::getDataPointer(uint32 offset, uint32 length,
+								   const char *operation) const {
+	if (!isDataRangeValid(offset, length))
+		error("Phantom PSOUND %s outside data image: offset 0x%04x, length %u",
+			  operation, (uint)offset, (uint)length);
+	return &_soundData[offset];
+}
+
+byte *PSound::getDataPointer(uint32 offset, uint32 length,
+							 const char *operation) {
+	return const_cast<byte *>(static_cast<const PSound *>(this)->getDataPointer(offset, length, operation));
+}
+
+byte PSound::readDataByte(uint32 offset) const {
+	return *getDataPointer(offset, 1, "byte read");
+}
+
+uint16 PSound::readDataUint16(uint32 offset) const {
+	return READ_LE_UINT16(getDataPointer(offset, 2, "word read"));
+}
+
+void PSound::writeDataByte(uint32 offset, byte value) {
+	*getDataPointer(offset, 1, "byte write") = value;
+}
+
+void PSound::writeDataUint16(uint32 offset, uint16 value) {
+	WRITE_LE_UINT16(getDataPointer(offset, 2, "word write"), value);
+}
+
+byte PSound::getBankMask(uint channel) const {
+	return readDataByte(_tableLayout.bank + channel);
+}
+
+byte PSound::getOplChannel(uint channel) const {
+	return readDataByte(_tableLayout.channel + channel);
+}
+
+byte PSound::getOperatorOffset(uint channel, uint operatorIndex) const {
+	return readDataByte(_tableLayout.operators + channel * 4 + operatorIndex);
+}
+
+const byte *PSound::getPatch(uint patchIndex) const {
+	if (patchIndex >= _patchCount)
+		patchIndex = 0;
+	return getDataPointer(_patchTableOffset + (uint32)patchIndex * kPatchSize,
+						  kPatchSize, "patch");
+}
+
+byte PSound::getPanningAttenuation(byte panning) const {
+	return readDataByte(_tableLayout.panning + (panning & 0x7f));
+}
+
+uint16 PSound::getFrequencyNumber(byte semitone) const {
+	return readDataUint16(_tableLayout.frequency + (semitone % 12) * 2);
+}
+
+void PSound::writeRegister(byte banks, byte reg, byte value) {
+	assert(_opl);
+	assert(banks && !(banks & ~kBothBanks));
+	if (banks & kFirstBank) {
+		_registerCache[0][reg] = value;
+		_opl->writeReg(reg, value);
+	}
+	if (banks & kSecondBank) {
+		_registerCache[1][reg] = value;
+		_opl->writeReg(0x100 | reg, value);
+	}
+}
+
+byte PSound::getCachedRegister(byte banks, byte reg) const {
+	assert(banks && !(banks & ~kBothBanks));
+	return _registerCache[(banks & kFirstBank) ? 0 : 1][reg];
+}
+
+void PSound::initializePas16() {
+	writeRegister(kSecondBank, 0x05, 0x01);
+	writeRegister(kSecondBank, 0x04, 0x3f);
+}
+
+void PSound::shutdownPas16() {
+	writeRegister(kSecondBank, 0x04, 0);
+	writeRegister(kSecondBank, 0x05, 0);
+}
+
+void PSound::resetDriver() {
+	const bool wasEnabled = _updatesEnabled;
+	_updatesEnabled = false;
+	for (uint i = 0; i < kChannelCount; ++i)
+		_channels[i].reset();
+	memset(_scriptVars, 0, sizeof(_scriptVars));
+	_resultFlag = 0;
+	_pollResult = 0;
+
+	// OPL3 mode is established only at driver initialization, matching the
+	// original card-selection path rather than being re-enabled by command 0.
+	for (int reg = 0x56; reg >= 0x40; --reg)
+		writeRegister(kBothBanks, reg, 0x3f);
+	for (int reg = 0xff; reg >= 0x60; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	for (int reg = 0x3f; reg >= 0x20; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	writeRegister(kBothBanks, 0x01, 0x20);
+	writeRegister(kBothBanks, 0xbd, 0xc0);
+
+	_updatesEnabled = wasEnabled;
+}
+
+void PSound::requestStop(uint firstChannel, uint endChannel) {
+	for (uint i = firstChannel; i < endChannel; ++i) {
+		if (_channels[i].activeCount) {
+			_channels[i].pendingStop = 0xff;
+			_channels[i].originalSequence = 0xffff;
+		}
+	}
+}
+
+void PSound::setCurrentSequence(uint firstChannel, uint endChannel,
+								uint16 sequenceOffset) {
+	getDataPointer(sequenceOffset, 1, "sequence position");
+	for (uint i = firstChannel; i < endChannel; ++i)
+		_channels[i].position = sequenceOffset;
+}
+
+void PSound::loadChannel(uint channel, uint16 sequenceOffset) {
+	assert(channel < kChannelCount);
+	getDataPointer(sequenceOffset, 1, "sequence start");
+	_channels[channel].load(sequenceOffset);
+}
+
+void PSound::playSound(uint16 sequenceOffset) {
+	// Native effect allocator: free 7,8,6; then pending-stop 6,7,8.
+	static const byte freeOrder[] = {7, 8, 6};
+	static const byte pendingOrder[] = {6, 7, 8};
+	for (uint i = 0; i < ARRAYSIZE(freeOrder); ++i) {
+		const byte channel = freeOrder[i];
+		if (!_channels[channel].activeCount) {
+			loadChannel(channel, sequenceOffset);
+			return;
+		}
+	}
+	for (uint i = 0; i < ARRAYSIZE(pendingOrder); ++i) {
+		const byte channel = pendingOrder[i];
+		if (_channels[channel].pendingStop == 0xff) {
+			loadChannel(channel, sequenceOffset);
+			return;
+		}
+	}
+}
+
+void PSound::playMusicAny(uint16 sequenceOffset) {
+	// This native section helper is music-only despite its generic role in the
+	// original code: it never considers effect channels 6-8.
+	for (uint i = 0; i < kMusicChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (uint i = 0; i < kMusicChannelCount; ++i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+}
+
+bool PSound::isSoundActive(uint16 sequenceOffset) const {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount &&
+			_channels[i].originalSequence == sequenceOffset)
+			return true;
+	}
+	return false;
+}
+
+uint16 PSound::nextRandom() {
+	const uint16 value = 0x9248 + _randomSeed;
+	_randomSeed = (value >> 3) | (value << 13);
+	return _randomSeed;
+}
+
+int PSound::command0() {
+	resetDriver();
+	return 0;
+}
+
+int PSound::command1() {
+	command3();
+	command5();
+	return 0;
+}
+
+int PSound::command2() {
+	setCurrentSequence(0, kMusicChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command3() {
+	requestStop(0, kMusicChannelCount);
+	return 0;
+}
+
+int PSound::command4() {
+	setCurrentSequence(kMusicChannelCount, kChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command5() {
+	requestStop(kMusicChannelCount, kChannelCount);
+	return 0;
+}
+
+int PSound::command6() {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		_channels[i].savedNoiseTicks = _channels[i].noiseTicks;
+		_channels[i].noiseTicks = 0;
+		keyOff(i);
+	}
+	_updatesEnabled = false;
+	return 0;
+}
+
+int PSound::command7() {
+	_updatesEnabled = true;
+	for (uint i = 0; i < kChannelCount; ++i) {
+		Channel &channel = _channels[i];
+		channel.noiseTicks = channel.savedNoiseTicks;
+		if (channel.activeCount) {
+			updateChannelLevels(i);
+			updateChannelFrequency(i, true);
+		}
+	}
+	bool anyNoise = false;
+	for (uint i = 0; i < kChannelCount; ++i)
+		anyNoise |= _channels[i].noiseTicks != 0;
+	if (anyNoise)
+		resultCheck();
+	return _channels[kChannelCount - 1].savedNoiseTicks;
+}
+
+int PSound::command8() {
+	int result = 0;
+	for (uint i = 0; i < kChannelCount; ++i)
+		result |= _channels[i].activeCount;
+	return result;
+}
+
+void PSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+	uint32 serviceTicks = _hostTimer.advance(1, kHostCallbackRateHz);
+	while (serviceTicks--) {
+		if (_noiseServiceEnabled)
+			serviceNoise();
+		if (_hostTimer.pollDue()) {
+			const int result = serviceUpdate();
+			if (result)
+				_noiseServiceEnabled = result > 0;
+		}
+	}
+}
+
+int PSound::serviceUpdate() {
+	update();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void PSound::serviceNoise() {
+	for (int i = kChannelCount - 1; i >= 0; --i) {
+		Channel &channel = _channels[i];
+		if (channel.noiseTicks) {
+			const uint16 random = nextRandom();
+			setNoiseFrequency(i,
+							  (random & channel.noiseMask) + channel.noiseBase);
+		}
+	}
+}
+
+void PSound::update() {
+	if (!_updatesEnabled)
+		return;
+	nextRandom();
+	++_frameNumber2;
+	++_frameCounter;
+	for (uint i = 0; i < kChannelCount; ++i)
+		updateChannel(i);
+	checkPendingStops();
+
+	bool anyNoise = false;
+	for (int i = kChannelCount - 1; i >= 0; --i) {
+		Channel &channel = _channels[i];
+		if (!channel.noiseTicks)
+			continue;
+		anyNoise = true;
+		channel.noiseBase += channel.noiseStep;
+		if (!--channel.noiseTicks)
+			keyOff(i);
+	}
+	if (!anyNoise && _resultFlag != -1) {
+		_resultFlag = -1;
+		_pollResult = -1;
+	}
+}
+
+void PSound::updateChannel(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	if (!channel.activeCount)
+		return;
+
+	if (channel.keyOnDelay && --channel.keyOnDelay == 0)
+		keyOff(channelIndex);
+
+	if (--channel.activeCount == 0) {
+		bool levelsDirty = false;
+		int budget = kOpcodeBudgetPerTick;
+		while (budget-- > 0) {
+			if (!isDataRangeValid(channel.position, 1)) {
+				finishChannel(channelIndex);
+				break;
+			}
+			const byte value = readDataByte(channel.position);
+			if (value <= 0xbd) {
+				if (!isDataRangeValid(channel.position, 2)) {
+					finishChannel(channelIndex);
+					break;
+				}
+				if (levelsDirty)
+					updateChannelLevels(channelIndex);
+				channel.note = value;
+				channel.activeCount = readDataByte(channel.position + 1);
+				channel.position += 2;
+				if (!channel.note || !channel.activeCount) {
+					keyOff(channelIndex);
+					if (!channel.activeCount)
+						finishChannel(channelIndex);
+				} else {
+					channel.keyOnDelay = channel.durationOverride ? channel.durationOverride : (byte)(channel.activeCount - channel.noteOffset);
+					updateChannelFrequency(channelIndex, true);
+				}
+				break;
+			}
+
+			if (!executeOpcode(channelIndex, value, levelsDirty)) {
+				finishChannel(channelIndex);
+				break;
+			}
+		}
+		if (budget < 0 && !channel.activeCount)
+			finishChannel(channelIndex);
+	}
+
+	if (channel.pitchBend)
+		updatePitchBend(channelIndex);
+
+	bool levelsDirty = false;
+	if (channel.volumeFadeCounter || channel.panningFadeCounter) {
+		if (--channel.volumeFadeCounter == 0) {
+			channel.volumeFadeCounter = channel.volumeFadeReload;
+			if (channel.volumeFadeStep) {
+				channel.volumeOffset += channel.volumeFadeStep;
+				levelsDirty = true;
+			}
+		}
+		if (--channel.panningFadeCounter == 0) {
+			channel.panningFadeCounter = channel.panningFadeReload;
+			if (channel.panningFadeStep) {
+				channel.panning += channel.panningFadeStep;
+				updatePanning(channelIndex);
+				levelsDirty = true;
+			}
+		}
+	}
+	if (levelsDirty)
+		updateChannelLevels(channelIndex);
+}
+
+bool PSound::isOpcodeDataValid(uint16 position, uint32 length) const {
+	return isDataRangeValid(position, length);
+}
+
+byte PSound::readOpcodeByte(uint16 position, uint16 delta) const {
+	return readDataByte((uint16)(position + delta));
+}
+
+uint16 PSound::readOpcodeWord(uint16 position, uint16 delta) const {
+	return readDataUint16((uint16)(position + delta));
+}
+
+bool PSound::isScriptVariableValid(byte index) const {
+	return index < kScriptVarCount;
+}
+
+bool PSound::transferOpcode(Channel &channel, uint16 position, bool take,
+		bool isCall) {
+	if (!isOpcodeDataValid(position, 5))
+		return false;
+	if (!take) {
+		channel.position = position + 5;
+		return true;
+	}
+	const uint16 target = readOpcodeWord(position, 3);
+	if (!isDataRangeValid(target, 1))
+		return false;
+	if (isCall)
+		channel.branchReturn = position + 5;
+	channel.position = target;
+	return true;
+}
+
+bool PSound::executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty) {
+	Channel &channel = _channels[channelIndex];
+	const uint16 position = channel.position;
+
+	switch (opcode) {
+	case 0xff: {
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		const uint16 count = (uint16)(int16)(int8)
+				readOpcodeByte(position, 1);
+		if (!channel.innerLoopCount) {
+			if (!count) {
+				channel.position = position + 2;
+				channel.innerLoopStart = channel.position;
+			} else {
+				channel.innerLoopCount = count;
+				channel.position = channel.innerLoopStart;
+			}
+		} else if (--channel.innerLoopCount) {
+			channel.position = channel.innerLoopStart;
+		} else {
+			channel.position = position + 2;
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+	case 0xfe: {
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		const uint16 count = (uint16)(int16)(int8)
+				readOpcodeByte(position, 1);
+		if (!channel.outerLoopCount) {
+			if (!count) {
+				channel.position = position + 2;
+				channel.outerLoopStart = channel.position;
+				channel.innerLoopStart = channel.position;
+				channel.innerLoopCount = 0;
+			} else {
+				channel.outerLoopCount = count;
+				channel.position = channel.outerLoopStart;
+				channel.innerLoopStart = channel.outerLoopStart;
+			}
+		} else if (--channel.outerLoopCount) {
+			channel.position = channel.outerLoopStart;
+			channel.innerLoopStart = channel.outerLoopStart;
+		} else {
+			channel.position = position + 2;
+			channel.outerLoopStart = channel.position;
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+	case 0xfd:
+		channel.loopStart = channel.originalSequence;
+		channel.position = channel.originalSequence;
+		channel.innerLoopStart = channel.originalSequence;
+		channel.outerLoopStart = channel.originalSequence;
+		channel.pitchBend = 0;
+		channel.volumeFadeStep = 0;
+		channel.panningFadeStep = 0;
+		channel.transpose = 0;
+		channel.volumeOffset = 0;
+		channel.volume = 0;
+		channel.volumeFadeCounter = 0;
+		channel.panningFadeCounter = 0;
+		channel.innerLoopCount = 0;
+		channel.outerLoopCount = 0;
+		channel.noteOffset = 0;
+		break;
+	case 0xfc: {
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1))
+			return false;
+		channel.loopStart = channel.position = channel.innerLoopStart =
+			channel.outerLoopStart = channel.originalSequence = target;
+		break;
+	}
+	case 0xfb: {
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1))
+			return false;
+		channel.position = target;
+		break;
+	}
+	case 0xfa: {
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1))
+			return false;
+		channel.branchReturn = position + 3;
+		channel.position = target;
+		break;
+	}
+	case 0xf9:
+		if (channel.branchReturn) {
+			channel.position = channel.branchReturn;
+			channel.branchReturn = 0;
+		} else {
+			++channel.position;
+		}
+		break;
+	case 0xf8:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.patch = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		loadPatch(channelIndex, channel.patch);
+		break;
+	case 0xf7:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.noteOffset = readOpcodeByte(position, 1);
+		channel.durationOverride = 0;
+		channel.position = position + 2;
+		break;
+	case 0xf6:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.durationOverride = readOpcodeByte(position, 1);
+		channel.noteOffset = 0;
+		channel.position = position + 2;
+		break;
+	case 0xf5:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.pitchBend = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xf4:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.volume = (byte)((int8)readOpcodeByte(position, 1) >> 1);
+		channel.position = position + 2;
+		levelsDirty = true;
+		break;
+	case 0xf3:
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		if (!channel.pendingStop) {
+			channel.volumeFadeReload = readOpcodeByte(position, 1);
+			channel.volumeFadeStep = (int8)readOpcodeByte(position, 2);
+			channel.volumeFadeCounter = 1;
+		}
+		channel.position = position + 3;
+		break;
+	case 0xf2:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.transpose = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xf1: {
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		const int8 value =
+			(int8)(((int8)readOpcodeByte(position, 1) >> 1) - 50);
+		if (!channel.pendingStop || value < channel.volumeOffset) {
+			channel.volumeOffset = value;
+			levelsDirty = true;
+		}
+		channel.position = position + 2;
+		break;
+	}
+	case 0xf0:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.panning = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		updatePanning(channelIndex);
+		levelsDirty = true;
+		break;
+	case 0xef:
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		channel.panningFadeReload = readOpcodeByte(position, 1);
+		channel.panningFadeStep = (int8)readOpcodeByte(position, 2);
+		channel.panningFadeCounter = 1;
+		channel.position = position + 3;
+		break;
+	case 0xee:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.noteTranspose = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xed:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.position = (uint16)(position +
+			(int8)readOpcodeByte(position, 1) + 3);
+		break;
+	case 0xec: {
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		const byte count = readOpcodeByte(position, 1);
+		if (!count || !isOpcodeDataValid(position, (uint32)count + 3))
+			return false;
+		const uint16 base = position + 2;
+		const byte selected = readDataByte(base +
+										   (nextRandom() & 0x7fff) % count);
+		const byte target = readDataByte(base + count);
+		if (!isDataRangeValid(base + count + target + 1, 1))
+			return false;
+		writeDataByte(base + count + target + 1, selected);
+		channel.position = position + count + 3;
+		break;
+	}
+	case 0xeb: {
+		if (!isOpcodeDataValid(position, 4))
+			return false;
+		const int low = (int8)readOpcodeByte(position, 1);
+		const int high = (int8)readOpcodeByte(position, 2);
+		const int range = high - low + 1;
+		if (range <= 0)
+			return false;
+		const byte target = readOpcodeByte(position, 3);
+		if (!isDataRangeValid(position + 4 + target, 1))
+			return false;
+		writeDataByte(position + 4 + target,
+					  (byte)(low + (nextRandom() & 0x7fff) % range));
+		channel.position = position + 4;
+		break;
+	}
+	case 0xea: {
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		const byte variable = readOpcodeByte(position, 1);
+		const byte count = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(variable) ||
+				!isOpcodeDataValid(position, (uint32)count + 4))
+			return false;
+		const uint16 base = position + 3;
+		const byte target = readDataByte(base + count);
+		if (!isDataRangeValid(base + _scriptVars[variable], 1) ||
+			!isDataRangeValid(base + target + 1, 1))
+			return false;
+		writeDataByte(base + target + 1,
+					  readDataByte(base + _scriptVars[variable]));
+		channel.position = position + count + 4;
+		break;
+	}
+	case 0xe9:
+	case 0xe8:
+	case 0xe7:
+	case 0xe4:
+	case 0xe3:
+	case 0xe2:
+	case 0xe1:
+	case 0xe0:
+	case 0xdf:
+	case 0xde:
+	case 0xdd:
+	case 0xdc:
+	case 0xdb:
+	case 0xda:
+	case 0xd9:
+	case 0xd8:
+	case 0xd7:
+	case 0xd6:
+	case 0xd5: {
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		const byte first = readOpcodeByte(position, 1);
+		const byte second = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(first))
+			return false;
+		if (opcode == 0xe9) {
+			_scriptVars[first] = second;
+		} else if (opcode == 0xe8) {
+			if (!isScriptVariableValid(second))
+				return false;
+			_scriptVars[first] = _scriptVars[second];
+		} else if (opcode == 0xe7) {
+			if (!isDataRangeValid(position + 3 + second, 1))
+				return false;
+			writeDataByte(position + 3 + second, _scriptVars[first]);
+		} else {
+			const bool usesVariable = (opcode & 1) != 0;
+			if (usesVariable && !isScriptVariableValid(second))
+				return false;
+			const byte operand = usesVariable ? _scriptVars[second] : second;
+			byte &destination = _scriptVars[first];
+			switch (opcode) {
+			case 0xe4:
+			case 0xe3:
+				destination += operand;
+				break;
+			case 0xe2:
+			case 0xe1:
+				destination -= operand;
+				break;
+			case 0xe0:
+			case 0xdf:
+				destination *= operand;
+				break;
+			case 0xde:
+			case 0xdd:
+				if (!operand)
+					return false;
+				destination /= operand;
+				break;
+			case 0xdc:
+			case 0xdb:
+				if (!operand)
+					return false;
+				destination %= operand;
+				break;
+			case 0xda:
+			case 0xd9:
+				destination &= operand;
+				break;
+			case 0xd8:
+			case 0xd7:
+				destination |= operand;
+				break;
+			case 0xd6:
+			case 0xd5:
+				destination ^= operand;
+				break;
+			default:
+				break;
+			}
+		}
+		channel.position = position + 3;
+		break;
+	}
+	case 0xe6:
+	case 0xe5: {
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		const byte variable = readOpcodeByte(position, 1);
+		if (!isScriptVariableValid(variable))
+			return false;
+		_scriptVars[variable] += opcode == 0xe6 ? 1 : (byte)-1;
+		channel.position = position + 2;
+		break;
+	}
+	case 0xd4:
+	case 0xd3:
+	case 0xd2:
+	case 0xd1:
+	case 0xd0:
+	case 0xcf:
+	case 0xce:
+	case 0xcd:
+	case 0xcc:
+	case 0xcb:
+	case 0xca:
+	case 0xc9:
+	case 0xc8:
+	case 0xc7:
+	case 0xc6:
+	case 0xc5: {
+		if (!isOpcodeDataValid(position, 5))
+			return false;
+		const byte first = readOpcodeByte(position, 1);
+		const byte second = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(first))
+			return false;
+		const bool variablePair =
+			(opcode >= 0xcd && opcode <= 0xd0) || opcode <= 0xc8;
+		if (variablePair && !isScriptVariableValid(second))
+			return false;
+		const byte lhs = _scriptVars[first];
+		const byte rhs = variablePair ? _scriptVars[second] : second;
+		bool take = false;
+		switch (opcode) {
+		case 0xd4:
+		case 0xcc:
+		case 0xd0:
+		case 0xc8:
+			take = lhs == rhs;
+			break;
+		case 0xd3:
+		case 0xcb:
+		case 0xcf:
+		case 0xc7:
+			take = lhs != rhs;
+			break;
+		case 0xd2:
+		case 0xca:
+		case 0xce:
+		case 0xc6:
+			take = lhs < rhs;
+			break;
+		case 0xd1:
+		case 0xc9:
+		case 0xcd:
+		case 0xc5:
+			take = lhs > rhs;
+			break;
+		default:
+			break;
+		}
+		if (!transferOpcode(channel, position, take, opcode <= 0xcc))
+			return false;
+		break;
+	}
+	case 0xc4:
+		if (!isOpcodeDataValid(position, 3) ||
+				!callFunction(readOpcodeWord(position, 1), channel))
+			return false;
+		channel.position = position + 3;
+		break;
+	case 0xc3:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		channel.position = position + 2;
+		break;
+	case 0xc2:
+		if (!isOpcodeDataValid(position, 4))
+			return false;
+		channel.position = position + 4;
+		break;
+	case 0xc1:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		_tempoScale = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xc0:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		_tempoReload = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		if (!_frameNumber2)
+			_tempoCurrent = _tempoReload;
+		break;
+	case 0xbf:
+		if (!isOpcodeDataValid(position, 3))
+			return false;
+		_tempoTarget = readOpcodeWord(position, 1);
+		channel.position = position + 3;
+		if (!_frameNumber2)
+			_tempoBase = _tempoTarget;
+		_tickEnabled = 1;
+		_tickCounter = 1;
+		break;
+	case 0xbe:
+		if (!isOpcodeDataValid(position, 2))
+			return false;
+		_tempoShift = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	default:
+		return false;
+	}
+	return true;
+}
+
+void PSound::finishChannel(uint channelIndex) {
+	keyOff(channelIndex);
+	_channels[channelIndex].activeCount = 0;
+	_channels[channelIndex].keyOnDelay = 0;
+}
+
+void PSound::checkPendingStops() {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		Channel &channel = _channels[i];
+		if (!channel.activeCount || !channel.pendingStop)
+			continue;
+		if ((byte)channel.volumeOffset == 0xd8) {
+			channel.position = _nullSequenceOffset;
+			channel.pendingStop = 0;
+		} else {
+			channel.volumeFadeStep = -1;
+			channel.volumeFadeReload = 4;
+			if (!channel.volumeFadeCounter)
+				channel.volumeFadeCounter = 1;
+		}
+	}
+}
+
+void PSound::programOperator(byte banks, uint channelIndex,
+							 uint operatorIndex, const byte *operatorData) {
+	const byte op = getOperatorOffset(channelIndex, operatorIndex);
+	const byte characteristics = (operatorData[9] & 0x0f) |
+								 ((operatorData[5] & 1) << 4) | ((operatorData[4] & 1) << 5) |
+								 ((operatorData[12] & 1) << 6) | ((operatorData[11] & 1) << 7);
+	const byte totalLevel = ((operatorData[7] & 3) << 6) |
+							clampLevel(0x3f - (operatorData[6] & 0x3f));
+	writeRegister(banks, 0x40 + op, 0x3f);
+	writeRegister(banks, 0x20 + op, characteristics);
+	writeRegister(banks, 0x60 + op,
+				  (operatorData[0] << 4) | (operatorData[1] & 0x0f));
+	writeRegister(banks, 0x80 + op,
+				  (operatorData[2] << 4) | (operatorData[3] & 0x0f));
+	writeRegister(banks, 0xe0 + op, operatorData[8] & 3);
+	writeRegister(banks, 0x40 + op, totalLevel);
+}
+
+void PSound::loadPatch(uint channelIndex, byte patchIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(patchIndex);
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const uint operatorCount = channelIndex < kMusicChannelCount ? 4 : 2;
+
+	keyOff(channelIndex);
+	channel.mode = patch[0x0d];
+	for (uint i = 0; i < 4; ++i)
+		channel.operatorTotalLevel[i] = patch[i * 14 + 6];
+	for (uint i = 0; i < operatorCount; ++i)
+		programOperator(banks, channelIndex, i, patch + i * 14);
+
+	if (channelIndex < kMusicChannelCount) {
+		const byte stereo = panningBits(channel.panning);
+		const byte feedback = (patch[0x0a] & 7) << 1;
+		writeRegister(banks, 0xc0 + oplChannel,
+					  stereo | feedback | (channel.mode >> 1));
+		writeRegister(banks, 0xc3 + oplChannel,
+					  stereo | feedback | (channel.mode & 1));
+	} else {
+		const byte value = ((patch[0x0a] & 7) << 1) |
+						   ((channel.mode & 1) ^ 1);
+		writeRegister(kFirstBank, 0xc0 + oplChannel, value | 0x20);
+		writeRegister(kSecondBank, 0xc0 + oplChannel, value | 0x10);
+	}
+
+	channel.noiseTicks = patch[0x38];
+	channel.noiseMask = READ_LE_UINT16(patch + 0x3a);
+	channel.noiseBase = READ_LE_UINT16(patch + 0x3c);
+	channel.noiseStep = (int16)READ_LE_UINT16(patch + 0x3e);
+	if (channel.noiseTicks)
+		resultCheck();
+	updatePanning(channelIndex);
+	updateChannelLevels(channelIndex);
+}
+
+void PSound::updatePanning(uint channelIndex) {
+	const byte oplChannel = getOplChannel(channelIndex);
+	if (channelIndex < kMusicChannelCount) {
+		const byte banks = getBankMask(channelIndex);
+		const byte stereo = panningBits(_channels[channelIndex].panning);
+		const byte first = 0xc0 + oplChannel;
+		const byte second = 0xc3 + oplChannel;
+		writeRegister(banks, first,
+					  (getCachedRegister(banks, first) & 0x0f) | stereo);
+		writeRegister(banks, second,
+					  (getCachedRegister(banks, second) & 0x0f) | stereo);
+	} else {
+		const byte reg = 0xc0 + oplChannel;
+		writeRegister(kFirstBank, reg,
+					  (getCachedRegister(kFirstBank, reg) & 0x0f) | 0x20);
+		writeRegister(kSecondBank, reg,
+					  (getCachedRegister(kSecondBank, reg) & 0x0f) | 0x10);
+	}
+}
+
+void PSound::updateChannelLevels(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(channel.patch);
+	int base = 0x7e - channel.volume - channel.volumeOffset -
+			   channel.patchAttenuation;
+	base += (255 - _masterVolume) * 63 / 255;
+
+	if (channelIndex >= kMusicChannelCount) {
+		const uint operators[2] = {1, 0};
+		const bool enabled[2] = {true, channel.mode == 0};
+		for (uint i = 0; i < 2; ++i) {
+			if (!enabled[i])
+				continue;
+			const uint opIndex = operators[i];
+			const byte op = getOperatorOffset(channelIndex, opIndex);
+			const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+			const int left = clampLevel(base -
+										channel.operatorTotalLevel[opIndex] +
+										getPanningAttenuation(channel.panning));
+			const int right = clampLevel(base -
+										 channel.operatorTotalLevel[opIndex] +
+										 getPanningAttenuation(0x7f - channel.panning));
+			writeRegister(kFirstBank, 0x40 + op, scaling | left);
+			writeRegister(kSecondBank, 0x40 + op, scaling | right);
+		}
+		return;
+	}
+
+	if (channel.panning > 0x2a && channel.panning < 0x55)
+		base += 6;
+	const byte banks = getBankMask(channelIndex);
+	const uint operators[4] = {3, 1, 0, 2};
+	const bool enabled[4] = {
+		true, channel.mode == 1, (channel.mode & 2) != 0, channel.mode == 3};
+	for (uint i = 0; i < 4; ++i) {
+		if (!enabled[i])
+			continue;
+		const uint opIndex = operators[i];
+		const byte op = getOperatorOffset(channelIndex, opIndex);
+		const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+		const int level = clampLevel(base - channel.operatorTotalLevel[opIndex]);
+		writeRegister(banks, 0x40 + op, scaling | level);
+	}
+}
+
+void PSound::updateChannelFrequency(uint channelIndex, bool keyOn) {
+	Channel &channel = _channels[channelIndex];
+	updateChannelLevels(channelIndex);
+	const byte note = (byte)(channel.note + channel.noteTranspose);
+	const int frequency = getFrequencyNumber(note % 12) + channel.transpose;
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	writeRegister(banks, 0xa0 + oplChannel, frequency & 0xff);
+	byte high = (((note / 12) & 7) << 2) | ((frequency >> 8) & 3);
+	if (keyOn)
+		high |= 0x20;
+	writeRegister(banks, 0xb0 + oplChannel, high);
+}
+
+void PSound::updatePitchBend(uint channelIndex) {
+	const Channel &channel = _channels[channelIndex];
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const byte lowReg = 0xa0 + oplChannel;
+	const byte highReg = 0xb0 + oplChannel;
+	int frequency = ((getCachedRegister(banks, highReg) & 0x1f) << 8) |
+					getCachedRegister(banks, lowReg);
+	frequency += channel.pitchBend;
+	writeRegister(banks, lowReg, frequency & 0xff);
+	writeRegister(banks, highReg,
+				  (getCachedRegister(banks, highReg) & 0x20) |
+					  ((frequency >> 8) & 0x1f));
+}
+
+void PSound::keyOff(uint channelIndex) {
+	const byte banks = getBankMask(channelIndex);
+	const byte reg = 0xb0 + getOplChannel(channelIndex);
+	writeRegister(banks, reg, getCachedRegister(banks, reg) & 0xdf);
+}
+
+void PSound::setNoiseFrequency(uint channelIndex, int frequency) {
+	const byte banks = getBankMask(channelIndex);
+	const byte channel = getOplChannel(channelIndex);
+	writeRegister(banks, 0xa0 + channel, frequency & 0xff);
+	writeRegister(banks, 0xb0 + channel,
+				  ((frequency >> 8) & 0x1f) | 0x20);
+}
+
+void PSound::resultCheck() {
+	if (_resultFlag != 1) {
+		_resultFlag = 1;
+		_pollResult = 1;
+	}
+}
+
+int PSound::stop() {
+	Common::StackLock lock(_driverMutex);
+	command0();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+int PSound::poll() {
+	Common::StackLock lock(_driverMutex);
+	return serviceUpdate();
+}
+
+void PSound::noise() {
+	Common::StackLock lock(_driverMutex);
+	serviceNoise();
+}
+
+void PSound::setVolume(int volume) {
+	Common::StackLock lock(_driverMutex);
+	_masterVolume = CLIP(volume, 0, 255);
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount)
+			updateChannelLevels(i);
+	}
+}
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/sound/psound.h b/engines/mads/phantom/sound/psound.h
new file mode 100644
index 00000000000..c1fb35a9165
--- /dev/null
+++ b/engines/mads/phantom/sound/psound.h
@@ -0,0 +1,223 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef MADS_PHANTOM_SOUND_PSOUND_H
+#define MADS_PHANTOM_SOUND_PSOUND_H
+
+#include "mads/core/native_sound_timer.h"
+#include "mads/core/sound_manager.h"
+
+namespace OPL {
+class OPL;
+}
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+struct PSoundTableLayout {
+	uint16 panning;
+	uint16 frequency;
+	uint16 bank;
+	uint16 channel;
+	uint16 operators;
+};
+
+struct PSoundDriverData {
+	const char *filename;
+	int dataOffset;
+	int initializedDataSize;
+	int totalDataSize;
+	uint16 randomSeedOffset;
+	uint16 nullSequenceOffset;
+	uint16 patchTableOffset;
+	byte patchCount;
+	PSoundTableLayout tables;
+};
+
+/** Interpreter for Return of the Phantom's PSOUND overlay family. */
+class PSound : public SoundDriver {
+public:
+	enum {
+		kChannelCount = 9,
+		kMusicChannelCount = 6,
+		kPatchSize = 0x40,
+		kScriptVarCount = 32,
+		kOpcodeBudgetPerTick = 256
+	};
+
+protected:
+	enum RegisterBank {
+		kFirstBank = 1,
+		kSecondBank = 2,
+		kBothBanks = kFirstBank | kSecondBank
+	};
+
+	/** Logical representation of the native 0x32-byte channel record. */
+	struct Channel {
+		byte activeCount;           // +00
+		int8 pitchBend;             // +01
+		int8 volumeFadeStep;        // +02
+		int8 panningFadeStep;       // +03
+		byte note;                  // +04
+		byte patch;                 // +05
+		byte volume;                // +06
+		byte noteOffset;            // +07
+		byte keyOnDelay;            // +08
+		byte volumeFadeCounter;     // +09
+		byte volumeFadeReload;      // +0a
+		byte panningFadeCounter;    // +0b
+		byte panningFadeReload;     // +0c
+		byte panning;               // +0d
+		int8 volumeOffset;          // +0e
+		byte mode;                  // +0f
+		byte operatorTotalLevel[4]; // +10..+13
+		uint16 loopStart;           // +14
+		uint16 position;            // +16
+		uint16 innerLoopStart;      // +18
+		uint16 outerLoopStart;      // +1a
+		uint16 innerLoopCount;      // +1c
+		uint16 outerLoopCount;      // +1e
+		uint16 originalSequence;    // +20
+		uint16 branchReturn;        // +22
+		uint16 noiseMask;           // +24
+		uint16 noiseBase;           // +26
+		int16 noiseStep;            // +28
+		byte noiseTicks;            // +2a
+		byte savedNoiseTicks;       // +2b
+		int8 transpose;             // +2c
+		int8 noteTranspose;         // +2d
+		byte pendingStop;           // +2e
+		int8 patchAttenuation;      // +2f
+		byte durationOverride;      // +30
+
+		void reset();
+		void load(uint16 sequenceOffset);
+	};
+
+	OPL::OPL *_opl;
+	byte _registerCache[2][256];
+	NativeSoundTimer _hostTimer;
+	Channel _channels[kChannelCount];
+	byte _scriptVars[kScriptVarCount];
+	int _masterVolume;
+	uint16 _randomSeed;
+	uint16 _frameCounter;
+	int16 _pollResult;
+	int8 _resultFlag;
+	uint16 _nullSequenceOffset;
+	uint16 _patchTableOffset;
+	PSoundTableLayout _tableLayout;
+	byte _patchCount;
+	bool _updatesEnabled;
+	bool _noiseServiceEnabled;
+	// The native BE-C1 opcodes update these fields, but the audited overlays'
+	// per-tick tempo hook is a no-op. Preserve the state without inventing a
+	// duration transform.
+	uint16 _tickEnabled;
+	uint16 _tickCounter;
+	uint16 _tempoReload;
+	uint16 _tempoTarget;
+	int16 _tempoShift;
+	uint16 _tempoBase;
+	uint16 _tempoCurrent;
+	uint16 _tempoScale;
+	int _frameNumber2;
+
+	PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData);
+	~PSound() override;
+
+	bool isDataRangeValid(uint32 offset, uint32 length) const;
+	const byte *getDataPointer(uint32 offset, uint32 length,
+							   const char *operation) const;
+	byte *getDataPointer(uint32 offset, uint32 length, const char *operation);
+	byte readDataByte(uint32 offset) const;
+	uint16 readDataUint16(uint32 offset) const;
+	void writeDataByte(uint32 offset, byte value);
+	void writeDataUint16(uint32 offset, uint16 value);
+
+	byte getBankMask(uint channel) const;
+	byte getOplChannel(uint channel) const;
+	byte getOperatorOffset(uint channel, uint operatorIndex) const;
+	const byte *getPatch(uint patchIndex) const;
+	byte getPanningAttenuation(byte panning) const;
+	uint16 getFrequencyNumber(byte semitone) const;
+	void writeRegister(byte banks, byte reg, byte value);
+	byte getCachedRegister(byte banks, byte reg) const;
+
+	void initializePas16();
+	void shutdownPas16();
+	void resetDriver();
+	int command0();
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+	int command5();
+	int command6();
+	int command7();
+	int command8();
+
+	void requestStop(uint firstChannel, uint endChannel);
+	void setCurrentSequence(uint firstChannel, uint endChannel,
+							uint16 sequenceOffset);
+	void loadChannel(uint channel, uint16 sequenceOffset);
+	void playSound(uint16 sequenceOffset);
+	void playMusicAny(uint16 sequenceOffset);
+	bool isSoundActive(uint16 sequenceOffset) const;
+
+	void onTimer();
+	int serviceUpdate();
+	void serviceNoise();
+	void update();
+	void updateChannel(uint channelIndex);
+	bool isOpcodeDataValid(uint16 position, uint32 length) const;
+	byte readOpcodeByte(uint16 position, uint16 delta) const;
+	uint16 readOpcodeWord(uint16 position, uint16 delta) const;
+	bool isScriptVariableValid(byte index) const;
+	bool transferOpcode(Channel &channel, uint16 position, bool take,
+		bool isCall);
+	bool executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty);
+	void finishChannel(uint channelIndex);
+	void checkPendingStops();
+
+	void loadPatch(uint channelIndex, byte patchIndex);
+	void programOperator(byte banks, uint channelIndex, uint operatorIndex,
+						 const byte *operatorData);
+	void updatePanning(uint channelIndex);
+	void updateChannelLevels(uint channelIndex);
+	void updateChannelFrequency(uint channelIndex, bool keyOn);
+	void updatePitchBend(uint channelIndex);
+	void keyOff(uint channelIndex);
+	void setNoiseFrequency(uint channelIndex, int frequency);
+	void resultCheck();
+
+	uint16 nextRandom();
+
+	/** Implement only callbacks proved reachable from this overlay's streams. */
+	virtual bool callFunction(uint16 targetOffset, Channel &channel) = 0;
+
+public:
+	static bool validateFile(const PSoundDriverData &driverData,
+							 const char *first8192Md5, Common::String *reason = nullptr);
+	bool isReady() const { return _opl != nullptr; }
+
+	int stop() override;
+	int poll() override;
+	void noise() override;
+	void setVolume(int volume) override;
+};
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
+
+#endif // MADS_PHANTOM_SOUND_PSOUND_H
diff --git a/engines/mads/phantom/sound/psound_phantom.cpp b/engines/mads/phantom/sound/psound_phantom.cpp
new file mode 100644
index 00000000000..d281bd2c151
--- /dev/null
+++ b/engines/mads/phantom/sound/psound_phantom.cpp
@@ -0,0 +1,949 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/phantom/sound/psound_phantom.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+namespace {
+
+const PSoundDriverData kPSound1Data = {
+	"PSOUND.PH1", 0x3000, 0x4486, 0x4880, 0x0058, 0x3bfe,
+	0x3c06, 34, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSound2Data = {
+	"PSOUND.PH2", 0x2dd0, 0x1c7a, 0x2070, 0x0058, 0x12ec,
+	0x147a, 32, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSound3Data = {
+	"PSOUND.PH3", 0x2e50, 0x357a, 0x3970, 0x0058, 0x05ce,
+	0x2d7a, 32, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSound4Data = {
+	"PSOUND.PH4", 0x2d20, 0x0a90, 0x0e80, 0x0058, 0x0484,
+	0x0510, 22, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSound5Data = {
+	"PSOUND.PH5", 0x2ee0, 0x4eb6, 0x52b0, 0x0058, 0x436c,
+	0x4476, 41, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSound9Data = {
+	"PSOUND.PH9", 0x2e50, 0x3016, 0x3410, 0x0058, 0x254c,
+	0x2616, 40, { 0x00a4, 0x0124, 0x0140, 0x014a, 0x0154 }
+};
+
+const PSoundDriverData kPSoundDemoData = {
+	"PSOUND.PHA", 0x2d80, 0x4508, 0x4900, 0x0058, 0x2501,
+	0x2508, 128, { 0x009c, 0x011c, 0x0138, 0x0142, 0x014c }
+};
+
+struct ValidationEntry {
+	const PSoundDriverData *driverData;
+	const char *first8192Md5;
+};
+
+const ValidationEntry kRetailValidation[] = {
+	{ &kPSound1Data, "b314aadd7e0ae7b857cf283740e2e1cc" },
+	{ &kPSound2Data, "1de247af9adaa72117c8b4dcb8a72bf9" },
+	{ &kPSound3Data, "8747dfc90c45fbab58cb5ec1710e7c37" },
+	{ &kPSound4Data, "69f8f318d491e90e5c063708a2f82db4" },
+	{ &kPSound5Data, "3d846fc65dece14b29b0495fb5341284" },
+	{ &kPSound9Data, "f7d7e85759526098168180d89d5723ae" }
+};
+
+const ValidationEntry kDemoValidation = {
+	&kPSoundDemoData, "190d286ea8b7227e1ff1a7cd3e980bd3"
+};
+
+const ValidationEntry *retailValidationForSection(int section) {
+	switch (section) {
+	case 1:
+		return &kRetailValidation[0];
+	case 2:
+		return &kRetailValidation[1];
+	case 3:
+		return &kRetailValidation[2];
+	case 4:
+		return &kRetailValidation[3];
+	case 5:
+		return &kRetailValidation[4];
+	case 9:
+		return &kRetailValidation[5];
+	default:
+		return nullptr;
+	}
+}
+
+} // namespace
+
+PhantomPSound::PhantomPSound(Audio::Mixer *mixer,
+		const PSoundDriverData &driverData) :
+		PSound(mixer, driverData) {
+}
+
+int PhantomPSound::dispatchBaseCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+		return command0();
+	case 1:
+		return command1();
+	case 2:
+		return command2();
+	case 3:
+		return command3();
+	case 4:
+		return command4();
+	case 5:
+		return command5();
+	case 6:
+		return command6();
+	case 7:
+		return command7();
+	case 8:
+		return command8();
+	default:
+		return 0;
+	}
+}
+
+void PhantomPSound::loadFixedChannels(const uint16 *sequences, uint count) {
+	assert(count <= kChannelCount);
+	for (uint i = 0; i < count; ++i)
+		loadChannel(i, sequences[i]);
+}
+
+void PhantomPSound::playSounds(const uint16 *sequences, uint count) {
+	for (uint i = 0; i < count; ++i)
+		playSound(sequences[i]);
+}
+
+void PhantomPSound::playMusic(const uint16 *sequences, uint count) {
+	for (uint i = 0; i < count; ++i)
+		playMusicAny(sequences[i]);
+}
+
+bool PhantomPSound::loadFixedIfInactive(uint16 guard,
+										const uint16 *sequences, uint count) {
+	if (isSoundActive(guard))
+		return false;
+	loadFixedChannels(sequences, count);
+	return true;
+}
+
+void PhantomPSound::leaveMalformedChannelSilent(const char *filename,
+												byte channel, uint16 sequenceOffset) {
+	// These immediates are not MZ-relocated and the original loader performs
+	// no bounds check. Reinterpreting them would invent a stream; leave only
+	// the affected channel silent while preserving every valid native root.
+	if (isDataRangeValid(sequenceOffset, 1))
+		error("%s malformed root 0x%04x unexpectedly resolves inside data",
+			  filename, sequenceOffset);
+	finishChannel(channel);
+	warning("%s contains malformed native sequence root 0x%04x for channel %u; "
+			"leaving that channel silent",
+			filename, sequenceOffset, channel);
+}
+
+bool PhantomPSound::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)targetOffset;
+	(void)channel;
+	return false;
+}
+
+int PhantomPSound::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	(void)param;
+	_frameCounter = 0;
+	if (commandId >= 0 && commandId <= 8)
+		return dispatchBaseCommand(commandId);
+	return executeCommand(commandId);
+}
+
+bool validatePhantomPSoundFile(int section, bool isDemo,
+							   Common::String *reason) {
+	const ValidationEntry *entry = isDemo ? &kDemoValidation : retailValidationForSection(section);
+	if (!entry) {
+		if (reason)
+			*reason = "unsupported section";
+		return false;
+	}
+	return PSound::validateFile(*entry->driverData, entry->first8192Md5,
+								reason);
+}
+
+PSound1::PSound1(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound1Data),
+		_previousSelector(0xffff), _olderSelector(0xffff) {
+}
+
+void PSound1::selectBackgroundMusic() {
+	static const uint16 tracks[][kMusicChannelCount] = {
+		{0x095d, 0x0984, 0x09f7, 0x0ab8, 0x0972, 0x0b67},
+		{0x16ba, 0x1796, 0x184b, 0x1908, 0x191a, 0x1927},
+		{0x192a, 0x1b20, 0x1ca5, 0x1e72, 0x1fdd, 0x1ff6},
+		{0x1ff8, 0x2285, 0x24c2, 0x24d4, 0x24e6, 0x24f3},
+		{0x0cc7, 0x0cec, 0x0df9, 0x0e9e, 0x0cdc, 0x0ea7}};
+	static const byte weightedTracks[] = {0, 3, 2, 1, 0, 4, 1, 2};
+	static const uint16 guards[] = {0x095d, 0x0cc7, 0x1ff8, 0x16ba, 0x192a};
+
+	if (_channels[0].activeCount) {
+		for (uint i = 0; i < ARRAYSIZE(guards); ++i) {
+			if (isSoundActive(guards[i]))
+				return;
+		}
+	}
+
+	command1();
+	uint16 selector;
+	do {
+		selector = nextRandom() & 7;
+	} while (selector == _previousSelector || selector == _olderSelector);
+	_olderSelector = _previousSelector;
+	_previousSelector = selector;
+	loadFixedChannels(tracks[weightedTracks[selector]], kMusicChannelCount);
+}
+
+int PSound1::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x10:
+		selectBackgroundMusic();
+		break;
+	case 0x18: {
+		static const uint16 sounds[] = {0x3a50, 0x3a5c};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x3a6a, 0x3a76};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x3a84);
+		break;
+	case 0x1b:
+		playSound(0x3a8c);
+		break;
+	case 0x20: {
+		static const uint16 music[] = {
+			0x2e8a, 0x2ec5, 0x2ef9, 0x2f23, 0x2f4f, 0x2f7b};
+		loadFixedIfInactive(0x2e8a, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x21: {
+		static const uint16 music[] = {
+			0x24f6, 0x272f, 0x28be, 0x2a8b, 0x2bc4, 0x2cf3};
+		loadFixedIfInactive(0x24f6, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x22: {
+		static const uint16 music[] = {
+			0x0178, 0x03a6, 0x04cf, 0x0600, 0x0701, 0x07df};
+		loadFixedIfInactive(0x0178, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x23: {
+		static const uint16 music[] = {
+			0x3454, 0x358c, 0x3652, 0x3708, 0x37be};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		leaveMalformedChannelSilent("PSOUND.PH1", 5, 0x69fe);
+		break;
+	}
+	case 0x24: {
+		static const uint16 music[] = {
+			0x2fd4, 0x310e, 0x31e2, 0x32a6, 0x336a, 0x336a};
+		loadFixedIfInactive(0x2fd4, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x25: {
+		static const uint16 music[] = {
+			0x38a6, 0x390b, 0x396b, 0x39bb, 0x3a06, 0x39b2};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x26: {
+		static const uint16 music[] = {
+			0x095d, 0x0984, 0x09f7, 0x0ab8, 0x0972, 0x0b67};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x27: {
+		static const uint16 music[] = {
+			0x0eaa, 0x1051, 0x116b, 0x1253, 0x1355, 0x1521};
+		loadFixedIfInactive(0x0eaa, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x40:
+		playSound(0x3a98);
+		break;
+	case 0x41: {
+		static const uint16 sounds[] = {0x3aa8, 0x3aba};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x42:
+		playSound(0x3acc);
+		break;
+	case 0x43:
+		playSound(0x3adc);
+		break;
+	case 0x44:
+		playSound(0x3b7c);
+		break;
+	case 0x45: {
+		static const uint16 sounds[] = {0x3afa, 0x3b17};
+		command5();
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x46: {
+		static const uint16 sounds[] = {0x3b32, 0x3b3c};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x47:
+		playSound(0x3b65);
+		break;
+	case 0x48:
+		playSound(0x3b83);
+		break;
+	case 0x49:
+		playSound(0x3b9a);
+		break;
+	case 0x4a:
+		playSound(0x3bf6);
+		break;
+	case 0x4b: {
+		static const uint16 sounds[] = {0x3bac, 0x3bcc};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x4c:
+		playSound(0x3b44);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSound2::PSound2(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound2Data) {
+}
+
+int PSound2::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x10: {
+		static const uint16 music[] = {0x0e8e, 0x0eed, 0x0f46};
+		if (!isSoundActive(0x0e8e)) {
+			command1();
+			playMusic(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x18: {
+		static const uint16 sounds[] = {0x12ee, 0x12fa};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x1308, 0x1314};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x1322);
+		break;
+	case 0x1b:
+		playSound(0x132a);
+		break;
+	case 0x20: {
+		static const uint16 music[] = {0x0f9e, 0x105e, 0x11b1};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x21: {
+		static const uint16 music[] = {
+			0x1258, 0x1299, 0x12a9, 0x12b9,
+			0x12c9, 0x12cb, 0x12cd, 0x12db};
+		loadFixedIfInactive(0x1258, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x22: {
+		static const uint16 music[] = {
+			0x02b2, 0x0483, 0x06b2, 0x0855, 0x0aa0, 0x0d2f, 0x0e36};
+		if (!isSoundActive(0x02b2)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x23: {
+		static const uint16 music[] = {
+			0x0178, 0x01f3, 0x020e, 0x0251, 0x027c};
+		if (!isSoundActive(0x0178)) {
+			command1();
+			playMusic(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x40:
+		playSound(0x140d);
+		break;
+	case 0x41:
+		playSound(0x1346);
+		break;
+	case 0x42:
+		playSound(0x133e);
+		break;
+	case 0x43:
+		playSound(0x1336);
+		break;
+	case 0x44: {
+		static const uint16 sounds[] = {0x1346, 0x1387, 0x13ca};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x45:
+		playSound(0x1415);
+		break;
+	case 0x46: {
+		static const uint16 sounds[] = {0x1425, 0x1442};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x47:
+		playSound(0x145d);
+		break;
+	case 0x48:
+		playSound(0x1467);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSound3::PSound3(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound3Data) {
+}
+
+int PSound3::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x10: {
+		static const uint16 music[] = {
+			0x0798, 0x0873, 0x08c3, 0x0935, 0x09bf, 0x09e7};
+		if (!isSoundActive(0x0798)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x18: {
+		static const uint16 sounds[] = {0x05da, 0x05e6};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x05f4, 0x0600};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x060e);
+		break;
+	case 0x1b:
+		playSound(0x0616);
+		break;
+	case 0x20: {
+		static const uint16 music[] = {
+			0x2c37, 0x2c86, 0x2ca6, 0x2ce6,
+			0x2d3c, 0x2d3c, 0x2d3e, 0x2d5c};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x21: {
+		static const uint16 music[] = {
+			0x122a, 0x144d, 0x16a8, 0x17ae, 0x198a, 0x1be8};
+		if (!isSoundActive(0x122a)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x22: {
+		static const uint16 music[] = {
+			0x1d68, 0x205d, 0x216d, 0x2365,
+			0x257d, 0x27c5, 0x290d, 0x2ac5};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x23: {
+		static const uint16 music[] = {
+			0x0a30, 0x0bd3, 0x0ceb, 0x0dcf, 0x0ecd, 0x1095};
+		if (!isSoundActive(0x0a30)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x24: {
+		static const uint16 music[] = {
+			0x0178, 0x01f0, 0x02b1, 0x0506, 0x0573};
+		if (!isSoundActive(0x0178)) {
+			command1();
+			playMusic(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x25:
+		playSound(0x0653);
+		break;
+	case 0x40: {
+		static const uint16 sounds[] = {0x0622, 0x0627};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x41: {
+		static const uint16 sounds[] = {0x067d, 0x06c0};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x42: {
+		static const uint16 sounds[] = {0x0622, 0x0641};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x43:
+		playSound(0x064b);
+		break;
+	case 0x44:
+		playSound(0x0669);
+		break;
+	case 0x45:
+		playSound(0x0671);
+		break;
+	case 0x46:
+		playSound(0x0722);
+		break;
+	case 0x47:
+		playSound(0x0701);
+		break;
+	case 0x48: {
+		static const uint16 sounds[] = {0x0739, 0x0622, 0x0627};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x49:
+		playSound(0x0745);
+		break;
+	case 0x4a:
+		playSound(0x0757);
+		break;
+	case 0x4b: {
+		static const uint16 sounds[] = {0x0622, 0x0627};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSound4::PSound4(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound4Data) {
+}
+
+bool PSound4::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2a70)
+		return false;
+
+	const uint tableIndex = nextRandom() & 0x0f;
+	writeDataUint16(0x019a, readDataUint16(0x0464 + tableIndex * 2));
+	return true;
+}
+
+int PSound4::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x10: {
+		static const uint16 music[] = {
+			0x0178, 0x03a6, 0x03cc, 0x03f0, 0x0422, 0x0416};
+		loadFixedIfInactive(0x0178, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x18: {
+		static const uint16 sounds[] = {0x0490, 0x049c};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x049e, 0x04aa};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x04b8);
+		break;
+	case 0x1b:
+		playSound(0x04c0);
+		break;
+	case 0x40:
+		playSound(0x04cc);
+		break;
+	case 0x41:
+		playSound(0x04d6);
+		break;
+	case 0x42:
+		playSound(0x04de);
+		break;
+	case 0x43:
+		playSound(0x04e6);
+		break;
+	case 0x44:
+		playSound(0x04ee);
+		break;
+	case 0x45:
+		playSound(0x04f6);
+		break;
+	case 0x46:
+		playSound(0x04fe);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSound5::PSound5(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound5Data) {
+}
+
+int PSound5::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x10: {
+		static const uint16 music[] = {
+			0x3bc2, 0x3c42, 0x3ca7, 0x3d0a, 0x3d59, 0x3de4};
+		if (!isSoundActive(0x3bc2)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x18: {
+		static const uint16 sounds[] = {0x4380, 0x438c};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x439a, 0x43a6};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x43b4);
+		break;
+	case 0x1b:
+		playSound(0x43bc);
+		break;
+	case 0x20: {
+		static const uint16 music[] = {
+			0x3e68, 0x3fb9, 0x4013, 0x40f7,
+			0x419d, 0x423d, 0x4289, 0x4311};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x21: {
+		static const uint16 music[] = {
+			0x1e66, 0x20d3, 0x2330, 0x2444, 0x2652, 0x28c8};
+		if (!isSoundActive(0x1e66)) {
+			command1();
+			loadFixedChannels(music, ARRAYSIZE(music));
+		}
+		break;
+	}
+	case 0x22: {
+		static const uint16 music[] = {
+			0x0cc8, 0x0e6a, 0x0fee, 0x108e, 0x1136,
+			0x1166, 0x11a1, 0x11de, 0x1219};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x23: {
+		static const uint16 music[] = {0x2a72, 0x3051, 0x35e1, 0x2a7d};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x24: {
+		static const uint16 music[] = {0x19c2, 0x1b1b, 0x1c4b, 0x19d4};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x25: {
+		static const uint16 music[] = {
+			0x123f, 0x1597, 0x16f4, 0x192b, 0x1236};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x26: {
+		static const uint16 music[] = {
+			0x0876, 0x09ae, 0x0a74, 0x0b2a, 0x0be0};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		leaveMalformedChannelSilent("PSOUND.PH5", 5, 0x704c);
+		break;
+	}
+	case 0x27: {
+		static const uint16 music[] = {
+			0x0178, 0x03a6, 0x04cf, 0x0600, 0x0701, 0x07df};
+		loadFixedIfInactive(0x0178, music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x40:
+		playSound(0x4430);
+		break;
+	case 0x41:
+		playSound(0x43c8);
+		break;
+	case 0x42:
+		playSound(0x43d0);
+		break;
+	case 0x43:
+		loadChannel(8, 0x43dc);
+		break;
+	case 0x44:
+		playSound(0x43e4);
+		break;
+	case 0x45:
+		playSound(0x43f4);
+		break;
+	case 0x46:
+		playSound(0x440c);
+		break;
+	case 0x47:
+		playSound(0x4416);
+		break;
+	case 0x48:
+		playSound(0x4428);
+		break;
+	case 0x49:
+		playSound(0x4378);
+		break;
+	case 0x4a:
+		playSound(0x4438);
+		break;
+	case 0x4b:
+		playSound(0x444a);
+		break;
+	case 0x4c:
+		playSound(0x4452);
+		break;
+	case 0x4d:
+		playSound(0x445c);
+		break;
+	case 0x4e:
+		playSound(0x4464);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSound9::PSound9(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSound9Data) {
+}
+
+int PSound9::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x18: {
+		static const uint16 sounds[] = {0x2558, 0x2564};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x19: {
+		static const uint16 sounds[] = {0x2572, 0x257e};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x1a:
+		playSound(0x258c);
+		break;
+	case 0x1b:
+		playSound(0x2594);
+		break;
+	case 0x20: {
+		static const uint16 music[] = {
+			0x1016, 0x107e, 0x10e0, 0x12e5, 0x1303, 0x1323, 0x1357};
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x22: {
+		static const uint16 music[] = {
+			0x237c, 0x23ba, 0x241b, 0x2462, 0x24c9, 0x2504};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x23: {
+		static const uint16 music[] = {
+			0x05f6, 0x0633, 0x065d, 0x0683, 0x06dd, 0x073b, 0x077b};
+		command0();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x24: {
+		static const uint16 music[] = {
+			0x223a, 0x226d, 0x22a1, 0x22d5, 0x230d, 0x2339};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x25: {
+		static const uint16 music[] = {
+			0x07b0, 0x0827, 0x091d, 0x099d, 0x09f5, 0x0c55, 0x0f77};
+		command0();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x26: {
+		static const uint16 music[] = {
+			0x138c, 0x166d, 0x177b, 0x1971,
+			0x1b87, 0x1dcd, 0x1f13, 0x20c9};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x27: {
+		static const uint16 music[] = {
+			0x0178, 0x02b2, 0x0386, 0x044a, 0x050e};
+		command0();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x40:
+		playSound(0x25e5);
+		break;
+	case 0x41:
+		playSound(0x25f7);
+		break;
+	case 0x42: {
+		static const uint16 sounds[] = {0x25f7, 0x25fc, 0x2605};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x43: {
+		static const uint16 sounds[] = {0x25bc, 0x25db};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x44:
+		playSound(0x25ef);
+		break;
+	case 0x45:
+		playSound(0x25a0);
+		break;
+	case 0x46: {
+		static const uint16 sounds[] = {0x25b0, 0x25b7};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	case 0x47: {
+		static const uint16 sounds[] = {0x25bc, 0x25c1};
+		playSounds(sounds, ARRAYSIZE(sounds));
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+PSoundDemo::PSoundDemo(Audio::Mixer *mixer) : PhantomPSound(mixer, kPSoundDemoData) {
+}
+
+int PSoundDemo::executeCommand(int commandId) {
+	switch (commandId) {
+	case 0x09: {
+		static const uint16 music[] = {
+			0x1d02, 0x1e53, 0x1ead, 0x1f91,
+			0x2037, 0x20d7, 0x2123, 0x21ab};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x10: {
+		static const uint16 music[] = {
+			0x0170, 0x0430, 0x0602, 0x0814, 0x097e, 0x0ade};
+		command5();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x16: {
+		static const uint16 music[] = {
+			0x149c, 0x1513, 0x1609, 0x1689, 0x16e1, 0x1941, 0x1c63};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x17: {
+		static const uint16 music[] = {
+			0x12e2, 0x131f, 0x1349, 0x136f, 0x13c9, 0x1427, 0x1467};
+		command1();
+		loadFixedChannels(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x18: {
+		static const uint16 music[] = {
+			0x2206, 0x2237, 0x2265, 0x2297, 0x22cd, 0x22f7};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x19: {
+		static const uint16 music[] = {
+			0x2338, 0x2376, 0x23d7, 0x241e, 0x2485, 0x24c0};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x1a: {
+		static const uint16 music[] = {
+			0x0d95, 0x0dbc, 0x0e2f, 0x0ef0, 0x0daa};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	case 0x1b: {
+		static const uint16 music[] = {
+			0x10ff, 0x1124, 0x1231, 0x12d6, 0x1114};
+		command1();
+		playMusic(music, ARRAYSIZE(music));
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/sound/psound_phantom.h b/engines/mads/phantom/sound/psound_phantom.h
new file mode 100644
index 00000000000..95fb6723f48
--- /dev/null
+++ b/engines/mads/phantom/sound/psound_phantom.h
@@ -0,0 +1,86 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef MADS_PHANTOM_SOUND_PSOUND_PHANTOM_H
+#define MADS_PHANTOM_SOUND_PSOUND_PHANTOM_H
+
+#include "mads/phantom/sound/psound.h"
+
+namespace MADS {
+namespace Phantom {
+namespace Sound {
+
+class PhantomPSound : public PSound {
+protected:
+	PhantomPSound(Audio::Mixer *mixer, const PSoundDriverData &driverData);
+
+	int dispatchBaseCommand(int commandId);
+	void loadFixedChannels(const uint16 *sequences, uint count);
+	void playSounds(const uint16 *sequences, uint count);
+	void playMusic(const uint16 *sequences, uint count);
+	bool loadFixedIfInactive(uint16 guard, const uint16 *sequences, uint count);
+	void leaveMalformedChannelSilent(const char *filename, byte channel,
+									 uint16 sequenceOffset);
+
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+	virtual int executeCommand(int commandId) = 0;
+
+public:
+	int command(int commandId, int param) override;
+};
+
+#define DECLARE_PHANTOM_PSOUND(_name)                  \
+	class PSound##_name final : public PhantomPSound { \
+	private:                                           \
+		int executeCommand(int commandId) override;    \
+                                                       \
+	public:                                            \
+		explicit PSound##_name(Audio::Mixer *mixer);   \
+	}
+
+class PSound1 final : public PhantomPSound {
+private:
+	uint16 _previousSelector;
+	uint16 _olderSelector;
+	int executeCommand(int commandId) override;
+	void selectBackgroundMusic();
+
+public:
+	explicit PSound1(Audio::Mixer *mixer);
+};
+
+DECLARE_PHANTOM_PSOUND(2);
+DECLARE_PHANTOM_PSOUND(3);
+
+class PSound4 final : public PhantomPSound {
+private:
+	int executeCommand(int commandId) override;
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+
+public:
+	explicit PSound4(Audio::Mixer *mixer);
+};
+
+DECLARE_PHANTOM_PSOUND(5);
+DECLARE_PHANTOM_PSOUND(9);
+DECLARE_PHANTOM_PSOUND(Demo);
+
+#undef DECLARE_PHANTOM_PSOUND
+
+/** Validate one exact retail section overlay or the separately built demo. */
+bool validatePhantomPSoundFile(int section, bool isDemo,
+							   Common::String *reason = nullptr);
+
+} // namespace Sound
+} // namespace Phantom
+} // namespace MADS
+
+#endif // MADS_PHANTOM_SOUND_PSOUND_PHANTOM_H
diff --git a/engines/mads/phantom/sound/sound.cpp b/engines/mads/phantom/sound/sound.cpp
index 49bb1661dc2..65b171238a9 100644
--- a/engines/mads/phantom/sound/sound.cpp
+++ b/engines/mads/phantom/sound/sound.cpp
@@ -20,8 +20,11 @@
  */
 
 #include "mads/phantom/sound/sound.h"
+#include "audio/fmopl.h"
+#include "common/textconsole.h"
 #include "mads/phantom/sound/asound_phantom.h"
 #include "mads/phantom/sound/isound_phantom.h"
+#include "mads/phantom/sound/psound_phantom.h"
 #include "mads/phantom/sound/rsound_phantom.h"
 
 namespace MADS {
@@ -74,15 +77,70 @@ SoundDriver *createISound(Audio::Mixer *mixer, int sectionNumber) {
 	}
 }
 
+SoundDriver *createPSound(Audio::Mixer *mixer, int sectionNumber) {
+	switch (sectionNumber) {
+	case 1:
+		return new PSound1(mixer);
+	case 2:
+		return new PSound2(mixer);
+	case 3:
+		return new PSound3(mixer);
+	case 4:
+		return new PSound4(mixer);
+	case 5:
+		return new PSound5(mixer);
+	case 9:
+		return new PSound9(mixer);
+	default:
+		return nullptr;
+	}
+}
+
 } // namespace
 
+PhantomSoundManager::PhantomSoundManager(Audio::Mixer *mixer,
+		bool &soundFlag, bool usePas, bool isDemo) :
+		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+	if (usePas && _driverType == SOUND_ADLIB) {
+		if (OPL::Config::detect(OPL::Config::kOpl3) >= 0) {
+			_driverType = SOUND_PAS;
+		} else {
+			warning("Pro Audio Spectrum 16 requires OPL3 output; "
+					"falling back to AdLib");
+		}
+	}
+}
+
 void PhantomSoundManager::validate() {
-	if (_driverType == SOUND_MT32) {
+	if (_driverType == SOUND_PAS) {
+		bool valid = true;
 		if (_isDemo) {
 			Common::String reason;
-			if (!RSoundDemoPHA::validate(&reason))
-				error("Cannot use Phantom demo RSOUND.PHA: %s",
+			valid = validatePhantomPSoundFile(1, true, &reason);
+			if (!valid)
+				warning("Cannot use Phantom demo PSOUND: %s; using AdLib",
 						reason.c_str());
+		} else {
+			for (uint index = 0; index < ARRAYSIZE(kRetailSections); ++index) {
+				Common::String reason;
+				if (!validatePhantomPSoundFile(kRetailSections[index], false,
+						&reason)) {
+					warning("Cannot use Phantom PSOUND section %d: %s; "
+							"using AdLib", kRetailSections[index],
+							reason.c_str());
+					valid = false;
+				}
+			}
+		}
+		if (valid)
+			return;
+		_driverType = SOUND_ADLIB;
+		ASound::validate(_isDemo);
+	} else if (_driverType == SOUND_MT32) {
+		if (_isDemo) {
+			Common::String reason;
+			if (!RSoundDemoPHA::validate(&reason))
+				error("Cannot use Phantom demo RSOUND.PHA: %s", reason.c_str());
 		} else {
 			RSound::validate();
 		}
@@ -108,32 +166,45 @@ void PhantomSoundManager::validate() {
 void PhantomSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_driverType == SOUND_MT32) {
-		// MT32
+	if (_driverType == SOUND_PAS) {
+		if (_isDemo)
+			_driver = new PSoundDemo(_mixer);
+		else
+			_driver = createPSound(_mixer, sectionNumber);
+		if (_driver && !static_cast<PSound *>(_driver)->isReady()) {
+			warning("Could not initialize Pro Audio Spectrum 16 OPL3 output; "
+					"falling back to AdLib");
+			removeDriver();
+			_driverType = SOUND_ADLIB;
+			loadDriver(sectionNumber);
+		}
+	} else if (_driverType == SOUND_MT32) {
 		if (_isDemo) {
 			_driver = new RSoundDemoPHA(_mixer);
-		} else switch (sectionNumber) {
-		case 1:
-			_driver = new RSound1(_mixer);
-			break;
-		case 2:
-			_driver = new RSound2(_mixer);
-			break;
-		case 3:
-			_driver = new RSound3(_mixer);
-			break;
-		case 4:
-			_driver = new RSound4(_mixer);
-			break;
-		case 5:
-			_driver = new RSound5(_mixer);
-			break;
-		case 9:
-			_driver = new RSound9(_mixer);
-			break;
-		default:
-			_driver = nullptr;
-			break;
+		} else {
+			switch (sectionNumber) {
+			case 1:
+				_driver = new RSound1(_mixer);
+				break;
+			case 2:
+				_driver = new RSound2(_mixer);
+				break;
+			case 3:
+				_driver = new RSound3(_mixer);
+				break;
+			case 4:
+				_driver = new RSound4(_mixer);
+				break;
+			case 5:
+				_driver = new RSound5(_mixer);
+				break;
+			case 9:
+				_driver = new RSound9(_mixer);
+				break;
+			default:
+				_driver = nullptr;
+				break;
+			}
 		}
 	} else if (_isDemo) {
 		_driver = new ASoundDemo(_mixer);
diff --git a/engines/mads/phantom/sound/sound.h b/engines/mads/phantom/sound/sound.h
index b811a589e8a..dcdc2eade92 100644
--- a/engines/mads/phantom/sound/sound.h
+++ b/engines/mads/phantom/sound/sound.h
@@ -39,9 +39,8 @@ protected:
 	void loadDriver(int sectionNum) override;
 
 public:
-	PhantomSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
-		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
-	}
+	PhantomSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool usePas,
+			bool isDemo);
 	~PhantomSoundManager() override {}
 
 	/**


Commit: fd033eff87a9d1e831a2d3ee184562684ec6460d
    https://github.com/scummvm/scummvm/commit/fd033eff87a9d1e831a2d3ee184562684ec6460d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: DRAGONSPHERE: Add PAS16 PSOUND support

Reimplement the verified retail and demo overlays through the maintained
PAS16 OPL3 path, preserving native timing and driver selection.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/dragonsphere/sound/psound.cpp
  A engines/mads/dragonsphere/sound/psound.h
  A engines/mads/dragonsphere/sound/psound_dragonsphere.cpp
  A engines/mads/dragonsphere/sound/psound_dragonsphere.h
    engines/mads/detection_tables.h
    engines/mads/dragonsphere/dragonsphere.cpp
    engines/mads/dragonsphere/sound/sound.cpp
    engines/mads/dragonsphere/sound/sound.h
    engines/mads/module.mk


diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index 74035f1341c..c873aee25c3 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -253,7 +253,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Dragonsphere,
 		0
@@ -268,7 +268,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Dragonsphere,
 		0
@@ -284,7 +284,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD | GF_INSTALLER,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO3(GUIO_NOMIDI, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
 		},
 		GType_Dragonsphere,
 		0
@@ -299,7 +299,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_DEMO,
-			GUIO4(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD)
+			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Dragonsphere,
 		0
diff --git a/engines/mads/dragonsphere/dragonsphere.cpp b/engines/mads/dragonsphere/dragonsphere.cpp
index 8b6e30d37b1..48b80429bfe 100644
--- a/engines/mads/dragonsphere/dragonsphere.cpp
+++ b/engines/mads/dragonsphere/dragonsphere.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "engines/util.h"
+#include "common/config-manager.h"
 #include "mads/console.h"
 #include "mads/core/attr.h"
 #include "mads/core/conv.h"
@@ -71,7 +72,8 @@ Common::Error DragonsphereEngine::run() {
 	}
 
 	// Set up sound manager
-	_soundManager = new Sound::DragonSoundManager(_mixer, _soundFlag, isDemo());
+	_soundManager = new Sound::DragonSoundManager(_mixer, _soundFlag,
+			ConfMan.getBool("use_pas"), isDemo());
 	_soundManager->validate();
 
 	// Run the game
diff --git a/engines/mads/dragonsphere/sound/psound.cpp b/engines/mads/dragonsphere/sound/psound.cpp
new file mode 100644
index 00000000000..8cd58ad265f
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/psound.cpp
@@ -0,0 +1,1185 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "audio/fmopl.h"
+#include "common/endian.h"
+#include "common/file.h"
+#include "common/func.h"
+#include "common/md5.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/dragonsphere/sound/psound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+namespace {
+
+const int kHostCallbackRateHz =
+		NativeSoundTimer::kPitClockHz / NativeSoundTimer::kHostTimerDivisor;
+
+int clampLevel(int value) {
+	return CLIP(value, 0, 63);
+}
+
+byte panningBits(byte panning) {
+	if (panning < 0x2b)
+		return 0x10;
+	if (panning < 0x55)
+		return 0x30;
+	return 0x20;
+}
+
+} // namespace
+
+void PSound::Channel::reset() {
+	memset(this, 0, sizeof(*this));
+	volumeFadeReload = 0xff;
+	panning = 0x40;
+}
+
+void PSound::Channel::load(uint16 sequenceOffset) {
+	reset();
+	loopStart = sequenceOffset;
+	position = sequenceOffset;
+	innerLoopStart = sequenceOffset;
+	outerLoopStart = sequenceOffset;
+	originalSequence = sequenceOffset;
+	activeCount = 1;
+}
+
+bool PSound::validateFile(const PSoundDriverData &driverData,
+		const char *first8192Md5, Common::String *reason) {
+	Common::File file;
+	if (!file.open(driverData.filename)) {
+		if (reason)
+			*reason = "file is missing";
+		return false;
+	}
+
+	const uint32 minimumSize = driverData.dataOffset +
+			driverData.initializedDataSize;
+	if ((uint32)file.size() < minimumSize) {
+		if (reason)
+			*reason = "initialized data is truncated";
+		return false;
+	}
+
+	file.seek(0);
+	const Common::String md5 = Common::computeStreamMD5AsString(file,
+			8192);
+	if (md5 != first8192Md5) {
+		if (reason)
+			*reason = "first-8192-byte signature does not match";
+		return false;
+	}
+
+	return true;
+}
+
+PSound::PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData) :
+		SoundDriver(mixer, driverData.filename, driverData.dataOffset,
+				driverData.initializedDataSize), _opl(nullptr) {
+	_masterVolume = 255;
+	_randomSeed = 0;
+	_frameCounter = 0;
+	_pollResult = 0;
+	_resultFlag = 0;
+	_nullSequenceOffset = driverData.nullSequenceOffset;
+	_patchTableOffset = driverData.patchTableOffset;
+	_tableLayout = driverData.tables;
+	_patchCount = driverData.patchCount;
+	_commandParam = 0;
+	_updatesEnabled = false;
+	_noiseServiceEnabled = false;
+	_tickEnabled = 1;
+	_tickCounter = 0;
+	_tempoReload = 0;
+	_tempoTarget = 0;
+	_tempoShift = 0;
+	_tempoBase = 0xa0;
+	_tempoCurrent = 0x28;
+	_tempoScale = 0x0a;
+	_frameNumber2 = 0;
+
+	if (_soundData.size() != (uint32)driverData.initializedDataSize)
+		error("PSOUND initialized data has unexpected size %u (expected %d)",
+				(uint)_soundData.size(), driverData.initializedDataSize);
+	if (driverData.totalDataSize < driverData.initializedDataSize ||
+			driverData.totalDataSize > 0xffff)
+		error("PSOUND has invalid mutable data size %d", driverData.totalDataSize);
+
+	const uint32 initializedSize = _soundData.size();
+	_soundData.resize(driverData.totalDataSize);
+	if (_soundData.size() > initializedSize)
+		memset(&_soundData[initializedSize], 0,
+				_soundData.size() - initializedSize);
+
+	if (!_patchCount ||
+			_patchTableOffset + (uint32)_patchCount * kPatchSize > initializedSize)
+		error("PSOUND patch table is outside initialized data");
+	if ((uint32)driverData.randomSeedOffset + 2 > initializedSize)
+		error("PSOUND random seed is outside initialized data");
+	if ((uint32)_tableLayout.panning + 0x80 > initializedSize ||
+			(uint32)_tableLayout.frequency + 24 > initializedSize ||
+			(uint32)_tableLayout.bank + kChannelCount > initializedSize ||
+			(uint32)_tableLayout.channel + kChannelCount > initializedSize ||
+			(uint32)_tableLayout.operators + kChannelCount * 4 > initializedSize)
+		error("PSOUND lookup table is outside initialized data");
+	if (_nullSequenceOffset >= initializedSize)
+		error("PSOUND null sequence is outside initialized data");
+
+	for (uint channel = 0; channel < kChannelCount; ++channel) {
+		const byte banks = getBankMask(channel);
+		if (!banks || (banks & ~kBothBanks))
+			error("PSOUND has invalid bank routing for channel %u", channel);
+		if (getOplChannel(channel) > 8)
+			error("PSOUND has invalid OPL routing for channel %u", channel);
+		for (uint op = 0; op < 4; ++op) {
+			if (getOperatorOffset(channel, op) > 0x15)
+				error("PSOUND has invalid operator routing for channel %u", channel);
+		}
+		_channels[channel].reset();
+	}
+	memset(_scriptVars, 0, sizeof(_scriptVars));
+	memset(_registerCache, 0, sizeof(_registerCache));
+	_randomSeed = readDataUint16(driverData.randomSeedOffset);
+
+	_opl = OPL::Config::create(OPL::Config::kOpl3);
+	if (!_opl || !_opl->init()) {
+		delete _opl;
+		_opl = nullptr;
+		return;
+	}
+	resetDriver();
+	_updatesEnabled = true;
+	_opl->start(new Common::Functor0Mem<void, PSound>(this,
+			&PSound::onTimer), kHostCallbackRateHz);
+}
+
+PSound::~PSound() {
+	if (_opl) {
+		_opl->stop();
+		delete _opl;
+	}
+}
+
+bool PSound::isDataRangeValid(uint32 offset, uint32 length) const {
+	return offset <= _soundData.size() && length <= _soundData.size() - offset;
+}
+
+const byte *PSound::getDataPointer(uint32 offset, uint32 length,
+		const char *operation) const {
+	if (!isDataRangeValid(offset, length))
+		error("PSOUND %s outside data image: offset 0x%04x, length %u",
+				operation, (uint)offset, (uint)length);
+	return &_soundData[offset];
+}
+
+byte *PSound::getDataPointer(uint32 offset, uint32 length,
+		const char *operation) {
+	return const_cast<byte *>(static_cast<const PSound *>(this)->
+			getDataPointer(offset, length, operation));
+}
+
+byte PSound::readDataByte(uint32 offset) const {
+	return *getDataPointer(offset, 1, "byte read");
+}
+
+uint16 PSound::readDataUint16(uint32 offset) const {
+	return READ_LE_UINT16(getDataPointer(offset, 2, "word read"));
+}
+
+void PSound::writeDataByte(uint32 offset, byte value) {
+	*getDataPointer(offset, 1, "byte write") = value;
+}
+
+void PSound::writeDataUint16(uint32 offset, uint16 value) {
+	WRITE_LE_UINT16(getDataPointer(offset, 2, "word write"), value);
+}
+
+byte *PSound::offsetPointer(uint16 offset, uint32 length,
+		const char *operation) {
+	return getDataPointer(offset, length, operation);
+}
+
+byte PSound::getBankMask(uint channel) const {
+	return readDataByte(_tableLayout.bank + channel);
+}
+
+byte PSound::getOplChannel(uint channel) const {
+	return readDataByte(_tableLayout.channel + channel);
+}
+
+byte PSound::getOperatorOffset(uint channel, uint operatorIndex) const {
+	return readDataByte(_tableLayout.operators + channel * 4 + operatorIndex);
+}
+
+const byte *PSound::getPatch(uint patchIndex) const {
+	if (patchIndex >= _patchCount)
+		patchIndex = 0;
+	return getDataPointer(_patchTableOffset + (uint32)patchIndex * kPatchSize,
+			kPatchSize, "patch");
+}
+
+byte PSound::getPanningAttenuation(byte panning) const {
+	return readDataByte(_tableLayout.panning + (panning & 0x7f));
+}
+
+uint16 PSound::getFrequencyNumber(byte semitone) const {
+	return readDataUint16(_tableLayout.frequency + (semitone % 12) * 2);
+}
+
+void PSound::writeRegister(byte banks, byte reg, byte value) {
+	assert(_opl);
+	assert(banks && !(banks & ~kBothBanks));
+	if (banks & kFirstBank) {
+		_registerCache[0][reg] = value;
+		_opl->writeReg(reg, value);
+	}
+	if (banks & kSecondBank) {
+		_registerCache[1][reg] = value;
+		_opl->writeReg(0x100 | reg, value);
+	}
+}
+
+byte PSound::getCachedRegister(byte banks, byte reg) const {
+	assert(banks && !(banks & ~kBothBanks));
+	return _registerCache[(banks & kFirstBank) ? 0 : 1][reg];
+}
+
+void PSound::resetDriver() {
+	const bool wasEnabled = _updatesEnabled;
+	_updatesEnabled = false;
+	for (uint i = 0; i < kChannelCount; ++i)
+		_channels[i].reset();
+	memset(_scriptVars, 0, sizeof(_scriptVars));
+	_resultFlag = 0;
+	_pollResult = 0;
+
+	writeRegister(kSecondBank, 0x05, 0x01);
+	writeRegister(kSecondBank, 0x04, 0x3f);
+	for (int reg = 0x56; reg >= 0x40; --reg)
+		writeRegister(kBothBanks, reg, 0x3f);
+	for (int reg = 0xff; reg >= 0x60; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	for (int reg = 0x3f; reg >= 0x20; --reg)
+		writeRegister(kBothBanks, reg, 0);
+	writeRegister(kBothBanks, 0x01, 0x20);
+	writeRegister(kBothBanks, 0xbd, 0xc0);
+
+	_updatesEnabled = wasEnabled;
+}
+
+void PSound::requestStop(uint firstChannel, uint endChannel) {
+	for (uint i = firstChannel; i < endChannel; ++i) {
+		if (_channels[i].activeCount) {
+			_channels[i].pendingStop = 0xff;
+			_channels[i].originalSequence = 0xffff;
+		}
+	}
+}
+
+void PSound::setCurrentSequence(uint firstChannel, uint endChannel,
+		uint16 sequenceOffset) {
+	offsetPointer(sequenceOffset, 1, "sequence position");
+	for (uint i = firstChannel; i < endChannel; ++i)
+		_channels[i].position = sequenceOffset;
+}
+
+void PSound::loadChannel(uint channel, uint16 sequenceOffset) {
+	offsetPointer(sequenceOffset, 1, "sequence start");
+	_channels[channel].load(sequenceOffset);
+}
+
+void PSound::playSound(uint16 sequenceOffset) {
+	for (uint i = kMusicChannelCount; i < kChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (int i = kChannelCount - 1; i >= kMusicChannelCount; --i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+}
+
+void PSound::playSoundAny(uint16 sequenceOffset) {
+	for (uint i = 0; i < kMusicChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (uint i = kMusicChannelCount; i < kChannelCount; ++i) {
+		if (!_channels[i].activeCount) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (int i = kChannelCount - 1; i >= kMusicChannelCount; --i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+	for (int i = kMusicChannelCount - 2; i >= 0; --i) {
+		if (_channels[i].pendingStop == 0xff) {
+			loadChannel(i, sequenceOffset);
+			return;
+		}
+	}
+}
+
+PSound::Channel *PSound::findActiveSound(uint16 sequenceOffset) {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount &&
+				_channels[i].originalSequence == sequenceOffset)
+			return &_channels[i];
+	}
+	return nullptr;
+}
+
+bool PSound::isSoundActive(uint16 sequenceOffset) const {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount &&
+				_channels[i].originalSequence == sequenceOffset)
+			return true;
+	}
+	return false;
+}
+
+uint16 PSound::nextRandom() {
+	const uint16 value = 0x9248 + _randomSeed;
+	_randomSeed = (value >> 3) | (value << 13);
+	return _randomSeed;
+}
+
+byte PSound::scaledCommandParameter(int param) const {
+	const byte value = param;
+	return value > 0x1e ? value - 0x1e : 0;
+}
+
+int PSound::command0() {
+	resetDriver();
+	return 0;
+}
+
+int PSound::command1() {
+	command3();
+	command5();
+	return 0;
+}
+
+int PSound::command2() {
+	setCurrentSequence(0, kMusicChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command3() {
+	requestStop(0, kMusicChannelCount);
+	return 0;
+}
+
+int PSound::command4() {
+	setCurrentSequence(kMusicChannelCount, kChannelCount, _nullSequenceOffset);
+	return 0;
+}
+
+int PSound::command5() {
+	requestStop(kMusicChannelCount, kChannelCount);
+	return 0;
+}
+
+int PSound::command6() {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		_channels[i].savedNoiseTicks = _channels[i].noiseTicks;
+		_channels[i].noiseTicks = 0;
+		keyOff(i);
+	}
+	_updatesEnabled = false;
+	return 0;
+}
+
+int PSound::command7() {
+	_updatesEnabled = true;
+	for (uint i = 0; i < kChannelCount; ++i) {
+		Channel &channel = _channels[i];
+		channel.noiseTicks = channel.savedNoiseTicks;
+		if (channel.activeCount) {
+			updateChannelLevels(i);
+			updateChannelFrequency(i, true);
+		}
+	}
+	bool anyNoise = false;
+	for (uint i = 0; i < kChannelCount; ++i)
+		anyNoise |= _channels[i].noiseTicks != 0;
+	if (anyNoise)
+		resultCheck();
+	return _channels[kChannelCount - 1].savedNoiseTicks;
+}
+
+int PSound::command8() {
+	int result = 0;
+	for (uint i = 0; i < kChannelCount; ++i)
+		result |= _channels[i].activeCount;
+	return result;
+}
+
+void PSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+	uint32 serviceTicks = _hostTimer.advance(1, kHostCallbackRateHz);
+	while (serviceTicks--) {
+		if (_noiseServiceEnabled)
+			serviceNoise();
+		if (_hostTimer.pollDue()) {
+			const int result = serviceUpdate();
+			if (result)
+				_noiseServiceEnabled = result > 0;
+		}
+	}
+}
+
+int PSound::serviceUpdate() {
+	update();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void PSound::serviceNoise() {
+	// The overlay explicitly walks its channel pointer table from 8 down to 0.
+	// This matters because every active channel advances the shared RNG.
+	for (int i = kChannelCount - 1; i >= 0; --i) {
+		Channel &channel = _channels[i];
+		if (channel.noiseTicks) {
+			const uint16 random = nextRandom();
+			setNoiseFrequency(i,
+					(random & channel.noiseMask) + channel.noiseBase);
+		}
+	}
+}
+
+void PSound::update() {
+	// Native export 3 checks the disabled sentinel before advancing the RNG.
+	if (!_updatesEnabled)
+		return;
+	nextRandom();
+	++_frameNumber2;
+	++_frameCounter;
+	for (uint i = 0; i < kChannelCount; ++i)
+		updateChannel(i);
+	serviceCallbacks();
+	checkPendingStops();
+
+	bool anyNoise = false;
+	// Match the native post-update sweep, which also runs from 8 down to 0.
+	for (int i = kChannelCount - 1; i >= 0; --i) {
+		Channel &channel = _channels[i];
+		if (!channel.noiseTicks)
+			continue;
+		anyNoise = true;
+		channel.noiseBase += channel.noiseStep;
+		if (!--channel.noiseTicks)
+			keyOff(i);
+	}
+	if (!anyNoise && _resultFlag != -1) {
+		_resultFlag = -1;
+		_pollResult = -1;
+	}
+}
+
+void PSound::updateChannel(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	if (!channel.activeCount)
+		return;
+
+	if (channel.keyOnDelay && --channel.keyOnDelay == 0)
+		keyOff(channelIndex);
+
+	if (--channel.activeCount == 0) {
+		bool levelsDirty = false;
+		int budget = kOpcodeBudgetPerTick;
+		while (budget-- > 0) {
+			if (!isDataRangeValid(channel.position, 1)) {
+				finishChannel(channelIndex);
+				break;
+			}
+			const byte value = readDataByte(channel.position);
+			if (!(value & 0x80)) {
+				if (!isDataRangeValid(channel.position, 2)) {
+					finishChannel(channelIndex);
+					break;
+				}
+				if (levelsDirty)
+					updateChannelLevels(channelIndex);
+				channel.note = value;
+				channel.activeCount = readDataByte(channel.position + 1);
+				channel.position += 2;
+				if (!channel.note || !channel.activeCount) {
+					keyOff(channelIndex);
+					if (!channel.activeCount)
+						finishChannel(channelIndex);
+				} else {
+					channel.keyOnDelay = channel.durationOverride ?
+							channel.durationOverride :
+							(byte)(channel.activeCount - channel.noteOffset);
+					updateChannelFrequency(channelIndex, true);
+				}
+				break;
+			}
+
+			if (value <= 0xbd ||
+					!executeOpcode(channelIndex, value, levelsDirty)) {
+				finishChannel(channelIndex);
+				break;
+			}
+		}
+		if (budget < 0 && !channel.activeCount)
+			finishChannel(channelIndex);
+	}
+
+	if (channel.pitchBend)
+		updatePitchBend(channelIndex);
+
+	bool levelsDirty = false;
+	if (channel.volumeFadeCounter || channel.panningFadeCounter) {
+		if (--channel.volumeFadeCounter == 0) {
+			channel.volumeFadeCounter = channel.volumeFadeReload;
+			if (channel.volumeFadeStep) {
+				channel.volumeOffset += channel.volumeFadeStep;
+				levelsDirty = true;
+			}
+		}
+
+		if (--channel.panningFadeCounter == 0) {
+			channel.panningFadeCounter = channel.panningFadeReload;
+			if (channel.panningFadeStep) {
+				channel.panning += channel.panningFadeStep;
+				updatePanning(channelIndex);
+				levelsDirty = true;
+			}
+		}
+	}
+	if (levelsDirty)
+		updateChannelLevels(channelIndex);
+}
+
+bool PSound::isOpcodeDataValid(uint16 position, uint32 length) const {
+	return isDataRangeValid(position, length);
+}
+
+byte PSound::readOpcodeByte(uint16 position, uint16 delta) const {
+	return readDataByte((uint16)(position + delta));
+}
+
+uint16 PSound::readOpcodeWord(uint16 position, uint16 delta) const {
+	return readDataUint16((uint16)(position + delta));
+}
+
+bool PSound::isScriptVariableValid(byte index) const {
+	return index < kScriptVarCount;
+}
+
+bool PSound::transferOpcode(Channel &channel, uint16 position, bool take) {
+	if (!isOpcodeDataValid(position, 5))
+		return false;
+	if (take) {
+		const uint16 target = readOpcodeWord(position, 3);
+		if (!isDataRangeValid(target, 1))
+			return false;
+		channel.branchReturn = position + 5;
+		channel.position = target;
+	} else {
+		channel.position = position + 5;
+	}
+	return true;
+}
+
+bool PSound::executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty) {
+	Channel &channel = _channels[channelIndex];
+	const uint16 position = channel.position;
+
+	switch (opcode) {
+	case 0xff: {
+		if (!isOpcodeDataValid(position, 2)) return false;
+		const uint16 count = (uint16)(int16)(int8)
+				readOpcodeByte(position, 1);
+		if (!channel.innerLoopCount) {
+			if (!count) {
+				channel.position = position + 2;
+				channel.innerLoopStart = channel.position;
+			} else {
+				channel.innerLoopCount = count;
+				channel.position = channel.innerLoopStart;
+			}
+		} else if (--channel.innerLoopCount) {
+			channel.position = channel.innerLoopStart;
+		} else {
+			channel.position = position + 2;
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+	case 0xfe: {
+		if (!isOpcodeDataValid(position, 2)) return false;
+		const uint16 count = (uint16)(int16)(int8)
+				readOpcodeByte(position, 1);
+		if (!channel.outerLoopCount) {
+			if (!count) {
+				channel.position = position + 2;
+				channel.outerLoopStart = channel.position;
+				channel.innerLoopStart = channel.position;
+				channel.innerLoopCount = 0;
+			} else {
+				channel.outerLoopCount = count;
+				channel.position = channel.outerLoopStart;
+				channel.innerLoopStart = channel.outerLoopStart;
+			}
+		} else if (--channel.outerLoopCount) {
+			channel.position = channel.outerLoopStart;
+			channel.innerLoopStart = channel.outerLoopStart;
+		} else {
+			channel.position = position + 2;
+			channel.outerLoopStart = channel.position;
+			channel.innerLoopStart = channel.position;
+		}
+		break;
+	}
+	case 0xfd:
+		channel.loopStart = channel.originalSequence;
+		channel.position = channel.originalSequence;
+		channel.innerLoopStart = channel.originalSequence;
+		channel.outerLoopStart = channel.originalSequence;
+		channel.pitchBend = 0;
+		channel.volumeFadeStep = 0;
+		channel.panningFadeStep = 0;
+		channel.transpose = 0;
+		channel.volumeOffset = 0;
+		channel.volume = 0;
+		channel.volumeFadeCounter = 0;
+		channel.panningFadeCounter = 0;
+		channel.innerLoopCount = 0;
+		channel.outerLoopCount = 0;
+		channel.noteOffset = 0;
+		break;
+	case 0xfc: {
+		if (!isOpcodeDataValid(position, 3)) return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1)) return false;
+		channel.loopStart = channel.position = channel.innerLoopStart =
+				channel.outerLoopStart = channel.originalSequence = target;
+		break;
+	}
+	case 0xfb: {
+		if (!isOpcodeDataValid(position, 3)) return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1)) return false;
+		channel.position = target;
+		break;
+	}
+	case 0xfa: {
+		if (!isOpcodeDataValid(position, 3)) return false;
+		const uint16 target = readOpcodeWord(position, 1);
+		if (!isDataRangeValid(target, 1)) return false;
+		channel.branchReturn = position + 3;
+		channel.position = target;
+		break;
+	}
+	case 0xf9:
+		if (channel.branchReturn) {
+			channel.position = channel.branchReturn;
+			channel.branchReturn = 0;
+		} else {
+			++channel.position;
+		}
+		break;
+	case 0xf8:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.patch = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		loadPatch(channelIndex, channel.patch);
+		break;
+	case 0xf7:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.noteOffset = readOpcodeByte(position, 1);
+		channel.durationOverride = 0;
+		channel.position = position + 2;
+		break;
+	case 0xf6:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.durationOverride = readOpcodeByte(position, 1);
+		channel.noteOffset = 0;
+		channel.position = position + 2;
+		break;
+	case 0xf5:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.pitchBend = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xf4:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.volume = (byte)((int8)readOpcodeByte(position, 1) >> 1);
+		channel.position = position + 2;
+		levelsDirty = true;
+		break;
+	case 0xf3:
+		if (!isOpcodeDataValid(position, 3)) return false;
+		if (!channel.pendingStop) {
+			channel.volumeFadeReload = readOpcodeByte(position, 1);
+			channel.volumeFadeStep = (int8)readOpcodeByte(position, 2);
+			channel.volumeFadeCounter = 1;
+		}
+		channel.position = position + 3;
+		break;
+	case 0xf2:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.transpose = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xf1: {
+		if (!isOpcodeDataValid(position, 2)) return false;
+		const int8 value =
+			(int8)(((int8)readOpcodeByte(position, 1) >> 1) - 50);
+		if (!channel.pendingStop || value < channel.volumeOffset) {
+			channel.volumeOffset = value;
+			levelsDirty = true;
+		}
+		channel.position = position + 2;
+		break;
+	}
+	case 0xf0:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.panning = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		updatePanning(channelIndex);
+		levelsDirty = true;
+		break;
+	case 0xef:
+		if (!isOpcodeDataValid(position, 3)) return false;
+		channel.panningFadeReload = readOpcodeByte(position, 1);
+		channel.panningFadeStep = (int8)readOpcodeByte(position, 2);
+		channel.panningFadeCounter = 1;
+		channel.position = position + 3;
+		break;
+	case 0xee:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.noteTranspose = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xed:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.position = (uint16)(position +
+			(int8)readOpcodeByte(position, 1) + 3);
+		break;
+	case 0xec: {
+		if (!isOpcodeDataValid(position, 2)) return false;
+		const byte count = readOpcodeByte(position, 1);
+		if (!count || !isOpcodeDataValid(position, (uint32)count + 3))
+			return false;
+		const uint16 base = position + 2;
+		const byte selected = readDataByte(base + (nextRandom() & 0x7fff) % count);
+		const byte target = readDataByte(base + count);
+		if (!isDataRangeValid(base + count + target + 1, 1)) return false;
+		writeDataByte(base + count + target + 1, selected);
+		channel.position = position + count + 3;
+		break;
+	}
+	case 0xeb: {
+		if (!isOpcodeDataValid(position, 4)) return false;
+		const int low = (int8)readOpcodeByte(position, 1);
+		const int high = (int8)readOpcodeByte(position, 2);
+		const int range = high - low + 1;
+		if (range <= 0) return false;
+		const byte target = readOpcodeByte(position, 3);
+		if (!isDataRangeValid(position + 4 + target, 1)) return false;
+		writeDataByte(position + 4 + target,
+				(byte)(low + (nextRandom() & 0x7fff) % range));
+		channel.position = position + 4;
+		break;
+	}
+	case 0xea: {
+		if (!isOpcodeDataValid(position, 3)) return false;
+		const byte variable = readOpcodeByte(position, 1);
+		const byte count = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(variable) ||
+				!isOpcodeDataValid(position, (uint32)count + 4)) return false;
+		const uint16 base = position + 3;
+		const byte target = readDataByte(base + count);
+		if (!isDataRangeValid(base + _scriptVars[variable], 1) ||
+				!isDataRangeValid(base + target + 1, 1)) return false;
+		writeDataByte(base + target + 1,
+				readDataByte(base + _scriptVars[variable]));
+		channel.position = position + count + 4;
+		break;
+	}
+	case 0xe9:
+	case 0xe8:
+	case 0xe7:
+	case 0xe4: case 0xe3: case 0xe2: case 0xe1:
+	case 0xe0: case 0xdf: case 0xde: case 0xdd:
+	case 0xdc: case 0xdb: case 0xda: case 0xd9:
+	case 0xd8: case 0xd7: case 0xd6: case 0xd5: {
+		if (!isOpcodeDataValid(position, 3)) return false;
+		const byte first = readOpcodeByte(position, 1);
+		const byte second = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(first)) return false;
+		if (opcode == 0xe9) {
+			_scriptVars[first] = second;
+		} else if (opcode == 0xe8) {
+			if (!isScriptVariableValid(second)) return false;
+			_scriptVars[first] = _scriptVars[second];
+		} else if (opcode == 0xe7) {
+			if (!isDataRangeValid(position + 3 + second, 1)) return false;
+			writeDataByte(position + 3 + second, _scriptVars[first]);
+		} else {
+			const bool usesVariable = (opcode & 1) != 0;
+			if (usesVariable && !isScriptVariableValid(second)) return false;
+			const byte operand = usesVariable ? _scriptVars[second] : second;
+			byte &destination = _scriptVars[first];
+			switch (opcode) {
+			case 0xe4: case 0xe3: destination += operand; break;
+			case 0xe2: case 0xe1: destination -= operand; break;
+			case 0xe0: case 0xdf: destination *= operand; break;
+			case 0xde: case 0xdd:
+				if (!operand) return false;
+				destination = opcode == 0xde ?
+						(byte)((int8)destination / (int8)operand) :
+						(byte)(destination / operand);
+				break;
+			case 0xdc: case 0xdb:
+				if (!operand) return false;
+				destination = opcode == 0xdc ?
+						(byte)((int8)destination % (int8)operand) :
+						(byte)(destination % operand);
+				break;
+			case 0xda: case 0xd9: destination &= operand; break;
+			case 0xd8: case 0xd7: destination |= operand; break;
+			case 0xd6: case 0xd5: destination ^= operand; break;
+			default: break;
+			}
+		}
+		channel.position = position + 3;
+		break;
+	}
+	case 0xe6:
+	case 0xe5: {
+		if (!isOpcodeDataValid(position, 2)) return false;
+		const byte variable = readOpcodeByte(position, 1);
+		if (!isScriptVariableValid(variable)) return false;
+		_scriptVars[variable] += opcode == 0xe6 ? 1 : (byte)-1;
+		channel.position = position + 2;
+		break;
+	}
+	case 0xd4: case 0xd3: case 0xd2: case 0xd1:
+	case 0xd0: case 0xcf: case 0xce: case 0xcd:
+	case 0xcc: case 0xcb: case 0xca: case 0xc9:
+	case 0xc8: case 0xc7: case 0xc6: case 0xc5: {
+		if (!isOpcodeDataValid(position, 5)) return false;
+		const byte first = readOpcodeByte(position, 1);
+		const byte second = readOpcodeByte(position, 2);
+		if (!isScriptVariableValid(first)) return false;
+		const bool variablePair =
+				(opcode <= 0xd0 && opcode >= 0xcd) || opcode <= 0xc8;
+		if (variablePair && !isScriptVariableValid(second)) return false;
+		bool take = false;
+		switch (opcode) {
+		case 0xd4: case 0xcc: take = second == _scriptVars[first]; break;
+		case 0xd3: case 0xcb: take = second != _scriptVars[first]; break;
+		case 0xd2: case 0xca: take = second > _scriptVars[first]; break;
+		case 0xd1: take = second >= _scriptVars[first]; break;
+		case 0xc9: take = second < _scriptVars[first]; break;
+		case 0xd0: case 0xc8:
+			take = _scriptVars[first] == _scriptVars[second]; break;
+		case 0xcf: case 0xc7:
+			take = _scriptVars[first] != _scriptVars[second]; break;
+		case 0xce: case 0xc6:
+			take = _scriptVars[first] > _scriptVars[second]; break;
+		case 0xcd: case 0xc5:
+			take = _scriptVars[first] < _scriptVars[second]; break;
+		default: break;
+		}
+		if (!transferOpcode(channel, position, take)) return false;
+		break;
+	}
+	case 0xc4:
+		if (!isOpcodeDataValid(position, 3) ||
+				!callFunction(readOpcodeWord(position, 1), channel)) return false;
+		channel.position = position + 3;
+		break;
+	case 0xc3:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		channel.position = position + 2;
+		break;
+	case 0xc2:
+		if (!isOpcodeDataValid(position, 4)) return false;
+		channel.position = position + 4;
+		break;
+	case 0xc1:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		_tempoScale = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	case 0xc0:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		_tempoReload = readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		if (!_frameNumber2)
+			_tempoCurrent = _tempoReload;
+		break;
+	case 0xbf:
+		if (!isOpcodeDataValid(position, 3)) return false;
+		_tempoTarget = readOpcodeWord(position, 1);
+		channel.position = position + 3;
+		if (!_frameNumber2)
+			_tempoBase = _tempoTarget;
+		_tickEnabled = 1;
+		_tickCounter = 1;
+		break;
+	case 0xbe:
+		if (!isOpcodeDataValid(position, 2)) return false;
+		_tempoShift = (int8)readOpcodeByte(position, 1);
+		channel.position = position + 2;
+		break;
+	default:
+		return false;
+	}
+	return true;
+}
+
+void PSound::finishChannel(uint channelIndex) {
+	keyOff(channelIndex);
+	_channels[channelIndex].activeCount = 0;
+	_channels[channelIndex].keyOnDelay = 0;
+}
+
+void PSound::checkPendingStops() {
+	for (uint i = 0; i < kChannelCount; ++i) {
+		Channel &channel = _channels[i];
+		if (!channel.activeCount || !channel.pendingStop)
+			continue;
+		if ((byte)channel.volumeOffset == 0xd8) {
+			channel.position = _nullSequenceOffset;
+			channel.pendingStop = 0;
+		} else {
+			channel.volumeFadeStep = -1;
+			channel.volumeFadeReload = 4;
+			if (!channel.volumeFadeCounter)
+				channel.volumeFadeCounter = 1;
+		}
+	}
+}
+
+void PSound::programOperator(byte banks, uint channelIndex,
+		uint operatorIndex, const byte *operatorData) {
+	const byte op = getOperatorOffset(channelIndex, operatorIndex);
+	const byte characteristics = (operatorData[9] & 0x0f) |
+			((operatorData[5] & 1) << 4) | ((operatorData[4] & 1) << 5) |
+			((operatorData[12] & 1) << 6) | ((operatorData[11] & 1) << 7);
+	const byte totalLevel = ((operatorData[7] & 3) << 6) |
+			clampLevel(0x3f - (operatorData[6] & 0x3f));
+	writeRegister(banks, 0x40 + op, 0x3f);
+	writeRegister(banks, 0x20 + op, characteristics);
+	writeRegister(banks, 0x60 + op,
+			(operatorData[0] << 4) | (operatorData[1] & 0x0f));
+	writeRegister(banks, 0x80 + op,
+			(operatorData[2] << 4) | (operatorData[3] & 0x0f));
+	writeRegister(banks, 0xe0 + op, operatorData[8] & 3);
+	writeRegister(banks, 0x40 + op, totalLevel);
+}
+
+void PSound::loadPatch(uint channelIndex, byte patchIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(patchIndex);
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const uint operatorCount = channelIndex < kMusicChannelCount ? 4 : 2;
+
+	keyOff(channelIndex);
+	channel.mode = patch[0x0d];
+	for (uint i = 0; i < 4; ++i)
+		channel.operatorTotalLevel[i] = patch[i * 14 + 6];
+	for (uint i = 0; i < operatorCount; ++i)
+		programOperator(banks, channelIndex, i, patch + i * 14);
+
+	if (channelIndex < kMusicChannelCount) {
+		const byte stereo = panningBits(channel.panning);
+		writeRegister(banks, 0xc0 + oplChannel,
+				stereo | ((patch[0x0a] & 7) << 1) |
+				((patch[0x0d] & 1) ^ 1));
+		writeRegister(banks, 0xc3 + oplChannel,
+				stereo | ((patch[0x26] & 7) << 1) |
+				((patch[0x29] & 1) ^ 1));
+	} else {
+		const byte value = ((patch[0x0a] & 7) << 1) |
+				((channel.mode & 1) ^ 1);
+		writeRegister(kFirstBank, 0xc0 + oplChannel, value | 0x20);
+		writeRegister(kSecondBank, 0xc0 + oplChannel, value | 0x10);
+	}
+
+	channel.noiseTicks = patch[0x38];
+	channel.noiseMask = READ_LE_UINT16(patch + 0x3a);
+	channel.noiseBase = READ_LE_UINT16(patch + 0x3c);
+	channel.noiseStep = (int16)READ_LE_UINT16(patch + 0x3e);
+	if (channel.noiseTicks)
+		resultCheck();
+	updatePanning(channelIndex);
+	updateChannelLevels(channelIndex);
+}
+
+void PSound::updatePanning(uint channelIndex) {
+	const byte oplChannel = getOplChannel(channelIndex);
+	if (channelIndex < kMusicChannelCount) {
+		const byte banks = getBankMask(channelIndex);
+		const byte stereo = panningBits(_channels[channelIndex].panning);
+		const byte first = 0xc0 + oplChannel;
+		const byte second = 0xc3 + oplChannel;
+		writeRegister(banks, first,
+				(getCachedRegister(banks, first) & 0x0f) | stereo);
+		writeRegister(banks, second,
+				(getCachedRegister(banks, second) & 0x0f) | stereo);
+	} else {
+		const byte reg = 0xc0 + oplChannel;
+		writeRegister(kFirstBank, reg,
+				(getCachedRegister(kFirstBank, reg) & 0x0f) | 0x20);
+		writeRegister(kSecondBank, reg,
+				(getCachedRegister(kSecondBank, reg) & 0x0f) | 0x10);
+	}
+}
+
+void PSound::updateChannelLevels(uint channelIndex) {
+	Channel &channel = _channels[channelIndex];
+	const byte *patch = getPatch(channel.patch);
+	int base = 0x7e - channel.volume - channel.volumeOffset -
+			channel.patchAttenuation;
+	base += (255 - _masterVolume) * 63 / 255;
+
+	if (channelIndex >= kMusicChannelCount) {
+		const uint operators[2] = { 1, 0 };
+		const bool enabled[2] = { true, channel.mode == 0 };
+		for (uint i = 0; i < 2; ++i) {
+			if (!enabled[i]) continue;
+			const uint opIndex = operators[i];
+			const byte op = getOperatorOffset(channelIndex, opIndex);
+			const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+			const int left = clampLevel(base -
+					channel.operatorTotalLevel[opIndex] +
+					getPanningAttenuation(channel.panning));
+			const int right = clampLevel(base -
+					channel.operatorTotalLevel[opIndex] +
+					getPanningAttenuation(0x7f - channel.panning));
+			writeRegister(kFirstBank, 0x40 + op, scaling | left);
+			writeRegister(kSecondBank, 0x40 + op, scaling | right);
+		}
+		return;
+	}
+
+	if (channel.panning > 0x2a && channel.panning < 0x55)
+		base += 6;
+	const byte banks = getBankMask(channelIndex);
+	const uint operators[4] = { 3, 1, 0, 2 };
+	const bool enabled[4] = {
+		true, channel.mode == 1, (channel.mode & 2) != 0, channel.mode == 3
+	};
+	for (uint i = 0; i < 4; ++i) {
+		if (!enabled[i]) continue;
+		const uint opIndex = operators[i];
+		const byte op = getOperatorOffset(channelIndex, opIndex);
+		const byte scaling = (patch[opIndex * 14 + 7] & 3) << 6;
+		const int level = clampLevel(base - channel.operatorTotalLevel[opIndex]);
+		writeRegister(banks, 0x40 + op, scaling | level);
+	}
+}
+
+void PSound::updateChannelFrequency(uint channelIndex, bool keyOn) {
+	Channel &channel = _channels[channelIndex];
+	updateChannelLevels(channelIndex);
+	const byte note = (byte)(channel.note + channel.noteTranspose);
+	const int frequency = getFrequencyNumber(note % 12) + channel.transpose;
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	writeRegister(banks, 0xa0 + oplChannel, frequency & 0xff);
+	byte high = (((note / 12) & 7) << 2) | ((frequency >> 8) & 3);
+	if (keyOn)
+		high |= 0x20;
+	writeRegister(banks, 0xb0 + oplChannel, high);
+}
+
+void PSound::updatePitchBend(uint channelIndex) {
+	const Channel &channel = _channels[channelIndex];
+	const byte banks = getBankMask(channelIndex);
+	const byte oplChannel = getOplChannel(channelIndex);
+	const byte lowReg = 0xa0 + oplChannel;
+	const byte highReg = 0xb0 + oplChannel;
+	int frequency = ((getCachedRegister(banks, highReg) & 0x1f) << 8) |
+			getCachedRegister(banks, lowReg);
+	frequency += channel.pitchBend;
+	writeRegister(banks, lowReg, frequency & 0xff);
+	writeRegister(banks, highReg,
+			(getCachedRegister(banks, highReg) & 0x20) |
+			((frequency >> 8) & 0x1f));
+}
+
+void PSound::keyOff(uint channelIndex) {
+	const byte banks = getBankMask(channelIndex);
+	const byte reg = 0xb0 + getOplChannel(channelIndex);
+	writeRegister(banks, reg, getCachedRegister(banks, reg) & 0xdf);
+}
+
+void PSound::setNoiseFrequency(uint channelIndex, int frequency) {
+	const byte banks = getBankMask(channelIndex);
+	const byte channel = getOplChannel(channelIndex);
+	writeRegister(banks, 0xa0 + channel, frequency & 0xff);
+	writeRegister(banks, 0xb0 + channel,
+			((frequency >> 8) & 0x1f) | 0x20);
+}
+
+void PSound::resultCheck() {
+	if (_resultFlag != 1) {
+		_resultFlag = 1;
+		_pollResult = 1;
+	}
+}
+
+int PSound::stop() {
+	Common::StackLock lock(_driverMutex);
+	command0();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+int PSound::poll() {
+	Common::StackLock lock(_driverMutex);
+	return serviceUpdate();
+}
+
+void PSound::noise() {
+	Common::StackLock lock(_driverMutex);
+	serviceNoise();
+}
+
+void PSound::setVolume(int volume) {
+	Common::StackLock lock(_driverMutex);
+	_masterVolume = CLIP(volume, 0, 255);
+	for (uint i = 0; i < kChannelCount; ++i) {
+		if (_channels[i].activeCount)
+			updateChannelLevels(i);
+	}
+}
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/sound/psound.h b/engines/mads/dragonsphere/sound/psound.h
new file mode 100644
index 00000000000..73879a38c89
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/psound.h
@@ -0,0 +1,229 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT file.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef MADS_DRAGONSPHERE_SOUND_PSOUND_H
+#define MADS_DRAGONSPHERE_SOUND_PSOUND_H
+
+#include "mads/core/native_sound_timer.h"
+#include "mads/core/sound_manager.h"
+
+namespace OPL {
+class OPL;
+}
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+struct PSoundTableLayout {
+	uint16 panning;
+	uint16 frequency;
+	uint16 bank;
+	uint16 channel;
+	uint16 operators;
+};
+
+struct PSoundDriverData {
+	const char *filename;
+	int dataOffset;
+	int initializedDataSize;
+	int totalDataSize;
+	uint16 randomSeedOffset;
+	uint16 nullSequenceOffset;
+	uint16 patchTableOffset;
+	byte patchCount;
+	PSoundTableLayout tables;
+};
+
+/** Interpreter for Dragonsphere's rich PSOUND overlay family. */
+class PSound : public SoundDriver {
+public:
+	enum {
+		kChannelCount = 9,
+		kMusicChannelCount = 6,
+		kPatchSize = 0x40,
+		kScriptVarCount = 32,
+		kOpcodeBudgetPerTick = 256
+	};
+
+protected:
+	enum RegisterBank {
+		kFirstBank = 1,
+		kSecondBank = 2,
+		kBothBanks = kFirstBank | kSecondBank
+	};
+
+	/** Logical layout of the native 0x32-byte channel record. */
+	struct Channel {
+		byte activeCount;              // +00
+		int8 pitchBend;                // +01
+		int8 volumeFadeStep;           // +02
+		int8 panningFadeStep;          // +03
+		byte note;                     // +04
+		byte patch;                    // +05
+		byte volume;                   // +06
+		byte noteOffset;               // +07
+		byte keyOnDelay;               // +08
+		byte volumeFadeCounter;        // +09
+		byte volumeFadeReload;         // +0a
+		byte panningFadeCounter;       // +0b
+		byte panningFadeReload;        // +0c
+		byte panning;                  // +0d
+		int8 volumeOffset;             // +0e
+		byte mode;                     // +0f
+		byte operatorTotalLevel[4];    // +10..+13
+		uint16 loopStart;              // +14
+		uint16 position;               // +16
+		uint16 innerLoopStart;         // +18
+		uint16 outerLoopStart;         // +1a
+		uint16 innerLoopCount;         // +1c
+		uint16 outerLoopCount;         // +1e
+		uint16 originalSequence;       // +20
+		uint16 branchReturn;           // +22
+		uint16 noiseMask;              // +24
+		uint16 noiseBase;              // +26
+		int16 noiseStep;               // +28
+		byte noiseTicks;               // +2a
+		byte savedNoiseTicks;          // +2b
+		int8 transpose;                // +2c
+		int8 noteTranspose;            // +2d
+		byte pendingStop;              // +2e
+		int8 patchAttenuation;         // +2f
+		byte durationOverride;         // +30
+
+		void reset();
+		void load(uint16 sequenceOffset);
+	};
+
+	OPL::OPL *_opl;
+	byte _registerCache[2][256];
+	NativeSoundTimer _hostTimer;
+	Channel _channels[kChannelCount];
+	byte _scriptVars[kScriptVarCount];
+	int _masterVolume;
+	uint16 _randomSeed;
+	uint16 _frameCounter;
+	int16 _pollResult;
+	int8 _resultFlag;
+	uint16 _nullSequenceOffset;
+	uint16 _patchTableOffset;
+	PSoundTableLayout _tableLayout;
+	byte _patchCount;
+	int _commandParam;
+	bool _updatesEnabled;
+	bool _noiseServiceEnabled;
+	// The native BE-C1 opcodes update these fields, but every audited overlay's
+	// per-tick tempo hook is a no-op. Preserve the state without applying an
+	// invented duration transform.
+	uint16 _tickEnabled;
+	uint16 _tickCounter;
+	uint16 _tempoReload;
+	uint16 _tempoTarget;
+	int16 _tempoShift;
+	uint16 _tempoBase;
+	uint16 _tempoCurrent;
+	uint16 _tempoScale;
+	int _frameNumber2;
+
+	PSound(Audio::Mixer *mixer, const PSoundDriverData &driverData);
+	~PSound() override;
+
+	bool isDataRangeValid(uint32 offset, uint32 length) const;
+	const byte *getDataPointer(uint32 offset, uint32 length,
+			const char *operation) const;
+	byte *getDataPointer(uint32 offset, uint32 length, const char *operation);
+	byte readDataByte(uint32 offset) const;
+	uint16 readDataUint16(uint32 offset) const;
+	void writeDataByte(uint32 offset, byte value);
+	void writeDataUint16(uint32 offset, uint16 value);
+	byte *offsetPointer(uint16 offset, uint32 length, const char *operation);
+
+	byte getBankMask(uint channel) const;
+	byte getOplChannel(uint channel) const;
+	byte getOperatorOffset(uint channel, uint operatorIndex) const;
+	const byte *getPatch(uint patchIndex) const;
+	byte getPanningAttenuation(byte panning) const;
+	uint16 getFrequencyNumber(byte semitone) const;
+	void writeRegister(byte banks, byte reg, byte value);
+	byte getCachedRegister(byte banks, byte reg) const;
+
+	void resetDriver();
+	int command0();
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+	int command5();
+	int command6();
+	int command7();
+	int command8();
+	int nullCommand() { return 0; }
+
+	void requestStop(uint firstChannel, uint endChannel);
+	void setCurrentSequence(uint firstChannel, uint endChannel,
+			uint16 sequenceOffset);
+	void loadChannel(uint channel, uint16 sequenceOffset);
+	void playSound(uint16 sequenceOffset);
+	void playSoundAny(uint16 sequenceOffset);
+	Channel *findActiveSound(uint16 sequenceOffset);
+	bool isSoundActive(uint16 sequenceOffset) const;
+
+	void onTimer();
+	int serviceUpdate();
+	void serviceNoise();
+	void update();
+	void updateChannel(uint channelIndex);
+	bool isOpcodeDataValid(uint16 position, uint32 length) const;
+	byte readOpcodeByte(uint16 position, uint16 delta) const;
+	uint16 readOpcodeWord(uint16 position, uint16 delta) const;
+	bool isScriptVariableValid(byte index) const;
+	bool transferOpcode(Channel &channel, uint16 position, bool take);
+	bool executeOpcode(uint channelIndex, byte opcode, bool &levelsDirty);
+	void finishChannel(uint channelIndex);
+	void checkPendingStops();
+
+	void loadPatch(uint channelIndex, byte patchIndex);
+	void programOperator(byte banks, uint channelIndex, uint operatorIndex,
+			const byte *operatorData);
+	void updatePanning(uint channelIndex);
+	void updateChannelLevels(uint channelIndex);
+	void updateChannelFrequency(uint channelIndex, bool keyOn);
+	void updatePitchBend(uint channelIndex);
+	void keyOff(uint channelIndex);
+	void setNoiseFrequency(uint channelIndex, int frequency);
+	void resultCheck();
+
+	uint16 nextRandom();
+	byte scaledCommandParameter(int param) const;
+
+	/** Implement verified native callbacks invoked by opcode C4. */
+	virtual bool callFunction(uint16 targetOffset, Channel &channel) = 0;
+
+	/** Service section-local deferred callbacks once per export-3 update. */
+	virtual void serviceCallbacks() {
+	}
+
+public:
+	static bool validateFile(const PSoundDriverData &driverData,
+			const char *first8192Md5, Common::String *reason = nullptr);
+	bool isReady() const { return _opl != nullptr; }
+
+	int stop() override;
+	int poll() override;
+	void noise() override;
+	void setVolume(int volume) override;
+};
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif // MADS_DRAGONSPHERE_SOUND_PSOUND_H
diff --git a/engines/mads/dragonsphere/sound/psound_dragonsphere.cpp b/engines/mads/dragonsphere/sound/psound_dragonsphere.cpp
new file mode 100644
index 00000000000..80ce10a838a
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/psound_dragonsphere.cpp
@@ -0,0 +1,2217 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/dragonsphere/sound/psound_dragonsphere.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+namespace {
+
+const PSoundDriverData kPSound1Data = {
+	"PSOUND.DR1", 0x3470, 0x428c, 0x4680, 0x0c2a, 0x06de,
+	0x101c, 56, { 0x0058, 0x00d8, 0x00f4, 0x00fe, 0x0108 }
+};
+
+const PSoundDriverData kPSound2Data = {
+	"PSOUND.DR2", 0x2f20, 0x1b3d, 0x1f30, 0x0058, 0x0826,
+	0x0c26, 37, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSound3Data = {
+	"PSOUND.DR3", 0x2ee0, 0x192f, 0x1d20, 0x0058, 0x016a,
+	0x0328, 36, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSound4Data = {
+	"PSOUND.DR4", 0x3110, 0x2833, 0x2c30, 0x0058, 0x0d2a,
+	0x016a, 47, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSound5Data = {
+	"PSOUND.DR5", 0x30c0, 0x27c9, 0x2bc0, 0x0058, 0x1fcc,
+	0x016a, 44, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSound6Data = {
+	"PSOUND.DR6", 0x32f0, 0x2cc3, 0x30c0, 0x0058, 0x0cba,
+	0x016a, 45, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSound9Data = {
+	"PSOUND.DR9", 0x33e0, 0x61ab, 0x65a0, 0x0058, 0x4b78,
+	0x016a, 52, { 0x0096, 0x0116, 0x0132, 0x013c, 0x0146 }
+};
+
+const PSoundDriverData kPSoundDemo1Data = {
+	"PSOUND.DR1", 0x3110, 0x4933, 0x4d30, 0x0970, 0x06da,
+	0x0d62, 128, { 0x0058, 0x00d8, 0x00f4, 0x00fe, 0x0108 }
+};
+
+const PSoundDriverData kPSoundDemo9Data = {
+	"PSOUND.DR9", 0x3170, 0x5f47, 0x6340, 0x0a44, 0x09d4,
+	0x0b64, 128, { 0x0a90, 0x0b10, 0x0b2c, 0x0b36, 0x0b40 }
+};
+
+struct ValidationEntry {
+	const PSoundDriverData *driverData;
+	const char *first8192Md5;
+};
+
+const ValidationEntry kRetailValidation[] = {
+	{ &kPSound1Data, "61922da6166387e3375431cebc9b7b1e" },
+	{ &kPSound2Data, "ba888c3a1942510beb58fcb6fae2f8c2" },
+	{ &kPSound3Data, "6835d68cac3a2ecf84cf0b38f80d433a" },
+	{ &kPSound4Data, "32e02460b6bfd28b6a928fc41fa16a91" },
+	{ &kPSound5Data, "ff00aed0b32ac3b1c3a3fec4cc4c1a4f" },
+	{ &kPSound6Data, "01bce80ca94793ea14965264df2a59fb" },
+	{ &kPSound9Data, "0f946874841f337fb2c267a70aff119a" }
+};
+
+const ValidationEntry kDemoValidation[] = {
+	{ &kPSoundDemo1Data, "baa780e3793975905516ad95ffa07383" },
+	{ &kPSoundDemo9Data, "cf13056c76a459a5739d5f2fa21dc4ff" }
+};
+
+const ValidationEntry *retailValidationForSection(int section) {
+	switch (section) {
+	case 1: return &kRetailValidation[0];
+	case 2: return &kRetailValidation[1];
+	case 3: return &kRetailValidation[2];
+	case 4: return &kRetailValidation[3];
+	case 5: return &kRetailValidation[4];
+	case 6: return &kRetailValidation[5];
+	case 9: return &kRetailValidation[6];
+	default: return nullptr;
+	}
+}
+
+const ValidationEntry *demoValidationForSection(int section) {
+	switch (section) {
+	case 1: return &kDemoValidation[0];
+	case 9: return &kDemoValidation[1];
+	default: return nullptr;
+	}
+}
+
+const int kPSound5AlternateMusic = 0x100;
+
+} // namespace
+
+bool validateDragonspherePSoundFile(int section, bool isDemo,
+		Common::String *reason) {
+	const ValidationEntry *entry = isDemo ?
+			demoValidationForSection(section) :
+			retailValidationForSection(section);
+	if (!entry) {
+		if (reason)
+			*reason = "unsupported section";
+		return false;
+	}
+
+	return PSound::validateFile(*entry->driverData, entry->first8192Md5,
+			reason);
+}
+
+DragonspherePSound::DragonspherePSound(Audio::Mixer *mixer,
+		const PSoundDriverData &driverData, byte maxMusicCommand,
+		bool resetClearsCallback) :
+		PSound(mixer, driverData), _callbackCounter(0), _callbackPeriod(0),
+		_pendingCommand(-1), _pendingLoadOnly(false), _musicIndex(-1),
+		_maxMusicCommand(maxMusicCommand),
+		_resetClearsCallback(resetClearsCallback) {
+}
+
+DragonspherePSoundDemo::DragonspherePSoundDemo(Audio::Mixer *mixer,
+		const PSoundDriverData &driverData, bool resetClearsCallback) :
+		DragonspherePSound(mixer, driverData, 0, resetClearsCallback) {
+}
+
+int DragonspherePSoundDemo::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	_commandParam = param;
+	_frameCounter = 0;
+	// The demo dispatchers call their group-32 handlers directly and never
+	// maintain the retail driver's saved music-command word.
+	return executeCommand(commandId, false);
+}
+
+int DragonspherePSound::dispatchBaseCommand(int commandId) {
+	switch (commandId) {
+	case 0: return resetSection();
+	case 1: return command1();
+	case 2: return command2();
+	case 3: return command3();
+	case 4: return command4();
+	case 5: return command5();
+	case 6: return command6();
+	case 7: return command7();
+	case 8: return command8();
+	default: return 0;
+	}
+}
+
+int DragonspherePSound::resetSection() {
+	if (_resetClearsCallback) {
+		_callbackCounter = 0;
+		_callbackPeriod = 0;
+		_pendingCommand = -1;
+		_pendingLoadOnly = false;
+	}
+	// Every retail section preserves the exported dispatcher's saved music
+	// command. Only the section callback hook varies between overlays.
+	return PSound::command0();
+}
+
+void DragonspherePSound::playSounds(const uint16 *sequences, uint count,
+		bool anyChannel) {
+	for (uint i = 0; i < count; ++i) {
+		if (anyChannel)
+			playSoundAny(sequences[i]);
+		else
+			playSound(sequences[i]);
+	}
+}
+
+void DragonspherePSound::loadChannels(const ChannelLoad *loads, uint count) {
+	for (uint i = 0; i < count; ++i)
+		loadChannel(loads[i].channel, loads[i].sequence);
+}
+
+bool DragonspherePSound::musicChannelsActive(uint count) const {
+	for (uint i = 0; i < count; ++i) {
+		if (_channels[i].activeCount)
+			return true;
+	}
+	return false;
+}
+
+void DragonspherePSound::applyMusicLoad(const MusicLoad &load) {
+	if (load.clearCallback) {
+		_pendingCommand = -1;
+		_pendingLoadOnly = false;
+	}
+	if (load.counter >= 0)
+		_callbackCounter = load.counter;
+	if (load.period >= 0)
+		_callbackPeriod = load.period;
+	if (load.musicIndex >= 0)
+		_musicIndex = load.musicIndex;
+
+	switch (load.reset) {
+	case kClearMusicIdentity:
+		command2();
+		break;
+	case kStopMusic:
+		command3();
+		break;
+	case kStopAll:
+		command1();
+		break;
+	case kKeepPlayback:
+		break;
+	}
+	loadChannels(load.channels, load.channelCount);
+}
+
+bool DragonspherePSound::startOrDeferMusic(int commandId, uint16 guard,
+		const MusicLoad &load, bool loadOnly, uint musicChannelCount) {
+	if (!loadOnly) {
+		if (guard && isSoundActive(guard))
+			return false;
+		if (musicChannelsActive(musicChannelCount)) {
+			deferCommand(commandId, true);
+			return false;
+		}
+	}
+	applyMusicLoad(load);
+	return true;
+}
+
+bool DragonspherePSound::startOrDeferMusicWhenActive(int commandId,
+		uint16 guard, const MusicLoad &load, bool loadOnly,
+		uint musicChannelCount) {
+	// Several overlays test the sequence guard only after establishing that
+	// music is already active. An idle driver therefore loads immediately,
+	// even if the same sequence remains on a non-music channel.
+	if (!loadOnly && musicChannelsActive(musicChannelCount)) {
+		if (guard && isSoundActive(guard))
+			return false;
+		deferCommand(commandId, true);
+		return false;
+	}
+	applyMusicLoad(load);
+	return true;
+}
+
+void DragonspherePSound::deferCommand(int commandId, bool loadOnly) {
+	_pendingCommand = commandId;
+	_pendingLoadOnly = loadOnly;
+}
+
+void DragonspherePSound::serviceCallbacks() {
+	// The native timer continues counting whenever a period is installed,
+	// even while no callback is pending. A later deferred command therefore
+	// joins the current phase instead of starting a fresh delay.
+	if (!_callbackPeriod)
+		return;
+	if (--_callbackCounter)
+		return;
+	_callbackCounter = _callbackPeriod;
+	if (_pendingCommand < 0)
+		return;
+
+	const int commandId = _pendingCommand;
+	const bool loadOnly = _pendingLoadOnly;
+	_pendingCommand = -1;
+	_pendingLoadOnly = false;
+	// Native code clears the stored near pointer before invoking it. The
+	// callback may explicitly install another one while it runs.
+	executeCommand(commandId, loadOnly);
+}
+
+int DragonspherePSound::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	_commandParam = param;
+	_frameCounter = 0;
+	// The exported native dispatcher records every accepted command in the
+	// 0x20 music bucket before calling its section handler. Deferred loader
+	// tails bypass the dispatcher and therefore leave this state untouched.
+	if (commandId >= 0x20 && commandId <= _maxMusicCommand)
+		_musicIndex = commandId;
+	return executeCommand(commandId, false);
+}
+
+// The native overlays dispatch five sparse command buckets. Keeping the
+// recovered handler offsets here makes unsupported slots and shared tails
+// reviewable without pretending that two equal offsets are two functions.
+const uint16 PSound1::_commandList[102] = {
+	0x253c,0x29c3,0x288c,0x29ca,0x28b4,0x29f2,0x28ca,0x2936,0x2a9b,
+	0,0,0,0,0,0,0, 0x21e4,0x311d,0x30fe,0,0,0,0,0,
+	0x22da,0x22e8,0x22f6,0x22fd,0x230b,0x2304,0x23cc,0x23de,
+	0x2234,0x2292,0x2410,0x2ffc,0x2d70,0x2dc4,0x2e12,0x2ec2,
+	0x2f08,0x2e6e,0x3165,0x320a,0x30a2,0x2fb0,0x31ba,0x2f64,0x320f,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0x2312,0x2318,0x231e,0x2324,0x232a,0x2336,0x233c,0x2342,
+	0x2348,0x234e,0x2354,0x235a,0x2360,0x2366,0x236c,0x2372,
+	0x2378,0x237e,0x2384,0x238a,0x2390,0x2396,0x239c,0x23a2,
+	0x23a8,0x23ba,0x23ae,0x23b4,0x2cd8,0x23c0,0x23c6,0x23d2,
+	0x23e5,0x23ec,0x230b,0x23fa,0x2401,0x2408
+};
+
+PSound1::PSound1(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound1Data, 48, true) {
+}
+
+bool PSound1::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	switch (targetOffset) {
+	case 0x29c3:
+		command1();
+		return true;
+	case 0x2f98:
+		// The stream installs command 16's entry point, not its loader tail.
+		_callbackCounter = 0xc0;
+		_callbackPeriod = 0x60;
+		deferCommand(16, false);
+		return true;
+	case 0x315c:
+		// The native callback re-enters command 41 and therefore repeats its
+		// active-sequence guard before deciding whether to defer again.
+		deferCommand(41, false);
+		return true;
+	case 0x2f08:
+		executeCommand(40, false);
+		return true;
+	default:
+		return false;
+	}
+}
+
+int PSound1::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	static const uint16 effectCommands24[] = {
+		0x06e0, 0x06ec, 0x06fa, 0x0706, 0x0714, 0x071c,
+		0x09d0, 0x0728, 0x08e8, 0x090e
+	};
+	static const byte effectCommandStarts24[] = { 0, 2, 4, 5, 6, 7, 8, 9, 10 };
+	static const uint16 effectCommands64[][2] = {
+		{ 0x0750, 0 }, { 0x0760, 0 }, { 0x0768, 0 }, { 0x0770, 0 },
+		{ 0x0780, 0 }, { 0x0778, 0 }, { 0x078c, 0 }, { 0x079e, 0 },
+		{ 0x07aa, 0 }, { 0x0796, 0 }, { 0x07ba, 0 }, { 0x07c2, 0 },
+		{ 0x07d2, 0 }, { 0x07da, 0 }, { 0x07ee, 0 }, { 0x07e2, 0 },
+		{ 0x07f6, 0 }, { 0x07fe, 0 }, { 0x0816, 0 }, { 0x0828, 0 },
+		{ 0x0836, 0 }, { 0x083e, 0 }, { 0x0846, 0 }, { 0x0778, 0 },
+		{ 0x085c, 0 }, { 0x0864, 0 }, { 0x0870, 0 }, { 0x0898, 0 },
+		{ 0, 0 }, { 0x08cc, 0 }, { 0x08d8, 0 }, { 0x08fa, 0x08f8 },
+		{ 0x0969, 0 }, { 0x09b6, 0x099a }, { 0x09d0, 0 }, { 0x09d8, 0 },
+		{ 0x09e4, 0 }, { 0x09f0, 0 }
+	};
+
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		const uint index = commandId - 64;
+		const uint16 first = effectCommands64[index][0];
+		if (commandId == 68 && isSoundActive(first))
+			return 0;
+		if (first)
+			playSound(first);
+		if (effectCommands64[index][1])
+			playSound(effectCommands64[index][1]);
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x012c }, { 1, 0x01d6 }, { 2, 0x027a },
+			{ 3, 0x0355 }, { 4, 0x03ab }, { 5, 0x03b9 }
+		};
+		_musicIndex = 0x10;
+		if (loadOnly || !isSoundActive(0x012c)) {
+			const MusicLoad load = {
+				kStopAll, -1, 0x90, 0x90, true,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3f12 }, { 1, 0x3f46 },
+			{ 2, 0x3f7b }, { 3, 0x3f9a }
+		};
+		if (loadOnly || !isSoundActive(0x3f12)) {
+			const MusicLoad load = {
+				kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		if (_musicIndex >= 0 && _musicIndex != 18)
+			return executeCommand(_musicIndex, false);
+		break;
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x03d5 }, { 6, 0x041b }, { 2, 0x0494 },
+			{ 3, 0x048b }, { 4, 0x04bd }, { 5, 0x03c8 }
+		};
+		const MusicLoad load = {
+			kStopAll, 0x20, 0xb0, 0xb0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x03d5, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 6, 0x04e8 }, { 1, 0x05cc }, { 2, 0x0631 },
+			{ 3, 0x0665 }, { 4, 0x065e }, { 5, 0x05bf },
+			{ 0, 0x056d }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0xb0, 0xb0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		applyMusicLoad(load);
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0a06 }, { 1, 0x0a7b }, { 2, 0x0aee },
+			{ 3, 0x0b81 }, { 4, 0x0bf6 }, { 5, 0x0a47 }
+		};
+		if (loadOnly || !isSoundActive(0x0a06)) {
+			const MusicLoad load = {
+				kStopAll, -1, -1, -1, false,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3a1a }, { 1, 0x3ab1 }, { 2, 0x3b3a },
+			{ 3, 0x3b90 }, { 4, 0x3b87 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x3a1a, load, loadOnly);
+		break;
+	}
+	case 36: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0c70 }, { 1, 0x0d43 }, { 2, 0x0e3a },
+			{ 3, 0x0ecf }, { 4, 0x0d36 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x80, 0x80, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x0c70, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0f46 }, { 1, 0x0fb5 },
+			{ 2, 0x0fa8 }, { 3, 0x0f9d }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x0f46, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1e1c }, { 1, 0x1efb }, { 2, 0x1ff3 },
+			{ 3, 0x2100 }, { 4, 0x1e2a }, { 5, 0x1ffc }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x1efb, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x245e }, { 1, 0x24b2 }, { 2, 0x2513 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0xb0, 0xb0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x245e, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x25b0 }, { 1, 0x2790 }, { 2, 0x28e1 },
+			{ 3, 0x2b7a }, { 4, 0x2d75 }, { 5, 0x28da }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0xa8, 0xa8, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x25b0, load, loadOnly);
+		break;
+	}
+	case 41: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x21f4 }, { 1, 0x2268 }, { 2, 0x22df },
+			{ 3, 0x2336 }, { 4, 0x240f }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x90, 0x90, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x21f4, load, loadOnly);
+		break;
+	}
+	case 42: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3fd5 }, { 1, 0x4007 }, { 2, 0x403c },
+			{ 3, 0x407d }, { 4, 0x40b8 }, { 5, 0x3fce }
+		};
+		const MusicLoad load = {
+			kStopAll, 0x29, 0x90, 0x90, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 43:
+	case 48: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x418e }, { 1, 0x41da }, { 2, 0x421f },
+			{ 3, 0x4244 }, { 4, 0x4265 }
+		};
+		if (!loadOnly)
+			writeDataByte(0x41da + 5, commandId == 43 ? 0x0b : 0x18);
+		const MusicLoad load = {
+			kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 44: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3cb4 }, { 1, 0x3d06 }, { 2, 0x3d45 },
+			{ 3, 0x3d6d }, { 4, 0x3dd3 }, { 5, 0x3e9d }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0xe0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x3cb4, load, loadOnly);
+		break;
+	}
+	case 45: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x34f4 }, { 1, 0x3624 }, { 2, 0x36d2 },
+			{ 3, 0x37b3 }, { 4, 0x3873 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 46: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x40d4 }, { 1, 0x414c }, { 2, 0x4155 },
+			{ 3, 0x40e6 }, { 4, 0x40f3 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x90, 0x90, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x40d4, load, loadOnly);
+		break;
+	}
+	case 47: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3010 }, { 1, 0x3084 }, { 2, 0x30fe },
+			{ 3, 0x3132 }, { 4, 0x33f3 }, { 5, 0x345d },
+			{ 6, 0x34bb }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, -1, -1, false,
+			channels, ARRAYSIZE(channels)
+		};
+		applyMusicLoad(load);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound2::_commandList[73] = {
+	// 0..8: common driver commands
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x05f2,0x04ca,0x0536,0x069b,
+	// 9..15: unsupported
+	0,0,0,0,0,0,0,
+	// 16..18: section music controls
+	0x2b62,0x2cdb,0x2cbc,
+	// 19..23: unsupported
+	0,0,0,0,0,
+	// 24..35: effects and long-form music
+	0x2ae0,0x2aee,0x2afc,0x2b03,0x2b5a,0x2b0a,0x2b1b,0x2b3e,
+	0x2a44,0x2a92,0x2b94,0x2c64,
+	// 36..63: unsupported
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,0,0,0,0,0,
+	// 64..72: effects
+	0x2b11,0x2c4e,0x2b22,0x2b29,0x2b30,0x2b37,0x2b37,0x2b45,0x2b53
+};
+
+PSound2::PSound2(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound2Data, 35, false) {
+}
+
+bool PSound2::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)targetOffset;
+	(void)channel;
+	return false;
+}
+
+int PSound2::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	static const uint16 effectCommands24[] = {
+		0x0828, 0x0834, 0x0842, 0x084e, 0x0864,
+		0x086c, 0x085c, 0x0878, 0x08b2, 0x0904
+	};
+	static const byte effectCommandStarts24[] = {
+		0, 2, 4, 5, 6, 7, 8, 9, 10
+	};
+
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		switch (commandId) {
+		case 64:
+			// Native code calls the ordinary allocator twice without changing CX.
+			playSound(0x08a0);
+			playSound(0x08a0);
+			break;
+		case 65: {
+			static const uint16 sequences[] = { 0x1566, 0x157b, 0x1589 };
+			playSounds(sequences, ARRAYSIZE(sequences), true);
+			break;
+		}
+		case 66: playSound(0x08c2); break;
+		case 67: playSound(0x08ca); break;
+		case 68: playSound(0x08d2); break;
+		case 69:
+		case 70: playSound(0x08dc); break;
+		case 71: {
+			static const uint16 sequences[] = { 0x095f, 0x099e };
+			playSounds(sequences, ARRAYSIZE(sequences));
+			break;
+		}
+		case 72: playSound(0x09b7); break;
+		default: break;
+		}
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0a66 }, { 1, 0x0ab9 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1a82 }, { 1, 0x1ab6 },
+			{ 2, 0x1aeb }, { 3, 0x1b0a }
+		};
+		if (loadOnly || !isSoundActive(0x1a82)) {
+			const MusicLoad load = {
+				kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		if (_musicIndex >= 0 && _musicIndex != 18)
+			return executeCommand(_musicIndex, false);
+		break;
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x016a }, { 1, 0x01cf }, { 2, 0x0271 },
+			{ 3, 0x032f }, { 4, 0x03bb }, { 5, 0x040b }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x043e }, { 1, 0x04e5 }, { 2, 0x0585 },
+			{ 3, 0x0627 }, { 4, 0x06f7 }, { 5, 0x077d }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x09ca }, { 1, 0x0a68 }, { 2, 0x0ab9 },
+			{ 3, 0x0b10 }, { 4, 0x0b65 }, { 5, 0x0bbe }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x15aa }, { 1, 0x16df }, { 2, 0x1775 },
+			{ 3, 0x184b }, { 4, 0x191f }, { 5, 0x19cb }
+		};
+		if (!loadOnly && musicChannelsActive()) {
+			if (!isSoundActive(0x15aa))
+				deferCommand(commandId, true);
+			break;
+		}
+		const MusicLoad load = {
+			kStopAll, -1, 0xc0, 0x50, true,
+			channels, ARRAYSIZE(channels)
+		};
+		applyMusicLoad(load);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound3::_commandList[74] = {
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x05f2,0x04ca,0x0536,0x069b,
+	0,0,0,0,0,0,0,
+	0x2c0e,0x2bcf,0x2bb0,
+	0,0,0,0,0,
+	0x2a62,0x2a70,0x2a7e,0x2a85,0x2b30,0x2a8c,0x2a93,0x2ac2,
+	0x2c6c,0x2ca2,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0x2a9a,0x2aa1,0x2aa8,0x2ac9,0x2ad0,0x2ad7,0x2ade,0x2ae5,
+	0x2b13,0x2b29
+};
+
+PSound3::PSound3(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound3Data, 33, true) {
+}
+
+bool PSound3::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2aec)
+		return false;
+
+	Channel *source = &_channels[0];
+	if (_channels[0].innerLoopCount) {
+		source = &_channels[2];
+		if (_channels[2].innerLoopCount)
+			source = &_channels[3];
+	}
+	byte note = source->note;
+	while (note < 0x45)
+		note += 12;
+	writeDataByte(0x02b0, note);
+	return true;
+}
+
+int PSound3::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	static const uint16 effectCommands24[] = {
+		0x016c, 0x0178, 0x0186, 0x0192, 0x01a8,
+		0x01b0, 0x01a0, 0x01bc, 0x01e4, 0x021c
+	};
+	static const byte effectCommandStarts24[] = {
+		0, 2, 4, 5, 6, 7, 8, 9, 10
+	};
+
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		switch (commandId) {
+		case 64: playSound(0x01f4); break;
+		case 65: playSound(0x0208); break;
+		case 66: {
+			Channel *active = findActiveSound(0x01f4);
+			if (active) {
+				active->innerLoopCount = 1;
+				active->outerLoopCount = 1;
+			}
+			playSound(0x0212);
+			break;
+		}
+		case 67: playSound(0x0277); break;
+		case 68: playSound(0x027f); break;
+		case 69: playSound(0x028b); break;
+		case 70: playSound(0x0293); break;
+		case 71: playSound(0x02a9); break;
+		case 72: {
+			static const uint16 sequences[] = { 0x02b4, 0x02d3, 0x02f2 };
+			playSounds(sequences, ARRAYSIZE(sequences));
+			break;
+		}
+		case 73: playSound(0x030f); break;
+		default: break;
+		}
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0ce4 }, { 1, 0x0db2 }, { 2, 0x0e1f },
+			{ 3, 0x0fa0 }, { 4, 0x1015 }, { 5, 0x10ef }
+		};
+		const MusicLoad load = {
+			kStopAll, 0x10, 0x90, 0x90, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusic(commandId, 0x0ce4, load, loadOnly);
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0c28 }, { 1, 0x0c5c },
+			{ 2, 0x0c91 }, { 3, 0x0cb0 }
+		};
+		if (loadOnly || !isSoundActive(0x0c28)) {
+			const MusicLoad load = {
+				kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		if (_musicIndex >= 0 && _musicIndex != 18)
+			return executeCommand(_musicIndex, false);
+		break;
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x121c }, { 1, 0x12d8 }, { 2, 0x1405 },
+			{ 3, 0x14b8 }, { 4, 0x14df }, { 5, 0x15c8 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, -1, -1, true,
+			channels, ARRAYSIZE(channels)
+		};
+		applyMusicLoad(load);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1644 }, { 1, 0x1724 }, { 2, 0x1790 },
+			{ 3, 0x17b1 }, { 4, 0x1894 }, { 5, 0x18c4 }
+		};
+		const MusicLoad load = {
+			kStopAll, -1, -1, -1, true,
+			channels, ARRAYSIZE(channels)
+		};
+		applyMusicLoad(load);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound4::_commandList[79] = {
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x2ba8,0x04ca,0x0536,0x069b,
+	0,0,0,0,0,0,0,
+	0x2da2,0x2ec7,0x2ea8,
+	0,0,0,0,0,
+	0x2a62,0x2a70,0x2a7e,0x2a85,0x2b1e,0x2a8c,0x2ad1,0x2b02,
+	0x2baa,0x2c02,0x2ba8,0x2c5a,0x2df8,0x2caa,0x2cfa,0x2e50,0x2d52,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,
+	0x2a99,0x2aa0,0x2aa7,0x2aae,0x2ab5,0x2abc,0x2ac3,0x2aca,
+	0x2ad8,0x2ae6,0x2aed,0x2af4,0x2afb,0x2b09,0x2b10
+};
+
+PSound4::PSound4(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound4Data, 40, false) {
+}
+
+bool PSound4::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)targetOffset;
+	(void)channel;
+	return false;
+}
+
+int PSound4::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8) {
+		// This section replaces the normal command 5 handler with a RET.
+		return commandId == 5 ? 0 : dispatchBaseCommand(commandId);
+	}
+
+	static const uint16 effectCommands24[] = {
+		0x0d2c, 0x0d38, 0x0d46, 0x0d52, 0x0d68,
+		0x0d70, 0x0d60, 0x0d7c, 0x0da4, 0x0eb2
+	};
+	static const byte effectCommandStarts24[] = {
+		0, 2, 4, 5, 6, 7, 8, 9, 10
+	};
+
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		if (commandId != 29 || !isSoundActive(effectCommands24[first]))
+			playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		static const uint16 effects[][2] = {
+			{ 0x0dd1, 0 }, { 0x0ded, 0 }, { 0x0df7, 0 }, { 0x0e03, 0 },
+			{ 0x0db4, 0 }, { 0x0e0b, 0 }, { 0x0e2b, 0 }, { 0x0e58, 0 },
+			{ 0x0e60, 0x0e72 }, { 0x0e72, 0 }, { 0x0e7a, 0 },
+			{ 0x0e82, 0 }, { 0x0e8a, 0 }, { 0x0f0d, 0 },
+			{ 0x0f15, 0x0f23 }
+		};
+		const uint index = commandId - 64;
+		playSound(effects[index][0]);
+		if (effects[index][1])
+			playSound(effects[index][1]);
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1c7f }, { 1, 0x1cd1 }, { 2, 0x1d0c },
+			{ 3, 0x1d57 }, { 4, 0x1c78 }
+		};
+		const MusicLoad load = {
+			kStopMusic, 0x10, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels)
+		};
+		startOrDeferMusicWhenActive(commandId, 0x1c7f, load, loadOnly);
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2778 }, { 1, 0x27ac }, { 2, 0x27e1 }, { 3, 0x2800 }
+		};
+		if (loadOnly || !isSoundActive(0x2778)) {
+			const MusicLoad load = {
+				kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels)
+			};
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		return executeCommand(_musicIndex <= 18 ? 16 : _musicIndex, false);
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0f42 }, { 1, 0x1010 }, { 2, 0x1208 },
+			{ 3, 0x12b4 }, { 4, 0x1302 }, { 5, 0x137e }
+		};
+		const MusicLoad load = { kStopMusic, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x0f42, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x140b }, { 1, 0x1435 }, { 2, 0x1464 },
+			{ 3, 0x14a5 }, { 4, 0x158c }, { 5, 0x1404 }
+		};
+		const MusicLoad load = { kStopMusic, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x140b, load, loadOnly);
+		break;
+	}
+	case 34:
+		break;
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1646 }, { 1, 0x162a }, { 2, 0x1639 },
+			{ 3, 0x16cc }, { 4, 0x16de }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x1646, load, loadOnly);
+		break;
+	}
+	case 36: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1e39 }, { 1, 0x20a3 }, { 2, 0x20dd },
+			{ 3, 0x20fe }, { 4, 0x1e2c }, { 5, 0x20d6 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x1e39, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1786 }, { 1, 0x17c8 }, { 2, 0x17f7 },
+			{ 3, 0x1830 }, { 4, 0x1871 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x40, 0x40, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x1786, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x18a6 }, { 1, 0x18ec }, { 2, 0x1923 },
+			{ 3, 0x1958 }, { 4, 0x199b }, { 5, 0x1ace }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x40, 0x40, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x18a6, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x22d2 }, { 1, 0x234c }, { 2, 0x23bf },
+			{ 3, 0x2484 }, { 4, 0x2543 }, { 5, 0x2604 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x48, 0x48, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x22d2, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1c6f }, { 1, 0x1ccc }, { 2, 0x1d07 },
+			{ 3, 0x1d57 }, { 4, 0x1c66 }
+		};
+		const MusicLoad load = { kStopMusic, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x1c66, load, loadOnly);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound5::_commandList[79] = {
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x05f2,0x04ca,0x0536,0x069b,
+	0,0,0,0,0,0,0,
+	0x2aee,0x2df9,0x2dda,
+	0,0,0,0,0,
+	0x2ca0,0x2cae,0x2cbc,0x2cc3,0x2d7a,0x2cca,0x2cd1,0x2d73,
+	0x2d82,0x2b98,0x2bf0,0x2c48,0x2c99,0x2e9a,0x2e38,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0,0,0,0,0,0,0,0,
+	0x2cd8,0x2cdf,0x2ce6,0x2ced,0x2cf4,0x2cfb,0x2d02,0x2d09,
+	0x2d10,0x2d1e,0x2d25,0x2d33,0x2d3a,0x2d41,0x2d6c
+};
+
+PSound5::PSound5(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound5Data, 38, true) {
+}
+
+void PSound5::loadAlternateMusic() {
+	static const ChannelLoad channels[] = {
+		{ 0, 0x0c87 }, { 1, 0x0dfe }, { 2, 0x0e98 },
+		{ 3, 0x1038 }, { 4, 0x1226 }, { 5, 0x1438 }
+	};
+	const MusicLoad load = {
+		kStopAll, 0x10, 0xc0, 0xc0, true,
+		channels, ARRAYSIZE(channels)
+	};
+	applyMusicLoad(load);
+}
+
+bool PSound5::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2b40)
+		return false;
+	if (musicChannelsActive())
+		deferCommand(kPSound5AlternateMusic, true);
+	else
+		loadAlternateMusic();
+	return true;
+}
+
+int PSound5::executeCommand(int commandId, bool loadOnly) {
+	if (commandId == kPSound5AlternateMusic) {
+		loadAlternateMusic();
+		return 0;
+	}
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	static const uint16 effectCommands24[] = {
+		0x1fce, 0x1fda, 0x1fe8, 0x1ff4, 0x200a,
+		0x2012, 0x2002, 0x201e, 0x2046, 0x212d
+	};
+	static const byte effectCommandStarts24[] = {
+		0, 2, 4, 5, 6, 7, 8, 9, 10
+	};
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		static const uint16 effects[][2] = {
+			{ 0x2069, 0 }, { 0x2073, 0 }, { 0x207b, 0 }, { 0x2089, 0 },
+			{ 0x20d7, 0 }, { 0x20af, 0 }, { 0x20cf, 0 }, { 0x20d7, 0 },
+			{ 0x20e1, 0x20ef }, { 0x20fd, 0 }, { 0x2105, 0x210f },
+			{ 0x2119, 0 }, { 0x2121, 0 }, { 0, 0 }, { 0x212d, 0 }
+		};
+		if (commandId == 77) {
+			Channel &channel = _channels[8];
+			if (!_commandParam) {
+				if (channel.loopStart == 0x2056)
+					channel.innerLoopStart = _nullSequenceOffset;
+			} else {
+				writeDataByte(0x2061, (byte(_commandParam) >> 1) + 0x40);
+				if (!isSoundActive(0x2056))
+					loadChannel(8, 0x2056);
+			}
+			return 0;
+		}
+		const uint index = commandId - 64;
+		playSound(effects[index][0]);
+		if (effects[index][1])
+			playSound(effects[index][1]);
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0c7a }, { 1, 0x0dfa }, { 2, 0x0e84 },
+			{ 3, 0x1024 }, { 4, 0x1218 }, { 5, 0x1434 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x23b4 }, { 1, 0x23e8 }, { 2, 0x241d }, { 3, 0x243c }
+		};
+		if (loadOnly || !isSoundActive(0x23b4)) {
+			const MusicLoad load = { kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels) };
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		return executeCommand(_musicIndex <= 18 ? 16 : _musicIndex, false);
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2188 }, { 1, 0x21d2 }, { 2, 0x2201 },
+			{ 3, 0x222b }, { 4, 0x22b4 }, { 5, 0x2347 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0x48, 0x48, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x14ea }, { 1, 0x1592 }, { 2, 0x1628 },
+			{ 3, 0x17be }, { 4, 0x1862 }, { 5, 0x1900 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x19a4 }, { 1, 0x1a24 }, { 2, 0x1ad2 },
+			{ 3, 0x1baa }, { 4, 0x1c46 }, { 5, 0x1db8 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1e02 }, { 1, 0x1e21 }, { 2, 0x1e5d },
+			{ 3, 0x1f37 }, { 4, 0x1f65 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 36:
+		loadChannel(3, 0x1f9d);
+		break;
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2700 }, { 1, 0x2745 }, { 2, 0x277b }, { 3, 0x27b9 }
+		};
+		const MusicLoad load = { kStopAll, -1, -1, -1, false,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2470 }, { 1, 0x24be }, { 2, 0x24fc },
+			{ 3, 0x25ba }, { 4, 0x25d2 }, { 5, 0x26e6 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x2470, load, loadOnly);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound6::_commandList[99] = {
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x05f2,0x04ca,0x0536,0x069b,
+	0,0,0,0,0,0,0,
+	0x2cb2,0x300f,0x2ff0,
+	0,0,0,0,0,
+	0x2b26,0x2b34,0x2b42,0x2b49,0x2caa,0x2b50,0x2c88,0x2c9c,
+	0x2d22,0x2d6c,0x2ddf,0x2da4,0x2e30,0x2e8a,0x2ee2,0x2f3a,
+	0x2f98,0x2b25,0x2b25,0x2b25,0x304e,0x30b2,0x2b25,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0x2b58,0x2c8e,0x2b83,0x2c95,0x2b8a,0x2b98,0x2b9f,0x2ba6,
+	0x2bad,0x2bbb,0x2bc2,0x2bc9,0x2bd0,0x2bd7,0x2bde,0x2be5,
+	0x2bec,0x2bfb,0x2c02,0x2c09,0x2c17,0x2c1e,0x2c25,0x2c2c,
+	0x2c33,0x2c3a,0x2c41,0x2c48,0x2c4f,0x2c56,0x2c5d,0x2c64,
+	0x2c6b,0x2c81,0x2ca3
+};
+
+PSound6::PSound6(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound6Data, 46, true) {
+}
+
+bool PSound6::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2e8a)
+		return false;
+	// The C4 target is command 37's entry point, including its guard and
+	// deferral logic, rather than a loader-only callback.
+	executeCommand(37, false);
+	return true;
+}
+
+int PSound6::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	static const uint16 effectCommands24[] = {
+		0x0cbc, 0x0cc8, 0x0cd6, 0x0ce2, 0x0cf8,
+		0x0d00, 0x0cf0, 0x0d0c, 0x0d75, 0x0f5b
+	};
+	static const byte effectCommandStarts24[] = {
+		0, 2, 4, 5, 6, 7, 8, 9, 10
+	};
+	if (commandId >= 24 && commandId <= 31) {
+		const uint first = effectCommandStarts24[commandId - 24];
+		const uint end = effectCommandStarts24[commandId - 23];
+		playSounds(effectCommands24 + first, end - first);
+		return 0;
+	}
+	if (commandId >= 64) {
+		if (commandId == 64) {
+			Channel &channel = _channels[8];
+			if (!_commandParam) {
+				if (channel.loopStart == 0x0d34)
+					channel.innerLoopStart = _nullSequenceOffset;
+			} else {
+				writeDataByte(0x0d3f, (byte(_commandParam) >> 1) + 0x40);
+				if (!isSoundActive(0x0d34))
+					loadChannel(8, 0x0d34);
+			}
+			return 0;
+		}
+		if (commandId == 80) {
+			Channel *active = findActiveSound(0x0e6a);
+			if (active)
+				active->outerLoopCount = 1;
+			return 0;
+		}
+		static const uint16 effects[][3] = {
+			{ 0, 0, 0 }, { 0x0f3b, 0, 0 }, { 0x0d47, 0, 0 },
+			{ 0x0f47, 0, 0 }, { 0x0d51, 0x0d63, 0 }, { 0x0d85, 0, 0 },
+			{ 0x0d97, 0, 0 }, { 0x0db3, 0, 0 }, { 0x0dff, 0x0ddc, 0 },
+			{ 0x0e2a, 0, 0 }, { 0x0e38, 0, 0 }, { 0x0d6b, 0, 0 },
+			{ 0x0e42, 0, 0 }, { 0x0e56, 0, 0 }, { 0x0e60, 0, 0 },
+			{ 0x0e6a, 0, 0 }, { 0, 0, 0 }, { 0x0e7c, 0, 0 },
+			{ 0x0e84, 0, 0 }, { 0x0dbb, 0x0ddc, 0 }, { 0x0e8c, 0, 0 },
+			{ 0x0e94, 0, 0 }, { 0x0ea0, 0, 0 }, { 0x0ea8, 0, 0 },
+			{ 0x0eb8, 0, 0 }, { 0x0ec0, 0, 0 }, { 0x0ec8, 0, 0 },
+			{ 0x0ed4, 0, 0 }, { 0x0edc, 0, 0 }, { 0x0ee6, 0, 0 },
+			{ 0x0eee, 0, 0 }, { 0x0ef6, 0, 0 },
+			{ 0x0f18, 0x0f18, 0x0f18 }, { 0x0f2f, 0, 0 },
+			{ 0x0fb6, 0, 0 }
+		};
+		const uint index = commandId - 64;
+		for (uint i = 0; i < 3 && effects[index][i]; ++i)
+			playSound(effects[index][i]);
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		if (!loadOnly && (isSoundActive(0x0fdc) ||
+				isSoundActive(0x1675) || isSoundActive(0x1850)))
+			break;
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0fdc }, { 1, 0x107e }, { 2, 0x10c4 },
+			{ 3, 0x0fce }, { 4, 0x1072 }, { 5, 0x0fc2 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0xc8, 0xc8, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 17: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2a3e }, { 1, 0x2a72 }, { 2, 0x2aa7 }, { 3, 0x2ac6 }
+		};
+		if (loadOnly || !isSoundActive(0x2a3e)) {
+			const MusicLoad load = { kClearMusicIdentity, -1, 0xc0, 0x60, true,
+				channels, ARRAYSIZE(channels) };
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 18:
+		command2();
+		return executeCommand(_musicIndex <= 18 ? 16 : _musicIndex, false);
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1120 }, { 1, 0x1168 }, { 2, 0x11a9 }, { 3, 0x11f2 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x3c, 0x3c, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x1120, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1278 }, { 1, 0x1307 }, { 2, 0x1357 },
+			{ 3, 0x137b }, { 4, 0x13c9 }, { 5, 0x1382 }
+		};
+		if (loadOnly || !isSoundActive(0x1278)) {
+			const MusicLoad load = { kStopAll, -1, -1, -1, false,
+				channels, ARRAYSIZE(channels) };
+			applyMusicLoad(load);
+		}
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x149f }, { 1, 0x150e }, { 2, 0x154d },
+			{ 3, 0x15da }, { 4, 0x14e9 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x1e, 0x1e, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x149f, load, loadOnly);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x149c }, { 1, 0x14e9 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x1e, 0x1e, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x149c, load, loadOnly);
+		break;
+	}
+	case 36: {
+		if (!loadOnly && (isSoundActive(0x161a) || isSoundActive(0x1850)))
+			break;
+		static const ChannelLoad channels[] = {
+			{ 0, 0x161a }, { 1, 0x1675 }, { 2, 0x1715 },
+			{ 3, 0x178e }, { 4, 0x17b5 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc8, 0xc8, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1850 }, { 1, 0x18a4 }, { 2, 0x18ff },
+			{ 3, 0x1a06 }, { 4, 0x1b09 }, { 5, 0x1906 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc8, 0xc8, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x1850, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1b50 }, { 1, 0x1ba4 }, { 2, 0x1bf6 },
+			{ 3, 0x1c80 }, { 4, 0x1d37 }, { 5, 0x1f20 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc8, 0xc8, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x1b50, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1f70 }, { 1, 0x20d1 }, { 2, 0x22dd },
+			{ 3, 0x2369 }, { 4, 0x23ab }, { 5, 0x2370 }, { 8, 0x24d3 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x64, 0x64, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x1f70, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2500 }, { 1, 0x25dc }, { 2, 0x26c2 },
+			{ 3, 0x27b4 }, { 4, 0x28da }, { 5, 0x29da }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x64, 0x64, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x2500, load, loadOnly);
+		break;
+	}
+	case 44: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2afa }, { 1, 0x2b5a }, { 2, 0x2b82 },
+			{ 3, 0x2bb2 }, { 4, 0x2be2 }, { 5, 0x2c44 }, { 8, 0x2b08 }
+		};
+		const MusicLoad load = { kStopAll, 0x10, 0x30, 0x30, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x2b0f, load, loadOnly);
+		break;
+	}
+	case 45: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2c7e }, { 1, 0x2c8e }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x1e, 0x1e, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x2c99, load, loadOnly);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSound9::_commandList[64] = {
+	0x013c,0x05c3,0x048c,0x05ca,0x04b4,0x05f2,0x04ca,0x0536,0x06b9,
+	0,0,0,0,0,0,0,
+	0x2af1,0x2fa0,0x2fae,
+	0,0,0,0,0,
+	0x2fa0,0x2fae,0x2fbc,0x2fc3,0x2ff4,0x2fdf,0x2fe6,0x2af1,
+	0x2b82,0x2da4,0x2dfc,0x2e50,0x2ea4,0x2bd0,0x2c1e,0x2ef8,
+	0x2f4c,0x2c6c,0x2d06,0x3030,0x2af1,0x2ff6,0x3013,0x2da4,
+	0x2fca,0x2fd1,0x2fd8,0x3186,0x3146,0x2a3e,0x2dfc,0x2af2,
+	0x2af1,0x3094,0x2b4a,0x3074,0x2af1,0x2fed,0x30f2,0x313f
+};
+
+PSound9::PSound9(Audio::Mixer *mixer) :
+		DragonspherePSound(mixer, kPSound9Data, 63, false) {
+}
+
+bool PSound9::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)targetOffset;
+	(void)channel;
+	return false;
+}
+
+int PSound9::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+
+	if (commandId == 17 || commandId == 24) {
+		static const uint16 sounds[] = { 0x4b7a, 0x4b86 };
+		playSounds(sounds, ARRAYSIZE(sounds));
+		return 0;
+	}
+	if (commandId == 18 || commandId == 25) {
+		static const uint16 sounds[] = { 0x4b94, 0x4ba0 };
+		playSounds(sounds, ARRAYSIZE(sounds));
+		return 0;
+	}
+	switch (commandId) {
+	case 26: playSound(0x4bae); return 0;
+	case 27: playSound(0x4bb6); return 0;
+	case 28: return 0;
+	case 29: playSound(0x4bc2); return 0;
+	case 30: playSound(0x4bea); return 0;
+	case 31: return 0;
+	case 45: {
+		static const uint16 sounds[] = { 0x4c2b, 0x4c9c, 0x4c1e, 0x4c8f };
+		playSounds(sounds, ARRAYSIZE(sounds), true);
+		return 0;
+	}
+	case 46: {
+		static const uint16 sounds[] = { 0x4cf8, 0x4d5e, 0x4ceb, 0x4d51 };
+		playSounds(sounds, ARRAYSIZE(sounds), true);
+		return 0;
+	}
+	case 48: playSound(0x4bfa); return 0;
+	case 49: playSound(0x4c04); return 0;
+	case 50: playSound(0x4c0c); return 0;
+	case 61: playSound(0x4c16); return 0;
+	default: break;
+	}
+
+	switch (commandId) {
+	case 16:
+	case 44:
+	case 56:
+	case 60:
+		break;
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x13e4 }, { 1, 0x1410 }, { 2, 0x14ca },
+			{ 3, 0x1502 }, { 4, 0x1638 }, { 5, 0x172c }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x62, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 33:
+	case 47: {
+		if (!loadOnly) {
+			writeDataByte(0x2ace, 0x17);
+			writeDataByte(0x2ae2, 0x17);
+			writeDataByte(0x2be6, 0x17);
+			writeDataByte(0x2bec, 0x17);
+			writeDataByte(0x2a02, 0x2f);
+			writeDataByte(0x2a18, 0x2f);
+			writeDataByte(0x2a6a, 0x28);
+			writeDataByte(0x2a80, 0x28);
+		}
+		static const ChannelLoad channels[] = {
+			{ 0, 0x29f6 }, { 1, 0x2a5e }, { 2, 0x2ac6 },
+			{ 3, 0x2b82 }, { 4, 0x2be0 }, { 5, 0x2c3a }, { 6, 0x2d9c }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x62, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 34:
+	case 54: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2e78 }, { 1, 0x3072 }, { 2, 0x3279 },
+			{ 3, 0x347c }, { 4, 0x35e5 }, { 5, 0x36ea }, { 6, 0x3741 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x38, 0x38, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x37ca }, { 1, 0x382b }, { 2, 0x3883 },
+			{ 3, 0x3949 }, { 4, 0x39b9 }, { 5, 0x3a15 }, { 6, 0x3ae1 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x50, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 36: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3c48 }, { 1, 0x3cbc }, { 2, 0x3d36 },
+			{ 3, 0x3d6a }, { 4, 0x402b }, { 5, 0x4095 }, { 6, 0x40f3 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x17b6 }, { 1, 0x1838 }, { 2, 0x18be },
+			{ 3, 0x1902 }, { 4, 0x19d4 }, { 5, 0x1a39 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x50, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1b72 }, { 1, 0x1bec }, { 2, 0x1c6c },
+			{ 3, 0x1c9e }, { 4, 0x1ca4 }, { 5, 0x1d06 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x412c }, { 1, 0x41b4 }, { 2, 0x4244 },
+			{ 3, 0x4336 }, { 4, 0x47b8 }, { 5, 0x482e }, { 6, 0x4860 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2e78 }, { 1, 0x3072 }, { 2, 0x3279 },
+			{ 3, 0x495a }, { 4, 0x4a4c }, { 5, 0x4ae8 }, { 6, 0x4b3c }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x38, 0x38, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 41: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1d68 }, { 1, 0x2295 }, { 2, 0x2410 },
+			{ 3, 0x24a5 }, { 4, 0x256a }, { 5, 0x27f3 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 42: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x1dac }, { 6, 0x1df2 }, { 2, 0x230c },
+			{ 3, 0x244e }, { 4, 0x24dc }, { 5, 0x264d }, { 1, 0x2890 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xa8, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 43: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x4db8 }, { 1, 0x4e31 }, { 2, 0x4e99 },
+			{ 3, 0x50a6 }, { 4, 0x50cc }, { 5, 0x50f2 }, { 6, 0x512a }
+		};
+		const MusicLoad load = { kKeepPlayback, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 51: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x600a }, { 1, 0x60a1 }, { 2, 0x612a },
+			{ 3, 0x6180 }, { 4, 0x6177 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 52: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x5b71 }, { 1, 0x5ddb }, { 2, 0x5e15 },
+			{ 3, 0x5e36 }, { 4, 0x5b64 }, { 5, 0x5e0e }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 53:
+		_callbackCounter = 0x4b0;
+		_callbackPeriod = 0x4b0;
+		deferCommand(1, false);
+		break;
+	case 55: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0e7a }, { 1, 0x0f38 }, { 2, 0x112b },
+			{ 3, 0x11cb }, { 4, 0x11e5 }, { 5, 0x1259 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0x0e7a, load, loadOnly);
+		break;
+	}
+	case 57: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x51f4 }, { 3, 0x526d }, { 2, 0x52b0 },
+			{ 1, 0x5324 }, { 4, 0x536c }, { 5, 0x53f2 }, { 8, 0x5206 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x30, 0x30, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusic(commandId, 0x526d, load, loadOnly);
+		break;
+	}
+	case 58: {
+		if (!loadOnly && isSoundActive(0x12d8))
+			break;
+		static const ChannelLoad channels[] = {
+			{ 0, 0x12d8 }, { 1, 0x1309 }, { 2, 0x1356 },
+			{ 3, 0x13a9 }, { 4, 0x13c8 }, { 5, 0x13d6 }
+		};
+		resetSection();
+		loadChannels(channels, ARRAYSIZE(channels));
+		break;
+	}
+	case 59: {
+		static const ChannelLoad channels[] = {
+			{ 1, 0x5160 }, { 2, 0x518a }, { 3, 0x51a9 }, { 4, 0x51d5 }
+		};
+		resetSection();
+		loadChannels(channels, ARRAYSIZE(channels));
+		break;
+	}
+	case 62: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x5459 }, { 1, 0x5653 }, { 2, 0x5858 },
+			{ 3, 0x544e }, { 4, 0x5a59 }, { 5, 0x5ab0 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x38, 0x38, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 63:
+		loadChannel(6, 0x5b39);
+		break;
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSoundDemo1::_commandList[89] = {
+	0x23f6,0x287d,0x2746,0x2884,0x276e,0x28ac,0x2784,0x27f0,0x2955,
+	0,0,0,0,0,0,0,
+	0x21ea,
+	0,0,0,0,0,0,0,
+	0x22a0,0x22ae,0x22bc,0x22c3,0x22ca,0x22cb,
+	0,0,
+	0x2218,0x226a,0x22d4,0x2e20,0x2bfe,0x2c4a,0x2c8e,0x2d2c,
+	0x2d6a,0x2ce0,0x2b8e,0x2e6c,0x2ebe,0x2dd4,
+	0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
+	0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,
+	0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,
+	0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,0x22d3,
+	0x22d3
+};
+
+PSoundDemo1::PSoundDemo1(Audio::Mixer *mixer) :
+		DragonspherePSoundDemo(mixer, kPSoundDemo1Data, true) {
+}
+
+bool PSoundDemo1::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)channel;
+	if (targetOffset != 0x2dbc)
+		return false;
+	_callbackCounter = 0xc0;
+	_callbackPeriod = 0x60;
+	deferCommand(16, false);
+	return true;
+}
+
+int PSoundDemo1::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+	if (commandId >= 64)
+		return 0;
+	if (commandId >= 24 && commandId <= 29) {
+		static const uint16 effects[][2] = {
+			{ 0x06dc, 0x06e8 }, { 0x06f6, 0x0702 }, { 0x0710, 0 },
+			{ 0x0718, 0 }, { 0, 0 }, { 0x0724, 0 }
+		};
+		const uint index = commandId - 24;
+		if (effects[index][0])
+			playSound(effects[index][0]);
+		if (effects[index][1])
+			playSound(effects[index][1]);
+		return 0;
+	}
+
+	switch (commandId) {
+	case 16: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x012c }, { 1, 0x01d5 }, { 2, 0x0278 },
+			{ 3, 0x0352 }, { 4, 0x03a7 }, { 5, 0x03b4 }
+		};
+		const MusicLoad load = { kStopAll, -1, -1, -1, false,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x03d1 }, { 6, 0x0417 }, { 2, 0x0490 },
+			{ 3, 0x0487 }, { 4, 0x04b9 }, { 5, 0x03c4 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xb0, 0xb0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 33: {
+		static const ChannelLoad channels[] = {
+			{ 6, 0x04e4 }, { 1, 0x05c8 }, { 2, 0x062d },
+			{ 3, 0x0661 }, { 4, 0x065a }, { 5, 0x05bb }, { 0, 0x0569 }
+		};
+		const MusicLoad load = { kStopAll, -1, -1, -1, false,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x074c }, { 1, 0x07c1 }, { 2, 0x0834 },
+			{ 3, 0x08c7 }, { 4, 0x093c }, { 5, 0x078d }
+		};
+		const MusicLoad load = { kStopAll, -1, -1, -1, false,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x443c }, { 1, 0x44d3 }, { 2, 0x455c },
+			{ 3, 0x45b2 }, { 4, 0x45a9 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 36: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x09b4 }, { 1, 0x0a87 }, { 2, 0x0b7e },
+			{ 3, 0x0c13 }, { 4, 0x0a7a }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x80, 0x80, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0c8a }, { 1, 0x0cf9 }, { 2, 0x0cec }, { 3, 0x0ce1 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2d62 }, { 1, 0x2e24 }, { 2, 0x2f1a },
+			{ 3, 0x3013 }, { 4, 0x2d70 }, { 5, 0x2f23 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x336e }, { 1, 0x33c2 }, { 2, 0x3423 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xb0, 0xb0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x34c0 }, { 1, 0x36a0 }, { 2, 0x37f1 },
+			{ 3, 0x3a88 }, { 4, 0x3c81 }, { 5, 0x37ea }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xa8, 0xa8, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 41: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3104 }, { 1, 0x3178 }, { 2, 0x31ef },
+			{ 3, 0x3246 }, { 4, 0x331f }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x90, 0x90, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 42:
+		break;
+	case 43: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x45de }, { 1, 0x4665 }, { 2, 0x467e },
+			{ 3, 0x4699 }, { 4, 0x46b6 }, { 5, 0x4624 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x50, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 44: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x46d6 }, { 1, 0x4728 }, { 2, 0x4767 },
+			{ 3, 0x478f }, { 4, 0x47f5 }, { 5, 0x48bf }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0xe0, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 45: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3f1a }, { 1, 0x4048 }, { 2, 0x40f6 },
+			{ 3, 0x41d7 }, { 4, 0x4297 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+const uint16 PSoundDemo9::_commandList[51] = {
+	0x0266,0x06ed,0x05b6,0x06f4,0x05de,0x071c,0x05f4,0x0660,0x07e3,
+	0,0,0,0,0,0,0,
+	0x0a46,
+	0,0,0,0,0,0,0,
+	0x0132,0x0140,0x014e,0x0155,0x0171,0x0171,
+	0,0,
+	0x0048,0x2cce,0x2d22,0x2d76,0x2dca,0x0096,0x00e4,0x2e1e,
+	0x2e72,0x2be0,0x2c7a,0x2f2c,0x2f00,0x2ec6,0x2ee3,0x2cce,
+	0x015c,0x0163,0x016a
+};
+
+PSoundDemo9::PSoundDemo9(Audio::Mixer *mixer) :
+		DragonspherePSoundDemo(mixer, kPSoundDemo9Data, false) {
+}
+
+bool PSoundDemo9::callFunction(uint16 targetOffset, Channel &channel) {
+	(void)targetOffset;
+	(void)channel;
+	return false;
+}
+
+int PSoundDemo9::executeCommand(int commandId, bool loadOnly) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList) ||
+			!_commandList[commandId])
+		return 0;
+	if (commandId <= 8)
+		return dispatchBaseCommand(commandId);
+	if (commandId >= 24 && commandId <= 29) {
+		static const uint16 effects[][2] = {
+			{ 0x09d6, 0x09e2 }, { 0x09f0, 0x09fc }, { 0x0a0a, 0 },
+			{ 0x0a12, 0 }, { 0, 0 }, { 0, 0 }
+		};
+		const uint index = commandId - 24;
+		if (effects[index][0])
+			playSound(effects[index][0]);
+		if (effects[index][1])
+			playSound(effects[index][1]);
+		return 0;
+	}
+	switch (commandId) {
+	case 45: {
+		static const uint16 sounds[] = { 0x5981, 0x59f2, 0x5974, 0x59e5 };
+		playSounds(sounds, ARRAYSIZE(sounds), true);
+		return 0;
+	}
+	case 46: {
+		static const uint16 sounds[] = { 0x5a4e, 0x5ab4, 0x5a41, 0x5aa7 };
+		playSounds(sounds, ARRAYSIZE(sounds), true);
+		return 0;
+	}
+	case 48: playSound(0x0a20); return 0;
+	case 49: playSound(0x0a2a); return 0;
+	case 50: playSound(0x0a32); return 0;
+	default: break;
+	}
+
+	switch (commandId) {
+	case 16:
+		break;
+	case 32: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0050 }, { 1, 0x007c }, { 2, 0x0136 },
+			{ 3, 0x016e }, { 4, 0x02a4 }, { 5, 0x0398 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x62, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 33:
+	case 47: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x37f2 }, { 1, 0x385a }, { 2, 0x38c2 },
+			{ 3, 0x397e }, { 4, 0x39dc }, { 5, 0x3a36 }, { 6, 0x3b98 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x62, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 34: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3c74 }, { 1, 0x3e6e }, { 2, 0x4075 },
+			{ 3, 0x4278 }, { 4, 0x43e1 }, { 5, 0x44e6 }, { 6, 0x453d }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x38, 0x38, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 35: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x45c6 }, { 1, 0x4627 }, { 2, 0x467f },
+			{ 3, 0x4745 }, { 4, 0x47b5 }, { 5, 0x4811 }, { 6, 0x48dd }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x50, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 36: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x4a44 }, { 1, 0x4ab8 }, { 2, 0x4b32 },
+			{ 3, 0x4b66 }, { 4, 0x4e27 }, { 5, 0x4e91 }, { 6, 0x4eef }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 37: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x0422 }, { 1, 0x04a4 }, { 2, 0x052a },
+			{ 3, 0x056e }, { 4, 0x0640 }, { 5, 0x06a5 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x50, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 38: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x07de }, { 1, 0x0858 }, { 2, 0x08d8 },
+			{ 3, 0x090a }, { 4, 0x0910 }, { 5, 0x0972 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 39: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x4f28 }, { 1, 0x4fb0 }, { 2, 0x5040 },
+			{ 3, 0x5132 }, { 4, 0x55b4 }, { 5, 0x562a }, { 6, 0x565c }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x28, 0x28, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 40: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x3c74 }, { 1, 0x3e6e }, { 2, 0x4075 },
+			{ 3, 0x5756 }, { 4, 0x5848 }, { 5, 0x58e4 }, { 6, 0x5938 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x38, 0x38, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 41: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2b64 }, { 1, 0x3091 }, { 2, 0x320c },
+			{ 3, 0x32a1 }, { 4, 0x3366 }, { 5, 0x35ef }
+		};
+		const MusicLoad load = { kStopAll, -1, 0x54, 0x54, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 42: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x2ba8 }, { 6, 0x2bee }, { 2, 0x3108 },
+			{ 3, 0x324a }, { 4, 0x32d8 }, { 5, 0x3449 }, { 1, 0x368c }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xa8, 0x50, true,
+			channels, ARRAYSIZE(channels) };
+		startOrDeferMusicWhenActive(commandId, 0, load, loadOnly);
+		break;
+	}
+	case 43: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x5bce }, { 1, 0x5c36 }, { 2, 0x5c98 },
+			{ 3, 0x5e9d }, { 4, 0x5ebb }, { 5, 0x5edb }, { 6, 0x5f13 }
+		};
+		const MusicLoad load = { kKeepPlayback, -1, 0x60, 0x60, true,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	case 44: {
+		static const ChannelLoad channels[] = {
+			{ 0, 0x5b0e }, { 1, 0x5b5a }, { 2, 0x5b88 }
+		};
+		const MusicLoad load = { kStopAll, -1, 0xc0, 0xc0, true,
+			channels, ARRAYSIZE(channels) };
+		applyMusicLoad(load);
+		break;
+	}
+	default:
+		break;
+	}
+	return 0;
+}
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/sound/psound_dragonsphere.h b/engines/mads/dragonsphere/sound/psound_dragonsphere.h
new file mode 100644
index 00000000000..34d27dc69ae
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/psound_dragonsphere.h
@@ -0,0 +1,142 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ */
+
+#ifndef MADS_DRAGONSPHERE_SOUND_PSOUND_DRAGONSPHERE_H
+#define MADS_DRAGONSPHERE_SOUND_PSOUND_DRAGONSPHERE_H
+
+#include "mads/dragonsphere/sound/psound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+/** Shared, game-local control layer used by the seven retail PSOUND overlays. */
+class DragonspherePSound : public PSound {
+protected:
+	enum MusicReset {
+		kKeepPlayback,
+		kClearMusicIdentity,
+		kStopMusic,
+		kStopAll
+	};
+
+	struct ChannelLoad {
+		byte channel;
+		uint16 sequence;
+	};
+	struct MusicLoad {
+		MusicReset reset;
+		int musicIndex;
+		int counter;
+		int period;
+		bool clearCallback;
+		const ChannelLoad *channels;
+		uint channelCount;
+	};
+
+	uint16 _callbackCounter;
+	uint16 _callbackPeriod;
+	int _pendingCommand;
+	bool _pendingLoadOnly;
+	int _musicIndex;
+	byte _maxMusicCommand;
+	bool _resetClearsCallback;
+
+	DragonspherePSound(Audio::Mixer *mixer, const PSoundDriverData &driverData,
+			byte maxMusicCommand, bool resetClearsCallback);
+
+	int dispatchBaseCommand(int commandId);
+	int resetSection();
+	void playSounds(const uint16 *sequences, uint count, bool anyChannel = false);
+	void loadChannels(const ChannelLoad *loads, uint count);
+	bool musicChannelsActive(uint count = kMusicChannelCount) const;
+	void applyMusicLoad(const MusicLoad &load);
+	bool startOrDeferMusic(int commandId, uint16 guard,
+			const MusicLoad &load, bool loadOnly,
+			uint musicChannelCount = kMusicChannelCount);
+	bool startOrDeferMusicWhenActive(int commandId, uint16 guard,
+			const MusicLoad &load, bool loadOnly,
+			uint musicChannelCount = kMusicChannelCount);
+	void deferCommand(int commandId, bool loadOnly);
+	void serviceCallbacks() override;
+	virtual int executeCommand(int commandId, bool loadOnly) = 0;
+
+public:
+	int command(int commandId, int param) override;
+};
+
+/** Demo control layer: its exported dispatcher has no saved music command. */
+class DragonspherePSoundDemo : public DragonspherePSound {
+protected:
+	DragonspherePSoundDemo(Audio::Mixer *mixer,
+			const PSoundDriverData &driverData, bool resetClearsCallback);
+
+public:
+	int command(int commandId, int param) override;
+};
+
+#define DECLARE_DRAGONSPHERE_PSOUND(_section, _count) \
+	class PSound##_section final : public DragonspherePSound { \
+	private: \
+		static const uint16 _commandList[_count]; \
+		int executeCommand(int commandId, bool loadOnly) override; \
+		bool callFunction(uint16 targetOffset, Channel &channel) override; \
+	public: \
+		explicit PSound##_section(Audio::Mixer *mixer); \
+	}
+
+DECLARE_DRAGONSPHERE_PSOUND(1, 102);
+DECLARE_DRAGONSPHERE_PSOUND(2, 73);
+DECLARE_DRAGONSPHERE_PSOUND(3, 74);
+DECLARE_DRAGONSPHERE_PSOUND(4, 79);
+DECLARE_DRAGONSPHERE_PSOUND(6, 99);
+DECLARE_DRAGONSPHERE_PSOUND(9, 64);
+
+class PSound5 final : public DragonspherePSound {
+private:
+	static const uint16 _commandList[79];
+	void loadAlternateMusic();
+	int executeCommand(int commandId, bool loadOnly) override;
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+public:
+	explicit PSound5(Audio::Mixer *mixer);
+};
+
+class PSoundDemo1 final : public DragonspherePSoundDemo {
+private:
+	static const uint16 _commandList[89];
+	int executeCommand(int commandId, bool loadOnly) override;
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+public:
+	explicit PSoundDemo1(Audio::Mixer *mixer);
+};
+
+class PSoundDemo9 final : public DragonspherePSoundDemo {
+private:
+	static const uint16 _commandList[51];
+	int executeCommand(int commandId, bool loadOnly) override;
+	bool callFunction(uint16 targetOffset, Channel &channel) override;
+public:
+	explicit PSoundDemo9(Audio::Mixer *mixer);
+};
+
+/** Validate one exact retail section overlay or separately built demo file. */
+bool validateDragonspherePSoundFile(int section, bool isDemo,
+		Common::String *reason = nullptr);
+
+#undef DECLARE_DRAGONSPHERE_PSOUND
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif // MADS_DRAGONSPHERE_SOUND_PSOUND_DRAGONSPHERE_H
diff --git a/engines/mads/dragonsphere/sound/sound.cpp b/engines/mads/dragonsphere/sound/sound.cpp
index 3d466842372..99374556c7d 100644
--- a/engines/mads/dragonsphere/sound/sound.cpp
+++ b/engines/mads/dragonsphere/sound/sound.cpp
@@ -20,15 +20,100 @@
  */
 
 #include "mads/dragonsphere/sound/sound.h"
+#include "audio/fmopl.h"
+#include "common/textconsole.h"
 #include "mads/dragonsphere/sound/asound_dragonsphere.h"
+#include "mads/dragonsphere/sound/psound_dragonsphere.h"
 #include "mads/dragonsphere/sound/rsound_dragonsphere.h"
 
 namespace MADS {
 namespace Dragonsphere {
 namespace Sound {
 
+namespace {
+
+const int kRetailSections[] = { 1, 2, 3, 4, 5, 6, 9 };
+
+SoundDriver *createPSound(Audio::Mixer *mixer, int sectionNumber,
+		bool isDemo) {
+	if (isDemo) {
+		switch (sectionNumber) {
+		case 1:
+			return new PSoundDemo1(mixer);
+		case 9:
+			return new PSoundDemo9(mixer);
+		default:
+			return nullptr;
+		}
+	}
+
+	switch (sectionNumber) {
+	case 1:
+		return new PSound1(mixer);
+	case 2:
+		return new PSound2(mixer);
+	case 3:
+		return new PSound3(mixer);
+	case 4:
+		return new PSound4(mixer);
+	case 5:
+		return new PSound5(mixer);
+	case 6:
+		return new PSound6(mixer);
+	case 9:
+		return new PSound9(mixer);
+	default:
+		return nullptr;
+	}
+}
+
+} // namespace
+
+DragonSoundManager::DragonSoundManager(Audio::Mixer *mixer,
+		bool &soundFlag, bool usePas, bool isDemo) :
+		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+	if (usePas && _driverType == SOUND_ADLIB) {
+		if (OPL::Config::detect(OPL::Config::kOpl3) >= 0) {
+			_driverType = SOUND_PAS;
+		} else {
+			warning("Pro Audio Spectrum 16 requires OPL3 output; "
+					"falling back to AdLib");
+		}
+	}
+}
+
 void DragonSoundManager::validate() {
-	if (_driverType == SOUND_MT32) {
+	if (_driverType == SOUND_PAS) {
+		bool valid = true;
+		if (_isDemo) {
+			const int demoSections[] = { 1, 9 };
+			for (uint index = 0; index < ARRAYSIZE(demoSections); ++index) {
+				Common::String reason;
+				if (!validateDragonspherePSoundFile(demoSections[index], true,
+						&reason)) {
+					warning("Cannot use Dragonsphere demo PSOUND section %d: "
+							"%s; using AdLib", demoSections[index],
+							reason.c_str());
+					valid = false;
+				}
+			}
+		} else {
+			for (uint index = 0; index < ARRAYSIZE(kRetailSections); ++index) {
+				Common::String reason;
+				if (!validateDragonspherePSoundFile(kRetailSections[index], false,
+						&reason)) {
+					warning("Cannot use Dragonsphere PSOUND section %d: %s; "
+							"using AdLib", kRetailSections[index],
+							reason.c_str());
+					valid = false;
+				}
+			}
+		}
+		if (valid)
+			return;
+		_driverType = SOUND_ADLIB;
+		ASound::validate(_isDemo);
+	} else if (_driverType == SOUND_MT32) {
 		if (_isDemo)
 			RSoundDemo::validate();
 		else
@@ -41,7 +126,16 @@ void DragonSoundManager::validate() {
 void DragonSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_driverType == SOUND_MT32) {
+	if (_driverType == SOUND_PAS) {
+		_driver = createPSound(_mixer, sectionNumber, _isDemo);
+		if (_driver && !static_cast<PSound *>(_driver)->isReady()) {
+			warning("Could not initialize Pro Audio Spectrum 16 OPL3 output; "
+					"falling back to AdLib");
+			removeDriver();
+			_driverType = SOUND_ADLIB;
+			loadDriver(sectionNumber);
+		}
+	} else if (_driverType == SOUND_MT32) {
 		// Roland MT32 drivers
 		if (_isDemo) {
 			switch (sectionNumber) {
diff --git a/engines/mads/dragonsphere/sound/sound.h b/engines/mads/dragonsphere/sound/sound.h
index 82332cdf570..49e69d771ab 100644
--- a/engines/mads/dragonsphere/sound/sound.h
+++ b/engines/mads/dragonsphere/sound/sound.h
@@ -39,9 +39,8 @@ protected:
 	void loadDriver(int sectionNum) override;
 
 public:
-	DragonSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
-		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
-	}
+	DragonSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool usePas,
+			bool isDemo);
 	~DragonSoundManager() override {}
 
 	/**
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 750a96a01cf..4cc8f0c5c9a 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -367,6 +367,8 @@ MODULE_OBJS := \
 	dragonsphere/rooms/room909.o \
 	dragonsphere/sound/asound.o \
 	dragonsphere/sound/asound_dragonsphere.o \
+	dragonsphere/sound/psound.o \
+	dragonsphere/sound/psound_dragonsphere.o \
 	dragonsphere/sound/rsound.o \
 	dragonsphere/sound/rsound_dragonsphere.o \
 	dragonsphere/sound/sound.o \


Commit: b25ea30d59ac16b03d82b4bd555e6e01fa16e44f
    https://github.com/scummvm/scummvm/commit/b25ea30d59ac16b03d82b4bd555e6e01fa16e44f
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: DRAGONSPHERE: Reimplement GSOUND overlays

Translate the verified General MIDI overlay family, restore its native
host cadence and special commands, and select it for retail
Dragonsphere when General MIDI is requested.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
  A engines/mads/dragonsphere/sound/gsound.cpp
  A engines/mads/dragonsphere/sound/gsound.h
  A engines/mads/dragonsphere/sound/gsound_dragonsphere.cpp
  A engines/mads/dragonsphere/sound/gsound_dragonsphere.h
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h
    engines/mads/detection_tables.h
    engines/mads/dragonsphere/sound/sound.cpp
    engines/mads/module.mk


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 6b20a316961..7b881683238 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -32,7 +32,8 @@ class Mixer;
 
 namespace MADS {
 
-SoundManager::SoundManager(Audio::Mixer *mixer, bool &soundFlag) : _mixer(mixer), _soundFlag(soundFlag) {
+SoundManager::SoundManager(Audio::Mixer *mixer, bool &soundFlag,
+		bool supportsGeneralMidi) : _mixer(mixer), _soundFlag(soundFlag) {
 	MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_PCSPK | MDT_ADLIB | MDT_MIDI | MDT_PREFER_MT32);
 	MusicType musicType = MidiDriver::getMusicType(dev);
 	if ((musicType == MT_GM || musicType == MT_GS) && ConfMan.getBool("native_mt32"))
@@ -41,6 +42,10 @@ SoundManager::SoundManager(Audio::Mixer *mixer, bool &soundFlag) : _mixer(mixer)
 	case MT_MT32:
 		_driverType = SOUND_MT32;
 		break;
+	case MT_GM:
+	case MT_GS:
+		_driverType = supportsGeneralMidi ? SOUND_GM : SOUND_ADLIB;
+		break;
 	case MT_PCSPK:
 		_driverType = SOUND_PCSPEAKER;
 		break;
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index a6e0a85eab1..58cd3b1fe8c 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -93,7 +93,7 @@ public:
 
 class SoundManager {
 protected:
-	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_PCSPEAKER, SOUND_PAS };
+	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_GM, SOUND_PCSPEAKER, SOUND_PAS };
 	Audio::Mixer *_mixer;
 	DriverType _driverType;
 	bool &_soundFlag;
@@ -112,7 +112,8 @@ protected:
 	virtual void loadDriver(int sectionNum) = 0;
 
 public:
-	SoundManager(Audio::Mixer *mixer, bool &soundFlag);
+	SoundManager(Audio::Mixer *mixer, bool &soundFlag,
+			bool supportsGeneralMidi = false);
 	virtual ~SoundManager();
 
 	/**
diff --git a/engines/mads/detection_tables.h b/engines/mads/detection_tables.h
index c873aee25c3..cb15eee0754 100644
--- a/engines/mads/detection_tables.h
+++ b/engines/mads/detection_tables.h
@@ -253,7 +253,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE | ADGF_CD,
-			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
+			GUIO6(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Dragonsphere,
 		0
@@ -268,7 +268,7 @@ static const MADSGameDescription gameDescriptions[] = {
 			Common::EN_ANY,
 			Common::kPlatformDOS,
 			ADGF_UNSTABLE,
-			GUIO5(GUIO_MIDIADLIB, GUIO_MIDIMT32, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
+			GUIO6(GUIO_MIDIADLIB, GUIO_MIDIMT32, GUIO_MIDIGM, GAMEOPTION_EASY_MOUSE, GAMEOPTION_ORIGINAL_SAVELOAD, GAMEOPTION_PAS)
 		},
 		GType_Dragonsphere,
 		0
diff --git a/engines/mads/dragonsphere/sound/gsound.cpp b/engines/mads/dragonsphere/sound/gsound.cpp
new file mode 100644
index 00000000000..7cd4c5f8df7
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/gsound.cpp
@@ -0,0 +1,1068 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "common/md5.h"
+#include "common/textconsole.h"
+#include "mads/dragonsphere/sound/gsound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+void GSoundChannel::reset(byte *startPtr) {
+	_activeCount = 0;
+	_pitchBendFadeStep = 0;
+	_volumeFadeStep = 0;
+	_panFadeStep = 0;
+	_note = 0;
+	_program = 0;
+	_velocity = 0;
+	_noteOffset = 0;
+	_keyOnDelayOverride = 0;
+	_keyOnDelay = 0;
+	_volumeFadeCounter = 0;
+	_volumeFadeReload = 0;
+	_pitchBendFadeCounter = 0;
+	_panFadeCounter = 0;
+	_panFadeReload = 0;
+	_pan = 0x40;
+	_volume = 100;
+	_pitchBend = 0x40;
+	_pitchBendFadeReload = 0;
+	_pitchBendFadeCount = 0;
+	_loopStartPtr = startPtr;
+	_pSrc = startPtr;
+	_innerLoopPtr = startPtr;
+	_outerLoopPtr = startPtr;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_soundData = startPtr;
+	_branchTarget = nullptr;
+	_field24 = 0;
+	_transpose = 0;
+	_pendingStop = 0;
+	_field27 = 0;
+}
+
+void GSoundChannel::load(byte *startPtr) {
+	reset(startPtr);
+	_activeCount = 1;
+	_owner->sendPitchBend(_midiChannel, 0x40);
+}
+
+void GSoundChannel::enableFade(byte flag) {
+	if (!_activeCount)
+		return;
+	_pendingStop = flag;
+	_soundData = _owner->_silenceStream;
+}
+
+GSound::GSound(Audio::Mixer *mixer, const GSoundDriverData &driverData) :
+		SoundDriver(mixer),
+		_driverData(driverData), _midiDriver(nullptr),
+		_driverCallbackDelta(0), _randomSeed(1234), _stateChanged(0),
+		_callbackCounter(0), _callbackPeriod(0), _deferredCommand(-1),
+		_musicIndex(0), _fadeCounter(0), _fadePeriod(0), _clockUnknown(0),
+		_clockCoarseTarget(0), _clockMediumTarget(0), _clockFine(0),
+		_clockCoarse(0), _clockMedium(0), _clockEnabled1(false),
+		_clockEnabled2(false), _masterVolume(255), _isReady(false),
+		_commandParam(0), _frameCounter(0), _tickCounter(0),
+		_isDisabled(false), _pollResult(0) {
+	if (!validateOverlay(driverData))
+		return;
+
+	Common::File file;
+	if (!file.open(driverData.filename))
+		return;
+	_soundData.resize(driverData.dataSize);
+	file.seek(0x200 + driverData.dataParagraph * 16);
+	if (file.read(&_soundData[0], driverData.initializedDataSize) !=
+			driverData.initializedDataSize) {
+		_soundData.clear();
+		return;
+	}
+
+	// SoundDriver reads the declared mutable image. The original file stores
+	// only initialized bytes; reproduce DOS BSS deterministically.
+	for (uint32 i = driverData.initializedDataSize; i < _soundData.size(); ++i)
+		_soundData[i] = 0;
+
+	_silenceStream[0] = 0;
+	_silenceStream[1] = 0;
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i) {
+		_channels[i]._owner = this;
+		_channels[i]._midiChannel = i + 1;
+		_channels[i].reset(_silenceStream);
+	}
+	for (int channel = 0; channel <= GSOUND_CHANNEL_COUNT; ++channel)
+		for (int note = 0; note < GSOUND_HELD_NOTE_COUNT; ++note)
+			_heldNotes[channel][note] = 0xFF;
+	for (int i = 0; i < GSOUND_SCRIPT_VARIABLE_COUNT; ++i)
+		_scriptVariables[i] = 0;
+
+	_clockCoarse = 112;
+	_clockMedium = 28;
+	_clockFine = 7;
+
+	_midiDriver = new MidiDriver_MT32GM(MusicType::MT_GM);
+	const int result = _midiDriver->open();
+	if (result) {
+		warning("GSOUND failed to open the General MIDI driver (error %d)",
+				result);
+		delete _midiDriver;
+		_midiDriver = nullptr;
+		return;
+	}
+
+	_driverCallbackDelta = _midiDriver->getBaseTempo();
+	resetMidiChannels();
+	_midiDriver->setTimerCallback(this, &timerCallback);
+	_isReady = true;
+}
+
+GSound::~GSound() {
+	_isDisabled = true;
+	if (!_midiDriver)
+		return;
+
+	_midiDriver->setTimerCallback(nullptr, nullptr);
+	resetMidiChannels();
+	_midiDriver->close();
+	Common::StackLock lock(_driverMutex);
+	delete _midiDriver;
+	_midiDriver = nullptr;
+}
+
+bool GSound::validateOverlay(const GSoundDriverData &driverData) {
+	Common::File file;
+	if (!file.open(driverData.filename))
+		return false;
+	if ((uint32)file.size() != driverData.fileSize)
+		return false;
+
+	const Common::String md5 = Common::computeStreamMD5AsString(file, 8192);
+	if (md5 != driverData.md5First8192)
+		return false;
+
+	file.seek(0x200 + 0x10);
+	char identity[22];
+	if (file.read(identity, 21) != 21)
+		return false;
+	identity[21] = 0;
+	if (Common::String(identity) != "Dragon GM  N12-21-93")
+		return false;
+
+	file.seek(0x200 + 0x2A);
+	if (file.readUint16LE() != driverData.dataParagraph ||
+			file.readUint16LE() != driverData.dataSize ||
+			file.readUint16LE() != 100 ||
+			file.readUint16LE() != GSOUND_EXPORT_COUNT)
+		return false;
+
+	for (int i = 0; i < GSOUND_EXPORT_COUNT; ++i) {
+		if (file.readUint16LE() != driverData.exports[i])
+			return false;
+	}
+
+	const uint32 dataOffset = 0x200 + driverData.dataParagraph * 16;
+	if (dataOffset + driverData.initializedDataSize != driverData.fileSize ||
+			driverData.initializedDataSize > driverData.dataSize)
+		return false;
+
+	file.seek(0x200 + driverData.exports[4]);
+	if (file.readByte() != 0xCB)
+		return false;
+
+	return true;
+}
+
+bool GSound::contains(const byte *ptr, uint32 count) const {
+	if (_soundData.empty())
+		return false;
+	const byte *start = &_soundData[0];
+	const byte *end = start + _soundData.size();
+	return ptr >= start && ptr <= end && count <= (uint32)(end - ptr);
+}
+
+byte *GSound::dataAt(uint16 offset) {
+	if (offset >= _soundData.size())
+		error("GSOUND data offset 0x%04x is outside %s", offset,
+				_driverData.filename);
+	return &_soundData[offset];
+}
+
+int8 GSound::readSignedByte(byte *&pSrc) {
+	return (int8)readByte(pSrc);
+}
+
+byte GSound::readByte(byte *&pSrc) {
+	if (!contains(pSrc + 1))
+		error("GSOUND bytecode read outside %s", _driverData.filename);
+	return *++pSrc;
+}
+
+uint16 GSound::readWord(byte *&pSrc) {
+	const byte low = readByte(pSrc);
+	const byte high = readByte(pSrc);
+	return low | (high << 8);
+}
+
+byte *GSound::readRoot(byte *&pSrc) {
+	return dataAt(readWord(pSrc));
+}
+
+void GSound::sendNoteOn(int channel, int note, int velocity) {
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | (channel & 0x0F),
+			note & 0x7F, velocity & 0x7F);
+}
+
+void GSound::sendProgramChange(int channel, int program) {
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE |
+			(channel & 0x0F), program & 0x7F, 0);
+}
+
+void GSound::sendControlChange(int channel, int controller, int value) {
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE |
+			(channel & 0x0F), controller & 0x7F, value & 0x7F);
+}
+
+void GSound::sendPitchBend(int channel, int value) {
+	_midiDriver->send(MidiDriver::MIDI_COMMAND_PITCH_BEND |
+			(channel & 0x0F), 0, value & 0x7F);
+}
+
+void GSound::sendVolume(GSoundChannel &channel) {
+	if (!channel._pendingStop)
+		sendControlChange(channel._midiChannel,
+				MidiDriver::MIDI_CONTROLLER_VOLUME,
+				scaleVolume(channel._volume));
+}
+
+int GSound::scaleVolume(int volume) const {
+	return scaleMidiVolume(volume, _masterVolume);
+}
+
+void GSound::sendPan(GSoundChannel &channel) {
+	sendControlChange(channel._midiChannel,
+			MidiDriver::MIDI_CONTROLLER_PANNING, channel._pan);
+}
+
+void GSound::setPitchBendSensitivity(int channel, int semitones) {
+	sendControlChange(channel, 101, 0);
+	sendControlChange(channel, 100, 0);
+	sendControlChange(channel, 6, semitones);
+	sendControlChange(channel, 38, 0);
+}
+
+void GSound::resetMidiChannels() {
+	// The original loops from native MIDI channel 9 through channel 0.
+	for (int channel = 9; channel >= 0; --channel) {
+		sendControlChange(channel, MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+		sendControlChange(channel,
+				MidiDriver::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
+		sendControlChange(channel, MidiDriver::MIDI_CONTROLLER_VOLUME,
+				scaleVolume(100));
+		sendControlChange(channel, MidiDriver::MIDI_CONTROLLER_PANNING, 64);
+		sendControlChange(channel, 91, 0);
+		sendControlChange(channel, 93, 0);
+		setPitchBendSensitivity(channel, 2);
+	}
+}
+
+void GSound::flushHeldNotes(GSoundChannel &channel) {
+	byte *notes = _heldNotes[channel._midiChannel];
+	for (int i = 0; i < GSOUND_HELD_NOTE_COUNT; ++i) {
+		if (notes[i] != 0xFF) {
+			sendNoteOn(channel._midiChannel, notes[i], 0);
+			notes[i] = 0xFF;
+		}
+	}
+}
+
+void GSound::resetChannels() {
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i)
+		_channels[i].reset(_silenceStream);
+	for (int channel = 0; channel <= GSOUND_CHANNEL_COUNT; ++channel)
+		for (int note = 0; note < GSOUND_HELD_NOTE_COUNT; ++note)
+			_heldNotes[channel][note] = 0xFF;
+}
+
+void GSound::resetMusicChannels() {
+	// Native command 2 resets channels 1-6 and 9.
+	for (int i = 0; i < 6; ++i)
+		_channels[i].reset(_silenceStream);
+	_channels[8].reset(_silenceStream);
+}
+
+void GSound::resetEffectChannels() {
+	_channels[6].reset(_silenceStream);
+	_channels[7].reset(_silenceStream);
+}
+
+void GSound::stopMusicChannels() {
+	_fadePeriod = 1;
+	for (int i = 0; i < 6; ++i)
+		_channels[i].enableFade(0xFF);
+	_channels[8].enableFade(0xFF);
+}
+
+void GSound::stopEffectChannels() {
+	_fadePeriod = 1;
+	_channels[6].enableFade(0xFF);
+	_channels[7].enableFade(0xFF);
+}
+
+bool GSound::anyChannelActive() const {
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i) {
+		if (_channels[i]._activeCount)
+			return true;
+	}
+	return false;
+}
+
+bool GSound::isSoundActive(uint16 offset) const {
+	if (offset >= _soundData.size())
+		return false;
+	const byte *root = &_soundData[offset];
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i) {
+		if (_channels[i]._activeCount && _channels[i]._soundData == root)
+			return true;
+	}
+	return false;
+}
+
+bool GSound::channelPlays(int channel, uint16 offset) const {
+	if (channel < 1 || channel > GSOUND_CHANNEL_COUNT ||
+			offset >= _soundData.size())
+		return false;
+	const GSoundChannel &state = _channels[channel - 1];
+	return state._activeCount && state._soundData == &_soundData[offset];
+}
+
+GSoundChannel *GSound::allocateChannel(uint16 offset, int first, int last) {
+	byte *root = dataAt(offset);
+	for (int i = first; i <= last; ++i) {
+		if (!_channels[i]._activeCount) {
+			_channels[i].load(root);
+			return &_channels[i];
+		}
+	}
+	for (int i = last; i >= first; --i) {
+		if (_channels[i]._pendingStop == 0xFF) {
+			_channels[i].load(root);
+			return &_channels[i];
+		}
+	}
+	return nullptr;
+}
+
+GSoundChannel *GSound::playEffect78(uint16 offset) {
+	return allocateChannel(offset, 6, 7);
+}
+
+GSoundChannel *GSound::playEffectAny(uint16 offset) {
+	return allocateChannel(offset, 0, 7);
+}
+
+GSoundChannel *GSound::playEffectChannel8(uint16 offset) {
+	return allocateChannel(offset, 7, 7);
+}
+
+void GSound::loadChannel(int channel, uint16 offset) {
+	if (channel < 1 || channel > GSOUND_CHANNEL_COUNT)
+		error("Invalid GSOUND channel %d", channel);
+	_channels[channel - 1].load(dataAt(offset));
+}
+
+void GSound::loadRoots(const GSoundCommandSpec &spec) {
+	for (int i = 0; i < spec.rootCount; ++i)
+		loadChannel(spec.roots[i].channel, spec.roots[i].offset);
+}
+
+void GSound::armTimer(uint16 counter, uint16 period) {
+	_deferredCommand = -1;
+	_callbackCounter = counter;
+	_callbackPeriod = period;
+}
+
+void GSound::deferCommand(int command) {
+	_deferredCommand = command;
+}
+
+void GSound::scheduleSpecial(int command, uint16 counter, uint16 period) {
+	armTimer(counter, period);
+	deferCommand(command);
+}
+
+int GSound::executeSpec(const GSoundCommandSpec &spec, bool fromDeferred) {
+	if (!fromDeferred && (spec.flags & kGSoundCheckFirstRoot)) {
+		for (int i = 0; i < spec.guardCount; ++i) {
+			if (isSoundActive(spec.guards[i]))
+				return 0;
+		}
+	}
+
+	if (!fromDeferred && (spec.flags & kGSoundDeferWhileActive) &&
+			anyChannelActive()) {
+		deferCommand(spec.command);
+		return 0;
+	}
+
+	if (spec.timerCounter || spec.timerPeriod)
+		armTimer(spec.timerCounter, spec.timerPeriod);
+	if (spec.flags & kGSoundResetAll)
+		command0();
+	else if (spec.flags & kGSoundStopAll)
+		command1();
+	else if (spec.flags & kGSoundStopMusic)
+		command3();
+	if (spec.flags & kGSoundSetMusicIndex)
+		_musicIndex = spec.musicIndex;
+
+	switch (spec.mode) {
+	case kGSoundNoOp:
+		break;
+	case kGSoundMusic:
+	case kGSoundDirectMusic:
+	case kGSoundDirectChannels:
+		loadRoots(spec);
+		break;
+	case kGSoundEffect78:
+		for (int i = 0; i < spec.rootCount; ++i)
+			playEffect78(spec.roots[i].offset);
+		break;
+	case kGSoundEffectAny:
+		for (int i = 0; i < spec.rootCount; ++i)
+			playEffectAny(spec.roots[i].offset);
+		break;
+	case kGSoundEffectChannel8:
+		for (int i = 0; i < spec.rootCount; ++i)
+			playEffectChannel8(spec.roots[i].offset);
+		break;
+	case kGSoundSpecial:
+		if (!executeSpecialCommand(spec.command, fromDeferred))
+			error("Unknown special GSOUND command %d in section %d",
+					spec.command, _driverData.section);
+		break;
+	}
+
+	return 0;
+}
+
+int GSound::executeCommand(int command, int param) {
+	_commandParam = param;
+	switch (command) {
+	case 0: return command0();
+	case 1: return command1();
+	case 2: return command2();
+	case 3: return command3();
+	case 4: return command4();
+	case 5: return command5();
+	case 6: return command6();
+	case 7: return command7();
+	case 8: return command8();
+	case 18:
+		// GSOUND.DR9 maps command 18 directly to a return stub. Unlike
+		// DR1-DR6, its dispatcher does not retain a music selector either.
+		if (_driverData.section == 9)
+			return 0;
+		command3();
+		if (_musicIndex != 18)
+			return executeCommand(_musicIndex, 0);
+		return 0;
+	default:
+		break;
+	}
+
+	const GSoundCommandSpec *spec = findCommandSpec(command);
+	return spec ? executeSpec(*spec, false) : 0;
+}
+
+int GSound::command0() {
+	_deferredCommand = -1;
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	resetChannels();
+	resetMidiChannels();
+	return 0;
+}
+
+int GSound::command1() {
+	command3();
+	command5();
+	return 0;
+}
+
+int GSound::command2() {
+	resetMusicChannels();
+	return 0;
+}
+
+int GSound::command3() {
+	stopMusicChannels();
+	return 0;
+}
+
+int GSound::command4() {
+	resetEffectChannels();
+	return 0;
+}
+
+int GSound::command5() {
+	stopEffectChannels();
+	return 0;
+}
+
+int GSound::command6() {
+	_isDisabled = true;
+	for (int channel = 1; channel <= GSOUND_CHANNEL_COUNT; ++channel)
+		sendControlChange(channel, MidiDriver::MIDI_CONTROLLER_VOLUME, 0);
+	return 0;
+}
+
+int GSound::command7() {
+	_isDisabled = false;
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i)
+		sendVolume(_channels[i]);
+	return 0;
+}
+
+int GSound::command8() {
+	int result = 0;
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i)
+		result |= _channels[i]._activeCount;
+	return result;
+}
+
+void GSound::tickDeferredCommand() {
+	if (!_callbackPeriod)
+		return;
+	if (--_callbackCounter)
+		return;
+
+	_callbackCounter = _callbackPeriod;
+	if (_deferredCommand < 0)
+		return;
+
+	const int command = _deferredCommand;
+	_deferredCommand = -1;
+	const GSoundCommandSpec *spec = findCommandSpec(command);
+	if (spec)
+		executeSpec(*spec, true);
+	else if (!executeSpecialCommand(command, true))
+		error("Unknown deferred GSOUND command %d", command);
+}
+
+void GSound::checkFade(GSoundChannel &channel) {
+	if (!channel._activeCount || !channel._pendingStop)
+		return;
+	if (channel._volume <= 0) {
+		flushHeldNotes(channel);
+		channel._pSrc = _silenceStream;
+		channel._pendingStop = 0;
+		return;
+	}
+	--channel._volume;
+	sendControlChange(channel._midiChannel,
+			MidiDriver::MIDI_CONTROLLER_VOLUME,
+			scaleVolume(channel._volume));
+}
+
+void GSound::checkFades() {
+	if (!_fadePeriod || --_fadeCounter > 0)
+		return;
+	_fadeCounter = _fadePeriod;
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i)
+		checkFade(_channels[i]);
+}
+
+void GSound::applyFades(GSoundChannel &channel) {
+	if (channel._volumeFadeStep && --channel._volumeFadeCounter == 0) {
+		channel._volumeFadeCounter = channel._volumeFadeReload;
+		channel._volume += channel._volumeFadeStep;
+		if ((byte)channel._volume > 0x7F) {
+			channel._volumeFadeStep = 0;
+			channel._volume = ((byte)channel._volume > 0xAF) ? 0 : 0x7F;
+		}
+		sendVolume(channel);
+	}
+	if (channel._pitchBendFadeStep) {
+		if (--channel._pitchBendFadeCounter == 0) {
+			channel._pitchBendFadeCounter = channel._pitchBendFadeReload;
+			channel._pitchBend += channel._pitchBendFadeStep;
+			sendPitchBend(channel._midiChannel, channel._pitchBend);
+		}
+		if (--channel._pitchBendFadeCount == 0)
+			channel._pitchBendFadeStep = 0;
+	}
+	if (channel._panFadeStep && --channel._panFadeCounter == 0) {
+		channel._panFadeCounter = channel._panFadeReload;
+		channel._pan += channel._panFadeStep;
+		sendPan(channel);
+	}
+}
+
+void GSound::pollChannel(GSoundChannel &channel) {
+	if (!channel._activeCount)
+		return;
+
+	if (channel._keyOnDelay && --channel._keyOnDelay == 0)
+		flushHeldNotes(channel);
+	if (--channel._activeCount)
+		goto fades;
+
+dispatch:
+	if (!contains(channel._pSrc))
+		error("GSOUND channel stream escaped %s", _driverData.filename);
+
+	{
+		byte *pSrc = channel._pSrc;
+		const byte opcode = *pSrc;
+		const int midiChannel = channel._midiChannel;
+
+		if (opcode <= 0xBB) {
+			if (!contains(pSrc, 2))
+				error("Truncated GSOUND note event in %s", _driverData.filename);
+			const int note = (int8)pSrc[0] + channel._transpose;
+			const int duration = pSrc[1];
+			channel._activeCount = duration;
+			channel._pSrc += 2;
+
+			if (note != channel._note)
+				flushHeldNotes(channel);
+			if (!duration)
+				return;
+			if (note) {
+				channel._keyOnDelay = channel._keyOnDelayOverride ?
+						channel._keyOnDelayOverride :
+						channel._activeCount - channel._noteOffset;
+				if (!(channel._noteOffset < 0 &&
+						_heldNotes[midiChannel][0] == (byte)note)) {
+					channel._note = note;
+					_heldNotes[midiChannel][0] = note;
+					sendNoteOn(midiChannel, note, channel._velocity);
+				}
+			}
+			goto fades;
+		}
+
+		switch (opcode) {
+		case 0xBC:
+			setPitchBendSensitivity(midiChannel, readByte(pSrc));
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xBD:
+			_clockUnknown = readByte(pSrc);
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xBE:
+			_clockCoarseTarget = readWord(pSrc);
+			if (!_tickCounter)
+				_clockCoarse = _clockCoarseTarget;
+			_clockEnabled1 = true;
+			_clockEnabled2 = true;
+			channel._pSrc += 3;
+			goto dispatch;
+		case 0xBF:
+			_clockMediumTarget = readByte(pSrc);
+			if (!_tickCounter)
+				_clockMedium = _clockMediumTarget;
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xC0:
+			_clockFine = readByte(pSrc);
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xC1:
+			sendControlChange(midiChannel, 93, readByte(pSrc));
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xC2:
+			sendControlChange(midiChannel, 91, readByte(pSrc));
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xC3:
+			readByte(pSrc);
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xC4: {
+			const uint16 target = readWord(pSrc);
+			if (!executeNativeCallback(target, channel))
+				error("Unknown Dragonsphere GSOUND callback 0x%04x in %s",
+						target, _driverData.filename);
+			channel._pSrc += 3;
+			goto dispatch;
+		}
+		case 0xC5: case 0xC6: case 0xC7: case 0xC8:
+		case 0xC9: case 0xCA: case 0xCB: case 0xCC:
+		case 0xCD: case 0xCE: case 0xCF: case 0xD0:
+		case 0xD1: case 0xD2: case 0xD3: case 0xD4: {
+			const bool saveReturn = opcode <= 0xCC;
+			const bool variableRhs = (opcode <= 0xC8) ||
+					(opcode >= 0xCD && opcode <= 0xD0);
+			const byte lhsIndex = readByte(pSrc);
+			const byte rhsValue = readByte(pSrc);
+			const byte lhs = _scriptVariables[lhsIndex & 0x1F];
+			const byte rhs = variableRhs ?
+					_scriptVariables[rhsValue & 0x1F] : rhsValue;
+			const int condition = (opcode -
+					(saveReturn ? 0xC5 : 0xCD)) & 3;
+			bool take = false;
+			switch (condition) {
+			case 0: take = lhs > rhs; break;
+			case 1: take = lhs < rhs; break;
+			case 2: take = lhs != rhs; break;
+			case 3: take = lhs == rhs; break;
+			}
+			if (take) {
+				if (saveReturn)
+					channel._branchTarget = channel._pSrc + 5;
+				channel._pSrc = readRoot(pSrc);
+			} else {
+				channel._pSrc += 5;
+			}
+			goto dispatch;
+		}
+		case 0xD5: case 0xD6: case 0xD7: case 0xD8:
+		case 0xD9: case 0xDA: case 0xDB: case 0xDC:
+		case 0xDD: case 0xDE: case 0xDF: case 0xE0:
+		case 0xE1: case 0xE2: case 0xE3: case 0xE4: {
+			const byte lhsIndex = readByte(pSrc) & 0x1F;
+			const byte rhsByte = readByte(pSrc);
+			const bool variableRhs = (opcode & 1) != 0;
+			const byte rhs = variableRhs ?
+					_scriptVariables[rhsByte & 0x1F] : rhsByte;
+			byte &lhs = _scriptVariables[lhsIndex];
+			lhs = applyArithmetic(opcode, lhs, rhs);
+			channel._pSrc += 3;
+			goto dispatch;
+		}
+		case 0xE5:
+			--_scriptVariables[readByte(pSrc) & 0x1F];
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xE6:
+			++_scriptVariables[readByte(pSrc) & 0x1F];
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xE7: {
+			const byte variable = readByte(pSrc) & 0x1F;
+			const int8 displacement = readSignedByte(pSrc);
+			byte *target = pSrc + 1 + displacement;
+			if (!contains(target))
+				error("GSOUND E7 write outside %s", _driverData.filename);
+			*target = _scriptVariables[variable];
+			channel._pSrc += 3;
+			goto dispatch;
+		}
+		case 0xE8: {
+			const byte destination = readByte(pSrc) & 0x1F;
+			const byte source = readByte(pSrc) & 0x1F;
+			_scriptVariables[destination] = _scriptVariables[source];
+			channel._pSrc += 3;
+			goto dispatch;
+		}
+		case 0xE9: {
+			const byte destination = readByte(pSrc) & 0x1F;
+			_scriptVariables[destination] = readByte(pSrc);
+			channel._pSrc += 3;
+			goto dispatch;
+		}
+		case 0xEA: {
+			const byte variable = readByte(pSrc) & 0x1F;
+			const byte length = readByte(pSrc);
+			byte *table = pSrc + 1;
+			if (!contains(table, length + 2))
+				error("Truncated GSOUND EA table in %s", _driverData.filename);
+			const byte value = table[_scriptVariables[variable]];
+			const int8 displacement = (int8)table[length];
+			byte *target = table + length + 1 + displacement;
+			if (!contains(target))
+				error("GSOUND EA write outside %s", _driverData.filename);
+			*target = value;
+			channel._pSrc += length + 4;
+			goto dispatch;
+		}
+		case 0xEB: {
+			const byte low = readByte(pSrc);
+			const byte high = readByte(pSrc);
+			const int range = high - low + 1;
+			const byte value = low + (range ? _randomSeed % range : 0);
+			const int8 displacement = readSignedByte(pSrc);
+			byte *target = pSrc + 1 + displacement;
+			if (!contains(target))
+				error("GSOUND EB write outside %s", _driverData.filename);
+			*target = value;
+			channel._pSrc += 4;
+			goto dispatch;
+		}
+		case 0xEC: {
+			const byte length = readByte(pSrc);
+			byte *table = pSrc + 1;
+			if (!contains(table, length + 1))
+				error("Truncated GSOUND EC table in %s", _driverData.filename);
+			const byte value = table[length ? _randomSeed % length : 0];
+			const int8 displacement = (int8)table[length];
+			byte *target = table + length + 1 + displacement;
+			if (!contains(target))
+				error("GSOUND EC write outside %s", _driverData.filename);
+			*target = value;
+			channel._pSrc += length + 3;
+			goto dispatch;
+		}
+		case 0xED: {
+			const int count = readByte(pSrc);
+			if (count > GSOUND_HELD_NOTE_COUNT)
+				error("GSOUND chord has %d notes", count);
+			if (!contains(pSrc + 1, count + 1))
+				error("Truncated GSOUND chord in %s", _driverData.filename);
+			const int first = (int8)pSrc[1] + channel._transpose;
+			if (first != _heldNotes[midiChannel][0])
+				flushHeldNotes(channel);
+			int i = 0;
+			for (; i < count; ++i) {
+				const int note = (int8)pSrc[1 + i] + channel._transpose;
+				if (!(channel._noteOffset < 0 &&
+						_heldNotes[midiChannel][i] == (byte)note)) {
+					_heldNotes[midiChannel][i] = note;
+					sendNoteOn(midiChannel, note, channel._velocity);
+				}
+			}
+			for (; i < GSOUND_HELD_NOTE_COUNT; ++i)
+				_heldNotes[midiChannel][i] = 0xFF;
+			channel._activeCount = pSrc[1 + count];
+			channel._keyOnDelay = channel._keyOnDelayOverride ?
+					channel._keyOnDelayOverride :
+					channel._activeCount - channel._noteOffset;
+			channel._pSrc += count + 3;
+			goto fades;
+		}
+		case 0xEE:
+			channel._transpose = readSignedByte(pSrc);
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xEF:
+			channel._panFadeReload = readByte(pSrc);
+			channel._panFadeStep = readSignedByte(pSrc);
+			channel._panFadeCounter = 1;
+			channel._pSrc += 3;
+			goto dispatch;
+		case 0xF0:
+			channel._pan = readByte(pSrc);
+			channel._pSrc += 2;
+			sendPan(channel);
+			goto dispatch;
+		case 0xF1:
+			channel._pitchBend = readByte(pSrc);
+			channel._pSrc += 2;
+			sendPitchBend(midiChannel, channel._pitchBend);
+			goto dispatch;
+		case 0xF2:
+			channel._volumeFadeReload = readByte(pSrc);
+			channel._volumeFadeStep = readSignedByte(pSrc);
+			channel._volumeFadeCounter = 1;
+			channel._pSrc += 3;
+			goto dispatch;
+		case 0xF3:
+			channel._velocity = readByte(pSrc);
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xF4:
+			channel._volume = readByte(pSrc);
+			channel._pSrc += 2;
+			sendVolume(channel);
+			goto dispatch;
+		case 0xF5:
+			channel._pitchBendFadeReload = readByte(pSrc);
+			channel._pitchBendFadeStep = readSignedByte(pSrc);
+			channel._pitchBendFadeCount = readByte(pSrc);
+			channel._pitchBendFadeCounter = 1;
+			channel._pSrc += 4;
+			goto dispatch;
+		case 0xF6:
+			channel._keyOnDelayOverride = readByte(pSrc);
+			channel._noteOffset = 0;
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xF7:
+			channel._noteOffset = readSignedByte(pSrc);
+			channel._keyOnDelayOverride = 0;
+			channel._pSrc += 2;
+			goto dispatch;
+		case 0xF8:
+			channel._program = readByte(pSrc);
+			channel._pSrc += 2;
+			sendProgramChange(midiChannel, channel._program);
+			goto dispatch;
+		case 0xF9:
+			if (channel._branchTarget) {
+				channel._pSrc = channel._branchTarget;
+				channel._branchTarget = nullptr;
+			} else {
+				++channel._pSrc;
+			}
+			goto dispatch;
+		case 0xFA:
+			channel._branchTarget = channel._pSrc + 3;
+			channel._pSrc = readRoot(pSrc);
+			goto dispatch;
+		case 0xFB:
+			channel._pSrc = readRoot(pSrc);
+			goto dispatch;
+		case 0xFC: {
+			byte *root = readRoot(pSrc);
+			channel._loopStartPtr = root;
+			channel._pSrc = root;
+			channel._innerLoopPtr = root;
+			channel._outerLoopPtr = root;
+			channel._soundData = root;
+			goto fades;
+		}
+		case 0xFD:
+			if (channel._soundData == nullptr) {
+				channel._pSrc = channel._loopStartPtr;
+			} else {
+				channel._loopStartPtr = channel._soundData;
+				channel._pSrc = channel._soundData;
+				channel._innerLoopPtr = channel._soundData;
+				channel._outerLoopPtr = channel._soundData;
+			}
+			goto fades;
+		case 0xFE: {
+			if (!channel._outerLoopCount) {
+				channel._outerLoopCount = readSignedByte(pSrc);
+				if (!channel._outerLoopCount) {
+					channel._pSrc += 2;
+					channel._outerLoopPtr = channel._pSrc;
+					channel._innerLoopCount = 0;
+					channel._outerLoopCount = 0;
+				} else {
+					channel._pSrc = channel._outerLoopPtr;
+				}
+			} else if (--channel._outerLoopCount == 0) {
+				channel._pSrc += 2;
+				channel._outerLoopPtr = channel._pSrc;
+			} else {
+				channel._pSrc = channel._outerLoopPtr;
+			}
+			channel._innerLoopPtr = channel._pSrc;
+			goto dispatch;
+		}
+		case 0xFF: {
+			if (!channel._innerLoopCount) {
+				channel._innerLoopCount = readSignedByte(pSrc);
+				if (!channel._innerLoopCount) {
+					channel._pSrc += 2;
+					channel._innerLoopPtr = channel._pSrc;
+				} else {
+					channel._pSrc = channel._innerLoopPtr;
+				}
+			} else if (--channel._innerLoopCount == 0) {
+				channel._pSrc += 2;
+				channel._innerLoopPtr = channel._pSrc;
+			} else {
+				channel._pSrc = channel._innerLoopPtr;
+			}
+			goto dispatch;
+		}
+		default:
+			error("Unknown GSOUND opcode 0x%02x in %s", opcode,
+					_driverData.filename);
+		}
+	}
+
+fades:
+	applyFades(channel);
+}
+
+void GSound::update() {
+	uint16 value = _randomSeed + 0x9249;
+	_randomSeed = (value >> 3) | (value << 13);
+	if (_isDisabled)
+		return;
+
+	++_frameCounter;
+	++_tickCounter;
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i)
+		pollChannel(_channels[i]);
+	tickDeferredCommand();
+	checkFades();
+}
+
+void GSound::onTimer() {
+	Common::StackLock lock(_driverMutex);
+	uint32 serviceTicks = _hostTimer.advance(_driverCallbackDelta, 1000000);
+	while (serviceTicks--) {
+		// Export 4 is an immediate RETF in every validated GSOUND overlay.
+		if (_hostTimer.pollDue())
+			poll();
+	}
+}
+
+void GSound::timerCallback(void *data) {
+	static_cast<GSound *>(data)->onTimer();
+}
+
+int GSound::stop() {
+	return command0();
+}
+
+int GSound::poll() {
+	update();
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void GSound::setVolume(int volume) {
+	_masterVolume = CLIP(volume, 0, 255);
+	for (int i = 0; i < GSOUND_CHANNEL_COUNT; ++i) {
+		if (_isDisabled)
+			sendControlChange(_channels[i]._midiChannel,
+					MidiDriver::MIDI_CONTROLLER_VOLUME, 0);
+		else
+			sendVolume(_channels[i]);
+	}
+}
+
+void GSound::setDataByte(uint16 offset, byte value) {
+	*dataAt(offset) = value;
+}
+
+byte GSound::getDataByte(uint16 offset) const {
+	if (offset >= _soundData.size())
+		error("GSOUND data offset 0x%04x is outside %s", offset,
+				_driverData.filename);
+	return _soundData[offset];
+}
+
+void GSound::setScriptVariable(byte index, byte value) {
+	_scriptVariables[index & 0x1F] = value;
+}
+
+byte GSound::getScriptVariable(byte index) const {
+	return _scriptVariables[index & 0x1F];
+}
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/sound/gsound.h b/engines/mads/dragonsphere/sound/gsound.h
new file mode 100644
index 00000000000..79e5da912fb
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/gsound.h
@@ -0,0 +1,305 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_DRAGONSPHERE_SOUND_GSOUND_H
+#define MADS_DRAGONSPHERE_SOUND_GSOUND_H
+
+#include "audio/mt32gm.h"
+#include "common/util.h"
+#include "mads/core/native_sound_timer.h"
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+enum {
+	GSOUND_CHANNEL_COUNT = 9,
+	GSOUND_HELD_NOTE_COUNT = 4,
+	GSOUND_SCRIPT_VARIABLE_COUNT = 32,
+	GSOUND_EXPORT_COUNT = 11
+};
+
+struct GSoundDriverData {
+	const char *filename;
+	uint32 fileSize;
+	const char *md5First8192;
+	uint16 dataParagraph;
+	uint16 dataSize;
+	uint16 initializedDataSize;
+	uint16 exports[GSOUND_EXPORT_COUNT];
+	byte commandMax[5];
+	byte section;
+};
+
+struct GSoundChannelRoot {
+	byte channel;
+	uint16 offset;
+};
+
+enum GSoundCommandMode {
+	kGSoundNoOp,
+	kGSoundMusic,
+	kGSoundDirectMusic,
+	kGSoundEffect78,
+	kGSoundEffectAny,
+	kGSoundEffectChannel8,
+	kGSoundDirectChannels,
+	kGSoundSpecial
+};
+
+enum GSoundCommandFlags {
+	kGSoundCheckFirstRoot = 1 << 0,
+	kGSoundDeferWhileActive = 1 << 1,
+	kGSoundStopMusic = 1 << 2,
+	kGSoundStopAll = 1 << 3,
+	kGSoundSetMusicIndex = 1 << 4,
+	kGSoundResetAll = 1 << 5
+};
+
+/**
+ * One command recovered from an individual Dragonsphere GSOUND overlay.
+ *
+ * The tables deliberately retain channel numbers and data offsets rather
+ * than translating them to a common MADS sound family. GSOUND's bytecode and
+ * command ABI are not interchangeable with RSOUND or PSOUND.
+ */
+struct GSoundCommandSpec {
+	byte command;
+	GSoundCommandMode mode;
+	byte flags;
+	byte musicIndex;
+	uint16 timerCounter;
+	uint16 timerPeriod;
+	byte guardCount;
+	uint16 guards[3];
+	byte rootCount;
+	GSoundChannelRoot roots[9];
+};
+
+class GSound;
+
+/** Native 0x28-byte GSOUND channel record represented in typed C++ state. */
+class GSoundChannel {
+public:
+	GSound *_owner;
+	int _midiChannel;
+
+	int _activeCount;
+	int _pitchBendFadeStep;
+	int _volumeFadeStep;
+	int _panFadeStep;
+	int _note;
+	int _program;
+	int _velocity;
+	int _noteOffset;
+	int _keyOnDelayOverride;
+	int _keyOnDelay;
+	int _volumeFadeCounter;
+	int _volumeFadeReload;
+	int _pitchBendFadeCounter;
+	int _panFadeCounter;
+	int _panFadeReload;
+	int _pan;
+	int _volume;
+	int _pitchBend;
+	int _pitchBendFadeReload;
+	int _pitchBendFadeCount;
+	byte *_loopStartPtr;
+	byte *_pSrc;
+	byte *_innerLoopPtr;
+	byte *_outerLoopPtr;
+	uint16 _innerLoopCount;
+	uint16 _outerLoopCount;
+	byte *_soundData;
+	byte *_branchTarget;
+	int _field24;
+	int _transpose;
+	int _pendingStop;
+	int _field27;
+
+	void reset(byte *startPtr);
+	void load(byte *startPtr);
+	void enableFade(byte flag);
+};
+
+/**
+ * Interpreter for Dragonsphere's native General MIDI `GSOUND.DR*` family.
+ *
+ * This class is intentionally independent of RSound. It shares only the
+ * common native host timer and ScummVM's MIDI interface. The original DOS
+ * MPU-401 probing and interrupt installation are replaced by MidiDriver.
+ */
+class GSound : public SoundDriver {
+	friend class GSoundChannel;
+
+private:
+	const GSoundDriverData &_driverData;
+	MidiDriver_MT32GM *_midiDriver;
+	uint32 _driverCallbackDelta;
+	NativeSoundTimer _hostTimer;
+	uint16 _randomSeed;
+	uint16 _stateChanged;
+	byte _heldNotes[GSOUND_CHANNEL_COUNT + 1][GSOUND_HELD_NOTE_COUNT];
+	byte _scriptVariables[GSOUND_SCRIPT_VARIABLE_COUNT];
+	byte _silenceStream[2];
+	uint16 _callbackCounter;
+	uint16 _callbackPeriod;
+	int _deferredCommand;
+	uint16 _musicIndex;
+	int _fadeCounter;
+	int _fadePeriod;
+	// These fields are writable by the native stream grammar, but the audited
+	// overlays contain no reader or service routine for them. Retain their
+	// state without inventing behavior that the driver did not execute.
+	int _clockUnknown;
+	int _clockCoarseTarget;
+	int _clockMediumTarget;
+	int _clockFine;
+	int _clockCoarse;
+	int _clockMedium;
+	bool _clockEnabled1;
+	bool _clockEnabled2;
+	int _masterVolume;
+	bool _isReady;
+
+	void update();
+	void pollChannel(GSoundChannel &channel);
+	void tickDeferredCommand();
+	void checkFades();
+	void checkFade(GSoundChannel &channel);
+	void flushHeldNotes(GSoundChannel &channel);
+	void applyFades(GSoundChannel &channel);
+
+	int8 readSignedByte(byte *&pSrc);
+	byte readByte(byte *&pSrc);
+	uint16 readWord(byte *&pSrc);
+	byte *readRoot(byte *&pSrc);
+	byte *dataAt(uint16 offset);
+	bool contains(const byte *ptr, uint32 count = 1) const;
+
+	void sendNoteOn(int channel, int note, int velocity);
+	void sendProgramChange(int channel, int program);
+	void sendControlChange(int channel, int controller, int value);
+	void sendPitchBend(int channel, int value);
+	void sendVolume(GSoundChannel &channel);
+	int scaleVolume(int volume) const;
+	void sendPan(GSoundChannel &channel);
+	void setPitchBendSensitivity(int channel, int semitones);
+	void resetMidiChannels();
+
+	void resetChannels();
+	void resetMusicChannels();
+	void resetEffectChannels();
+	void stopMusicChannels();
+	void stopEffectChannels();
+	bool anyChannelActive() const;
+	bool isSoundActive(uint16 offset) const;
+	GSoundChannel *playEffect78(uint16 offset);
+	GSoundChannel *playEffectAny(uint16 offset);
+	GSoundChannel *playEffectChannel8(uint16 offset);
+	GSoundChannel *allocateChannel(uint16 offset, int first, int last);
+	void loadRoots(const GSoundCommandSpec &spec);
+	int executeSpec(const GSoundCommandSpec &spec, bool fromDeferred);
+	void armTimer(uint16 counter, uint16 period);
+	void deferCommand(int command);
+
+	int command0();
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+	int command5();
+	int command6();
+	int command7();
+	int command8();
+
+	void onTimer();
+	static void timerCallback(void *data);
+
+protected:
+	static int scaleMidiVolume(int volume, int masterVolume) {
+		return CLIP(volume, 0, 127) * CLIP(masterVolume, 0, 255) / 255;
+	}
+
+	static byte applyArithmetic(byte opcode, byte lhs, byte rhs) {
+		switch (opcode) {
+		case 0xD5: case 0xD6: return lhs ^ rhs;
+		case 0xD7: case 0xD8: return lhs | rhs;
+		case 0xD9: case 0xDA: return lhs & rhs;
+		case 0xDB: case 0xDC: return rhs ? lhs % rhs : lhs;
+		case 0xDD: case 0xDE: return rhs ? lhs / rhs : lhs;
+		case 0xDF: case 0xE0: return lhs * rhs;
+		case 0xE1: case 0xE2: return lhs - rhs;
+		case 0xE3: case 0xE4: return lhs + rhs;
+		default: return lhs;
+		}
+	}
+
+	GSoundChannel _channels[GSOUND_CHANNEL_COUNT];
+	int _commandParam;
+	uint32 _frameCounter;
+	uint32 _tickCounter;
+	bool _isDisabled;
+	int _pollResult;
+
+	virtual const GSoundCommandSpec *findCommandSpec(int command) const = 0;
+	virtual bool executeSpecialCommand(int command, bool fromDeferred) = 0;
+	virtual bool executeNativeCallback(uint16 targetOffset,
+			GSoundChannel &channel) = 0;
+
+	int executeCommand(int command, int param);
+	void loadChannel(int channel, uint16 offset);
+	void setDataByte(uint16 offset, byte value);
+	byte getDataByte(uint16 offset) const;
+	void setScriptVariable(byte index, byte value);
+	byte getScriptVariable(byte index) const;
+	void setMusicIndex(uint16 index) { _musicIndex = index; }
+	void scheduleSpecial(int command, uint16 counter, uint16 period);
+	bool soundActive(uint16 offset) const { return isSoundActive(offset); }
+	bool channelsActive() const { return anyChannelActive(); }
+	void stopMusic() { command3(); }
+	void stopAll() { command1(); }
+	void armNativeTimer(uint16 counter, uint16 period) {
+		armTimer(counter, period);
+	}
+	void deferNativeCommand(int command) { deferCommand(command); }
+	bool channelPlays(int channel, uint16 offset) const;
+	void playNativeEffectAny(uint16 offset) { playEffectAny(offset); }
+
+public:
+	GSound(Audio::Mixer *mixer, const GSoundDriverData &driverData);
+	~GSound() override;
+
+	static bool validateOverlay(const GSoundDriverData &driverData);
+	bool isReady() const { return _isReady; }
+
+	int stop() override;
+	int poll() override;
+	void noise() override {}
+	void setVolume(int volume) override;
+};
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif // MADS_DRAGONSPHERE_SOUND_GSOUND_H
diff --git a/engines/mads/dragonsphere/sound/gsound_dragonsphere.cpp b/engines/mads/dragonsphere/sound/gsound_dragonsphere.cpp
new file mode 100644
index 00000000000..bdbf5452e4c
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/gsound_dragonsphere.cpp
@@ -0,0 +1,600 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mads/dragonsphere/sound/gsound_dragonsphere.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+#define R(channel, offset) { channel, offset }
+#define S(command, mode, flags, counter, period, guardCount, guard1, guard2, guard3, rootCount, ...) \
+	{ command, mode, flags, 0, counter, period, guardCount, \
+		{ guard1, guard2, guard3 }, rootCount, { __VA_ARGS__ } }
+#define N(command) { command, kGSoundNoOp, 0, 0, 0, 0, 0, { 0, 0, 0 }, 0, {} }
+#define X(command) { command, kGSoundSpecial, 0, 0, 0, 0, 0, { 0, 0, 0 }, 0, {} }
+#define F1(command, a) S(command, kGSoundEffect78, 0, 0, 0, 0, 0, 0, 0, 1, R(0, a))
+#define F2(command, a, b) S(command, kGSoundEffect78, 0, 0, 0, 0, 0, 0, 0, 2, R(0, a), R(0, b))
+#define F3(command, a, b, c) S(command, kGSoundEffect78, 0, 0, 0, 0, 0, 0, 0, 3, R(0, a), R(0, b), R(0, c))
+#define MFLAGS (kGSoundCheckFirstRoot | kGSoundDeferWhileActive | kGSoundStopMusic)
+#define DFLAGS (kGSoundCheckFirstRoot | kGSoundStopMusic)
+#define M1(command, counter, period, c1, a) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 1, R(c1, a))
+#define M2(command, counter, period, c1, a, c2, b) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 2, R(c1, a), R(c2, b))
+#define M3(command, counter, period, c1, a, c2, b, c3, c) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 3, R(c1, a), R(c2, b), R(c3, c))
+#define M4(command, counter, period, c1, a, c2, b, c3, c, c4, d) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 4, R(c1, a), R(c2, b), R(c3, c), R(c4, d))
+#define M5(command, counter, period, c1, a, c2, b, c3, c, c4, d, c5, e) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 5, R(c1, a), R(c2, b), R(c3, c), R(c4, d), R(c5, e))
+#define M6(command, counter, period, c1, a, c2, b, c3, c, c4, d, c5, e, c6, f) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 6, R(c1, a), R(c2, b), R(c3, c), R(c4, d), R(c5, e), R(c6, f))
+#define M7(command, counter, period, c1, a, c2, b, c3, c, c4, d, c5, e, c6, f, c7, g) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 7, R(c1, a), R(c2, b), R(c3, c), R(c4, d), R(c5, e), R(c6, f), R(c7, g))
+#define M8(command, counter, period, c1, a, c2, b, c3, c, c4, d, c5, e, c6, f, c7, g, c8, h) S(command, kGSoundMusic, MFLAGS, counter, period, 1, a, 0, 0, 8, R(c1, a), R(c2, b), R(c3, c), R(c4, d), R(c5, e), R(c6, f), R(c7, g), R(c8, h))
+#define D4(command, c1, a, c2, b, c3, c, c4, d) S(command, kGSoundDirectMusic, DFLAGS, 0, 0, 1, a, 0, 0, 4, R(c1, a), R(c2, b), R(c3, c), R(c4, d))
+
+static const GSoundDriverData kDriver1 = {
+	"GSOUND.DR1", 22172, "9cd1f97006d15d66d7312d492a0f67d4",
+	0x021d, 0x34a0, 0x32cc,
+	{ 0x1937, 0x1974, 0x1a0b, 0x1a6a, 0x1a88, 0x1a7e,
+		0x1a83, 0x1a84, 0x1a85, 0x1a86, 0x1a87 },
+	{ 8, 18, 31, 48, 101 }, 1
+};
+static const GSoundDriverData kDriver2 = {
+	"GSOUND.DR2", 12673, "7b0ad8a5cb56993516e0f88dbb86327d",
+	0x01d7, 0x13f0, 0x1211,
+	{ 0x1923, 0x1960, 0x19f7, 0x1a56, 0x1a74, 0x1a6a,
+		0x1a6f, 0x1a70, 0x1a71, 0x1a72, 0x1a73 },
+	{ 8, 18, 31, 35, 72 }, 2
+};
+static const GSoundDriverData kDriver3 = {
+	"GSOUND.DR3", 11970, "ce473005db35abd08c440de56b674f6c",
+	0x01d3, 0x1170, 0x0f92,
+	{ 0x1937, 0x1974, 0x1a0b, 0x1a6a, 0x1a88, 0x1a7e,
+		0x1a83, 0x1a84, 0x1a85, 0x1a86, 0x1a87 },
+	{ 8, 18, 31, 33, 73 }, 3
+};
+static const GSoundDriverData kDriver4 = {
+	"GSOUND.DR4", 15499, "19c8c5b3eba38a9d028e5ef84e6c91a6",
+	0x01ed, 0x1d90, 0x1bbb,
+	{ 0x19ed, 0x1a2a, 0x1ac1, 0x1b20, 0x1b3e, 0x1b34,
+		0x1b39, 0x1b3a, 0x1b3b, 0x1b3c, 0x1b3d },
+	{ 8, 18, 31, 40, 78 }, 4
+};
+static const GSoundDriverData kDriver5 = {
+	"GSOUND.DR5", 15262, "b34a5feed61c725db6043147546c64d2",
+	0x01ee, 0x1c90, 0x1abe,
+	{ 0x194b, 0x1988, 0x1a1f, 0x1a7e, 0x1a9c, 0x1a92,
+		0x1a97, 0x1a98, 0x1a99, 0x1a9a, 0x1a9b },
+	{ 8, 18, 31, 38, 78 }, 5
+};
+static const GSoundDriverData kDriver6 = {
+	"GSOUND.DR6", 17325, "ef84204bd776b66e636b809b305d0f55",
+	0x020c, 0x22c0, 0x20ed,
+	{ 0x1937, 0x1974, 0x1a0b, 0x1a6a, 0x1a88, 0x1a7e,
+		0x1a83, 0x1a84, 0x1a85, 0x1a86, 0x1a87 },
+	{ 8, 18, 31, 46, 98 }, 6
+};
+static const GSoundDriverData kDriver9 = {
+	"GSOUND.DR9", 30809, "7789a8e19b836876a7fcb60563dbb32e",
+	0x021b, 0x5680, 0x54a9,
+	{ 0x07c9, 0x0806, 0x0899, 0x08f8, 0x0916, 0x090c,
+		0x0911, 0x0912, 0x0913, 0x0914, 0x0915 },
+	{ 8, 18, 31, 63, 0 }, 9
+};
+
+bool validateDragonsphereGSoundFiles() {
+	const GSoundDriverData *drivers[] = {
+		&kDriver1, &kDriver2, &kDriver3, &kDriver4,
+		&kDriver5, &kDriver6, &kDriver9
+	};
+	for (uint i = 0; i < ARRAYSIZE(drivers); ++i) {
+		if (!GSound::validateOverlay(*drivers[i]))
+			return false;
+	}
+	return true;
+}
+
+static const GSoundCommandSpec kCommands1[] = {
+	M4(16, 0x90, 0x90, 1, 0x083c, 2, 0x08eb, 3, 0x0968, 4, 0x09e5),
+	D4(17, 1, 0x0a40, 2, 0x0a72, 3, 0x0aa5, 4, 0x0abe),
+	F2(24, 0x00b1, 0x00c3), F2(25, 0x00d7, 0x00e9),
+	F1(26, 0x00fd), F1(27, 0x0107), F2(28, 0x0127, 0x014b),
+	N(29), F1(30, 0x016f), F1(31, 0x01a2),
+	M4(32, 0xb0, 0xb0, 1, 0x0df6, 2, 0x0e40, 3, 0x0edd, 4, 0x0efc),
+	M4(33, 0xb0, 0xb0, 1, 0x0bf8, 2, 0x0ce6, 3, 0x0d4d, 4, 0x0d7e),
+	M3(34, 0x50, 0x50, 1, 0x2d54, 2, 0x2dca, 3, 0x2de2),
+	M4(35, 0x60, 0x60, 1, 0x2498, 2, 0x2533, 3, 0x25ba, 4, 0x2607),
+	M4(36, 0x80, 0x80, 1, 0x21da, 2, 0x22a0, 3, 0x238f, 4, 0x242a),
+	M2(37, 0xc0, 0xc0, 1, 0x317c, 2, 0x31d9),
+	M4(38, 0x60, 0x60, 1, 0x0f24, 2, 0x0fcb, 3, 0x10a7, 4, 0x1196),
+	M3(39, 0xb0, 0xb0, 1, 0x128c, 2, 0x1318, 3, 0x136f),
+	M5(40, 0xa8, 0xa8, 1, 0x13d0, 2, 0x1548, 3, 0x16ff, 4, 0x19c4, 5, 0x1bc1),
+	M5(41, 0x90, 0x90, 1, 0x1e5c, 2, 0x1ed0, 9, 0x1f49, 4, 0x1fa4, 5, 0x207f),
+	M5(42, 0x90, 0x90, 1, 0x0af8, 2, 0x0b26, 3, 0x0b5a, 4, 0x0b9a, 5, 0x0bd4),
+	X(43),
+	M6(44, 0x60, 0xe0, 1, 0x2dfa, 2, 0x2e42, 3, 0x2e8b, 4, 0x2eb5, 5, 0x303b, 6, 0x3103),
+	M5(45, 0x60, 0x60, 1, 0x277a, 2, 0x28e6, 3, 0x29ce, 4, 0x2acb, 5, 0x2bbb),
+	M2(46, 0x90, 0x90, 1, 0x3242, 2, 0x3297),
+	M4(47, 0x60, 0x60, 1, 0x0f9a, 2, 0x104c, 3, 0x1163, 4, 0x122a),
+	X(48),
+	F2(64, 0x023e, 0x024d), F1(65, 0x0270), F1(66, 0x0284),
+	F1(67, 0x02ca), F1(68, 0x02f6), F1(69, 0x0302),
+	F1(70, 0x0324), F1(71, 0x034c), F1(72, 0x035c),
+	F1(73, 0x0372), F1(74, 0x0386), F1(75, 0x03aa),
+	F1(76, 0x03e0), F2(77, 0x0402, 0x040e), F1(78, 0x041a),
+	F1(79, 0x0441), F2(80, 0x0466, 0x048f), F2(81, 0x04ba, 0x04ba),
+	F1(82, 0x04e1), F1(83, 0x0545), F1(84, 0x0574),
+	F2(85, 0x057e, 0x0592), F2(86, 0x0592, 0x05bb), F1(87, 0x05e4),
+	F2(88, 0x05fe, 0x0617), F2(89, 0x0630, 0x0642), F1(90, 0x0654),
+	F1(91, 0x067e), N(92), F2(93, 0x01f8, 0x021b), F1(94, 0x02a8),
+	F2(95, 0x06b4, 0x0716), F1(96, 0x0763), F1(97, 0x0787), N(98),
+	F1(99, 0x07a1), F2(100, 0x03f6, 0x0402), F2(101, 0x07f7, 0x07b3)
+};
+
+static const GSoundCommandSpec kCommands2[] = {
+	M2(16, 0x60, 0x60, 1, 0x1056, 2, 0x10a7),
+	D4(17, 1, 0x0386, 2, 0x03b8, 3, 0x03eb, 4, 0x0404),
+	F2(24, 0x00a8, 0x00ba), F2(25, 0x00ce, 0x00e0), F1(26, 0x00f4),
+	F1(27, 0x00fe), F2(28, 0x011e, 0x0142), N(29), F1(30, 0x016f), F1(31, 0x01a2),
+	M6(32, 0x60, 0x60, 1, 0x043e, 2, 0x0491, 3, 0x052f, 4, 0x05f1, 5, 0x067b, 6, 0x06cb),
+	M6(33, 0x60, 0x60, 1, 0x06f4, 2, 0x0795, 3, 0x0831, 4, 0x08d9, 5, 0x09ad, 6, 0x0a31),
+	M6(34, 0x60, 0x60, 1, 0x0fb8, 2, 0x1058, 3, 0x10a7, 4, 0x10fc, 5, 0x1151, 6, 0x11b2),
+	M6(35, 0xc0, 0x50, 1, 0x0ad8, 2, 0x0c17, 3, 0x0ca5, 4, 0x0d7f, 5, 0x0e57, 6, 0x0ef9),
+	F1(64, 0x01f8), F2(65, 0x0202, 0x021e), F1(66, 0x023e), F1(67, 0x025d),
+	F1(68, 0x0283), F2(69, 0x0293, 0x02a9), F2(70, 0x02c1, 0x02d1),
+	F2(71, 0x02e1, 0x031f), F2(72, 0x0337, 0x035e)
+};
+
+static const GSoundCommandSpec kCommands3[] = {
+	M6(16, 0x70, 0x70, 1, 0x038e, 2, 0x0458, 3, 0x04b7, 4, 0x0638, 5, 0x06ad, 9, 0x0783),
+	D4(17, 1, 0x0874, 2, 0x08a6, 3, 0x08d9, 4, 0x08f2),
+	F2(24, 0x00a8, 0x00ba), F2(25, 0x00ce, 0x00e0), F1(26, 0x00f4),
+	F1(27, 0x00fe), F2(28, 0x0117, 0x013b), N(29), F1(30, 0x0161), F1(31, 0x018d),
+	M6(32, 0x70, 0x70, 1, 0x0bea, 2, 0x0ca6, 3, 0x0d5f, 4, 0x0e1a, 5, 0x0e49, 6, 0x0f14),
+	M6(33, 0x70, 0x70, 1, 0x092c, 2, 0x0a02, 3, 0x0a66, 4, 0x0a77, 5, 0x0b74, 9, 0x0ba0),
+	F1(64, 0x01e3), F1(65, 0x0235), F1(66, 0x024f), F1(67, 0x0272),
+	F2(68, 0x0299, 0x02a5), F1(69, 0x02b3), F2(70, 0x02d1, 0x02fb),
+	F1(71, 0x0265), F2(72, 0x0349, 0x0366), F1(73, 0x0383)
+};
+
+static const GSoundCommandSpec kCommands4[] = {
+	M5(16, 0xc0, 0xc0, 1, 0x156b, 2, 0x15ad, 3, 0x15eb, 4, 0x1557, 9, 0x1644),
+	D4(17, 1, 0x03c4, 2, 0x03f6, 3, 0x0429, 4, 0x0442),
+	F2(24, 0x00a8, 0x00ba), F2(25, 0x00ce, 0x00e0), F1(26, 0x00f4),
+	F1(27, 0x00fe), F2(28, 0x011e, 0x0142), N(29), F1(30, 0x016f), F1(31, 0x01a2),
+	M6(32, 0x60, 0x60, 1, 0x092e, 2, 0x0a16, 3, 0x0c2e, 4, 0x0ce4, 5, 0x0d02, 6, 0x0d86),
+	M6(33, 0xc0, 0xc0, 1, 0x0e17, 2, 0x0e37, 3, 0x0e69, 4, 0x0eb4, 5, 0x0e06, 9, 0x0f7d),
+	M6(34, 0xc0, 0xc0, 1, 0x0e17, 2, 0x0e37, 3, 0x0e69, 4, 0x0eb4, 5, 0x0e06, 9, 0x0f7d),
+	M1(35, 0x54, 0x54, 1, 0x1020),
+	M5(36, 0x54, 0x54, 1, 0x047c, 2, 0x06e4, 3, 0x0735, 4, 0x0752, 5, 0x0923),
+	M5(37, 0x40, 0x40, 1, 0x1096, 2, 0x10d8, 3, 0x1109, 4, 0x1144, 5, 0x1185),
+	M5(38, 0x40, 0x40, 1, 0x11be, 2, 0x1220, 3, 0x125b, 4, 0x1290, 9, 0x12d7),
+	M6(39, 0x48, 0x48, 1, 0x1724, 2, 0x1798, 3, 0x1805, 4, 0x18c8, 5, 0x1973, 9, 0x1a32),
+	M5(40, 0xc0, 0xc0, 1, 0x1566, 2, 0x15a8, 3, 0x15e6, 4, 0x1548, 9, 0x1644),
+	F1(64, 0x01f8), F1(65, 0x0262), F1(66, 0x026c), F1(67, 0x02da),
+	F2(68, 0x0280, 0x0280), F1(69, 0x022d), F2(70, 0x02a8, 0x02bc),
+	F1(71, 0x02d0), F1(72, 0x0304), F1(73, 0x033b), F1(74, 0x0319),
+	F1(75, 0x0359), F2(76, 0x036f, 0x037f), F1(77, 0x038f), F1(78, 0x039f)
+};
+
+static const GSoundCommandSpec kCommands5[] = {
+	M6(16, 0xc0, 0xc0, 1, 0x1494, 2, 0x15f8, 3, 0x1688, 4, 0x1764, 5, 0x17e8, 9, 0x1a08),
+	D4(17, 1, 0x0440, 2, 0x0472, 3, 0x04a5, 4, 0x04be),
+	F2(24, 0x00a8, 0x00ba), F2(25, 0x00ce, 0x00e0), F1(26, 0x00f4),
+	F1(27, 0x00fe), F2(28, 0x011e, 0x0142), N(29), F1(30, 0x016f), F1(31, 0x01a4),
+	M5(32, 0x48, 0x48, 1, 0x04f8, 2, 0x053b, 3, 0x0569, 4, 0x0599, 9, 0x0628),
+	M6(33, 0x60, 0x60, 1, 0x0db4, 2, 0x0e62, 3, 0x0ef6, 4, 0x1088, 5, 0x1156, 9, 0x11f8),
+	M6(34, 0xc0, 0xc0, 1, 0x07dc, 2, 0x089a, 3, 0x095d, 4, 0x0a58, 5, 0x0af9, 9, 0x0c7c),
+	M5(35, 0xc0, 0xc0, 1, 0x0cb2, 2, 0x0cc5, 3, 0x0cfa, 4, 0x0d47, 5, 0x0d6e),
+	S(36, kGSoundDirectChannels, kGSoundStopMusic, 0, 0, 0, 0, 0, 0, 1, R(4, 0x0d83)),
+	M4(37, 0x60, 0x60, 1, 0x0720, 2, 0x075d, 3, 0x0791, 4, 0x07c5),
+	M6(38, 0xc0, 0xc0, 1, 0x084e, 2, 0x0919, 3, 0x0994, 4, 0x0ae1, 5, 0x0b6a, 9, 0x0c97),
+	F1(64, 0x028d), F1(65, 0x025d), N(66), F1(67, 0x0345),
+	F2(68, 0x03b9, 0x03b9), F1(69, 0x0382), F3(70, 0x02f9, 0x030d, 0x02e9),
+	F2(71, 0x02b3, 0x02d0), F1(72, 0x0321), F1(73, 0x01fa),
+	F2(74, 0x023d, 0x024d), F1(75, 0x0212), F2(76, 0x030d, 0x02e9),
+	N(77), F1(78, 0x03e7)
+};
+
+static const GSoundCommandSpec kCommands6[] = {
+	X(16), D4(17, 1, 0x06da, 2, 0x070c, 3, 0x073f, 4, 0x0758),
+	F2(24, 0x00a8, 0x00ba), F2(25, 0x00ce, 0x00e0), F1(26, 0x00f4),
+	F1(27, 0x00fe), F2(28, 0x011e, 0x0142), N(29), F1(30, 0x0470), F1(31, 0x016f),
+	X(32), X(33),
+	M4(34, 0x1e, 0x1e, 1, 0x07be, 2, 0x0802, 3, 0x0841, 4, 0x08d4),
+	M1(35, 0x1e, 0x1e, 1, 0x07be),
+	M4(36, 0xc8, 0xc8, 1, 0x1f1e, 2, 0x1f73, 3, 0x200f, 4, 0x2082),
+	M5(37, 0xc8, 0xc8, 1, 0x1c2a, 2, 0x1c7c, 3, 0x1cda, 4, 0x1dd8, 5, 0x1ed7),
+	M6(38, 0xc8, 0xc8, 1, 0x17f6, 2, 0x1850, 3, 0x18aa, 4, 0x1936, 5, 0x19ef, 6, 0x1bda),
+	M6(39, 0x64, 0x64, 1, 0x1266, 2, 0x1391, 3, 0x159b, 4, 0x1629, 5, 0x164f, 9, 0x1777),
+	M6(40, 0x64, 0x64, 1, 0x0baa, 2, 0x0c94, 3, 0x0d7a, 4, 0x0e6e, 5, 0x0f8e, 6, 0x108c),
+	N(41), N(42), N(43),
+	M6(44, 0x30, 0x30, 1, 0x10fe, 2, 0x1148, 3, 0x1172, 4, 0x11a6, 5, 0x11d6, 9, 0x122e),
+	X(45), N(46),
+	N(64), F1(65, 0x01c5), F1(66, 0x03e4), F2(67, 0x01ea, 0x0206),
+	F2(68, 0x0222, 0x022e), F1(69, 0x0453), F1(70, 0x02a0), F1(71, 0x042a),
+	F2(72, 0x030e, 0x032c), F1(73, 0x02e0), F2(74, 0x02f4, 0x0300),
+	F1(75, 0x0290), F1(76, 0x03b6), F1(77, 0x0390), F1(78, 0x02ac),
+	F2(79, 0x02bc, 0x02ce), X(80), F1(81, 0x023e), F2(82, 0x025c, 0x0276),
+	F2(83, 0x0344, 0x036a), F1(84, 0x03ee), F1(85, 0x040c),
+	F2(86, 0x04a3, 0x04c4), F1(87, 0x0533), F2(88, 0x04e5, 0x050c),
+	F1(89, 0x053f), F2(90, 0x055d, 0x0560), F1(91, 0x058c),
+	F2(92, 0x05b3, 0x05c5), F1(93, 0x05d7), F1(94, 0x05e5),
+	F1(95, 0x061b), X(96), F2(97, 0x0651, 0x0654), F2(98, 0x0680, 0x06b6)
+};
+
+static const GSoundCommandSpec kCommands9[] = {
+	N(16), N(17), N(18), N(24), N(25), N(26), N(27), N(28), N(29), N(30), N(31),
+	M6(32, 0x54, 0x54, 1, 0x04a4, 2, 0x04cb, 3, 0x0580, 4, 0x05bb, 5, 0x06f0, 9, 0x07db),
+	M7(33, 0x54, 0x54, 1, 0x0826, 2, 0x0888, 3, 0x08c0, 9, 0x0989, 5, 0x09d5, 6, 0x0a35, 7, 0x0b9f),
+	M7(34, 0x38, 0x38, 1, 0x0c7e, 2, 0x0e71, 3, 0x107c, 9, 0x127b, 5, 0x13fa, 6, 0x1539, 7, 0x1598),
+	M7(35, 0x50, 0x50, 1, 0x1630, 2, 0x169e, 3, 0x16d2, 4, 0x179e, 9, 0x180e, 6, 0x1832, 7, 0x18b0),
+	M7(36, 0x28, 0x28, 1, 0x1a14, 2, 0x1a8a, 3, 0x1b06, 4, 0x1b3a, 5, 0x1df2, 6, 0x1e5e, 7, 0x1eca),
+	M6(37, 0x50, 0x50, 1, 0x1f0c, 2, 0x1fa2, 3, 0x2038, 4, 0x2082, 9, 0x2156, 6, 0x21ba),
+	M7(38, 0x28, 0x28, 1, 0x22f0, 2, 0x236a, 3, 0x23ea, 4, 0x1b3a, 5, 0x241e, 9, 0x2482, 7, 0x24e4),
+	M7(39, 0x28, 0x28, 1, 0x2522, 2, 0x25b0, 3, 0x2644, 4, 0x2742, 5, 0x2bbe, 9, 0x2c58, 7, 0x2ca0),
+	M7(40, 0x38, 0x38, 1, 0x0c7e, 2, 0x0e71, 3, 0x107c, 9, 0x2d9c, 5, 0x2e92, 6, 0x2f81, 7, 0x2ff3),
+	M6(41, 0x54, 0x54, 1, 0x3054, 3, 0x3526, 9, 0x36d7, 5, 0x3772, 6, 0x3857, 7, 0x3ae0),
+	M7(42, 0x50, 0x50, 1, 0x309f, 2, 0x30f3, 3, 0x35a7, 9, 0x3708, 5, 0x37ab, 6, 0x393c, 7, 0x3b7f),
+	M3(43, 0x60, 0x60, 1, 0x0110, 2, 0x0172, 3, 0x01c2), N(44),
+	X(45), X(46),
+	M7(47, 0x54, 0x54, 1, 0x0826, 2, 0x0888, 3, 0x08c0, 9, 0x0989, 5, 0x09d5, 6, 0x0a35, 7, 0x0b9f),
+	S(48, kGSoundEffectChannel8, 0, 0, 0, 0, 0, 0, 0, 1, R(0, 0x00bc)),
+	S(49, kGSoundEffectChannel8, 0, 0, 0, 0, 0, 0, 0, 1, R(0, 0x00de)),
+	S(50, kGSoundEffectChannel8, 0, 0, 0, 0, 0, 0, 0, 1, R(0, 0x00d2)),
+	M4(51, 0x60, 0x60, 1, 0x4a78, 2, 0x4b15, 3, 0x4b9c, 4, 0x4be9),
+	M5(52, 0x54, 0x54, 1, 0x45c6, 2, 0x482e, 3, 0x487f, 4, 0x489c, 5, 0x4a6d),
+	X(53),
+	M7(54, 0x60, 0x60, 1, 0x3ce0, 2, 0x3cf1, 3, 0x3cfb, 4, 0x3d05, 5, 0x3d2f, 6, 0x3d39, 7, 0x3d63),
+	M7(55, 0x60, 0x60, 1, 0x3d72, 2, 0x3e96, 3, 0x4093, 4, 0x413b, 5, 0x4159, 6, 0x41bd, 7, 0x421f),
+	N(56),
+	M8(57, 0x30, 0x30, 1, 0x422e, 2, 0x42b5, 3, 0x4333, 4, 0x4375, 5, 0x43b1, 6, 0x4423, 7, 0x4451, 9, 0x4487),
+	M4(58, 0x90, 0x90, 1, 0x44d6, 2, 0x450f, 3, 0x4529, 4, 0x4563),
+	S(59, kGSoundDirectChannels, kGSoundStopAll, 0x54, 0x54, 1, 0x457e, 0, 0, 3, R(1, 0x457e), R(2, 0x4591), R(3, 0x45ab)),
+	N(60),
+	S(61, kGSoundEffectChannel8, 0, 0, 0, 0, 0, 0, 0, 1, R(0, 0x00f6)),
+	M5(62, 0x40, 0x40, 1, 0x4d9e, 2, 0x4f97, 3, 0x51a2, 4, 0x53a1, 5, 0x5412),
+	X(63)
+};
+
+GSoundDragonsphere::GSoundDragonsphere(Audio::Mixer *mixer,
+		const GSoundDriverData &driverData,
+		const GSoundCommandSpec *commandSpecs, uint commandSpecCount) :
+		GSound(mixer, driverData), _commandSpecs(commandSpecs),
+		_commandSpecCount(commandSpecCount), _section(driverData.section) {
+}
+
+bool GSoundDragonsphere::validCommand(int commandId) const {
+	if (commandId < 0)
+		return false;
+	if (commandId < 16)
+		return commandId <= (_section == 9 ? kDriver9.commandMax[0] :
+				_section == 1 ? kDriver1.commandMax[0] : kDriver2.commandMax[0]);
+
+	const GSoundDriverData *data = nullptr;
+	switch (_section) {
+	case 1: data = &kDriver1; break;
+	case 2: data = &kDriver2; break;
+	case 3: data = &kDriver3; break;
+	case 4: data = &kDriver4; break;
+	case 5: data = &kDriver5; break;
+	case 6: data = &kDriver6; break;
+	case 9: data = &kDriver9; break;
+	default: return false;
+	}
+	if (commandId < 24)
+		return commandId <= data->commandMax[1];
+	if (commandId < 32)
+		return commandId <= data->commandMax[2];
+	if (commandId < 64)
+		return commandId <= data->commandMax[3];
+	return data->commandMax[4] && commandId <= data->commandMax[4];
+}
+
+const GSoundCommandSpec *GSoundDragonsphere::findCommandSpec(int commandId) const {
+	for (uint i = 0; i < _commandSpecCount; ++i) {
+		if (_commandSpecs[i].command == commandId)
+			return &_commandSpecs[i];
+	}
+	return nullptr;
+}
+
+int GSoundDragonsphere::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (!validCommand(commandId))
+		return 0;
+
+	// DR1-DR6 preserve the active 32-bucket selector for command 18.
+	// DR9 has a materially different dispatcher and does not perform this store.
+	if (_section != 9 && (commandId == 16 ||
+			(commandId >= 32 && commandId < 64)))
+		setMusicIndex(commandId);
+	return executeCommand(commandId, param);
+}
+
+bool GSoundDragonsphere::runDeferredMusic(int internalCommand, uint16 guard,
+		uint16 counter, uint16 period, const GSoundChannelRoot *roots,
+		uint rootCount) {
+	if (soundActive(guard))
+		return true;
+	if (channelsActive()) {
+		scheduleSpecial(internalCommand, counter, period);
+		return true;
+	}
+	armNativeTimer(counter, period);
+	stopMusic();
+	for (uint i = 0; i < rootCount; ++i)
+		loadChannel(roots[i].channel, roots[i].offset);
+	return true;
+}
+
+bool GSoundDragonsphere::executeSpecialCommand(int commandId,
+		bool fromDeferred) {
+	if (_section == 1 && commandId == 0x101) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x2630), R(2, 0x266e), R(3, 0x26df)
+		};
+		armNativeTimer(0x50, 0x50);
+		stopMusic();
+		for (uint i = 0; i < ARRAYSIZE(roots); ++i)
+			loadChannel(roots[i].channel, roots[i].offset);
+		return true;
+	}
+	if (_section == 5 && commandId == 0x105) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x14a5), R(2, 0x1602), R(3, 0x1692),
+			R(4, 0x176e), R(5, 0x17f8), R(9, 0x1a0e)
+		};
+		armNativeTimer(0xc0, 0xc0);
+		stopMusic();
+		for (uint i = 0; i < ARRAYSIZE(roots); ++i)
+			loadChannel(roots[i].channel, roots[i].offset);
+		return true;
+	}
+	if (_section == 1 && (commandId == 43 || commandId == 48)) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x20d4), R(2, 0x2120), R(3, 0x2165),
+			R(4, 0x218c), R(5, 0x21b5)
+		};
+		if (soundActive(0x20d4))
+			return true;
+		if (channelsActive()) {
+			deferNativeCommand(commandId);
+			return true;
+		}
+		// The two public entries differ only in this verified stream byte.
+		setDataByte(0x2121, commandId == 43 ? 0x3c : 0x30);
+		armNativeTimer(0x54, 0x54);
+		stopMusic();
+		for (uint i = 0; i < ARRAYSIZE(roots); ++i)
+			loadChannel(roots[i].channel, roots[i].offset);
+		return true;
+	}
+	if (_section == 6 && commandId == 16) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x0a7a), R(2, 0x0b04), R(3, 0x0b4d)
+		};
+		if (!fromDeferred) {
+			if (soundActive(0x0a7a) || soundActive(0x1f1e) ||
+					soundActive(0x1c2a))
+				return true;
+			if (channelsActive()) {
+				scheduleSpecial(commandId, 0xc8, 0xc8);
+				return true;
+			}
+		}
+		armNativeTimer(0xc8, 0xc8);
+		setMusicIndex(16);
+		stopMusic();
+		for (uint i = 0; i < ARRAYSIZE(roots); ++i)
+			loadChannel(roots[i].channel, roots[i].offset);
+		return true;
+	}
+	if (_section == 6 && (commandId == 32 || commandId == 33)) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x0918), R(2, 0x0960), R(3, 0x09a5), R(9, 0x09ca)
+		};
+		if (!fromDeferred) {
+			if (commandId == 33) {
+				if (soundActive(0x0a59))
+					return true;
+				setDataByte(0x0a78, 0xff);
+			}
+			if (_channels[0]._activeCount && !channelPlays(1, 0x0918)) {
+				if (channelPlays(4, 0x0a59))
+					_channels[3].enableFade(0xff);
+				scheduleSpecial(commandId, 0x3c, 0x3c);
+				return true;
+			}
+		}
+		if (!channelPlays(1, 0x0918)) {
+			armNativeTimer(0x3c, 0x3c);
+			stopMusic();
+			for (uint i = 0; i < ARRAYSIZE(roots); ++i)
+				loadChannel(roots[i].channel, roots[i].offset);
+		}
+		if (getDataByte(0x0a78) == 0xff) {
+			setDataByte(0x0a78, 0);
+			loadChannel(4, 0x0a59);
+		}
+		return true;
+	}
+	if (_section == 6 && commandId == 45) {
+		if (!fromDeferred) {
+			if (soundActive(0x20b0))
+				return true;
+			if (channelsActive()) {
+				scheduleSpecial(commandId, 0x1e, 0x1e);
+				return true;
+			}
+		}
+		armNativeTimer(0x1e, 0x1e);
+		stopAll();
+		playNativeEffectAny(0x20b0);
+		return true;
+	}
+	if (_section == 6 && commandId == 80) {
+		executeCommand(4, 0);
+		return true;
+	}
+	if (_section == 6 && commandId == 96) {
+		if (!fromDeferred && channelsActive()) {
+			scheduleSpecial(commandId, 0x5a, 0x5a);
+			return true;
+		}
+		armNativeTimer(0x5a, 0x5a);
+		stopAll();
+		for (int channel = 1; channel <= 4; ++channel)
+			loadChannel(channel, 0x0792);
+		return true;
+	}
+	if (_section == 9 && commandId == 45) {
+		playNativeEffectAny(0x4c12);
+		playNativeEffectAny(0x4c74);
+		return true;
+	}
+	if (_section == 9 && commandId == 46) {
+		playNativeEffectAny(0x4cc1);
+		playNativeEffectAny(0x4d18);
+		return true;
+	}
+	if (_section == 9 && commandId == 53) {
+		if (!fromDeferred)
+			scheduleSpecial(commandId, 0x04b0, 0x04b0);
+		else
+			stopMusic();
+		return true;
+	}
+	if (_section == 9 && commandId == 63) {
+		playNativeEffectAny(0x4d70);
+		return true;
+	}
+
+	return false;
+}
+
+bool GSoundDragonsphere::executeNativeCallback(uint16 targetOffset,
+		GSoundChannel &channel) {
+	(void)channel;
+	if (_section == 1 && targetOffset == 0x1cae) {
+		executeCommand(16, 0);
+		return true;
+	}
+	if (_section == 1 && targetOffset == 0x1ddc) {
+		executeCommand(32, 0);
+		return true;
+	}
+	if (_section == 1 && targetOffset == 0x1ee2) {
+		executeCommand(40, 0);
+		return true;
+	}
+	if (_section == 1 && targetOffset == 0x1f2a) {
+		executeCommand(41, 0);
+		return true;
+	}
+	if (_section == 1 && targetOffset == 0x2053) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x2630), R(2, 0x266e), R(3, 0x26df)
+		};
+		return runDeferredMusic(0x101, 0x2630, 0x50, 0x50,
+				roots, ARRAYSIZE(roots));
+	}
+	if (_section == 3 && targetOffset == 0x1b82) {
+		// The native callback starts with channel 1. A non-null saved branch
+		// target selects channel 3, and a second one selects channel 4.
+		GSoundChannel *selected = &_channels[0];
+		if (selected->_branchTarget) {
+			selected = &_channels[2];
+			if (selected->_branchTarget)
+				selected = &_channels[3];
+		}
+		byte note = selected->_note;
+		while (note < 0x58)
+			note += 12;
+		setDataByte(0x026e, note);
+		return true;
+	}
+	if (_section == 5 && targetOffset == 0x1e81) {
+		static const GSoundChannelRoot roots[] = {
+			R(1, 0x14a5), R(2, 0x1602), R(3, 0x1692),
+			R(4, 0x176e), R(5, 0x17f8), R(9, 0x1a0e)
+		};
+		return runDeferredMusic(0x105, 0x14a5, 0xc0, 0xc0,
+				roots, ARRAYSIZE(roots));
+	}
+	if (_section == 6 && targetOffset == 0x1ffc) {
+		executeCommand(37, 0);
+		return true;
+	}
+	if (_section == 9 && targetOffset == 0x1ba6) {
+		executeCommand(32, 0);
+		return true;
+	}
+	return false;
+}
+
+GSound1::GSound1(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver1, kCommands1,
+				ARRAYSIZE(kCommands1)) {
+}
+GSound2::GSound2(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver2, kCommands2,
+				ARRAYSIZE(kCommands2)) {
+}
+GSound3::GSound3(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver3, kCommands3,
+				ARRAYSIZE(kCommands3)) {
+}
+GSound4::GSound4(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver4, kCommands4,
+				ARRAYSIZE(kCommands4)) {
+}
+GSound5::GSound5(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver5, kCommands5,
+				ARRAYSIZE(kCommands5)) {
+}
+GSound6::GSound6(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver6, kCommands6,
+				ARRAYSIZE(kCommands6)) {
+}
+GSound9::GSound9(Audio::Mixer *mixer) :
+		GSoundDragonsphere(mixer, kDriver9, kCommands9,
+				ARRAYSIZE(kCommands9)) {
+}
+
+#undef D4
+#undef M8
+#undef M7
+#undef M6
+#undef M5
+#undef M4
+#undef M3
+#undef M2
+#undef M1
+#undef DFLAGS
+#undef MFLAGS
+#undef F3
+#undef F2
+#undef F1
+#undef X
+#undef N
+#undef S
+#undef R
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/sound/gsound_dragonsphere.h b/engines/mads/dragonsphere/sound/gsound_dragonsphere.h
new file mode 100644
index 00000000000..605f264da28
--- /dev/null
+++ b/engines/mads/dragonsphere/sound/gsound_dragonsphere.h
@@ -0,0 +1,103 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_DRAGONSPHERE_SOUND_GSOUND_DRAGONSPHERE_H
+#define MADS_DRAGONSPHERE_SOUND_GSOUND_DRAGONSPHERE_H
+
+#include "mads/dragonsphere/sound/gsound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+namespace Sound {
+
+/**
+ * Dragonsphere-specific GSOUND controllers.
+ *
+ * Their tables are tied to validated retail overlays, and no table may be
+ * reused by another MADS game merely because its driver has the same name.
+ */
+class GSoundDragonsphere : public GSound {
+private:
+	const GSoundCommandSpec *_commandSpecs;
+	uint _commandSpecCount;
+	byte _section;
+
+	bool validCommand(int command) const;
+	bool runDeferredMusic(int internalCommand, uint16 guard,
+			uint16 counter, uint16 period, const GSoundChannelRoot *roots,
+			uint rootCount);
+
+protected:
+	GSoundDragonsphere(Audio::Mixer *mixer,
+			const GSoundDriverData &driverData,
+			const GSoundCommandSpec *commandSpecs, uint commandSpecCount);
+
+	const GSoundCommandSpec *findCommandSpec(int command) const override;
+	bool executeSpecialCommand(int command, bool fromDeferred) override;
+	bool executeNativeCallback(uint16 targetOffset,
+			GSoundChannel &channel) override;
+
+public:
+	int command(int commandId, int param) override;
+};
+
+class GSound1 : public GSoundDragonsphere {
+public:
+	explicit GSound1(Audio::Mixer *mixer);
+};
+
+class GSound2 : public GSoundDragonsphere {
+public:
+	explicit GSound2(Audio::Mixer *mixer);
+};
+
+class GSound3 : public GSoundDragonsphere {
+public:
+	explicit GSound3(Audio::Mixer *mixer);
+};
+
+class GSound4 : public GSoundDragonsphere {
+public:
+	explicit GSound4(Audio::Mixer *mixer);
+};
+
+class GSound5 : public GSoundDragonsphere {
+public:
+	explicit GSound5(Audio::Mixer *mixer);
+};
+
+class GSound6 : public GSoundDragonsphere {
+public:
+	explicit GSound6(Audio::Mixer *mixer);
+};
+
+class GSound9 : public GSoundDragonsphere {
+public:
+	explicit GSound9(Audio::Mixer *mixer);
+};
+
+bool validateDragonsphereGSoundFiles();
+
+} // namespace Sound
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif // MADS_DRAGONSPHERE_SOUND_GSOUND_DRAGONSPHERE_H
diff --git a/engines/mads/dragonsphere/sound/sound.cpp b/engines/mads/dragonsphere/sound/sound.cpp
index 99374556c7d..0f618053ef4 100644
--- a/engines/mads/dragonsphere/sound/sound.cpp
+++ b/engines/mads/dragonsphere/sound/sound.cpp
@@ -23,6 +23,7 @@
 #include "audio/fmopl.h"
 #include "common/textconsole.h"
 #include "mads/dragonsphere/sound/asound_dragonsphere.h"
+#include "mads/dragonsphere/sound/gsound_dragonsphere.h"
 #include "mads/dragonsphere/sound/psound_dragonsphere.h"
 #include "mads/dragonsphere/sound/rsound_dragonsphere.h"
 
@@ -67,11 +68,24 @@ SoundDriver *createPSound(Audio::Mixer *mixer, int sectionNumber,
 	}
 }
 
+SoundDriver *createGSound(Audio::Mixer *mixer, int sectionNumber) {
+	switch (sectionNumber) {
+	case 1: return new GSound1(mixer);
+	case 2: return new GSound2(mixer);
+	case 3: return new GSound3(mixer);
+	case 4: return new GSound4(mixer);
+	case 5: return new GSound5(mixer);
+	case 6: return new GSound6(mixer);
+	case 9: return new GSound9(mixer);
+	default: return nullptr;
+	}
+}
+
 } // namespace
 
 DragonSoundManager::DragonSoundManager(Audio::Mixer *mixer,
 		bool &soundFlag, bool usePas, bool isDemo) :
-		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+		SoundManager(mixer, soundFlag, !isDemo), _isDemo(isDemo) {
 	if (usePas && _driverType == SOUND_ADLIB) {
 		if (OPL::Config::detect(OPL::Config::kOpl3) >= 0) {
 			_driverType = SOUND_PAS;
@@ -83,7 +97,13 @@ DragonSoundManager::DragonSoundManager(Audio::Mixer *mixer,
 }
 
 void DragonSoundManager::validate() {
-	if (_driverType == SOUND_PAS) {
+	if (_driverType == SOUND_GM) {
+		if (!_isDemo && validateDragonsphereGSoundFiles())
+			return;
+		warning("Cannot use Dragonsphere General MIDI sound data; using AdLib");
+		_driverType = SOUND_ADLIB;
+		ASound::validate(_isDemo);
+	} else if (_driverType == SOUND_PAS) {
 		bool valid = true;
 		if (_isDemo) {
 			const int demoSections[] = { 1, 9 };
@@ -126,7 +146,16 @@ void DragonSoundManager::validate() {
 void DragonSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_driverType == SOUND_PAS) {
+	if (_driverType == SOUND_GM) {
+		_driver = createGSound(_mixer, sectionNumber);
+		if (_driver && !static_cast<GSound *>(_driver)->isReady()) {
+			warning("Could not initialize Dragonsphere General MIDI output; "
+					"falling back to AdLib");
+			removeDriver();
+			_driverType = SOUND_ADLIB;
+			loadDriver(sectionNumber);
+		}
+	} else if (_driverType == SOUND_PAS) {
 		_driver = createPSound(_mixer, sectionNumber, _isDemo);
 		if (_driver && !static_cast<PSound *>(_driver)->isReady()) {
 			warning("Could not initialize Pro Audio Spectrum 16 OPL3 output; "
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 4cc8f0c5c9a..5db16de117e 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -367,6 +367,8 @@ MODULE_OBJS := \
 	dragonsphere/rooms/room909.o \
 	dragonsphere/sound/asound.o \
 	dragonsphere/sound/asound_dragonsphere.o \
+	dragonsphere/sound/gsound.o \
+	dragonsphere/sound/gsound_dragonsphere.o \
 	dragonsphere/sound/psound.o \
 	dragonsphere/sound/psound_dragonsphere.o \
 	dragonsphere/sound/rsound.o \


Commit: 8628b42cd7d435701e5c01da41f3f62b1a5b8cde
    https://github.com/scummvm/scummvm/commit/8628b42cd7d435701e5c01da41f3f62b1a5b8cde
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Preserve queued sound command parameters

Store each queued command with its parameter and replay both values in
order after held sound output resumes.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/core/sound.cpp
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h


diff --git a/engines/mads/core/sound.cpp b/engines/mads/core/sound.cpp
index 076ff63531f..56681441d0e 100644
--- a/engines/mads/core/sound.cpp
+++ b/engines/mads/core/sound.cpp
@@ -29,8 +29,9 @@ int sound_play(int soundNum) {
 	return sound_queue(soundNum);
 }
 
-int sound_queue(int soundNum, int /*distance*/) {
-	return (g_engine->_soundManager) ? g_engine->_soundManager->command(soundNum) : 0;
+int sound_queue(int soundNum, int distance) {
+	return (g_engine->_soundManager) ?
+		g_engine->_soundManager->command(soundNum, distance) : 0;
 }
 
 void sound_queue_hold() {
diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 7b881683238..7f66cf06856 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -105,8 +105,8 @@ void SoundManager::startQueuedCommands() {
 	_newSoundsPaused = false;
 
 	while (!_queuedCommands.empty()) {
-		int commandId = _queuedCommands.pop();
-		command(commandId);
+		const QueuedCommand queuedCommand = _queuedCommands.pop();
+		command(queuedCommand._commandId, queuedCommand._param);
 	}
 }
 
@@ -119,8 +119,10 @@ void SoundManager::setVolume(int volume) {
 
 int SoundManager::command(int commandId, int param) {
 	if (_newSoundsPaused) {
-		if (_queuedCommands.size() < 8)
-			_queuedCommands.push(commandId);
+		if (_queuedCommands.size() < 8) {
+			QueuedCommand queuedCommand = { commandId, param };
+			_queuedCommands.push(queuedCommand);
+		}
 		return _queuedCommands.size() - 1;
 	} else if (_driver) {
 		// Note: I don't know any way to identify music commands versus sfx
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index 58cd3b1fe8c..f16c0d0e9bf 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -93,6 +93,11 @@ public:
 
 class SoundManager {
 protected:
+	struct QueuedCommand {
+		int _commandId;
+		int _param;
+	};
+
 	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_GM, SOUND_PCSPEAKER, SOUND_PAS };
 	Audio::Mixer *_mixer;
 	DriverType _driverType;
@@ -101,7 +106,7 @@ protected:
 	bool _pollSoundEnabled = false;
 	bool _soundPollFlag = false;
 	bool _newSoundsPaused = false;
-	Common::Queue<int> _queuedCommands;
+	Common::Queue<QueuedCommand> _queuedCommands;
 	int _masterVolume = 255;
 
 protected:


Commit: b91d5844d7ea1747f7eb2e303f8ee8075692680d
    https://github.com/scummvm/scummvm/commit/b91d5844d7ea1747f7eb2e303f8ee8075692680d
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Return native sound command results

Return each selected driver's command result so callers can observe
native status queries while preserving muted and queued-command
behavior.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/core/sound_manager.cpp


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 7f66cf06856..352710c2e47 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -128,7 +128,7 @@ int SoundManager::command(int commandId, int param) {
 		// Note: I don't know any way to identify music commands versus sfx
 		// commands, so if sfx is mute, then so is music
 		if (_soundFlag)
-			_driver->command(commandId, param);
+			return _driver->command(commandId, param);
 	}
 
 	return 0;


Commit: e48ce89c6f6ef66e79ab66b9cc89dfefed1b07e6
    https://github.com/scummvm/scummvm/commit/e48ce89c6f6ef66e79ab66b9cc89dfefed1b07e6
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Remove obsolete sound manager state

Remove polling flags and the unused enable switch that have had no
reader since their introduction, without changing driver or queue
lifetimes.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 352710c2e47..4a885ff6749 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -80,7 +80,6 @@ bool SoundManager::isDriverActive() {
 void SoundManager::closeDriver() {
 	if (_driver) {
 		command(0);
-		setEnabled(false);
 		stop();
 
 		removeDriver();
@@ -92,11 +91,6 @@ void SoundManager::removeDriver() {
 	_driver = nullptr;
 }
 
-void SoundManager::setEnabled(bool flag) {
-	_pollSoundEnabled = flag;
-	_soundPollFlag = false;
-}
-
 void SoundManager::pauseNewCommands() {
 	_newSoundsPaused = true;
 }
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index f16c0d0e9bf..af34c331987 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -103,8 +103,6 @@ protected:
 	DriverType _driverType;
 	bool &_soundFlag;
 	SoundDriver *_driver = nullptr;
-	bool _pollSoundEnabled = false;
-	bool _soundPollFlag = false;
 	bool _newSoundsPaused = false;
 	Common::Queue<QueuedCommand> _queuedCommands;
 	int _masterVolume = 255;
@@ -160,12 +158,6 @@ public:
 	 */
 	void removeDriver();
 
-	/**
-	 * Sets the enabled status of the sound
-	 * @flag		True if sound should be enabled
-	 */
-	void setEnabled(bool flag);
-
 	/**
 	 * Temporarily pause the playback of any new sound commands
 	 */


Commit: 6ed1126f0fa25abca74ad152f017fb2995ff2896
    https://github.com/scummvm/scummvm/commit/6ed1126f0fa25abca74ad152f017fb2995ff2896
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Warn about unavailable section sound drivers

Warn and continue silently when a valid section has no mapped sound
driver, while leaving corruption checks and Macintosh audio unchanged.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/core/sound_manager.cpp


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 4a885ff6749..cc3525c1eff 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -24,6 +24,7 @@
 #include "common/config-manager.h"
 #include "common/file.h"
 #include "common/memstream.h"
+#include "common/textconsole.h"
 #include "mads/core/sound_manager.h"
 
 namespace Audio {
@@ -68,6 +69,11 @@ void SoundManager::init(int sectionNumber) {
 	// Load the correct driver for the section
 	removeDriver();
 	loadDriver(sectionNumber);
+	if (!_driver) {
+		warning("No MADS sound driver is available for section %d",
+			sectionNumber);
+		return;
+	}
 
 	// Set volume for newly loaded driver
 	_driver->setVolume(_masterVolume);


Commit: 019a7afd0ae92e42d3e99ca8f0659afd5dfe29a8
    https://github.com/scummvm/scummvm/commit/019a7afd0ae92e42d3e99ca8f0659afd5dfe29a8
Author: fusefib (fibofuse at gmail.com)
Date: 2026-08-12T16:18:54+10:00

Commit Message:
MADS: Add sound driver debugger commands

Add debugger commands that report the selected section driver and issue
commands through that active driver without overriding device selection.

Assisted-by: Codex:GPT-5.6-sol

Changed paths:
    engines/mads/console.cpp
    engines/mads/console.h


diff --git a/engines/mads/console.cpp b/engines/mads/console.cpp
index 2ce9dafb9b5..471177039b0 100644
--- a/engines/mads/console.cpp
+++ b/engines/mads/console.cpp
@@ -27,6 +27,7 @@
 #include "mads/core/matte.h"
 #include "mads/core/mem.h"
 #include "mads/core/text.h"
+#include "mads/mads.h"
 
 namespace MADS {
 
@@ -35,6 +36,9 @@ Console::Console() : GUI::Debugger() {
 	registerCmd("teleport", WRAP_METHOD(Console, cmdTeleport));
 	registerCmd("walkable", WRAP_METHOD(Console, cmdWalkable));
 	registerCmd("quotes", WRAP_METHOD(Console, cmdQuotes));
+	registerCmd("soundcommand", WRAP_METHOD(Console, cmdSoundCommand));
+	registerCmd("soundsection", WRAP_METHOD(Console, cmdSoundSection));
+	registerCmd("soundstop", WRAP_METHOD(Console, cmdSoundStop));
 	registerCmd("text", WRAP_METHOD(Console, cmdText));
 }
 
@@ -153,6 +157,56 @@ bool Console::cmdQuotes(int argc, const char **argv) {
 	return true;
 }
 
+bool Console::cmdSoundCommand(int argc, const char **argv) {
+	if (argc < 2 || argc > 3) {
+		debugPrintf("Usage: %s <command> [parameter]\n", argv[0]);
+		return true;
+	}
+	if (!g_engine->_soundManager->isLoaded()) {
+		debugPrintf("No section sound driver is loaded. Use soundsection first.\n");
+		return true;
+	}
+
+	const int commandId = strToInt(argv[1]);
+	const int parameter = argc == 3 ? strToInt(argv[2]) : 0;
+	const int result = g_engine->_soundManager->command(commandId, parameter);
+	debugPrintf("Sound command %d(%d) returned %d.\n",
+		commandId, parameter, result);
+	return true;
+}
+
+bool Console::cmdSoundSection(int argc, const char **argv) {
+	if (argc != 2) {
+		debugPrintf("Usage: %s <section 1-9>\n", argv[0]);
+		return true;
+	}
+
+	const int section = strToInt(argv[1]);
+	if (section < 1 || section > 9) {
+		debugPrintf("Section must be between 1 and 9.\n");
+		return true;
+	}
+
+	g_engine->_soundManager->init(section);
+	debugPrintf(
+		g_engine->_soundManager->isLoaded()
+			? "Loaded section %d for the configured audio device.\n"
+			: "No sound driver is available for section %d.\n",
+		section);
+	return true;
+}
+
+bool Console::cmdSoundStop(int argc, const char **argv) {
+	if (argc != 1) {
+		debugPrintf("Usage: %s\n", argv[0]);
+		return true;
+	}
+
+	g_engine->_soundManager->stop();
+	debugPrintf("Stopped the current section sound driver.\n");
+	return true;
+}
+
 static bool textBufferContains(const char *haystack, uint16 haystackLen, const char *needle) {
 	size_t needleLen = strlen(needle);
 	if (needleLen == 0 || haystackLen < needleLen)
diff --git a/engines/mads/console.h b/engines/mads/console.h
index 6fec4c0a7d2..100b5f19ce9 100644
--- a/engines/mads/console.h
+++ b/engines/mads/console.h
@@ -34,6 +34,9 @@ private:
 	bool cmdWalkable(int argc, const char **argv);
 	bool cmdDepth(int argc, const char **argv);
 	bool cmdQuotes(int argc, const char **argv);
+	bool cmdSoundCommand(int argc, const char **argv);
+	bool cmdSoundSection(int argc, const char **argv);
+	bool cmdSoundStop(int argc, const char **argv);
 	bool cmdText(int argc, const char **argv);
 
 public:




More information about the Scummvm-git-logs mailing list