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

dreammaster noreply at scummvm.org
Sun Jul 26 09:32:43 UTC 2026


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

Summary:
3fec380be2 MADS: NEBULAR: Beginning of Roland MT32 Support
60263a3762 MADS: Add Nebular's sound manager refactor to the other games
c30730b28c MADS: NEBULAR: Implemented RSound1 through RSound4


Commit: 3fec380be245a52cd99613b6ca1db8df317ae5a1
    https://github.com/scummvm/scummvm/commit/3fec380be245a52cd99613b6ca1db8df317ae5a1
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-26T19:32:30+10:00

Commit Message:
MADS: NEBULAR: Beginning of Roland MT32 Support

Refactored the ASound classes to have their own independant
OPL instance rather than the overall SoundManager, and
added initial Claude generated implementation of
Rex Nebular's Roland MT32 sound driver rsound.009

Assisted-by: Claude:claude-sonnet-5

Changed paths:
  A engines/mads/nebular/rsound.cpp
  A engines/mads/nebular/rsound.h
  A engines/mads/nebular/rsound_nebular.cpp
  A engines/mads/nebular/rsound_nebular.h
  A engines/mads/nebular/sound.cpp
  A engines/mads/nebular/sound.h
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h
    engines/mads/dragonsphere/asound.cpp
    engines/mads/dragonsphere/asound.h
    engines/mads/dragonsphere/asound_dragonsphere.cpp
    engines/mads/dragonsphere/asound_dragonsphere.h
    engines/mads/module.mk
    engines/mads/nebular/asound.cpp
    engines/mads/nebular/asound.h
    engines/mads/nebular/asound_nebular.cpp
    engines/mads/nebular/asound_nebular.h
    engines/mads/nebular/nebular.cpp
    engines/mads/phantom/asound.cpp
    engines/mads/phantom/asound.h
    engines/mads/phantom/asound_phantom.cpp
    engines/mads/phantom/asound_phantom.h


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index c645dbda9ad..3ff2608bce8 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -20,6 +20,7 @@
  */
 
 #include "audio/fmopl.h"
+#include "audio/mididrv.h"
 #include "common/file.h"
 #include "common/memstream.h"
 #include "mads/core/sound_manager.h"
@@ -31,8 +32,9 @@ class Mixer;
 namespace MADS {
 
 SoundManager::SoundManager(Audio::Mixer *mixer, bool &soundFlag) : _mixer(mixer), _soundFlag(soundFlag) {
-	_opl = OPL::Config::create();
-	_opl->init();
+	MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_ADLIB | MDT_MIDI | MDT_PREFER_MT32);
+	MusicType musicType = MidiDriver::getMusicType(dev);
+	_isMT32 = musicType == MT_MT32;
 }
 
 SoundManager::~SoundManager() {
@@ -40,17 +42,13 @@ SoundManager::~SoundManager() {
 		_driver->stop();
 		delete _driver;
 	}
-
-	delete _opl;
 }
 
 void SoundManager::init(int sectionNumber) {
 	assert(sectionNumber > 0 && sectionNumber < 10);
 
-	if (_driver != nullptr)
-		delete _driver;
-
 	// Load the correct driver for the section
+	removeDriver();
 	loadDriver(sectionNumber);
 
 	// Set volume for newly loaded driver
@@ -124,8 +122,8 @@ void SoundManager::noise() {
 
 //====================================================================
 
-SoundDriver::SoundDriver(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
-		int dataOffset, int dataSize) : _mixer(mixer), _opl(opl) {
+SoundDriver::SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize) : _mixer(mixer) {
 	// Open up the appropriate sound file
 	Common::File soundFile;
 	if (!soundFile.open(filename))
@@ -136,8 +134,4 @@ SoundDriver::SoundDriver(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path
 	soundFile.read(&_soundData[0], dataSize);
 }
 
-SoundDriver::~SoundDriver() {
-	_opl->stop();
-}
-
 } // namespace MADS
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index 377f15ae2d2..eef61d89943 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -31,10 +31,6 @@ namespace Audio {
 class Mixer;
 }
 
-namespace OPL {
-class OPL;
-}
-
 namespace MADS {
 
 #define CALLBACKS_PER_SECOND 60
@@ -42,7 +38,6 @@ namespace MADS {
 class SoundDriver {
 protected:
 	Audio::Mixer *_mixer;
-	OPL::OPL *_opl;
 	Common::Array<byte> _soundData;
 	Common::Mutex _driverMutex;
 
@@ -54,9 +49,9 @@ protected:
 	}
 
 public:
-	SoundDriver(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
+	SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
 		int dataOffset, int dataSize);
-	virtual ~SoundDriver();
+	virtual ~SoundDriver() {}
 
 	/**
 	 * Execute a player command. Most commands represent sounds to play, but some
@@ -90,8 +85,8 @@ public:
 class SoundManager {
 protected:
 	Audio::Mixer *_mixer;
+	bool _isMT32;
 	bool &_soundFlag;
-	OPL::OPL *_opl = nullptr;
 	SoundDriver *_driver = nullptr;
 	bool _pollSoundEnabled = false;
 	bool _soundPollFlag = false;
diff --git a/engines/mads/dragonsphere/asound.cpp b/engines/mads/dragonsphere/asound.cpp
index da2b586283e..749ee5b68c6 100644
--- a/engines/mads/dragonsphere/asound.cpp
+++ b/engines/mads/dragonsphere/asound.cpp
@@ -235,9 +235,9 @@ void AdlibChannel::processChannelFade() {
 		_fadePeriodCounter = 1;
 }
 
-ASound::ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
+ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename,
 	int dataOffset, int dataSize)
-	: SoundDriver(mixer, opl, filename, dataOffset, dataSize) {
+	: SoundDriver(mixer, filename, dataOffset, dataSize) {
 	AdlibChannel::_isDisabled = false;
 
 	/* Standard OPL timer-reset sequence. */
diff --git a/engines/mads/dragonsphere/asound.h b/engines/mads/dragonsphere/asound.h
index 335b099af8a..f02efb5dd8f 100644
--- a/engines/mads/dragonsphere/asound.h
+++ b/engines/mads/dragonsphere/asound.h
@@ -22,6 +22,7 @@
 #ifndef MADS_DRAGONSPHERE_ASOUND_H
 #define MADS_DRAGONSPHERE_ASOUND_H
 
+#include "audio/fmopl.h"
 #include "common/mutex.h"
 #include "common/queue.h"
 #include "common/util.h"
@@ -193,6 +194,8 @@ protected:
 	typedef void (ASound::*CallbackFunction)();
 
 private:
+	OPL::OPL *_opl = OPL::Config::create();
+
 	// ---- callback / tick state ------------------------------------------
 	uint16 _callbackCounter = 0;  // per-tick countdown
 	uint16 _callbackPeriod = 0;  // reload value; 0 = callback disabled
@@ -607,15 +610,15 @@ public:
 	/**
 	 * Constructor.
 	 * @param mixer       Mixer instance
-	 * @param opl         OPL chip instance
 	 * @param filename    Path to the .DR1 (or equivalent) sound-driver file
 	 * @param dataOffset  Offset in the file of the data segment
 	 * @param dataSize    Size of the data segment
 	 */
-	ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
+	ASound(Audio::Mixer *mixer, const Common::Path &filename,
 		int dataOffset, int dataSize);
 
 	~ASound() override {
+		delete _opl;
 	}
 
 	/** Stop all currently playing sounds (wraps command0). */
diff --git a/engines/mads/dragonsphere/asound_dragonsphere.cpp b/engines/mads/dragonsphere/asound_dragonsphere.cpp
index 00e7dda45c2..db34e862ba8 100644
--- a/engines/mads/dragonsphere/asound_dragonsphere.cpp
+++ b/engines/mads/dragonsphere/asound_dragonsphere.cpp
@@ -69,30 +69,30 @@ void DragonSoundManager::loadDriver(int sectionNumber) {
 	switch (sectionNumber) {
 	case 1:
 		if (_isDemo)
-			_driver = new ASoundDemo1(_mixer, _opl);
+			_driver = new ASoundDemo1(_mixer);
 		else
-			_driver = new ASound1(_mixer, _opl);
+			_driver = new ASound1(_mixer);
 		break;
 	case 2:
-		_driver = new ASound2(_mixer, _opl);
+		_driver = new ASound2(_mixer);
 		break;
 	case 3:
-		_driver = new ASound3(_mixer, _opl);
+		_driver = new ASound3(_mixer);
 		break;
 	case 4:
-		_driver = new ASound4(_mixer, _opl);
+		_driver = new ASound4(_mixer);
 		break;
 	case 5:
-		_driver = new ASound5(_mixer, _opl);
+		_driver = new ASound5(_mixer);
 		break;
 	case 6:
-		_driver = new ASound6(_mixer, _opl);
+		_driver = new ASound6(_mixer);
 		break;
 	case 9:
 		if (_isDemo)
-			_driver = new ASoundDemo9(_mixer, _opl);
+			_driver = new ASoundDemo9(_mixer);
 		else
-			_driver = new ASound9(_mixer, _opl);
+			_driver = new ASound9(_mixer);
 		break;
 	default:
 		_driver = nullptr;
@@ -145,8 +145,8 @@ const ASound1::CommandPtr ASound1::_commandList[102] = {
 	&ASound1::command100, &ASound1::command101
 };
 
-ASound1::ASound1(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr1", 0x2520, 0x49e0) {
+ASound1::ASound1(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr1", 0x2520, 0x49e0) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -787,8 +787,8 @@ const ASound2::CommandPtr ASound2::_commandList[76] = {
 	&ASound2::command72,    nullptr,             nullptr,             nullptr
 };
 
-ASound2::ASound2(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr2", 0x1fa0, 0x2950) {
+ASound2::ASound2(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr2", 0x1fa0, 0x2950) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -1076,8 +1076,8 @@ const ASound3::CommandPtr ASound3::_commandList[77] = {
 	nullptr,             nullptr,             nullptr
 };
 
-ASound3::ASound3(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr3", 0x1f30, 0x2750) {
+ASound3::ASound3(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr3", 0x1f30, 0x2750) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
@@ -1351,8 +1351,8 @@ const ASound4::CommandPtr ASound4::_commandList[82] = {
 	&ASound4::command80, &ASound4::command81
 };
 
-ASound4::ASound4(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr4", 0x2120, 0x31d0) {
+ASound4::ASound4(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr4", 0x2120, 0x31d0) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -1712,8 +1712,8 @@ const ASound5::CommandPtr ASound5::_commandList[82] = {
 	&ASound5::command80, &ASound5::command81
 };
 
-ASound5::ASound5(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr5", 0x20d0, 0x2ee0) {
+ASound5::ASound5(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr5", 0x20d0, 0x2ee0) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -2046,8 +2046,8 @@ const ASound6::CommandPtr ASound6::_commandList[102] = {
 	&ASound6::command100, &ASound6::command101
 };
 
-ASound6::ASound6(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr6", 0x2370, 0x3870) {
+ASound6::ASound6(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr6", 0x2370, 0x3870) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -2507,8 +2507,8 @@ const ASound9::CommandPtr ASound9::_commandList[65] = {
 	nullptr,
 };
 
-ASound9::ASound9(Audio::Mixer *mixer, OPL::OPL *opl) :
-		ASound(mixer, opl, "asound.dr9", 0x23a0, 0x6a10) {
+ASound9::ASound9(Audio::Mixer *mixer) :
+		ASound(mixer, "asound.dr9", 0x23a0, 0x6a10) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
@@ -3107,8 +3107,8 @@ const ASoundDemo1::CommandPtr ASoundDemo1::_commandList[93] = {
 	&ASoundDemo1::command90, &ASoundDemo1::command91, &ASoundDemo1::command92
 };
 
-ASoundDemo1::ASoundDemo1(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr1", 0x23e0, 0x4900) {
+ASoundDemo1::ASoundDemo1(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr1", 0x23e0, 0x4900) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
@@ -3571,8 +3571,8 @@ const ASoundDemo9::CommandPtr ASoundDemo9::_commandList[51] = {
 	&ASoundDemo9::command50
 };
 
-ASoundDemo9::ASoundDemo9(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.dr9", 0x23a0, 0x62b0) {
+ASoundDemo9::ASoundDemo9(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.dr9", 0x23a0, 0x62b0) {
 	auto samplesStream = getDataStream(0x1dc);
 	for (int i = 0; i < 182; ++i)
 		_samples.push_back(AdlibSample(samplesStream));
diff --git a/engines/mads/dragonsphere/asound_dragonsphere.h b/engines/mads/dragonsphere/asound_dragonsphere.h
index e1fb6b29e41..2b4864d1ec6 100644
--- a/engines/mads/dragonsphere/asound_dragonsphere.h
+++ b/engines/mads/dragonsphere/asound_dragonsphere.h
@@ -171,7 +171,7 @@ private:
 	int command101();
 
 public:
-	ASound1(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound1(Audio::Mixer *mixer);
 	~ASound1() override {}
 	int command(int commandId, int param) override;
 };
@@ -218,7 +218,7 @@ private:
 	int command68(); int command69_70(); int command71(); int command72();
 
 public:
-	ASound2(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound2(Audio::Mixer *mixer);
 	~ASound2() override {}
 	int command(int commandId, int param) override;
 };
@@ -287,7 +287,7 @@ private:
 	int command73();
 
 public:
-	ASound3(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound3(Audio::Mixer *mixer);
 	~ASound3() override {}
 	int command(int commandId, int param) override;
 };
@@ -343,7 +343,7 @@ private:
 	int command80(); int command81();
 
 public:
-	ASound4(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound4(Audio::Mixer *mixer);
 	~ASound4() override {}
 	int command(int commandId, int param) override;
 };
@@ -402,7 +402,7 @@ private:
 	int command80(); int command81();
 
 public:
-	ASound5(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound5(Audio::Mixer *mixer);
 	~ASound5() override {}
 	int command(int commandId, int param) override;
 };
@@ -463,7 +463,7 @@ private:
 	int command100(); int command101();
 
 public:
-	ASound6(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound6(Audio::Mixer *mixer);
 	~ASound6() override {}
 	int command(int commandId, int param) override;
 };
@@ -514,7 +514,7 @@ private:
 	int command90(); int command91(); int command92();
 
 public:
-	ASoundDemo1(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASoundDemo1(Audio::Mixer *mixer);
 	~ASoundDemo1() override {}
 	int command(int commandId, int param) override;
 };
@@ -563,7 +563,7 @@ private:
 	static const CommandPtr _commandList[65];
 
 public:
-	ASound9(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound9(Audio::Mixer *mixer);
 	~ASound9() override {}
 	int command(int commandId, int param) override;
 };
@@ -617,7 +617,7 @@ private:
 	int command50();
 
 public:
-	ASoundDemo9(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASoundDemo9(Audio::Mixer *mixer);
 	~ASoundDemo9() override {}
 	int command(int commandId, int param) override;
 };
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index f044b1b41b5..4cd57314887 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -68,6 +68,8 @@ MODULE_OBJS := \
 	nebular/nebular.o \
 	nebular/asound.o \
 	nebular/asound_nebular.o \
+	nebular/rsound.o \
+	nebular/rsound_nebular.o \
 	nebular/console.o \
 	nebular/copy.o \
 	nebular/extra.o \
@@ -75,6 +77,7 @@ MODULE_OBJS := \
 	nebular/main.o \
 	nebular/main_menu.o \
 	nebular/menus.o \
+	nebular/sound.o \
 	nebular/popup.o \
 	nebular/mads/mads.o \
 	nebular/rooms/room101.o \
diff --git a/engines/mads/nebular/asound.cpp b/engines/mads/nebular/asound.cpp
index d7bdce5d689..0e7f2061469 100644
--- a/engines/mads/nebular/asound.cpp
+++ b/engines/mads/nebular/asound.cpp
@@ -20,6 +20,8 @@
  */
 
 #include "audio/fmopl.h"
+#include "common/file.h"
+#include "common/md5.h"
 #include "common/memstream.h"
 #include "mads/nebular/asound.h"
 
@@ -114,8 +116,8 @@ AdlibSample::AdlibSample(Common::SeekableReadStream &s) {
 
 /*-----------------------------------------------------------------------*/
 
-ASound::ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
-	int dataOffset, int dataSize) : SoundDriver(mixer, opl, filename, dataOffset, dataSize) {
+ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset, int dataSize) :
+		SoundDriver(mixer, filename, dataOffset, dataSize) {
 	// Initialize fields
 	_commandParam = 0;
 	_activeChannelPtr = nullptr;
@@ -164,6 +166,33 @@ ASound::ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
 	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer), CALLBACKS_PER_SECOND);
 }
 
+void ASound::validate() {
+	Common::File f;
+	static const char *const MD5[] = {
+		"205398468de2c8873b7d4d73d5be8ddc",
+		"f9b2d944a2fb782b1af5c0ad592306d3",
+		"7431f8dad77d6ddfc24e6f3c0c4ac7df",
+		"eb1f3f5a4673d3e73d8ac1818c957cf4",
+		"f936dd853073fa44f3daac512e91c476",
+		"3dc139d3e02437a6d9b732072407c366",
+		"af0edab2934947982e9a405476702e03",
+		"8cbc25570b50ba41c9b5361cad4fbedc",
+		"a31e4783e098f633cbb6689adb41dd4f"
+	};
+
+	for (int i = 1; i <= 9; ++i) {
+		Common::Path filename(Common::String::format("ASOUND.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])
+			error("Invalid sound file - %s", filename.toString().c_str());
+	}
+}
+
+
 void ASound::adlibInit() {
 	write(4, 0x60);
 	write(4, 0x80);
@@ -666,5 +695,173 @@ int ASound::command8() {
 	return result;
 }
 
+/*-----------------------------------------------------------------------*/
+
+RexASound::RexASound(Audio::Mixer *mixer,  const Common::Path &filename, int dataOffset, int dataSize) :
+		ASound(mixer, filename, dataOffset, dataSize) {
+	_chanCommandCount = 15;
+}
+
+void RexASound::channelCommand(byte *&pSrc, bool &updateFlag) {
+	AdlibChannel *chan = _activeChannelPtr;
+	int cmdNum = 255 - *pSrc;
+
+	switch (cmdNum) {
+	case 0:
+		if (!chan->_innerLoopCount) {
+			if (*++pSrc == 0) {
+				chan->_pSrc += 2;
+				chan->_innerLoopPtr = chan->_pSrc;
+				chan->_innerLoopCount = 0;
+			} else {
+				chan->_innerLoopCount = *pSrc;
+				chan->_pSrc = chan->_innerLoopPtr;
+			}
+		} else if (--chan->_innerLoopCount) {
+			chan->_pSrc = chan->_innerLoopPtr;
+		} else {
+			chan->_pSrc += 2;
+			chan->_innerLoopPtr = chan->_pSrc;
+		}
+		break;
+
+	case 1:
+		if (!chan->_outerLoopCount) {
+			if (*++pSrc == 0) {
+				chan->_pSrc += 2;
+				chan->_outerLoopPtr = chan->_pSrc;
+				chan->_innerLoopPtr = chan->_pSrc;
+				chan->_innerLoopCount = 0;
+				chan->_outerLoopCount = 0;
+			} else {
+				chan->_outerLoopCount = *pSrc;
+				chan->_pSrc = chan->_outerLoopPtr;
+				chan->_innerLoopPtr = chan->_outerLoopPtr;
+			}
+		} else if (--chan->_outerLoopCount) {
+			chan->_outerLoopPtr = chan->_pSrc;
+			chan->_innerLoopPtr = chan->_pSrc;
+		} else {
+			chan->_pSrc += 2;
+			chan->_outerLoopPtr = chan->_pSrc;
+			chan->_innerLoopPtr = chan->_pSrc;
+		}
+		break;
+
+	case 2:
+		// Loop sound data
+		chan->_pitchBend = 0;
+		chan->_volumeFadeStep = chan->_attenFadeStep = 0;
+		chan->_volume = chan->_noteOffset = 0;
+		chan->_transpose = chan->_volumeOffset = 0;
+		chan->_keyOnDelay = 0;
+		chan->_volumeFadeCounter = 0;
+		chan->_attenFadeCounter = 0;
+		chan->_innerLoopCount = 0;
+		chan->_outerLoopCount = 0;
+		chan->_patchAttenuation = 0x40;
+		chan->_ptr1 = chan->_soundData;
+		chan->_pSrc = chan->_soundData;
+		chan->_innerLoopPtr = chan->_soundData;
+		chan->_outerLoopPtr = chan->_soundData;
+
+		chan->_pSrc += 2;
+		break;
+
+	case 3:
+		chan->_sampleIndex = *++pSrc;
+		chan->_pSrc += 2;
+		loadSample(chan->_sampleIndex);
+		break;
+
+	case 4:
+		chan->_noteOffset = *++pSrc;
+		chan->_pSrc += 2;
+		break;
+
+	case 5:
+		chan->_pitchBend = *++pSrc;
+		chan->_pSrc += 2;
+		break;
+
+	case 6:
+		++pSrc;
+		if (chan->_pendingStop) {
+			chan->_pSrc += 2;
+		} else {
+			chan->_volume = *pSrc >> 1;
+			updateFlag = true;
+			chan->_pSrc += 2;
+		}
+		break;
+
+	case 7:
+		++pSrc;
+		if (!chan->_pendingStop) {
+			chan->_volumeFadeReload = *pSrc;
+			chan->_volumeFadeStep = *++pSrc;
+			chan->_volumeFadeCounter = 1;
+		}
+
+		chan->_pSrc += 3;
+		break;
+
+	case 8:
+		chan->_transpose = (int8) * ++pSrc;
+		chan->_pSrc += 2;
+		break;
+
+	case 9:
+	{
+		int v1 = *++pSrc;
+		++pSrc;
+		int v2 = (v1 - 1) & getRandomNumber();
+		int v3 = pSrc[v2];
+		int v4 = pSrc[v1];
+
+		pSrc[v4 + v1 + 1] = v3;
+		chan->_pSrc += v1 + 3;
+		break;
+	}
+
+	case 10:
+		++pSrc;
+		if (chan->_pendingStop) {
+			chan->_pSrc += 2;
+		} else {
+			chan->_volumeOffset = *pSrc >> 1;
+			updateFlag = true;
+			chan->_pSrc += 2;
+		}
+		break;
+
+	case 11:
+		chan->_patchAttenuation = *++pSrc;
+		updateFlag = true;
+		chan->_pSrc += 2;
+		break;
+
+	case 12:
+		chan->_attenFadeReload = *++pSrc;
+		chan->_attenFadeStep = *++pSrc;
+		chan->_attenFadeCounter = 1;
+		chan->_pSrc += 2;
+		break;
+
+	case 13:
+		++pSrc;
+		chan->_pSrc += 2;
+		break;
+
+	case 14:
+		chan->_octaveTranspose = *++pSrc;
+		chan->_pSrc += 2;
+		break;
+
+	default:
+		break;
+	}
+}
+
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/asound.h b/engines/mads/nebular/asound.h
index 817175a897e..687ac82aede 100644
--- a/engines/mads/nebular/asound.h
+++ b/engines/mads/nebular/asound.h
@@ -22,6 +22,7 @@
 #ifndef MADS_NEBULAR_ASOUND_H
 #define MADS_NEBULAR_ASOUND_H
 
+#include "audio/fmopl.h"
 #include "mads/core/sound_manager.h"
 
 namespace MADS {
@@ -133,6 +134,7 @@ struct RegisterValue {
  */
 class ASound : public SoundDriver {
 private:
+	OPL::OPL *_opl = OPL::Config::create();
 	uint16 _randomSeed;
 	int _masterVolume;
 
@@ -327,29 +329,30 @@ public:
 	int _activeChannelReg;
 	int _outputReg;         // scratch OPL operator register offset used within loadSample()
 	bool _amDep, _vibDep, _splitPoint;
+
+public:
+	/**
+	 * Validates the Adlib sound files
+	 */
+	static void validate();
+
 public:
 	/**
 	 * Constructor
 	 * @param mixer			Mixer
-	 * @param opl			OPL
 	 * @param filename		Specifies the adlib sound player file to use
 	 * @param dataOffset	Offset in the file of the data segment
 	 * @param dataSize		Size of the data segment
 	 */
-	ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
-		int dataOffset, int dataSize);
+	ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset, int dataSize);
 
 	/**
 	 * Destructor
 	 */
 	~ASound() override {
+		delete _opl;
 	}
 
-	/**
-	 * Validates the Adlib sound files
-	 */
-	static void validate();
-
 	/**
 	 * Stop all currently playing sounds
 	 */
@@ -378,6 +381,15 @@ public:
 	void setVolume(int volume) override;
 };
 
+// TODO: Merge RexASound into ASound
+class RexASound : public ASound {
+protected:
+	void channelCommand(byte *&pSrc, bool &updateFlag) override;
+
+public:
+	RexASound(Audio::Mixer *mixer,  const Common::Path &filename, int dataOffset, int dataSize);
+};
+
 } // namespace RexNebular
 } // namespace MADS
 
diff --git a/engines/mads/nebular/asound_nebular.cpp b/engines/mads/nebular/asound_nebular.cpp
index 6c901782551..d3a26455677 100644
--- a/engines/mads/nebular/asound_nebular.cpp
+++ b/engines/mads/nebular/asound_nebular.cpp
@@ -20,248 +20,11 @@
  */
 
 #include "audio/fmopl.h"
-#include "common/algorithm.h"
-#include "common/file.h"
-#include "common/md5.h"
 #include "mads/nebular/asound_nebular.h"
 
-namespace Audio {
-class Mixer;
-}
-
 namespace MADS {
 namespace RexNebular {
 
-void RexSoundManager::validate() {
-	Common::File f;
-	static const char *const MD5[] = {
-		"205398468de2c8873b7d4d73d5be8ddc",
-		"f9b2d944a2fb782b1af5c0ad592306d3",
-		"7431f8dad77d6ddfc24e6f3c0c4ac7df",
-		"eb1f3f5a4673d3e73d8ac1818c957cf4",
-		"f936dd853073fa44f3daac512e91c476",
-		"3dc139d3e02437a6d9b732072407c366",
-		"af0edab2934947982e9a405476702e03",
-		"8cbc25570b50ba41c9b5361cad4fbedc",
-		"a31e4783e098f633cbb6689adb41dd4f"
-	};
-
-	for (int i = 1; i <= 9; ++i) {
-		Common::Path filename(Common::String::format("ASOUND.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])
-			error("Invalid sound file - %s", filename.toString().c_str());
-	}
-}
-
-void RexSoundManager::loadDriver(int sectionNumber) {
-	switch (sectionNumber) {
-	case 1:
-		_driver = new RexNebular::ASound1(_mixer, _opl);
-		break;
-	case 2:
-		_driver = new RexNebular::ASound2(_mixer, _opl);
-		break;
-	case 3:
-		_driver = new RexNebular::ASound3(_mixer, _opl);
-		break;
-	case 4:
-		_driver = new RexNebular::ASound4(_mixer, _opl);
-		break;
-	case 5:
-		_driver = new RexNebular::ASound5(_mixer, _opl);
-		break;
-	case 6:
-		_driver = new RexNebular::ASound6(_mixer, _opl);
-		break;
-	case 7:
-		_driver = new RexNebular::ASound7(_mixer, _opl);
-		break;
-	case 8:
-		_driver = new RexNebular::ASound8(_mixer, _opl);
-		break;
-	case 9:
-		_driver = new RexNebular::ASound9(_mixer, _opl);
-		break;
-	default:
-		_driver = nullptr;
-		return;
-	}
-}
-
-/*-----------------------------------------------------------------------*/
-
-RexASound::RexASound(Audio::Mixer *mixer, OPL::OPL *opl,
-		const Common::Path &filename, int dataOffset, int dataSize) :
-		ASound(mixer, opl, filename, dataOffset, dataSize) {
-	_chanCommandCount = 15;
-}
-
-void RexASound::channelCommand(byte *&pSrc, bool &updateFlag) {
-	AdlibChannel *chan = _activeChannelPtr;
-	int cmdNum = 255 - *pSrc;
-
-	switch (cmdNum) {
-	case 0:
-		if (!chan->_innerLoopCount) {
-			if (*++pSrc == 0) {
-				chan->_pSrc += 2;
-				chan->_innerLoopPtr = chan->_pSrc;
-				chan->_innerLoopCount = 0;
-			} else {
-				chan->_innerLoopCount = *pSrc;
-				chan->_pSrc = chan->_innerLoopPtr;
-			}
-		} else if (--chan->_innerLoopCount) {
-			chan->_pSrc = chan->_innerLoopPtr;
-		} else {
-			chan->_pSrc += 2;
-			chan->_innerLoopPtr = chan->_pSrc;
-		}
-		break;
-
-	case 1:
-		if (!chan->_outerLoopCount) {
-			if (*++pSrc == 0) {
-				chan->_pSrc += 2;
-				chan->_outerLoopPtr = chan->_pSrc;
-				chan->_innerLoopPtr = chan->_pSrc;
-				chan->_innerLoopCount = 0;
-				chan->_outerLoopCount = 0;
-			} else {
-				chan->_outerLoopCount = *pSrc;
-				chan->_pSrc = chan->_outerLoopPtr;
-				chan->_innerLoopPtr = chan->_outerLoopPtr;
-			}
-		} else if (--chan->_outerLoopCount) {
-			chan->_outerLoopPtr = chan->_pSrc;
-			chan->_innerLoopPtr = chan->_pSrc;
-		} else {
-			chan->_pSrc += 2;
-			chan->_outerLoopPtr = chan->_pSrc;
-			chan->_innerLoopPtr = chan->_pSrc;
-		}
-		break;
-
-	case 2:
-		// Loop sound data
-		chan->_pitchBend = 0;
-		chan->_volumeFadeStep = chan->_attenFadeStep = 0;
-		chan->_volume = chan->_noteOffset = 0;
-		chan->_transpose = chan->_volumeOffset = 0;
-		chan->_keyOnDelay = 0;
-		chan->_volumeFadeCounter = 0;
-		chan->_attenFadeCounter = 0;
-		chan->_innerLoopCount = 0;
-		chan->_outerLoopCount = 0;
-		chan->_patchAttenuation = 0x40;
-		chan->_ptr1 = chan->_soundData;
-		chan->_pSrc = chan->_soundData;
-		chan->_innerLoopPtr = chan->_soundData;
-		chan->_outerLoopPtr = chan->_soundData;
-
-		chan->_pSrc += 2;
-		break;
-
-	case 3:
-		chan->_sampleIndex = *++pSrc;
-		chan->_pSrc += 2;
-		loadSample(chan->_sampleIndex);
-		break;
-
-	case 4:
-		chan->_noteOffset = *++pSrc;
-		chan->_pSrc += 2;
-		break;
-
-	case 5:
-		chan->_pitchBend = *++pSrc;
-		chan->_pSrc += 2;
-		break;
-
-	case 6:
-		++pSrc;
-		if (chan->_pendingStop) {
-			chan->_pSrc += 2;
-		} else {
-			chan->_volume = *pSrc >> 1;
-			updateFlag = true;
-			chan->_pSrc += 2;
-		}
-		break;
-
-	case 7:
-		++pSrc;
-		if (!chan->_pendingStop) {
-			chan->_volumeFadeReload = *pSrc;
-			chan->_volumeFadeStep = *++pSrc;
-			chan->_volumeFadeCounter = 1;
-		}
-
-		chan->_pSrc += 3;
-		break;
-
-	case 8:
-		chan->_transpose = (int8) * ++pSrc;
-		chan->_pSrc += 2;
-		break;
-
-	case 9:
-	{
-		int v1 = *++pSrc;
-		++pSrc;
-		int v2 = (v1 - 1) & getRandomNumber();
-		int v3 = pSrc[v2];
-		int v4 = pSrc[v1];
-
-		pSrc[v4 + v1 + 1] = v3;
-		chan->_pSrc += v1 + 3;
-		break;
-	}
-
-	case 10:
-		++pSrc;
-		if (chan->_pendingStop) {
-			chan->_pSrc += 2;
-		} else {
-			chan->_volumeOffset = *pSrc >> 1;
-			updateFlag = true;
-			chan->_pSrc += 2;
-		}
-		break;
-
-	case 11:
-		chan->_patchAttenuation = *++pSrc;
-		updateFlag = true;
-		chan->_pSrc += 2;
-		break;
-
-	case 12:
-		chan->_attenFadeReload = *++pSrc;
-		chan->_attenFadeStep = *++pSrc;
-		chan->_attenFadeCounter = 1;
-		chan->_pSrc += 2;
-		break;
-
-	case 13:
-		++pSrc;
-		chan->_pSrc += 2;
-		break;
-
-	case 14:
-		chan->_octaveTranspose = *++pSrc;
-		chan->_pSrc += 2;
-		break;
-
-	default:
-		break;
-	}
-}
-
 /*-----------------------------------------------------------------------*/
 
 const ASound1::CommandPtr ASound1::_commandList[42] = {
@@ -278,8 +41,7 @@ const ASound1::CommandPtr ASound1::_commandList[42] = {
 	&ASound1::command40, &ASound1::command41
 };
 
-ASound1::ASound1(Audio::Mixer *mixer, OPL::OPL *opl)
-	: RexASound(mixer, opl, "asound.001", 0x1520, 0x17b0) {
+ASound1::ASound1(Audio::Mixer *mixer) : RexASound(mixer, "asound.001", 0x1520, 0x17b0) {
 	_cmd23Toggle = false;
 
 	// Load sound samples
@@ -580,8 +342,7 @@ const ASound2::CommandPtr ASound2::_commandList[44] = {
 	&ASound2::command40, &ASound2::command41, &ASound2::command42, &ASound2::command43
 };
 
-ASound2::ASound2(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.002", 0x15E0, 0x4b70) {
+ASound2::ASound2(Audio::Mixer *mixer) : RexASound(mixer, "asound.002", 0x15E0, 0x4b70) {
 	_command12Param = 0xFD;
 
 	// Load sound samples
@@ -952,8 +713,7 @@ const ASound3::CommandPtr ASound3::_commandList[61] = {
 	&ASound3::command60
 };
 
-ASound3::ASound3(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.003", 0x15B0, 0x5020) {
+ASound3::ASound3(Audio::Mixer *mixer) : RexASound(mixer, "asound.003", 0x15B0, 0x5020) {
 	_command39Flag = false;
 
 	// Load sound samples
@@ -1357,8 +1117,7 @@ const ASound4::CommandPtr ASound4::_commandList[61] = {
 	&ASound4::command60
 };
 
-ASound4::ASound4(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.004", 0x14F0, 0x2930) {
+ASound4::ASound4(Audio::Mixer *mixer) : RexASound(mixer, "asound.004", 0x14F0, 0x2930) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x122);
 	for (int i = 0; i < 210; ++i)
@@ -1614,8 +1373,7 @@ const ASound5::CommandPtr ASound5::_commandList[42] = {
 	&ASound5::command40, &ASound5::command41
 };
 
-ASound5::ASound5(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.005", 0x15E0, 0x2200) {
+ASound5::ASound5(Audio::Mixer *mixer) : RexASound(mixer, "asound.005", 0x15E0, 0x2200) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x144);
 	for (int i = 0; i < 164; ++i)
@@ -1856,8 +1614,7 @@ const ASound6::CommandPtr ASound6::_commandList[30] = {
 	&ASound6::nullCommand, &ASound6::command29
 };
 
-ASound6::ASound6(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.006", 0x1390, 0x22d0) {
+ASound6::ASound6(Audio::Mixer *mixer) : RexASound(mixer, "asound.006", 0x1390, 0x22d0) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x122);
 	for (int i = 0; i < 200; ++i)
@@ -2013,8 +1770,7 @@ const ASound7::CommandPtr ASound7::_commandList[38] = {
 	&ASound7::command36, &ASound7::command37
 };
 
-ASound7::ASound7(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.007", 0x1460, 0x2cf0) {
+ASound7::ASound7(Audio::Mixer *mixer) : RexASound(mixer, "asound.007", 0x1460, 0x2cf0) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x122);
 	for (int i = 0; i < 214; ++i)
@@ -2222,8 +1978,7 @@ const ASound8::CommandPtr ASound8::_commandList[38] = {
 	&ASound8::command36, &ASound8::command37
 };
 
-ASound8::ASound8(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.008", 0x1490, 0x1810) {
+ASound8::ASound8(Audio::Mixer *mixer) : RexASound(mixer, "asound.008", 0x1490, 0x1810) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x122);
 	for (int i = 0; i < 174; ++i)
@@ -2478,8 +2233,7 @@ const ASound9::CommandPtr ASound9::_commandList[52] = {
 	&ASound9::command48, &ASound9::command49, &ASound9::command50, &ASound9::command51
 };
 
-ASound9::ASound9(Audio::Mixer *mixer, OPL::OPL *opl) :
-		RexASound(mixer, opl, "asound.009", 0x16F0, 0x85a0) {
+ASound9::ASound9(Audio::Mixer *mixer) : RexASound(mixer, "asound.009", 0x16F0, 0x85a0) {
 	_callbackCounter = _callbackPeriod = 0;
 	_callbackFnPtr = nullptr;
 
diff --git a/engines/mads/nebular/asound_nebular.h b/engines/mads/nebular/asound_nebular.h
index 73e7e82dbf1..420ceb98dd4 100644
--- a/engines/mads/nebular/asound_nebular.h
+++ b/engines/mads/nebular/asound_nebular.h
@@ -27,28 +27,6 @@
 namespace MADS {
 namespace RexNebular {
 
-class RexSoundManager : public SoundManager {
-protected:
-	void loadDriver(int sectionNum) override;
-
-public:
-	RexSoundManager(Audio::Mixer *mixer, bool &soundFlag) : SoundManager(mixer, soundFlag) {
-	}
-	~RexSoundManager() override {
-	}
-
-	void validate() override;
-};
-
-class RexASound : public ASound {
-protected:
-	void channelCommand(byte *&pSrc, bool &updateFlag) override;
-
-public:
-	RexASound(Audio::Mixer *mixer, OPL::OPL *opl,
-		const Common::Path &filename, int dataOffset, int dataSize);
-};
-
 class ASound1 : public RexASound {
 private:
 	typedef int (ASound1:: *CommandPtr)();
@@ -92,7 +70,7 @@ private:
 	void command111213();
 	int command2627293032();
 public:
-	ASound1(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound1(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -144,7 +122,7 @@ private:
 	void command9Randomize();
 	void command9Apply(byte *data, int val, int incr);
 public:
-	ASound2(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound2(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -204,7 +182,7 @@ private:
 	void command9Randomize();
 	void command9Apply(byte *data, int val, int incr);
 public:
-	ASound3(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound3(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -242,7 +220,7 @@ private:
 
 	void method1();
 public:
-	ASound4(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound4(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -288,7 +266,7 @@ private:
 	int command42();
 	int command43();
 public:
-	ASound5(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound5(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -317,7 +295,7 @@ private:
 	int command25();
 	int command29();
 public:
-	ASound6(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound6(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -349,7 +327,7 @@ private:
 	int command36();
 	int command37();
 public:
-	ASound7(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound7(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -392,7 +370,7 @@ private:
 	void method1(byte *pData);
 	void adjustRange(byte *pData, byte v, int incr);
 public:
-	ASound8(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound8(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
@@ -471,7 +449,7 @@ private:
 	void loadCommand47();
 	void loadCommand50();
 public:
-	ASound9(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound9(Audio::Mixer *mixer);
 
 	int command(int commandId, int param) override;
 };
diff --git a/engines/mads/nebular/nebular.cpp b/engines/mads/nebular/nebular.cpp
index 6d8da1c9f8e..63ad35d5e84 100644
--- a/engines/mads/nebular/nebular.cpp
+++ b/engines/mads/nebular/nebular.cpp
@@ -44,7 +44,7 @@
 #include "mads/nebular/popup.h"
 #include "mads/nebular/mads/inventory.h"
 #include "mads/nebular/mads/words.h"
-#include "mads/nebular/asound_nebular.h"
+#include "mads/nebular/sound.h"
 #include "mads/nebular/rooms/section1.h"
 #include "mads/nebular/rooms/section2.h"
 #include "mads/nebular/rooms/section3.h"
diff --git a/engines/mads/nebular/rsound.cpp b/engines/mads/nebular/rsound.cpp
new file mode 100644
index 00000000000..6fa15a53853
--- /dev/null
+++ b/engines/mads/nebular/rsound.cpp
@@ -0,0 +1,694 @@
+/* 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/memstream.h"
+#include "mads/nebular/rsound.h"
+
+namespace MADS {
+namespace RexNebular {
+
+void Channel::reset(byte *startPtr) {
+	_pitchBendFadeStep = 0;
+	_panFadeStep = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_noteOffset = 0;
+	_pendingStop = 0;
+	_volumeFadeReload = 0xFF;
+	_pitchBend = 64;
+	_pan = 64;
+	_loopStartPtr = startPtr;
+	_pSrc = startPtr;
+	_innerLoopPtr = startPtr;
+	_outerLoopPtr = startPtr;
+	_soundData = startPtr;
+}
+
+void Channel::enable(int flag) {
+	if (_activeCount) {
+		_pendingStop = flag;
+
+		// WORKAROUND: matches the same apparent original-code quirk
+		// documented in AdlibChannel::enable() - the original set
+		// _soundData to the flag value here, which we replace with
+		// a simple null pointer.
+		_soundData = nullptr;
+	}
+}
+
+void Channel::load(byte *pData) {
+	reset(pData);
+	_activeCount = 1;
+	_owner->sendPitchBend(_midiChannel, 0x40);
+}
+
+/*-----------------------------------------------------------------------*/
+
+RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize) : SoundDriver(mixer, filename, dataOffset, dataSize) {
+	_commandParam = 0;
+	_frameCounter = 0;
+	_isDisabled = false;
+	_masterVolume = 255;
+	_randomSeed = 1234;
+	_lastMidiStatus = 0;
+	_noteTriggeredThisPoll = false;
+	_pollResult = 0;
+	_resultFlag = 0;
+
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
+		_channels[i]._owner = this;
+		_channels[i]._midiChannel = i + 1;
+	}
+
+	// rsound_init calls sub_10132 (resetAllChannels) directly, then later
+	// (via initDeviceOnce, on successful hardware detection) calls
+	// rsound_command0, which resets the channels again and sends the
+	// GM-reset messages to the device. Since we don't do real hardware
+	// detection here, just go straight to command0() - matches ASound's
+	// constructor calling command0() directly.
+	command0();
+
+	//_opl->start(new Common::Functor0Mem<void, RSound>(this, &RSound::onTimer), CALLBACKS_PER_SECOND);
+}
+
+void RSound::validate() {
+	Common::File f;
+	static const char *const MD5[] = {
+		"205398468de2c8873b7d4d73d5be8ddc",
+		"f9b2d944a2fb782b1af5c0ad592306d3",
+		"7431f8dad77d6ddfc24e6f3c0c4ac7df",
+		"eb1f3f5a4673d3e73d8ac1818c957cf4",
+		"f936dd853073fa44f3daac512e91c476",
+		"3dc139d3e02437a6d9b732072407c366",
+		"af0edab2934947982e9a405476702e03",
+		"8cbc25570b50ba41c9b5361cad4fbedc",
+		"a31e4783e098f633cbb6689adb41dd4f"
+	};
+
+	for (int i = 1; i <= 9; ++i) {
+		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])
+			error("Invalid sound file - %s", filename.toString().c_str());
+	}
+}
+
+
+int RSound::stop() {
+	command0();
+	int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+int RSound::poll() {
+	update();
+
+	int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void RSound::resultCheck() {
+	if (_resultFlag != 1) {
+		_resultFlag = 1;
+		_pollResult = 1;
+	}
+}
+
+Channel *RSound::playSound(int offset) {
+	return playSoundData(loadData(offset));
+}
+
+Channel *RSound::playSoundData(byte *pData, int startingChannel) {
+	// Scan for a free channel. Deliberately excludes channel 9 (index 8),
+	// matching the disassembly - playSound() never reaches it.
+	for (int i = startingChannel; i < RSOUND_CHANNEL_COUNT - 1; ++i) {
+		if (!_channels[i]._activeCount) {
+			_channels[i].load(pData);
+			return &_channels[i];
+		}
+	}
+
+	// None found; fall back to an interruptable (pending-stop) channel
+	for (int i = RSOUND_CHANNEL_COUNT - 2; i >= startingChannel; --i) {
+		if (_channels[i]._pendingStop == 0xFF) {
+			_channels[i].load(pData);
+			return &_channels[i];
+		}
+	}
+
+	return nullptr;
+}
+
+bool RSound::isSoundActive(byte *pData) {
+	// Deliberately excludes channel 9 (index 8), matching the disassembly -
+	// same as playSoundData()'s scan never reaching channel 9 either.
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT - 1; ++i) {
+		if (_channels[i]._activeCount && _channels[i]._soundData == pData)
+			return true;
+	}
+
+	return false;
+}
+
+int RSound::getRandomNumber() {
+	int v = 0x9249 + (int)_randomSeed;
+	_randomSeed = ((v >> 3) | (v << 13)) & 0xFFFF;
+	return _randomSeed;
+}
+
+void RSound::onTimer() {
+	Common::StackLock slock(_driverMutex);
+	poll();
+}
+
+void RSound::setVolume(int volume) {
+	_masterVolume = volume;
+	if (!volume)
+		command0();
+}
+
+/*-----------------------------------------------------------------------*/
+// 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);
+}
+
+void RSound::sendProgramChange(int midiChannel, int program) {
+	sendStatus(midiChannel, 0xC0);
+	sendMidiByte(program);
+}
+
+void RSound::sendVolume(int midiChannel, int volume) {
+	sendStatus(midiChannel, 0xB0);
+	sendMidiByte(7); // CC#7: Channel Volume
+	sendMidiByte(volume);
+}
+
+void RSound::sendPitchBend(int midiChannel, int value) {
+	sendStatus(midiChannel, 0xE0);
+	sendMidiByte(0);     // LSB always 0 - only coarse (MSB) control is used
+	sendMidiByte(value);
+}
+
+void RSound::sendPan(int midiChannel, int value) {
+	sendStatus(midiChannel, 0xB0);
+	sendMidiByte(10); // CC#10: Pan
+	sendMidiByte(value);
+}
+
+void RSound::muteChannel(int midiChannel) {
+	sendStatus(midiChannel, 0xB0);
+	sendMidiByte(7);
+	sendMidiByte(0);
+}
+
+void RSound::restoreChannelVolume(int midiChannel, int volume) {
+	sendStatus(midiChannel, 0xB0);
+	sendMidiByte(7);
+	sendMidiByte(volume);
+}
+
+void RSound::sendSysEx(int offset) {
+	warning("RSound: SysEx block at offset %04X not yet implemented (device init handshake)", offset);
+}
+
+/*-----------------------------------------------------------------------*/
+
+void RSound::Channel_flushHeldNotes(Channel *channel) {
+	byte *slots = _heldNotes[channel->_midiChannel];
+
+	for (int i = 0; i < 4; ++i) {
+		if (slots[i] == 0xFF)
+			break;
+
+		sendStatus(channel->_midiChannel, 0x90);
+		sendMidiByte(slots[i]);
+		sendMidiByte(0); // velocity 0 = note off
+		slots[i] = 0xFF;
+	}
+}
+
+void RSound::Channel_checkFade(Channel *channel) {
+	if (!channel->_activeCount)
+		return;
+	if (!channel->_pendingStop)
+		return;
+
+	if (channel->_volume != 0) {
+		--channel->_volume;
+		sendVolume(channel->_midiChannel, channel->_volume);
+	} else {
+		// Fully silent - recycle the channel to the fixed "silence" stream
+		// (unk_14566 in the disassembly, at offset 0x3246 relative to
+		// seg001's load address - matches sub_1029F exactly)
+		channel->_pSrc = loadData(0x3246);
+		channel->_pendingStop = 0;
+	}
+}
+
+void RSound::checkFadingChannels() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		Channel_checkFade(&_channels[i]);
+}
+
+void RSound::Channel_pollActive(Channel *channel) {
+	if (!channel->_activeCount)
+		return;
+
+	int midiChannel = channel->_midiChannel;
+
+	if (channel->_keyOnDelay > 0 && --channel->_keyOnDelay == 0)
+		Channel_flushHeldNotes(channel);
+
+	if (--channel->_activeCount <= 0) {
+		for (;;) {
+			byte *pSrc = channel->_pSrc;
+
+			if (*pSrc < 0x80) {
+				// Plain (note, duration) pair
+				if (_noteTriggeredThisPoll) {
+					// Already fired a note-on this poll tick - defer to next tick
+					_noteTriggeredThisPoll = false;
+					break;
+				}
+
+				byte note = pSrc[0];
+				byte duration = pSrc[1];
+				channel->_note = note;
+				channel->_activeCount = duration;
+				channel->_pSrc = pSrc + 2;
+
+				if (!note || !duration) {
+					Channel_flushHeldNotes(channel);
+				} else {
+					channel->_keyOnDelay = channel->_activeCount - channel->_noteOffset;
+
+					bool skipRetrigger = false;
+					if ((int8)channel->_noteOffset < 0 && _heldNotes[midiChannel][0] == note)
+						skipRetrigger = true;
+
+					if (!skipRetrigger) {
+						Channel_flushHeldNotes(channel);
+						sendNoteOn(midiChannel, note, channel->_velocity);
+					}
+					_heldNotes[midiChannel][0] = note;
+				}
+
+				break;
+			}
+
+			// Opcode dispatch (bytes 0xF1-0xFF)
+			int opcode = *pSrc - 0xF1;
+			switch (opcode) {
+			case 0: // 0xF1: no-op (consume and ignore)
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 1: { // 0xF2: self-modifying randomize (byte swap in the data stream)
+				int v1 = *++pSrc;
+				++pSrc;
+				int v2 = (v1 - 1) & getRandomNumber();
+				int v3 = pSrc[v2];
+				int v4 = pSrc[v1];
+				pSrc[v4 + v1 + 1] = v3;
+				channel->_pSrc += v1 + 3;
+				break;
+			}
+
+			case 2: // 0xF3: setup pan fade
+				channel->_panFadeReload = pSrc[1];
+				channel->_panFadeStep = pSrc[2];
+				channel->_panFadeCounter = 1;
+				channel->_pSrc = pSrc + 3;
+				break;
+
+			case 3: // 0xF4: set pan directly and send
+				channel->_pan = pSrc[1];
+				sendPan(midiChannel, channel->_pan);
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 4: { // 0xF5: chord note-on trigger (up to 4 simultaneous notes)
+				byte noteCount = pSrc[1];
+				byte *notes = pSrc + 2;
+				byte *slots = _heldNotes[midiChannel];
+
+				int i;
+				for (i = 0; i < noteCount; ++i) {
+					byte note = notes[i];
+					if (slots[i] != note) {
+						sendNoteOn(midiChannel, note, channel->_velocity);
+						slots[i] = note;
+					}
+				}
+				for (; i < 4; ++i)
+					slots[i] = 0xFF;
+
+				byte duration = notes[noteCount];
+				channel->_activeCount = duration;
+				if (channel->_noteOffset != 0xFF) {
+					channel->_keyOnDelay = (duration < channel->_noteOffset) ?
+						duration : duration - channel->_noteOffset;
+				} else {
+					channel->_keyOnDelay = duration + 1;
+				}
+
+				channel->_pSrc = pSrc + noteCount + 3;
+				_noteTriggeredThisPoll = true;
+				break;
+			}
+
+			case 5: // 0xF6: set volume directly and send (priority-gated while pending-stop)
+				if (!channel->_pendingStop || channel->_volume >= pSrc[1]) {
+					channel->_volume = pSrc[1];
+					sendVolume(midiChannel, channel->_volume);
+				}
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 6: // 0xF7: set pitch bend directly and send
+				channel->_pitchBend = pSrc[1];
+				sendPitchBend(midiChannel, channel->_pitchBend);
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 7: // 0xF8: setup volume fade (gated by pending-stop)
+				if (!channel->_pendingStop) {
+					channel->_volumeFadeReload = pSrc[1];
+					channel->_volumeFadeCounter = pSrc[1];
+					channel->_volumeFadeStep = pSrc[2];
+				}
+				channel->_pSrc = pSrc + 3;
+				break;
+
+			case 8: // 0xF9: set velocity
+				channel->_velocity = pSrc[1];
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 9: // 0xFA: setup pitch-bend ramp
+				channel->_pitchBendFadeReload = pSrc[1];
+				channel->_pitchBendFadeStep = pSrc[2];
+				channel->_pitchBendFadeCount = pSrc[3];
+				channel->_pitchBendFadeCounter = 1;
+				channel->_pSrc = pSrc + 4;
+				break;
+
+			case 10: // 0xFB: set note offset
+				channel->_noteOffset = pSrc[1];
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 11: // 0xFC: program change
+				channel->_program = pSrc[1];
+				sendProgramChange(midiChannel, channel->_program);
+				channel->_pSrc = pSrc + 2;
+				break;
+
+			case 12: { // 0xFD: full reset / loop restart, preserving pending-stop
+				int pendingStop = channel->_pendingStop;
+				channel->reset(channel->_loopStartPtr);
+				channel->_pendingStop = pendingStop;
+				break;
+			}
+
+			case 13: // 0xFE: outer loop
+				if (!channel->_outerLoopCount) {
+					if (*++pSrc == 0) {
+						channel->_pSrc += 2;
+						channel->_outerLoopPtr = channel->_pSrc;
+						channel->_innerLoopPtr = channel->_pSrc;
+						channel->_innerLoopCount = 0;
+						channel->_outerLoopCount = 0;
+					} else {
+						channel->_outerLoopCount = *pSrc;
+						channel->_pSrc = channel->_outerLoopPtr;
+						channel->_innerLoopPtr = channel->_outerLoopPtr;
+					}
+				} else if (--channel->_outerLoopCount) {
+					channel->_outerLoopPtr = channel->_pSrc;
+					channel->_innerLoopPtr = channel->_pSrc;
+				} else {
+					channel->_pSrc += 2;
+					channel->_outerLoopPtr = channel->_pSrc;
+					channel->_innerLoopPtr = channel->_pSrc;
+				}
+				break;
+
+			case 14: // 0xFF: inner loop
+				if (!channel->_innerLoopCount) {
+					if (*++pSrc == 0) {
+						channel->_pSrc += 2;
+						channel->_innerLoopPtr = channel->_pSrc;
+						channel->_innerLoopCount = 0;
+					} else {
+						channel->_innerLoopCount = *pSrc;
+						channel->_pSrc = channel->_innerLoopPtr;
+					}
+				} else if (--channel->_innerLoopCount) {
+					channel->_pSrc = channel->_innerLoopPtr;
+				} else {
+					channel->_pSrc += 2;
+					channel->_innerLoopPtr = channel->_pSrc;
+				}
+				break;
+
+			default:
+				break;
+			}
+		}
+	}
+
+	// Fade tail: pitch bend, volume, pan
+	if (channel->_pitchBendFadeStep) {
+		if (!--channel->_pitchBendFadeCounter) {
+			channel->_pitchBendFadeCounter = channel->_pitchBendFadeReload;
+			channel->_pitchBend += channel->_pitchBendFadeStep;
+			sendPitchBend(midiChannel, channel->_pitchBend);
+		}
+		if (!--channel->_pitchBendFadeCount)
+			channel->_pitchBendFadeStep = 0;
+	}
+
+	if (channel->_volumeFadeStep) {
+		if (!--channel->_volumeFadeCounter) {
+			channel->_volumeFadeCounter = channel->_volumeFadeReload;
+			int8 newVolume = (int8)channel->_volume + (int8)channel->_volumeFadeStep;
+			if (newVolume < 0) {
+				channel->_volumeFadeStep = 0;
+				newVolume = 0;
+			} else if (newVolume >= 0x7F) {
+				channel->_volumeFadeStep = 0;
+				newVolume = 0x7F;
+			}
+			channel->_volume = newVolume;
+			sendVolume(midiChannel, newVolume);
+		}
+	}
+
+	if (channel->_panFadeStep) {
+		if (!--channel->_panFadeCounter) {
+			channel->_panFadeCounter = channel->_panFadeReload;
+			byte newPan = channel->_pan + channel->_panFadeStep;
+			if (newPan > 0x7F) {
+				newPan = (newPan ^ 0x7F) & 0x7F;
+				channel->_panFadeCounter = 0;
+			}
+			channel->_pan = newPan;
+			sendPan(midiChannel, newPan);
+		}
+	}
+}
+
+void RSound::pollAllChannels() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		Channel_pollActive(&_channels[i]);
+}
+
+void RSound::update() {
+	getRandomNumber();
+	if (_isDisabled)
+		return;
+
+	tickCallback();
+
+	++_frameCounter;
+	pollAllChannels();
+	checkFadingChannels();
+}
+
+/*-----------------------------------------------------------------------*/
+
+/**
+ * Zeroes _activeCount and the three fade-step fields for channels in
+ * [first, last) - matches the shared shape of sub_10132/sub_101A0/sub_101EA.
+ * Deliberately does NOT touch the loop pointers, volume, program, pan etc,
+ * matching the original.
+ */
+void RSound::resetChannelRange(int first, int last) {
+	bool wasDisabled = _isDisabled;
+	_isDisabled = true;
+
+	for (int i = first; i < last; ++i) {
+		_channels[i]._activeCount = 0;
+		_channels[i]._pitchBendFadeStep = 0;
+		_channels[i]._volumeFadeStep = 0;
+		_channels[i]._panFadeStep = 0;
+	}
+
+	_isDisabled = wasDisabled;
+}
+
+void RSound::resetHeldNotes() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT + 1; ++i)
+		for (int j = 0; j < 4; ++j)
+			_heldNotes[i][j] = 0xFF;
+}
+
+/**
+ * Matches sub_10132: resets all 9 channels and the held-notes table.
+ * Called both from the constructor (mirroring rsound_init) and from
+ * command0.
+ */
+void RSound::resetAllChannels() {
+	resetChannelRange(0, RSOUND_CHANNEL_COUNT);
+	resetHeldNotes();
+}
+
+/**
+ * Sends the GM-reset Control Change sequence (all notes off, reset all
+ * controllers, volume=100, pan=center) to MIDI channels [first, last]
+ * (both inclusive, 1-based). Shared tail used by command0/command2/command4.
+ */
+void RSound::sendGmReset(int first, int last) {
+	for (int ch = first; ch <= last; ++ch) {
+		sendStatus(ch, 0xB0);
+		sendMidiByte(0x7B); // All Notes Off
+		sendMidiByte(0);
+		sendMidiByte(0x79); // Reset All Controllers
+		sendMidiByte(0);
+		sendMidiByte(7);    // Channel Volume
+		sendMidiByte(100);
+		sendMidiByte(10);   // Pan
+		sendMidiByte(0x40);
+	}
+}
+
+int RSound::command0() {
+	bool isDisabled = _isDisabled;
+	_isDisabled = true;
+
+	resetAllChannels();
+	sendGmReset(1, RSOUND_CHANNEL_COUNT);
+
+	// Matches the trailing "lea ax, unk_1138F; jmp sub_1041E" in rsound_command0.
+	// TODO: 0x6F is unk_1138F's address (0x1138F) relative to seg001's load
+	// address (0x11320) - the exact data-segment offset convention still
+	// needs verifying once dataOffset/dataSize below are confirmed.
+	sendSysEx(0x6F);
+
+	_isDisabled = isDisabled;
+	return 0;
+}
+
+int RSound::command1() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		_channels[i].enable(0xFF);
+	return 0;
+}
+
+int RSound::command2() {
+	// Channels 1-5, matching sub_101A0 (also reinitializes the held-notes
+	// table) plus the GM-reset CC sequence for those same channels.
+	resetChannelRange(0, 5);
+	resetHeldNotes();
+	sendGmReset(1, 5);
+	return 0;
+}
+
+int RSound::command3() {
+	for (int i = 0; i < 5; ++i)
+		_channels[i].enable(0xFF);
+	return 0;
+}
+
+int RSound::command4() {
+	// Channels 6-9, matching sub_101EA (does NOT touch the held-notes
+	// table) plus the GM-reset CC sequence for those same channels.
+	resetChannelRange(5, RSOUND_CHANNEL_COUNT);
+	sendGmReset(6, RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound::command5() {
+	for (int i = 5; i < RSOUND_CHANNEL_COUNT; ++i)
+		_channels[i].enable(0xFF);
+	return 0;
+}
+
+int RSound::command6() {
+	for (int ch = 1; ch <= RSOUND_CHANNEL_COUNT; ++ch)
+		muteChannel(ch);
+	return 0;
+}
+
+int RSound::command7() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		restoreChannelVolume(_channels[i]._midiChannel, _channels[i]._volume);
+	return 0;
+}
+
+int RSound::command8() {
+	int result = 0;
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		result |= _channels[i]._activeCount;
+
+	return result;
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/rsound.h b/engines/mads/nebular/rsound.h
new file mode 100644
index 00000000000..ac80bab2e25
--- /dev/null
+++ b/engines/mads/nebular/rsound.h
@@ -0,0 +1,276 @@
+/* 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_RSOUND_H
+#define MADS_NEBULAR_RSOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace RexNebular {
+
+class RSound;
+
+#define RSOUND_CHANNEL_COUNT 9
+
+/**
+ * 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;
+ * field names/roles were derived by tracing Channel_pollActive() (the
+ * per-channel opcode interpreter) and cross-referencing against the
+ * equivalent AdlibChannel fields in asound.h.
+ */
+class Channel {
+public:
+	RSound *_owner = nullptr;
+	int _midiChannel = 0;        // 1-9: the MIDI channel this struct drives
+
+	int _activeCount = 0;        // gate/duration countdown; also freshly loaded from the duration byte of a note event
+	int _pitchBendFadeStep = 0;  // per-step delta added to _pitchBend each ramp tick
+	int _volumeFadeStep = 0;     // per-step delta added to _volume each ramp tick
+	int _panFadeStep = 0;        // per-step delta added to _pan each ramp tick
+	int _note = 0;               // MIDI note number, read from the note/duration stream
+	int _program = 0;            // patch/instrument number, sent as a Program Change
+	int _velocity = 0;           // note velocity, used by RSound::sendNoteOn()
+	int _noteOffset = 0;         // subtracted from _activeCount to derive _keyOnDelay; 0xFF = sustain full duration +1 (no early cutoff)
+	int _keyOnDelay = 0;         // countdown to RSound::Channel_flushHeldNotes()
+	int _volumeFadeCounter = 0;  // counts down to 0 before applying _volumeFadeStep
+	int _pitchBendFadeCounter = 0; // counts down to 0 before applying _pitchBendFadeStep
+	int _panFadeCounter = 0;     // counts down to 0 before applying _panFadeStep
+	int _volume = 0;             // current channel volume (MIDI CC#7)
+	int _pitchBend = 0;          // current pitch bend value (status 0xEn, coarse/MSB only)
+	int _pan = 0;                // current pan value (MIDI CC#10); 64 = center
+	int _volumeFadeReload = 0;   // reload value for _volumeFadeCounter
+	int _pitchBendFadeReload = 0; // reload value for _pitchBendFadeCounter
+	int _panFadeReload = 0;      // reload value for _panFadeCounter
+	int _pitchBendFadeCount = 0; // total ramp steps remaining before the pitch-bend ramp halts
+	int _pendingStop = 0;        // non-zero while the channel is fading out to silence
+	byte *_loopStartPtr = nullptr; // fixed restart anchor used by the full-reset/loop-restart opcode
+	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;
+	byte *_soundData = nullptr;    // identity pointer used by RSound::isSoundActive()
+
+public:
+	Channel() {}
+
+	/**
+	 * Partial reset used both when loading a new sound and when the
+	 * loop-restart opcode fires. Deliberately does NOT touch volume,
+	 * program, velocity, key-on state or active/duration - matches
+	 * Channel_reset() in the original disassembly.
+	 */
+	void reset(byte *startPtr);
+
+	/**
+	 * Marks the channel as pending-stop (fading toward silence).
+	 */
+	void enable(int flag);
+
+	/**
+	 * Loads a brand new sound into the channel: resets it, marks it
+	 * active, and resets pitch bend to center on the real device.
+	 */
+	void load(byte *pData);
+};
+
+/**
+ * Base class for the Roland MT-32 / MPU-401 sound player resource files.
+ * 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:
+	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)
+
+	void update();
+	void pollAllChannels();
+	void Channel_pollActive(Channel *channel);
+
+	/**
+	 * Zeroes _activeCount and the three fade-step fields for channels in
+	 * [first, last) - matches the shared shape of sub_10132/sub_101A0/sub_101EA.
+	 */
+	void resetChannelRange(int first, int last);
+
+	/**
+	 * Resets the per-MIDI-channel held-note tracking table to empty.
+	 */
+	void resetHeldNotes();
+
+	/**
+	 * Matches sub_10132: 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.
+	 */
+	void checkFadingChannels();
+	void Channel_checkFade(Channel *channel);
+
+	/**
+	 * Sends Note-Off (velocity 0) for all currently-held notes on the
+	 * given channel's MIDI channel, then clears the held-note table for it.
+	 */
+	void Channel_flushHeldNotes(Channel *channel);
+
+protected:
+	int _commandParam;
+
+	byte *loadData(int offset) {
+		return &_soundData[offset];
+	}
+
+	/**
+	 * Hook called once per update() frame, immediately after the disabled
+	 * check and before channel polling. Only RSound9's driver data makes
+	 * use of a recurring deferred-callback timer (g_callbackCounter/
+	 * g_callbackPeriod/_soundPtr in the original disassembly, mirroring
+	 * the identical mechanism in ASound9); every other driver leaves
+	 * this as a no-op.
+	 */
+	virtual void tickCallback() {
+	}
+
+	void resultCheck();
+
+	/**
+	 * Play the specified sound, using any free channel from 6 to 8.
+	 * Channel 9 is deliberately never touched here - matches the
+	 * disassembly, which never includes it in this scan. Returns the
+	 * channel that was used (or nullptr if none was free), since some
+	 * commands poke the just-loaded channel's loop pointer directly
+	 * afterward.
+	 */
+	Channel *playSound(int offset);
+
+	Channel *playSoundData(byte *pData, int startingChannel = 5);
+
+	/**
+	 * Checks to see whether the given block of data is already loaded into a channel.
+	 */
+	bool isSoundActive(byte *pData);
+
+	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);
+	void sendPitchBend(int midiChannel, int value);
+	void sendPan(int midiChannel, int value);
+	void muteChannel(int midiChannel);
+	void restoreChannelVolume(int midiChannel, int volume);
+
+	/**
+	 * Sends the GM-reset Control Change sequence (all notes off, reset all
+	 * controllers, volume=100, pan=center) to MIDI channels [first, last]
+	 * (inclusive, 1-based). Shared tail used by command0/command2/command4.
+	 */
+	void sendGmReset(int first, int last);
+
+	/**
+	 * Sends a device-specific SysEx block (device init/handshake data).
+	 * TODO: the original's sendSysEx (sub_1041E) uses a two-pointer scheme
+	 * (a fixed header table plus a per-call payload table used only for
+	 * checksum purposes) that isn't fully resolved without the raw data
+	 * segment. Currently just logs a placeholder.
+	 */
+	void sendSysEx(int offset);
+
+	virtual int command0();
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+	int command5();
+	int command6();
+	int command7();
+	int command8();
+
+	int nullCommand() {
+		return 0;
+	}
+
+public:
+	Channel _channels[RSOUND_CHANNEL_COUNT];
+	int _frameCounter;
+	bool _isDisabled;
+	int _pollResult;
+	int _resultFlag;
+
+public:
+	static void validate();
+
+public:
+	/**
+	 * Constructor
+	 * @param mixer			Mixer
+	 * @param filename		Specifies the Roland sound player file to use
+	 * @param dataOffset	Offset in the file of the data segment
+	 * @param dataSize		Size of the data segment
+	 */
+	RSound(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize);
+
+	~RSound() override {
+	}
+
+	int stop() override;
+	int poll() override;
+	void noise() override {
+		// No equivalent in the Roland driver - noise() is an Adlib/OPL-only concept
+	}
+	void setVolume(int volume) override;
+
+	int getFrameCounter() {
+		return _frameCounter;
+	}
+
+private:
+	void onTimer();
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/rsound_nebular.cpp b/engines/mads/nebular/rsound_nebular.cpp
new file mode 100644
index 00000000000..80486a749c4
--- /dev/null
+++ b/engines/mads/nebular/rsound_nebular.cpp
@@ -0,0 +1,461 @@
+/* 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/nebular/rsound_nebular.h"
+
+namespace MADS {
+namespace RexNebular {
+
+const RSound9::CommandPtr RSound9::_commandList[52] = {
+	&RSound9::command0, &RSound9::command1, &RSound9::command2, &RSound9::command3,
+	&RSound9::command4, &RSound9::command5, &RSound9::command6, &RSound9::command7,
+	&RSound9::command8, &RSound9::command9, &RSound9::command10, &RSound9::command11,
+	&RSound9::command12, &RSound9::command13, &RSound9::command14, &RSound9::command15,
+	&RSound9::command16, &RSound9::command17, &RSound9::command18, &RSound9::command19,
+	&RSound9::command20, &RSound9::command21, &RSound9::command22, &RSound9::command23,
+	&RSound9::command24, &RSound9::command25, &RSound9::command26, &RSound9::command27,
+	&RSound9::command28, &RSound9::command29, &RSound9::command30, &RSound9::command31,
+	&RSound9::command32, &RSound9::command33, &RSound9::command34, &RSound9::command35,
+	&RSound9::command36, &RSound9::command37, &RSound9::command38, &RSound9::command39,
+	&RSound9::command40, &RSound9::command41, &RSound9::command42, &RSound9::command43,
+	&RSound9::command44_46, &RSound9::command45, &RSound9::command44_46, &RSound9::command47,
+	&RSound9::command48, &RSound9::command49, &RSound9::command50, &RSound9::command51
+};
+
+RSound9::RSound9(Audio::Mixer *mixer) : RSound(mixer, "rsound.009", 0x1520, 0x8920) {
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+}
+
+int RSound9::command(int commandId, int param) {
+	if (commandId > 51)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+void RSound9::tickCallback() {
+	if (!_callbackPeriod)
+		return;
+	if (--_callbackCounter)
+		return;
+
+	_callbackCounter = _callbackPeriod;
+	if (_callbackFnPtr)
+		(this->*_callbackFnPtr)();
+}
+
+int RSound9::command0() {
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+	return RSound::command0();
+}
+
+int RSound9::command9() {
+	_callbackCounter = 1848;
+	_callbackPeriod = 84;
+	_channels[0].load(loadData(0x16E4));
+	_channels[1].load(loadData(0x1E9E));
+	_channels[2].load(loadData(0x2F9C));
+	_channels[3].load(loadData(0x2644));
+	_channels[4].load(loadData(0x1FF4));
+	_channels[5].load(loadData(0x2382));
+	_channels[6].load(loadData(0x1AAE));
+	return 0;
+}
+
+int RSound9::command10() {
+	_channels[0].load(loadData(0x31E2));
+	_channels[1].load(loadData(0x31FA));
+	_channels[2].load(loadData(0x3212));
+	_channels[3].load(loadData(0x322C));
+	return 0;
+}
+
+int RSound9::command11() {
+	_channels[7].load(loadData(0x33E2));
+	return 0;
+}
+
+int RSound9::command12() {
+	_channels[7].load(loadData(0x342E));
+	return 0;
+}
+
+int RSound9::command13() {
+	_channels[7].load(loadData(0x343A));
+	return 0;
+}
+
+int RSound9::command14() {
+	_channels[7].load(loadData(0x3442));
+	return 0;
+}
+
+int RSound9::command15() {
+	_channels[7].load(loadData(0x3462));
+	return 0;
+}
+
+int RSound9::command16() {
+	_channels[7].load(loadData(0x347A));
+	return 0;
+}
+
+int RSound9::command17() {
+	_channels[7].load(loadData(0x3470));
+	return 0;
+}
+
+int RSound9::command18() {
+	playSound(0x3248);
+	return 0;
+}
+
+int RSound9::command19() {
+	playSound(0x3262);
+	return 0;
+}
+
+int RSound9::command20() {
+	int v = (getRandomNumber() & 24) + 77;
+	byte *pData = loadData(0x3284);
+	pData[6] = v & 0x7F;
+	playSound(0x3284);
+	return 0;
+}
+
+int RSound9::command21() {
+	byte *pData = loadData(0x3298);
+	pData[9] = 70;
+	if (!isSoundActive(pData))
+		playSound(0x3298);
+	return 0;
+}
+
+int RSound9::command22() {
+	byte *pData = loadData(0x3298);
+	pData[9] = 45;
+	if (!isSoundActive(pData))
+		playSound(0x3298);
+	return 0;
+}
+
+int RSound9::command23() {
+	Channel *chan = playSound(0x32B0);
+	if (chan)
+		chan->_innerLoopPtr = loadData(0x32CE);
+
+	chan = playSound(0x32B6);
+	if (chan)
+		chan->_innerLoopPtr = loadData(0x32CE);
+
+	chan = playSound(0x32C8);
+	if (chan)
+		chan->_innerLoopPtr = loadData(0x32CE);
+	return 0;
+}
+
+int RSound9::command24() {
+	playSound(0x32E0);
+	return 0;
+}
+
+int RSound9::command25() {
+	playSound(0x32F6);
+	return 0;
+}
+
+int RSound9::command26() {
+	playSound(0x331A);
+	return 0;
+}
+
+int RSound9::command27() {
+	playSound(0x3332);
+	return 0;
+}
+
+int RSound9::command28() {
+	int v = (getRandomNumber() & 28) + 15;
+	byte *pData = loadData(0x334A);
+	pData[6] = v & 0x7F;
+	_channels[7].load(pData);
+	return 0;
+}
+
+int RSound9::command29() {
+	int v = (getRandomNumber() & 12) + 33;
+	byte *pData = loadData(0x335E);
+	pData[6] = v & 0x7F;
+	playSound(0x335E);
+	return 0;
+}
+
+int RSound9::command30() {
+	playSound(0x3386);
+	return 0;
+}
+
+int RSound9::command31() {
+	playSound(0x3396);
+	playSound(0x33A4);
+	playSound(0x33B2);
+	return 0;
+}
+
+int RSound9::command32() {
+	playSound(0x33C0);
+	return 0;
+}
+
+int RSound9::command33() {
+	playSound(0x33CA);
+	return 0;
+}
+
+int RSound9::command34() {
+	command1();
+	_callbackCounter = 96;
+	_callbackPeriod = 96;
+
+	*loadData(0x6841) = 2;
+	*loadData(0x7DCD) = 2;
+	*loadData(0x8791) = 2;
+
+	_channels[0].load(loadData(0x4D2A));
+	_channels[1].load(loadData(0x51AA));
+	_channels[2].load(loadData(0x5634));
+	_channels[3].load(loadData(0x6844));
+	_channels[4].load(loadData(0x7DD0));
+	return 0;
+}
+
+int RSound9::command35() {
+	playSound(0x344C);
+	return 0;
+}
+
+int RSound9::command36() {
+	playSound(0x334A);
+
+	Channel *chan = playSound(0x32C2);
+	if (chan)
+		chan->_innerLoopPtr = loadData(0x3378);
+
+	chan = playSound(0x32BC);
+	if (chan)
+		chan->_innerLoopPtr = loadData(0x3368);
+	return 0;
+}
+
+int RSound9::command37() {
+	int v = (getRandomNumber() & 2) + 72;
+	byte *pData = loadData(0x349C);
+	pData[6] = v & 0x7F;
+	playSound(0x349C);
+	return 0;
+}
+
+int RSound9::command38() {
+	_callbackFnPtr = &RSound9::loadCommand38;
+	return 0;
+}
+
+void RSound9::loadCommand38() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x1878));
+	_channels[1].load(loadData(0x1F64));
+	_channels[2].load(loadData(0x308E));
+	_channels[3].load(loadData(0x2A10));
+	_channels[4].load(loadData(0x21C8));
+	_channels[5].load(loadData(0x2558));
+	_channels[6].load(loadData(0x1E9A));
+}
+
+int RSound9::command39() {
+	_callbackFnPtr = &RSound9::loadCommand39;
+	return 0;
+}
+
+void RSound9::loadCommand39() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x1A24));
+	_channels[1].load(loadData(0x1FD0));
+	_channels[2].load(loadData(0x318A));
+	_channels[3].load(loadData(0x2E4A));
+	_channels[4].load(loadData(0x2380));
+	_channels[5].load(loadData(0x2642));
+	_channels[6].load(loadData(0x1E9C));
+}
+
+int RSound9::command40() {
+	_callbackFnPtr = &RSound9::loadCommand40;
+	return 0;
+}
+
+void RSound9::loadCommand40() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x4F00));
+	_channels[1].load(loadData(0x534A));
+	_channels[2].load(loadData(0x5CDE));
+	_channels[3].load(loadData(0x6F8E));
+	_channels[4].load(loadData(0x8110));
+}
+
+int RSound9::command41() {
+	_callbackFnPtr = &RSound9::loadCommand41;
+	return 0;
+}
+
+void RSound9::loadCommand41() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x4F26));
+	_channels[1].load(loadData(0x53BC));
+	_channels[2].load(loadData(0x5DFE));
+	_channels[3].load(loadData(0x747E));
+	_channels[4].load(loadData(0x8340));
+}
+
+int RSound9::command42() {
+	_callbackFnPtr = &RSound9::loadCommand42;
+	return 0;
+}
+
+void RSound9::loadCommand42() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x4F30));
+	_channels[1].load(loadData(0x555C));
+	_channels[2].load(loadData(0x6582));
+	_channels[3].load(loadData(0x7A2E));
+	_channels[4].load(loadData(0x8480));
+}
+
+int RSound9::command43() {
+	_callbackCounter = 80;
+	_callbackPeriod = 80;
+	_channels[0].load(loadData(0x34BE));
+	_channels[1].load(loadData(0x3A46));
+	_channels[2].load(loadData(0x3F52));
+	_channels[3].load(loadData(0x439A));
+	return 0;
+}
+
+int RSound9::command44_46() {
+	_callbackFnPtr = &RSound9::loadCommand44_46;
+	return 0;
+}
+
+void RSound9::loadCommand44_46() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x3518));
+	_channels[1].load(loadData(0x3AA2));
+	_channels[2].load(loadData(0x403A));
+	_channels[3].load(loadData(0x4486));
+}
+
+int RSound9::command45() {
+	_callbackFnPtr = &RSound9::loadCommand45;
+	return 0;
+}
+
+void RSound9::loadCommand45() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x37AC));
+	_channels[1].load(loadData(0x3CF8));
+	_channels[2].load(loadData(0x41E6));
+	_channels[3].load(loadData(0x4630));
+}
+
+int RSound9::command47() {
+	_callbackFnPtr = &RSound9::loadCommand47;
+	return 0;
+}
+
+void RSound9::loadCommand47() {
+	_callbackFnPtr = nullptr;
+	_channels[0].load(loadData(0x47D6));
+	_channels[1].load(loadData(0x48FA));
+	_channels[2].load(loadData(0x4A20));
+	_channels[3].load(loadData(0x4BAE));
+}
+
+int RSound9::command48() {
+	byte *pData = loadData(0x34B0);
+	pData[6] ^= 0x1F;
+	pData[0xA] ^= 0x1F;
+	playSound(0x34B0);
+	return 0;
+}
+
+int RSound9::command49() {
+	_channels[0].load(loadData(0x12CA));
+	_channels[1].load(loadData(0x132A));
+	_channels[2].load(loadData(0x1384));
+	_channels[3].load(loadData(0x1666));
+	_channels[4].load(loadData(0x1682));
+	_channels[5].load(loadData(0x16A0));
+	_channels[6].load(loadData(0x16BE));
+	return 0;
+}
+
+int RSound9::command50() {
+	_callbackFnPtr = &RSound9::loadCommand50;
+	return 0;
+}
+
+void RSound9::loadCommand50() {
+	_callbackFnPtr = nullptr;
+
+	*loadData(0x6841) = 0;
+	*loadData(0x7DCD) = 0;
+	*loadData(0x8791) = 0;
+
+	_channels[0].load(loadData(0x50B8));
+	_channels[1].load(loadData(0x7DCE));
+	_channels[2].load(loadData(0x676A));
+	_channels[3].load(loadData(0x7D08));
+	_channels[4].load(loadData(0x85C4));
+}
+
+int RSound9::command51() {
+	command1();
+	_callbackCounter = 96;
+	_callbackPeriod = 96;
+
+	*loadData(0x6841) = 2;
+	*loadData(0x7DCD) = 2;
+	*loadData(0x8791) = 2;
+
+	_channels[0].load(loadData(0x4D3C));
+	_channels[1].load(loadData(0x51EE));
+	_channels[2].load(loadData(0x5A62));
+	_channels[3].load(loadData(0x6986));
+	_channels[4].load(loadData(0x7E5C));
+	return 0;
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/rsound_nebular.h b/engines/mads/nebular/rsound_nebular.h
new file mode 100644
index 00000000000..b46f81422d0
--- /dev/null
+++ b/engines/mads/nebular/rsound_nebular.h
@@ -0,0 +1,113 @@
+/* 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_RSOUND_NEBULAR_H
+#define MADS_NEBULAR_RSOUND_NEBULAR_H
+
+#include "mads/nebular/rsound.h"
+
+namespace MADS {
+namespace RexNebular {
+
+class RSound9 : public RSound {
+private:
+	/**
+	 * Deferred sound-loader callback state (g_callbackCounter/
+	 * g_callbackPeriod/_soundPtr in the original disassembly). Mirrors
+	 * ASound9's identical mechanism: arms a recurring timer that
+	 * re-invokes a scheduled loader function every _callbackPeriod
+	 * ticks, without ever clearing the pointer itself (the loader body
+	 * clears it if it wants the recurrence to stop).
+	 */
+	typedef void (RSound9:: *CallbackFunction)();
+	int _callbackCounter;
+	int _callbackPeriod;
+	CallbackFunction _callbackFnPtr;
+
+	typedef int (RSound9:: *CommandPtr)();
+	static const CommandPtr _commandList[52];
+
+	void tickCallback() override;
+	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 command44_46();
+	int command45();
+	int command47();
+	int command48();
+	int command49();
+	int command50();
+	int command51();
+
+	// Deferred loader bodies, scheduled via _callbackFnPtr by the commands above
+	void loadCommand38();
+	void loadCommand39();
+	void loadCommand40();
+	void loadCommand41();
+	void loadCommand42();
+	void loadCommand44_46();
+	void loadCommand45();
+	void loadCommand47();
+	void loadCommand50();
+public:
+	RSound9(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/sound.cpp b/engines/mads/nebular/sound.cpp
new file mode 100644
index 00000000000..cc18da4f072
--- /dev/null
+++ b/engines/mads/nebular/sound.cpp
@@ -0,0 +1,81 @@
+/* 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/nebular/sound.h"
+#include "mads/nebular/asound_nebular.h"
+#include "mads/nebular/rsound_nebular.h"
+
+namespace MADS {
+namespace RexNebular {
+
+void RexSoundManager::validate() {
+	if (_isMT32)
+		RSound::validate();
+	else
+		ASound::validate();
+}
+
+void RexSoundManager::loadDriver(int sectionNumber) {
+	removeDriver();
+
+	if (_isMT32) {
+		// Roland MT32 drivers
+		assert(sectionNumber == 9);
+		_driver = new RexNebular::RSound9(_mixer);
+
+	} else {
+		// Adlib drivers
+		switch (sectionNumber) {
+		case 1:
+			_driver = new RexNebular::ASound1(_mixer);
+			break;
+		case 2:
+			_driver = new RexNebular::ASound2(_mixer);
+			break;
+		case 3:
+			_driver = new RexNebular::ASound3(_mixer);
+			break;
+		case 4:
+			_driver = new RexNebular::ASound4(_mixer);
+			break;
+		case 5:
+			_driver = new RexNebular::ASound5(_mixer);
+			break;
+		case 6:
+			_driver = new RexNebular::ASound6(_mixer);
+			break;
+		case 7:
+			_driver = new RexNebular::ASound7(_mixer);
+			break;
+		case 8:
+			_driver = new RexNebular::ASound8(_mixer);
+			break;
+		case 9:
+			_driver = new RexNebular::ASound9(_mixer);
+			break;
+		default:
+			return;
+		}
+	}
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/sound.h b/engines/mads/nebular/sound.h
new file mode 100644
index 00000000000..28587c480a0
--- /dev/null
+++ b/engines/mads/nebular/sound.h
@@ -0,0 +1,46 @@
+/* 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_SOUND_H
+#define MADS_SOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace RexNebular {
+
+class RexSoundManager : public SoundManager {
+protected:
+	void loadDriver(int sectionNum) override;
+
+public:
+	RexSoundManager(Audio::Mixer *mixer, bool &soundFlag) : SoundManager(mixer, soundFlag) {
+	}
+	~RexSoundManager() override {
+	}
+
+	void validate() override;
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/phantom/asound.cpp b/engines/mads/phantom/asound.cpp
index 964c92b90b4..e55863b8cc4 100644
--- a/engines/mads/phantom/asound.cpp
+++ b/engines/mads/phantom/asound.cpp
@@ -196,9 +196,8 @@ void AdlibChannel::processChannelFade() {
 
 // =========================================================================
 
-ASound::ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
-		int dataOffset, int dataSize) :
-		SoundDriver(mixer, opl, filename, dataOffset, dataSize) {
+ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset, int dataSize) :
+		SoundDriver(mixer, filename, dataOffset, dataSize) {
 	AdlibChannel::_isDisabled = false;
 
 	write(4, 0x60);		// Mask off both adlib timers
diff --git a/engines/mads/phantom/asound.h b/engines/mads/phantom/asound.h
index e93798dd274..4f3a11f9120 100644
--- a/engines/mads/phantom/asound.h
+++ b/engines/mads/phantom/asound.h
@@ -22,6 +22,7 @@
 #ifndef MADS_PHANTOM_ASOUND_H
 #define MADS_PHANTOM_ASOUND_H
 
+#include "audio/fmopl.h"
 #include "common/mutex.h"
 #include "common/queue.h"
 #include "common/util.h"
@@ -129,6 +130,7 @@ struct AdlibSample {
 
 class ASound : public SoundDriver {
 private:
+	OPL::OPL *_opl = OPL::Config::create();
 	uint16 _callbackCounter = 0;		// Period counter
 	uint16 _callbackPeriod = 0;			// Period reload
 	AdlibChannel *_activeChannelPtr = NULL;
@@ -435,17 +437,17 @@ public:
 	/**
 	 * Constructor
 	 * @param mixer			Mixer
-	 * @param opl			OPL
 	 * @param filename		Specifies the adlib sound player file to use
 	 * @param dataOffset	Offset in the file of the data segment
 	 */
-	ASound(Audio::Mixer *mixer, OPL::OPL *opl, const Common::Path &filename,
+	ASound(Audio::Mixer *mixer, const Common::Path &filename,
 		int dataOffset, int dataSize);
 
 	/**
 	 * Destructor
 	 */
 	~ASound() override {
+		delete _opl;
 	}
 
 	/**
diff --git a/engines/mads/phantom/asound_phantom.cpp b/engines/mads/phantom/asound_phantom.cpp
index e728e1da899..1dbca1ab0db 100644
--- a/engines/mads/phantom/asound_phantom.cpp
+++ b/engines/mads/phantom/asound_phantom.cpp
@@ -69,28 +69,28 @@ void PhantomSoundManager::validate() {
 
 void PhantomSoundManager::loadDriver(int sectionNumber) {
 	if (_isDemo) {
-		_driver = new ASoundDemo(_mixer, _opl);
+		_driver = new ASoundDemo(_mixer);
 		return;
 	}
 
 	switch (sectionNumber) {
 	case 1:
-		_driver = new ASound1(_mixer, _opl);
+		_driver = new ASound1(_mixer);
 		break;
 	case 2:
-		_driver = new ASound2(_mixer, _opl);
+		_driver = new ASound2(_mixer);
 		break;
 	case 3:
-		_driver = new ASound3(_mixer, _opl);
+		_driver = new ASound3(_mixer);
 		break;
 	case 4:
-		_driver = new ASound4(_mixer, _opl);
+		_driver = new ASound4(_mixer);
 		break;
 	case 5:
-		_driver = new ASound5(_mixer, _opl);
+		_driver = new ASound5(_mixer);
 		break;
 	case 9:
-		_driver = new ASound9(_mixer, _opl);
+		_driver = new ASound9(_mixer);
 		break;
 	default:
 		_driver = nullptr;
@@ -115,8 +115,8 @@ const ASound1::CommandPtr ASound1::_commandList[40] = {
 	&ASound1::command36, &ASound1::command37, &ASound1::command38, &ASound1::command39
 };
 
-ASound1::ASound1(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.ph1", 0x21e0, 0x4d20) {
+ASound1::ASound1(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.ph1", 0x21e0, 0x4d20) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -398,8 +398,8 @@ const ASound2::CommandPtr ASound2::_commandList[73] = {
 	&ASound2::command72
 };
 
-ASound2::ASound2(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.ph2", 0x2040, 0x2300) {
+ASound2::ASound2(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.ph2", 0x2040, 0x2300) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -628,8 +628,8 @@ const ASound3::CommandPtr ASound3::_commandList[77] = {
 	nullptr
 };
 
-ASound3::ASound3(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.ph3", 0x20c0, 0x31a0) {
+ASound3::ASound3(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.ph3", 0x20c0, 0x31a0) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -907,8 +907,8 @@ const ASound4::CommandPtr ASound4::_commandList[71] = {
 	&ASound4::command68, &ASound4::command69, &ASound4::command70
 };
 
-ASound4::ASound4(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.ph4", 0x1f90, 0x14d0) {
+ASound4::ASound4(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.ph4", 0x1f90, 0x14d0) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -1063,8 +1063,8 @@ const ASound5::CommandPtr ASound5::_commandList[79] = {
 	&ASound5::command76, &ASound5::command77, &ASound5::command78
 };
 
-ASound5::ASound5(Audio::Mixer *mixer, OPL::OPL *opl)
-		: ASound(mixer, opl, "asound.ph5", 0x2140, 0x5cd0) {
+ASound5::ASound5(Audio::Mixer *mixer)
+		: ASound(mixer, "asound.ph5", 0x2140, 0x5cd0) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -1370,8 +1370,8 @@ const ASound9::CommandPtr ASound9::_commandList[72] = {
 	&ASound9::command68, &ASound9::command69, &ASound9::command70, &ASound9::command71
 };
 
-ASound9::ASound9(Audio::Mixer *mixer, OPL::OPL *opl) :
-		ASound(mixer, opl, "asound.ph9", 0x20c0, 0x3690) {
+ASound9::ASound9(Audio::Mixer *mixer) :
+		ASound(mixer, "asound.ph9", 0x20c0, 0x3690) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d4);
 	for (int i = 0; i < 120; ++i)
@@ -1596,8 +1596,8 @@ const ASoundDemo::CommandPtr ASoundDemo::_commandList[30] = {
 	&ASoundDemo::command28, &ASoundDemo::command29
 };
 
-ASoundDemo::ASoundDemo(Audio::Mixer *mixer, OPL::OPL *opl) :
-		ASound(mixer, opl, "asound.pha", 0x2240, 0x6f30) {
+ASoundDemo::ASoundDemo(Audio::Mixer *mixer) :
+		ASound(mixer, "asound.pha", 0x2240, 0x6f30) {
 	// Load sound samples
 	auto samplesStream = getDataStream(0x1d8);
 	for (int i = 0; i < 256; ++i)
diff --git a/engines/mads/phantom/asound_phantom.h b/engines/mads/phantom/asound_phantom.h
index 5b509eb457a..d5db05d15f8 100644
--- a/engines/mads/phantom/asound_phantom.h
+++ b/engines/mads/phantom/asound_phantom.h
@@ -102,7 +102,7 @@ private:
 	int command39();
 
 public:
-	ASound1(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound1(Audio::Mixer *mixer);
 	~ASound1() override {}
 	int command(int commandId, int param) override;
 };
@@ -155,7 +155,7 @@ private:
 	int command72();
 
 public:
-	ASound2(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound2(Audio::Mixer *mixer);
 	~ASound2() override {}
 	int command(int commandId, int param) override;
 };
@@ -218,7 +218,7 @@ private:
 	int command75();
 
 public:
-	ASound3(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound3(Audio::Mixer *mixer);
 	~ASound3() override {}
 	int command(int commandId, int param) override;
 };
@@ -266,7 +266,7 @@ private:
 	int command70();
 
 public:
-	ASound4(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound4(Audio::Mixer *mixer);
 	~ASound4() override {}
 	int command(int commandId, int param) override;
 };
@@ -333,7 +333,7 @@ private:
 	int command78();
 
 public:
-	ASound5(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound5(Audio::Mixer *mixer);
 	~ASound5() override {}
 	int command(int commandId, int param) override;
 };
@@ -381,7 +381,7 @@ private:
 	static const CommandPtr _commandList[72];
 
 public:
-	ASound9(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASound9(Audio::Mixer *mixer);
 	~ASound9() override {
 	}
 	int command(int commandId, int param) override;
@@ -424,7 +424,7 @@ private:
 	static const CommandPtr _commandList[30];
 
 public:
-	ASoundDemo(Audio::Mixer *mixer, OPL::OPL *opl);
+	ASoundDemo(Audio::Mixer *mixer);
 	~ASoundDemo() override {
 	}
 	int command(int commandId, int param) override;


Commit: 60263a376223cf8d6c6590a53b90ac48411313be
    https://github.com/scummvm/scummvm/commit/60263a376223cf8d6c6590a53b90ac48411313be
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-26T19:32:30+10:00

Commit Message:
MADS: Add Nebular's sound manager refactor to the other games

Changed paths:
  A engines/mads/dragonsphere/sound.cpp
  A engines/mads/dragonsphere/sound.h
  A engines/mads/phantom/sound.cpp
  A engines/mads/phantom/sound.h
    engines/mads/core/sound_manager.h
    engines/mads/dragonsphere/asound.cpp
    engines/mads/dragonsphere/asound.h
    engines/mads/dragonsphere/asound_dragonsphere.cpp
    engines/mads/dragonsphere/asound_dragonsphere.h
    engines/mads/dragonsphere/dragonsphere.cpp
    engines/mads/module.mk
    engines/mads/nebular/asound.cpp
    engines/mads/nebular/asound_nebular.cpp
    engines/mads/nebular/sound.h
    engines/mads/phantom/asound.cpp
    engines/mads/phantom/asound.h
    engines/mads/phantom/asound_phantom.cpp
    engines/mads/phantom/asound_phantom.h
    engines/mads/phantom/phantom.cpp


diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index eef61d89943..06125a4c79e 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -105,10 +105,13 @@ public:
 	SoundManager(Audio::Mixer *mixer, bool &soundFlag);
 	virtual ~SoundManager();
 
-	virtual void validate() = 0;
-
 	bool _preferRoland;
 
+	/**
+	 * Validate the sound driver files needed for data
+	 */
+	virtual void validate() = 0;
+
 	/**
 	 * Initializes the sound driver for a given game section
 	 */
diff --git a/engines/mads/dragonsphere/asound.cpp b/engines/mads/dragonsphere/asound.cpp
index 749ee5b68c6..1f456ffd42a 100644
--- a/engines/mads/dragonsphere/asound.cpp
+++ b/engines/mads/dragonsphere/asound.cpp
@@ -20,6 +20,8 @@
  */
 
 #include "audio/fmopl.h"
+#include "common/file.h"
+#include "common/md5.h"
 #include "mads/dragonsphere/asound.h"
 
 namespace MADS {
@@ -255,6 +257,43 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename,
 		CALLBACKS_PER_SECOND);
 }
 
+void ASound::validate(bool isDemo) {
+	Common::File f;
+	static const char *const MD5[] = {
+		"cac84f53ccf18ca56f4c03352037790f",
+		"2dcdbe18ca5225384cdb97ceb7f5642a",
+		"c6001b0dfe32cb9399ab60742b631c2e",
+		"1596b657c6171e13714eaf114bf94641",
+		"ecbb8bdf1e2e36fcacedce79761e625b",
+		"29639869e5faed1378dd2cdc1d132889",
+		nullptr,
+		nullptr,
+		"379fcc9af2142f15a3e7166eee6dd49d"
+	};
+	static const char *const MD5_DEMO[] = {
+		"c4fc9e9d7e2392c69ea7b3ca997e832d",
+		nullptr, nullptr, nullptr, nullptr,
+		nullptr, nullptr, nullptr,
+		"21432c2dd055d0d505ede8fecc77b29a"
+	};
+
+	for (int i = 1; i <= 9; ++i) {
+		if (i == 7 || i == 8)
+			continue;
+		if (isDemo && i != 1 && i != 9)
+			continue;
+
+		Common::Path filename(Common::String::format("asound.dr%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 != (isDemo ? MD5_DEMO[i - 1] : MD5[i - 1]))
+			error("Invalid sound file - %s", filename.toString().c_str());
+	}
+}
+
 int ASound::stop() {
 	command0();
 	int result = _pollResult;
diff --git a/engines/mads/dragonsphere/asound.h b/engines/mads/dragonsphere/asound.h
index f02efb5dd8f..8d7aedc72c6 100644
--- a/engines/mads/dragonsphere/asound.h
+++ b/engines/mads/dragonsphere/asound.h
@@ -606,6 +606,12 @@ protected:
 	 */
 	int command18();
 
+public:
+	/**
+	 * Validate the sound driver files needed for data
+	 */
+	static void validate(bool isDemo);
+
 public:
 	/**
 	 * Constructor.
diff --git a/engines/mads/dragonsphere/asound_dragonsphere.cpp b/engines/mads/dragonsphere/asound_dragonsphere.cpp
index db34e862ba8..9d91a3889fe 100644
--- a/engines/mads/dragonsphere/asound_dragonsphere.cpp
+++ b/engines/mads/dragonsphere/asound_dragonsphere.cpp
@@ -19,87 +19,11 @@
  *
  */
 
-#include "common/endian.h"
-#include "common/file.h"
-#include "common/md5.h"
-#include "common/textconsole.h"
 #include "mads/dragonsphere/asound_dragonsphere.h"
 
 namespace MADS {
 namespace Dragonsphere {
 
-void DragonSoundManager::validate() {
-	Common::File f;
-	static const char *const MD5[] = {
-		"cac84f53ccf18ca56f4c03352037790f",
-		"2dcdbe18ca5225384cdb97ceb7f5642a",
-		"c6001b0dfe32cb9399ab60742b631c2e",
-		"1596b657c6171e13714eaf114bf94641",
-		"ecbb8bdf1e2e36fcacedce79761e625b",
-		"29639869e5faed1378dd2cdc1d132889",
-		nullptr,
-		nullptr,
-		"379fcc9af2142f15a3e7166eee6dd49d"
-	};
-	static const char *const MD5_DEMO[] = {
-		"c4fc9e9d7e2392c69ea7b3ca997e832d",
-		nullptr, nullptr, nullptr, nullptr,
-		nullptr, nullptr, nullptr,
-		"21432c2dd055d0d505ede8fecc77b29a"
-	};
-
-	for (int i = 1; i <= 9; ++i) {
-		if (i == 7 || i == 8)
-			continue;
-		if (_isDemo && i != 1 && i != 9)
-			continue;
-
-		Common::Path filename(Common::String::format("asound.dr%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 != (_isDemo ? MD5_DEMO[i - 1] : MD5[i - 1]))
-			error("Invalid sound file - %s", filename.toString().c_str());
-	}
-}
-
-void DragonSoundManager::loadDriver(int sectionNumber) {
-	switch (sectionNumber) {
-	case 1:
-		if (_isDemo)
-			_driver = new ASoundDemo1(_mixer);
-		else
-			_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 6:
-		_driver = new ASound6(_mixer);
-		break;
-	case 9:
-		if (_isDemo)
-			_driver = new ASoundDemo9(_mixer);
-		else
-			_driver = new ASound9(_mixer);
-		break;
-	default:
-		_driver = nullptr;
-		return;
-	}
-}
-
 /*-----------------------------------------------------------------------*/
 /* ASound1  (asound.dr1)                                                  *
  *-----------------------------------------------------------------------*/
diff --git a/engines/mads/dragonsphere/asound_dragonsphere.h b/engines/mads/dragonsphere/asound_dragonsphere.h
index 2b4864d1ec6..f6c397a1c36 100644
--- a/engines/mads/dragonsphere/asound_dragonsphere.h
+++ b/engines/mads/dragonsphere/asound_dragonsphere.h
@@ -19,31 +19,14 @@
  *
  */
 
-#ifndef MADS_DRAGONSPHERE_SOUND_H
-#define MADS_DRAGONSPHERE_SOUND_H
+#ifndef MADS_DRAGONSPHERE_ASOUND_DRAGONSPHERE_H
+#define MADS_DRAGONSPHERE_ASOUND_DRAGONSPHERE_H
 
 #include "mads/dragonsphere/asound.h"
 
 namespace MADS {
 namespace Dragonsphere {
 
-class DragonSoundManager : public SoundManager {
-private:
-	bool _isDemo;
-
-protected:
-	void loadDriver(int sectionNum) override;
-
-public:
-	DragonSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
-		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
-	}
-	~DragonSoundManager() override {
-	}
-
-	void validate() override;
-};
-
 /**
  * ASound1  (asound.dr1, _dataOffset = 0x2520, _dataSize = 0x49e0)
  *
diff --git a/engines/mads/dragonsphere/dragonsphere.cpp b/engines/mads/dragonsphere/dragonsphere.cpp
index 1c93a6492d8..db50fe32c12 100644
--- a/engines/mads/dragonsphere/dragonsphere.cpp
+++ b/engines/mads/dragonsphere/dragonsphere.cpp
@@ -39,7 +39,7 @@
 #include "mads/dragonsphere/dragonsphere.h"
 #include "mads/dragonsphere/global.h"
 #include "mads/dragonsphere/main.h"
-#include "mads/dragonsphere/asound_dragonsphere.h"
+#include "mads/dragonsphere/sound.h"
 #include "mads/dragonsphere/rooms/section1.h"
 #include "mads/dragonsphere/rooms/section2.h"
 #include "mads/dragonsphere/rooms/section3.h"
diff --git a/engines/mads/dragonsphere/sound.cpp b/engines/mads/dragonsphere/sound.cpp
new file mode 100644
index 00000000000..3640a3c3cb6
--- /dev/null
+++ b/engines/mads/dragonsphere/sound.cpp
@@ -0,0 +1,81 @@
+/* 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.h"
+#include "mads/dragonsphere/asound_dragonsphere.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+void DragonSoundManager::validate() {
+	if (_isMT32) {
+		// TODO
+	} else {
+		ASound::validate(_isDemo);
+	}
+}
+
+void DragonSoundManager::loadDriver(int sectionNumber) {
+	removeDriver();
+
+	if (_isMT32) {
+		// Roland MT32 drivers
+		assert(0 == 1);
+
+	} else {
+		// Adlib drivers
+		switch (sectionNumber) {
+		case 1:
+			if (_isDemo)
+				_driver = new ASoundDemo1(_mixer);
+			else
+				_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 6:
+			_driver = new ASound6(_mixer);
+			break;
+		case 9:
+			if (_isDemo)
+				_driver = new ASoundDemo9(_mixer);
+			else
+				_driver = new ASound9(_mixer);
+			break;
+		default:
+			_driver = nullptr;
+			return;
+		}
+	}
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/sound.h b/engines/mads/dragonsphere/sound.h
new file mode 100644
index 00000000000..0c5a6ba0b3d
--- /dev/null
+++ b/engines/mads/dragonsphere/sound.h
@@ -0,0 +1,55 @@
+/* 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_H
+#define MADS_DRAGONSPHERE_SOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+class DragonSoundManager : public SoundManager {
+private:
+	bool _isDemo;
+
+protected:
+	/**
+	 * Load the appropriate sound driver for the current section of the game.
+	 */
+	void loadDriver(int sectionNum) override;
+
+public:
+	DragonSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
+		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+	}
+	~DragonSoundManager() override {}
+
+	/**
+	 * Validate the sound driver files needed for data
+	 */
+	void validate() override;
+};
+
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 4cd57314887..eccf8ce6084 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -266,6 +266,7 @@ MODULE_OBJS := \
 	phantom/main_menu.o \
 	phantom/menus.o \
 	phantom/main.o \
+	phantom/sound.o \
 	dragonsphere/mads/mads.o \
 	dragonsphere/rooms/section1.o \
 	dragonsphere/rooms/room101.o \
@@ -346,6 +347,7 @@ MODULE_OBJS := \
 	dragonsphere/main.o \
 	dragonsphere/main_menu.o \
 	dragonsphere/menus.o \
+	dragonsphere/sound.o \
 	forest/forest.o \
 	forest/digi.o \
 	forest/extra.o \
diff --git a/engines/mads/nebular/asound.cpp b/engines/mads/nebular/asound.cpp
index 0e7f2061469..22c5124cc76 100644
--- a/engines/mads/nebular/asound.cpp
+++ b/engines/mads/nebular/asound.cpp
@@ -192,7 +192,6 @@ void ASound::validate() {
 	}
 }
 
-
 void ASound::adlibInit() {
 	write(4, 0x60);
 	write(4, 0x80);
diff --git a/engines/mads/nebular/asound_nebular.cpp b/engines/mads/nebular/asound_nebular.cpp
index d3a26455677..a3961d0446a 100644
--- a/engines/mads/nebular/asound_nebular.cpp
+++ b/engines/mads/nebular/asound_nebular.cpp
@@ -19,7 +19,6 @@
  *
  */
 
-#include "audio/fmopl.h"
 #include "mads/nebular/asound_nebular.h"
 
 namespace MADS {
diff --git a/engines/mads/nebular/sound.h b/engines/mads/nebular/sound.h
index 28587c480a0..90131696a1f 100644
--- a/engines/mads/nebular/sound.h
+++ b/engines/mads/nebular/sound.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef MADS_SOUND_H
-#define MADS_SOUND_H
+#ifndef MADS_NEBULAR_SOUND_H
+#define MADS_NEBULAR_SOUND_H
 
 #include "mads/core/sound_manager.h"
 
@@ -29,6 +29,10 @@ namespace RexNebular {
 
 class RexSoundManager : public SoundManager {
 protected:
+	/**
+	 * Load the particular section sound handler
+	 * @param sectionNum	Section number
+	 */
 	void loadDriver(int sectionNum) override;
 
 public:
@@ -37,6 +41,9 @@ public:
 	~RexSoundManager() override {
 	}
 
+	/**
+	 * Validate the sound driver files needed for data
+	 */
 	void validate() override;
 };
 
diff --git a/engines/mads/phantom/asound.cpp b/engines/mads/phantom/asound.cpp
index e55863b8cc4..9858a9317d1 100644
--- a/engines/mads/phantom/asound.cpp
+++ b/engines/mads/phantom/asound.cpp
@@ -20,6 +20,8 @@
  */
 
 #include "audio/fmopl.h"
+#include "common/file.h"
+#include "common/md5.h"
 #include "mads/phantom/asound.h"
 
 namespace MADS {
@@ -213,6 +215,45 @@ ASound::ASound(Audio::Mixer *mixer, const Common::Path &filename, int dataOffset
 	_opl->start(new Common::Functor0Mem<void, ASound>(this, &ASound::onTimer), CALLBACKS_PER_SECOND);
 }
 
+void ASound::validate(bool isDemo) {
+	Common::File f;
+	static const char *const MD5[] = {
+		"8edcb79a8c3514eac0835496326a72ae",
+		"4b81a46440f8404d9eda1ce5ae2c5579",
+		"11d8d441e47ad1ccd8faafd6572a17d0",
+		"4cd5c4d45126e60ca701690489ab8afa",
+		"588357d711bbcdabdf7d7e5d96013ce5",
+		nullptr,
+		nullptr,
+		nullptr,
+		"3d4843074c1dcbfd7919179c58aec9bc"
+	};
+
+	if (!isDemo) {
+		for (int i = 1; i <= 9; ++i) {
+			if (i >= 6 && i <= 8)
+				continue;
+			Common::Path filename(Common::String::format("asound.ph%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])
+				error("Invalid sound file - %s", filename.toString().c_str());
+		}
+	} else {
+		// For demo we only have one sound driver
+		if (!f.open("asound.pha"))
+			error("Could not process - asound.pha");
+		Common::String md5str = Common::computeStreamMD5AsString(f, 8192);
+		f.close();
+
+		if (md5str != "49aecc9c42b3d2f3be01611747f9649a")
+			error("Invalid sound file - asound.pha");
+	}
+}
+
 int ASound::stop() {
 	command0();
 	int result = _pollResult;
diff --git a/engines/mads/phantom/asound.h b/engines/mads/phantom/asound.h
index 4f3a11f9120..93b7c86961f 100644
--- a/engines/mads/phantom/asound.h
+++ b/engines/mads/phantom/asound.h
@@ -433,6 +433,12 @@ protected:
 	 */
 	virtual void callFunction(uint16 offset);
 
+public:
+	/**
+	 * Validates the presence of the sound driver files.
+	 */
+	static void validate(bool isDemo);
+
 public:
 	/**
 	 * Constructor
diff --git a/engines/mads/phantom/asound_phantom.cpp b/engines/mads/phantom/asound_phantom.cpp
index 1dbca1ab0db..270129ca06d 100644
--- a/engines/mads/phantom/asound_phantom.cpp
+++ b/engines/mads/phantom/asound_phantom.cpp
@@ -19,85 +19,11 @@
  *
  */
 
-#include "common/endian.h"
-#include "common/file.h"
-#include "common/md5.h"
-#include "common/textconsole.h"
 #include "mads/phantom/asound_phantom.h"
 
 namespace MADS {
 namespace Phantom {
 
-void PhantomSoundManager::validate() {
-	Common::File f;
-	static const char *const MD5[] = {
-		"8edcb79a8c3514eac0835496326a72ae",
-		"4b81a46440f8404d9eda1ce5ae2c5579",
-		"11d8d441e47ad1ccd8faafd6572a17d0",
-		"4cd5c4d45126e60ca701690489ab8afa",
-		"588357d711bbcdabdf7d7e5d96013ce5",
-		nullptr,
-		nullptr,
-		nullptr,
-		"3d4843074c1dcbfd7919179c58aec9bc"
-	};
-
-	if (!_isDemo) {
-		for (int i = 1; i <= 9; ++i) {
-			if (i >= 6 && i <= 8)
-				continue;
-			Common::Path filename(Common::String::format("asound.ph%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])
-				error("Invalid sound file - %s", filename.toString().c_str());
-		}
-	} else {
-		// For demo we only have one sound driver
-		if (!f.open("asound.pha"))
-			error("Could not process - asound.pha");
-		Common::String md5str = Common::computeStreamMD5AsString(f, 8192);
-		f.close();
-
-		if (md5str != "49aecc9c42b3d2f3be01611747f9649a")
-			error("Invalid sound file - asound.pha");
-	}
-}
-
-void PhantomSoundManager::loadDriver(int sectionNumber) {
-	if (_isDemo) {
-		_driver = new ASoundDemo(_mixer);
-		return;
-	}
-
-	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;
-		return;
-	}
-}
-
 /*-----------------------------------------------------------------------*/
 /* ASound1  (asound.ph1)                                                  *
  *-----------------------------------------------------------------------*/
diff --git a/engines/mads/phantom/asound_phantom.h b/engines/mads/phantom/asound_phantom.h
index d5db05d15f8..a2afb3bd590 100644
--- a/engines/mads/phantom/asound_phantom.h
+++ b/engines/mads/phantom/asound_phantom.h
@@ -19,30 +19,14 @@
  *
  */
 
-#ifndef MADS_PHANTOM_SOUND_H
-#define MADS_PHANTOM_SOUND_H
+#ifndef MADS_PHANTOM_ASOUND_PHANTOM_H
+#define MADS_PHANTOM_ASOUND_PHANTOM_H
 
 #include "mads/phantom/asound.h"
 
 namespace MADS {
 namespace Phantom {
 
-class PhantomSoundManager : public SoundManager {
-private:
-	bool _isDemo;
-
-protected:
-	void loadDriver(int sectionNum) override;
-
-public:
-	PhantomSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) : SoundManager(mixer, soundFlag), _isDemo(isDemo) {
-	}
-	~PhantomSoundManager() override {
-	}
-
-	void validate() override;
-};
-
 /**
  * ASound1  (asound.ph1, _dataOffset = 0x21e0)
  *
diff --git a/engines/mads/phantom/phantom.cpp b/engines/mads/phantom/phantom.cpp
index 6a189b7b251..d4f6cd53a60 100644
--- a/engines/mads/phantom/phantom.cpp
+++ b/engines/mads/phantom/phantom.cpp
@@ -35,7 +35,7 @@
 #include "mads/core/text.h"
 #include "mads/phantom/phantom.h"
 #include "mads/phantom/main.h"
-#include "mads/phantom/asound_phantom.h"
+#include "mads/phantom/sound.h"
 #include "mads/phantom/catacombs.h"
 #include "mads/phantom/global.h"
 #include "mads/phantom/rooms/section1.h"
diff --git a/engines/mads/phantom/sound.cpp b/engines/mads/phantom/sound.cpp
new file mode 100644
index 00000000000..88555f62e23
--- /dev/null
+++ b/engines/mads/phantom/sound.cpp
@@ -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/>.
+ *
+ */
+
+#include "mads/phantom/sound.h"
+#include "mads/phantom/asound_phantom.h"
+
+namespace MADS {
+namespace Phantom {
+
+void PhantomSoundManager::validate() {
+	if (_isMT32) {
+		assert(0 == 1);
+	} else {
+		ASound::validate(_isDemo);
+	}
+}
+
+void PhantomSoundManager::loadDriver(int sectionNumber) {
+	removeDriver();
+
+	if (_isMT32) {
+		// Roland MT32 drivers
+		assert(0 == 1);
+
+	} else {
+		// Adlib drivers
+		if (_isDemo) {
+			_driver = new ASoundDemo(_mixer);
+			return;
+		}
+
+		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;
+			return;
+		}
+	}
+}
+
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/sound.h b/engines/mads/phantom/sound.h
new file mode 100644
index 00000000000..33f6cfa544f
--- /dev/null
+++ b/engines/mads/phantom/sound.h
@@ -0,0 +1,55 @@
+/* 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_H
+#define MADS_PHANTOM_SOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Phantom {
+
+class PhantomSoundManager : public SoundManager {
+private:
+	bool _isDemo;
+
+protected:
+	/**
+	 * Load the appropriate sound driver for the current section of the game.
+	 */
+	void loadDriver(int sectionNum) override;
+
+public:
+	PhantomSoundManager(Audio::Mixer *mixer, bool &soundFlag, bool isDemo) :
+		SoundManager(mixer, soundFlag), _isDemo(isDemo) {
+	}
+	~PhantomSoundManager() override {}
+
+	/**
+	 * Validate the sound driver files needed for data
+	 */
+	void validate() override;
+};
+
+} // namespace Phantom
+} // namespace MADS
+
+#endif


Commit: c30730b28c9b7d589866406a65c0a0e838410307
    https://github.com/scummvm/scummvm/commit/c30730b28c9b7d589866406a65c0a0e838410307
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-26T19:32:30+10:00

Commit Message:
MADS: NEBULAR: Implemented RSound1 through RSound4

Assisted-by: Claude:claude-sonnet-5

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


diff --git a/engines/mads/nebular/rsound.cpp b/engines/mads/nebular/rsound.cpp
index 6fa15a53853..1ebbbfc5dbe 100644
--- a/engines/mads/nebular/rsound.cpp
+++ b/engines/mads/nebular/rsound.cpp
@@ -146,9 +146,13 @@ Channel *RSound::playSound(int offset) {
 }
 
 Channel *RSound::playSoundData(byte *pData, int startingChannel) {
-	// Scan for a free channel. Deliberately excludes channel 9 (index 8),
-	// matching the disassembly - playSound() never reaches it.
-	for (int i = startingChannel; i < RSOUND_CHANNEL_COUNT - 1; ++i) {
+	// playSound() (startingChannel=5) deliberately excludes channel 9
+	// (index 8), matching rsound.009's disassembly. playSoundAny()
+	// (startingChannel=0) reaches all 9 channels.
+	int endChannel = (startingChannel == 0) ? RSOUND_CHANNEL_COUNT : RSOUND_CHANNEL_COUNT - 1;
+
+	// Scan for a free channel
+	for (int i = startingChannel; i < endChannel; ++i) {
 		if (!_channels[i]._activeCount) {
 			_channels[i].load(pData);
 			return &_channels[i];
@@ -156,7 +160,7 @@ Channel *RSound::playSoundData(byte *pData, int startingChannel) {
 	}
 
 	// None found; fall back to an interruptable (pending-stop) channel
-	for (int i = RSOUND_CHANNEL_COUNT - 2; i >= startingChannel; --i) {
+	for (int i = endChannel - 1; i >= startingChannel; --i) {
 		if (_channels[i]._pendingStop == 0xFF) {
 			_channels[i].load(pData);
 			return &_channels[i];
diff --git a/engines/mads/nebular/rsound.h b/engines/mads/nebular/rsound.h
index ac80bab2e25..f95bc459292 100644
--- a/engines/mads/nebular/rsound.h
+++ b/engines/mads/nebular/rsound.h
@@ -37,6 +37,27 @@ class RSound;
  * field names/roles were derived by tracing Channel_pollActive() (the
  * per-channel opcode interpreter) and cross-referencing against the
  * equivalent AdlibChannel fields in asound.h.
+ *
+ * Confirmed against the real DOS struct layout (IDA struct dump,
+ * sizeof=0x22): every field below from _activeCount through _soundData
+ * matches the original both in name and in byte offset/order exactly:
+ *   0x00 _activeCount    0x0C _volume          0x18 _innerLoopPtr
+ *   0x01 _pitchBendFadeStep 0x0D _pitchBend     0x1A _outerLoopPtr
+ *   0x02 _volumeFadeStep  0x0E _pan             0x1C _innerLoopCount
+ *   0x03 _panFadeStep     0x0F _volumeFadeReload 0x1E _outerLoopCount
+ *   0x04 _note            0x10 _pitchBendFadeReload 0x20 _soundData
+ *   0x05 _program         0x11 _panFadeReload
+ *   0x06 _velocity        0x12 _pitchBendFadeCount
+ *   0x07 _noteOffset      0x13 _pendingStop
+ *   0x08 _keyOnDelay      0x14 _loopStartPtr
+ *   0x09 _volumeFadeCounter 0x16 _pSrc
+ *   0x0A _pitchBendFadeCounter
+ *   0x0B _panFadeCounter
+ * _owner and _midiChannel below are NOT part of the original struct (it
+ * has no equivalent fields) - they're C++-side conveniences so Channel
+ * methods and callers don't need the MIDI channel number (array index+1)
+ * threaded through separately. Any future raw "[bx+N]" disassembly offset
+ * can be mapped directly via the table above.
  */
 class Channel {
 public:
@@ -179,6 +200,15 @@ protected:
 	 */
 	Channel *playSound(int offset);
 
+	/**
+	 * Play the specified sound using any channel from 0 to 8, including
+	 * channel 9 - confirmed distinct from playSound() by rsound.001's
+	 * disassembly (rsound_command30/32/38).
+	 */
+	Channel *playSoundAny(int offset) {
+		return playSoundData(loadData(offset), 0);
+	}
+
 	Channel *playSoundData(byte *pData, int startingChannel = 5);
 
 	/**
diff --git a/engines/mads/nebular/rsound_nebular.cpp b/engines/mads/nebular/rsound_nebular.cpp
index 80486a749c4..2aada741147 100644
--- a/engines/mads/nebular/rsound_nebular.cpp
+++ b/engines/mads/nebular/rsound_nebular.cpp
@@ -24,6 +24,1213 @@
 namespace MADS {
 namespace RexNebular {
 
+const RSound1::CommandPtr RSound1::_commandList[42] = {
+	&RSound1::command0, &RSound1::command1, &RSound1::command2, &RSound1::command3,
+	&RSound1::command4, &RSound1::command5, &RSound1::command6, &RSound1::command7,
+	&RSound1::command8, &RSound1::command9, &RSound1::command10, &RSound1::command11,
+	&RSound1::command12, &RSound1::command13, &RSound1::command14, &RSound1::command15,
+	&RSound1::command16, &RSound1::command17, &RSound1::command18, &RSound1::command19,
+	&RSound1::command20, &RSound1::command21, &RSound1::command22, &RSound1::command23,
+	&RSound1::command24, &RSound1::command25, &RSound1::command26, &RSound1::command27,
+	&RSound1::command28, &RSound1::command29, &RSound1::command30, &RSound1::command31,
+	&RSound1::command32, &RSound1::command33, &RSound1::command34, &RSound1::command35,
+	&RSound1::command36, &RSound1::command37, &RSound1::command38, &RSound1::command39,
+	&RSound1::command40, &RSound1::command41
+};
+
+RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.001", 0x1350, 0x1A90) {
+}
+
+int RSound1::command(int commandId, int param) {
+	if (commandId > 41)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound1::clampParam() {
+	return (_commandParam > 0x40) ? _commandParam - 0x40 : 0;
+}
+
+void RSound1::method1() {
+	byte *pData = loadData(0x1166);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x13BC));
+		_channels[2].load(loadData(0x155C));
+		_channels[3].load(loadData(0x15D8));
+	}
+}
+
+int RSound1::command9() {
+	playSound(0xAD4);
+	return 0;
+}
+
+int RSound1::command10() {
+	byte *pData = loadData(0xCE4);
+	if (!isSoundActive(pData)) {
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0xD18));
+		_channels[2].load(loadData(0xE9C));
+		_channels[3].load(loadData(0xEE8));
+	}
+	return 0;
+}
+
+int RSound1::command11() {
+	method1();
+	_channels[0]._volume = 0;
+	sendVolume(1, _channels[0]._volume);
+	_channels[1]._volume = 0;
+	sendVolume(2, _channels[1]._volume);
+	return 0;
+}
+
+int RSound1::command12() {
+	method1();
+	_channels[0]._volume = 80;
+	sendVolume(1, _channels[0]._volume);
+	_channels[1]._volume = 0;
+	sendVolume(2, _channels[1]._volume);
+	return 0;
+}
+
+int RSound1::command13() {
+	method1();
+	_channels[0]._volume = 80;
+	sendVolume(1, _channels[0]._volume);
+	_channels[1]._volume = 80;
+	sendVolume(2, _channels[1]._volume);
+	return 0;
+}
+
+int RSound1::command14() {
+	playSound(0x16C2);
+	return 0;
+}
+
+int RSound1::command15() {
+	byte *pData = loadData(0xF3A);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[4].load(pData);
+		_channels[5].load(loadData(0x102A));
+		_channels[6].load(loadData(0x110E));
+	}
+	return 0;
+}
+
+int RSound1::command16() {
+	playSound(0xADE);
+	return 0;
+}
+
+int RSound1::command17() {
+	playSound(0xAE8);
+	return 0;
+}
+
+int RSound1::command18() {
+	playSound(0xAF2);
+	return 0;
+}
+
+int RSound1::command19() {
+	command1();
+	playSound(0xB04);
+	return 0;
+}
+
+int RSound1::command20() {
+	playSound(0xB5E);
+	return 0;
+}
+
+int RSound1::command21() {
+	playSound(0xB4C);
+	return 0;
+}
+
+int RSound1::command22() {
+	playSound(0xB6E);
+	return 0;
+}
+
+int RSound1::command23() {
+	byte *pData = loadData(0xB7A);
+	pData[6] ^= 0x1F;
+	playSound(0xB7A);
+	return 0;
+}
+
+int RSound1::command24() {
+	playSound(0xB84);
+	return 0;
+}
+
+int RSound1::command25() {
+	playSound(0xB8E);
+	return 0;
+}
+
+int RSound1::command26() {
+	byte *pData = loadData(0xCCA);
+	int v1 = (getRandomNumber() & 24) + 45;
+	pData[8] = v1;
+	int v2 = clampParam() + 64;
+	pData[5] = v2;
+	_channels[7].load(pData);
+	return 0;
+}
+
+int RSound1::command27() {
+	byte *pData = loadData(0xCBE);
+	int v1 = (getRandomNumber() & 24) + 45;
+	pData[8] = v1;
+	int v2 = clampParam() + 64;
+	pData[5] = v2;
+	_channels[7].load(pData);
+	return 0;
+}
+
+int RSound1::command28() {
+	playSound(0xB9E);
+	return 0;
+}
+
+int RSound1::command29() {
+	byte *pData = loadData(0xC6C);
+	int v = (clampParam() >> 1) + 32;
+	pData[0xB] = v;
+	if (!isSoundActive(pData))
+		playSound(0xC6C);
+	return 0;
+}
+
+int RSound1::command30() {
+	byte *pData = loadData(0xC80);
+	int v = clampParam() + 63;
+	pData[0xB] = v;
+	if (!isSoundActive(pData))
+		playSoundAny(0xC80);
+	return 0;
+}
+
+int RSound1::command31() {
+	playSound(0xBBE);
+	return 0;
+}
+
+int RSound1::command32() {
+	byte *pData = loadData(0xC96);
+	int half = clampParam() >> 1;
+	pData[0xB] = pData[0x17] = half + 68;
+	pData[0x11] = pData[0x1D] = half + 20;
+	if (!isSoundActive(pData))
+		playSoundAny(0xC96);
+	return 0;
+}
+
+int RSound1::command33() {
+	playSound(0xBD0);
+	playSound(0xBDA);
+	return 0;
+}
+
+int RSound1::command34() {
+	byte *pData = loadData(0xBE8);
+	int v = (getRandomNumber() & 12) + 45;
+	pData[9] = v;
+	pData[0x10] = v + 36;
+	playSound(0xBE8);
+	return 0;
+}
+
+int RSound1::command35() {
+	playSound(0xBFC);
+	playSound(0xC0E);
+	playSound(0xC20);
+	return 0;
+}
+
+int RSound1::command36() {
+	playSound(0xC3E);
+	return 0;
+}
+
+int RSound1::command37() {
+	byte *pData = loadData(0xC4C);
+	int r = getRandomNumber() & 15;
+	pData[6] = r + 42;
+	pData[3] = 62 - (r << 1);
+	playSound(0xC4C);
+	return 0;
+}
+
+int RSound1::command38() {
+	playSoundAny(0xC56);
+	playSound(0xC60);
+	return 0;
+}
+
+int RSound1::command39() {
+	byte *pData = loadData(0x1818);
+	if (!isSoundActive(pData)) {
+		_channels[4].load(pData);
+		_channels[5].load(loadData(0x1848));
+		_channels[6].load(loadData(0x1874));
+		_channels[7].load(loadData(0x18B4));
+		_channels[8].load(loadData(0x18CE));
+	}
+	return 0;
+}
+
+int RSound1::command40() {
+	playSound(0xC34);
+	return 0;
+}
+
+int RSound1::command41() {
+	playSound(0xCD6);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const uint16 RSound2::_table1[16] = {
+	0x3234, 0x3250, 0x326A, 0x3284, 0x329E, 0x32D6, 0x3304, 0x333C,
+	0x3352, 0x3378, 0x33B6, 0x33D0, 0x33EA, 0x3404, 0x341E, 0x343E
+};
+
+const RSound2::CommandPtr RSound2::_commandList[44] = {
+	&RSound2::command0, &RSound2::command1, &RSound2::command2, &RSound2::command3,
+	&RSound2::command4, &RSound2::command5, &RSound2::command6, &RSound2::command7,
+	&RSound2::command8, &RSound2::command9, &RSound2::command10, &RSound2::command11,
+	&RSound2::command12, &RSound2::command13, &RSound2::command14, &RSound2::command15,
+	&RSound2::command16, &RSound2::command17, &RSound2::command18, &RSound2::command19,
+	&RSound2::command20, &RSound2::command21, &RSound2::command22, &RSound2::command23,
+	&RSound2::command24, &RSound2::command25, &RSound2::command26, &RSound2::command27,
+	&RSound2::command28, &RSound2::command29, &RSound2::command30, &RSound2::command31,
+	&RSound2::command32, &RSound2::command33, &RSound2::command34, &RSound2::command35,
+	&RSound2::command36, &RSound2::command37, &RSound2::command38, &RSound2::command39,
+	&RSound2::command40, &RSound2::command41, &RSound2::command42, &RSound2::command43
+};
+
+RSound2::RSound2(Audio::Mixer *mixer) : RSound(mixer, "rsound.002", 0x1390, 0x42F0) {
+}
+
+int RSound2::command(int commandId, int param) {
+	if (commandId > 43)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound2::command5() {
+	_pitchCycleCounter = 47;
+	_channels[3].enable(0xFF);
+	_channels[4].enable(0xFF);
+	_channels[5].enable(0xFF);
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+	return 0;
+}
+
+int RSound2::command9() {
+	byte *pData = loadData(0x103C);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x11B2));
+		_channels[8].load(loadData(0x127E));
+	}
+	return 0;
+}
+
+int RSound2::command10() {
+	byte *pData = loadData(0x132E);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[2].load(pData);
+		_channels[8].load(loadData(0x1384));
+	}
+	return 0;
+}
+
+int RSound2::command11() {
+	byte *pData = loadData(0x1548);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[2].load(pData);
+		_channels[8].load(loadData(0x1648));
+	}
+	return 0;
+}
+
+int RSound2::command12() {
+	byte *pData = loadData(0xE52);
+	_pitchCycleCounter += 16;
+	pData[3] = _pitchCycleCounter & 0x7F;
+	playSound(0xE52);
+	return 0;
+}
+
+int RSound2::command13() {
+	playSound(0xE5C);
+	playSound(0xE66);
+	return 0;
+}
+
+int RSound2::command14() {
+	playSound(0xE70);
+	playSound(0xE8A);
+	return 0;
+}
+
+int RSound2::command15() {
+	byte *pData = loadData(0x1DFC);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x1DFC);
+		playSoundAny(0x222E);
+		playSoundAny(0x2648);
+		_channels[8].load(loadData(0x26A2));
+	}
+	return 0;
+}
+
+int RSound2::command16() {
+	byte *pData = loadData(0x3456);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x3456);
+		playSoundAny(0x3572);
+		playSoundAny(0x367E);
+		playSoundAny(0x37C6);
+		playSoundAny(0x39AE);
+		playSoundAny(0x3A3A);
+	}
+	return 0;
+}
+
+int RSound2::command17() {
+	byte *pData = loadData(0x3AC0);
+	if (!isSoundActive(pData)) {
+		playSound(0x3AC0);
+		playSound(0x3C70);
+		playSound(0x3E16);
+		playSound(0x3FBE);
+	}
+	return 0;
+}
+
+int RSound2::command18() {
+	if (_channels[7]._activeCount)
+		return 0;
+
+	int idx = (getRandomNumber() & 30) >> 1;
+	_channels[7].load(loadData(_table1[idx]));
+	return 0;
+}
+
+int RSound2::command19() {
+	byte *pData = loadData(0x2A64);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x2A64);
+		playSoundAny(0x2BE2);
+		playSoundAny(0x2DAC);
+		playSoundAny(0x2ECE);
+		playSoundAny(0x3026);
+		playSoundAny(0x30C2);
+	}
+	return 0;
+}
+
+int RSound2::command20() {
+	playSound(0xF1E);
+	playSound(0xF1E);
+	playSound(0xF1E);
+	playSound(0xF1E);
+	return 0;
+}
+
+int RSound2::command21() {
+	playSound(0xF58);
+	return 0;
+}
+
+int RSound2::command22() {
+	playSound(0xF44);
+	return 0;
+}
+
+int RSound2::command23() {
+	playSound(0xEBA);
+	return 0;
+}
+
+int RSound2::command24() {
+	playSound(0xEB0);
+	return 0;
+}
+
+int RSound2::command25() {
+	playSound(0xEA6);
+	return 0;
+}
+
+int RSound2::command26() {
+	playSound(0xF4E);
+	return 0;
+}
+
+int RSound2::command27() {
+	Channel *ch = playSound(0xFAC);
+	if (ch)
+		ch->_innerLoopPtr = loadData(0xFB8);
+
+	ch = playSound(0xFB2);
+	if (ch)
+		ch->_innerLoopPtr = loadData(0xFB8);
+
+	playSound(0xFB8);
+	return 0;
+}
+
+int RSound2::command28() {
+	byte *pData = loadData(0xEDA);
+	int r = getRandomNumber();
+	int v1 = r & 0x7F;
+	pData[7] = v1;
+	int v2 = (v1 & 0x0F) + 0x43;
+	pData[8] = v2;
+	int v3 = v2 + 0x0C;
+	pData[0xA] = v3;
+	playSound(0xEDA);
+	return 0;
+}
+
+int RSound2::command29() {
+	playSoundAny(0xF80);
+	return 0;
+}
+
+int RSound2::command30() {
+	playSound(0xF14);
+	byte *pData = loadData(0xF0A);
+	pData[3] = 40;
+	playSound(0xF0A);
+	return 0;
+}
+
+int RSound2::command31() {
+	byte *pData = loadData(0xF0A);
+	pData[3] = 0x18;
+	playSound(0xF0A);
+	return 0;
+}
+
+int RSound2::command32() {
+	playSound(0xEC4);
+	return 0;
+}
+
+int RSound2::command33() {
+	playSound(0xED0);
+	return 0;
+}
+
+int RSound2::command34() {
+	playSound(0xEE8);
+	return 0;
+}
+
+int RSound2::command35() {
+	playSound(0xEF8);
+	return 0;
+}
+
+int RSound2::command36() {
+	playSound(0xFF4);
+	playSound(0x1008);
+	return 0;
+}
+
+int RSound2::command37() {
+	playSound(0xFCC);
+	return 0;
+}
+
+int RSound2::command38() {
+	byte *pData = loadData(0x2B0E);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x2B0E);
+		playSoundAny(0x2CD0);
+		playSoundAny(0x2E46);
+		playSoundAny(0x2F7C);
+		playSoundAny(0x3074);
+		playSoundAny(0x317E);
+	}
+	return 0;
+}
+
+int RSound2::command39() {
+	playSound(0xFDA);
+	return 0;
+}
+
+int RSound2::command40() {
+	playSound(0xFE6);
+	return 0;
+}
+
+int RSound2::command41() {
+	playSoundAny(0xF62);
+	return 0;
+}
+
+int RSound2::command42() {
+	playSound(0xF92);
+	return 0;
+}
+
+int RSound2::command43() {
+	playSound(0x1018);
+	playSound(0x102A);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound3::CommandPtr RSound3::_commandList[61] = {
+	&RSound3::command0, &RSound3::command1, &RSound3::command2, &RSound3::command3,
+	&RSound3::command4, &RSound3::command5, &RSound3::command6, &RSound3::command7,
+	&RSound3::command8, &RSound3::command9, &RSound3::command10, &RSound3::command11,
+	&RSound3::nullCommand, &RSound3::command13, &RSound3::command14, &RSound3::command15,
+	&RSound3::command16, &RSound3::command17, &RSound3::command18, &RSound3::command19,
+	&RSound3::command20, &RSound3::command21, &RSound3::command22, &RSound3::command23,
+	&RSound3::command24, &RSound3::command25, &RSound3::command26, &RSound3::command27x42,
+	&RSound3::command28, &RSound3::command29, &RSound3::command30, &RSound3::command31,
+	&RSound3::command32, &RSound3::command33, &RSound3::command34, &RSound3::command35,
+	&RSound3::command36, &RSound3::command37, &RSound3::command38, &RSound3::command39,
+	&RSound3::command40, &RSound3::command41, &RSound3::command27x42, &RSound3::command43,
+	&RSound3::command44, &RSound3::command45, &RSound3::command46, &RSound3::command47x49,
+	&RSound3::command48, &RSound3::command47x49, &RSound3::command50, &RSound3::command51,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::command57, &RSound3::nullCommand, &RSound3::command59,
+	&RSound3::command60
+};
+
+RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.003", 0x14E0, 0x4C60) {
+}
+
+int RSound3::command(int commandId, int param) {
+	if (commandId > 60)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	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();
+	_channels[4].enable(0xFF);
+	_channels[5].enable(0xFF);
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+}
+
+int RSound3::command1() {
+	command3();
+	resetUpperChannelsTail();
+	return 0;
+}
+
+int RSound3::command5() {
+	if (!isSoundActive(loadData(0x1AE6)))
+		resetUpperChannelsTail();
+	return 0;
+}
+
+int RSound3::command9() {
+	command1();
+	_byte10742 = (byte)_commandParam;
+	return 0;
+}
+
+int RSound3::command10() {
+	byte *pData = loadData(0x14FE);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x1630));
+		_channels[2].load(loadData(0x186E));
+		_channels[3].load(loadData(0x1A68));
+		_channels[8].load(loadData(0x1AA6));
+	}
+	return 0;
+}
+
+void RSound3::setVariantByte(byte value) {
+	loadData(0x204A)[1] = value;
+	loadData(0x229C)[1] = value;
+	loadData(0x2748)[1] = value;
+	loadData(0x2C56)[1] = value;
+}
+
+int RSound3::command11() {
+	if (!isSoundActive(loadData(0x1AE6))) {
+		setVariantByte(0x64);
+		_channels[0].load(loadData(0x1AE6));
+		_channels[1].load(loadData(0x1E00));
+		_channels[2].load(loadData(0x1E66));
+		_channels[3].load(loadData(0x204A));
+		_channels[4].load(loadData(0x229C));
+		_channels[5].load(loadData(0x2748));
+		_channels[6].load(loadData(0x2C56));
+	}
+	return 0;
+}
+
+int RSound3::command13() {
+	command1();
+	playSoundAny(0x1364);
+	playSoundAny(0x1364);
+	playSoundAny(0x1364);
+	playSoundAny(0x1364);
+	playSoundAny(0x1364);
+	return 0;
+}
+
+int RSound3::command14() {
+	_channels[0].load(loadData(0x32DC));
+	_channels[1].load(loadData(0x32FC));
+	_channels[2].load(loadData(0x331C));
+	_channels[3].load(loadData(0x333E));
+	_channels[4].load(loadData(0x335C));
+	_channels[5].load(loadData(0x339E));
+	_channels[6].load(loadData(0x33DE));
+	_channels[7].load(loadData(0x341E));
+	return 0;
+}
+
+void RSound3::sendDualVolume(byte volume) {
+	_channels[0]._volume = volume;
+	sendVolume(1, volume);
+	_channels[0]._volume = volume;
+	sendVolume(2, volume);
+}
+
+int RSound3::command15() {
+	setVariantByte(0x60);
+	sendDualVolume(0x60);
+
+	if (_channels[3]._activeCount && _channels[3]._soundData == loadData(0x204A)) {
+		_channels[2]._pendingStop = 0xFF;
+		_channels[3]._pendingStop = 0xFF;
+		_channels[4]._pendingStop = 0xFF;
+		_channels[5]._pendingStop = 0xFF;
+		_channels[6]._pendingStop = 0xFF;
+		_channels[7]._pendingStop = 0xFF;
+		sub1074E();
+		return 0;
+	}
+
+	command1();
+	_channels[0].load(loadData(0x1AE6));
+	_channels[1].load(loadData(0x1E00));
+	return 0;
+}
+
+int RSound3::command16() {
+	_command16AltFlag = !_command16AltFlag;
+
+	if (_command16AltFlag) {
+		byte *pData = loadData(0x345E);
+		if (!isSoundActive(pData)) {
+			_channels[0].load(pData);
+			_channels[1].load(loadData(0x364C));
+			_channels[2].load(loadData(0x3806));
+			_channels[3].load(loadData(0x399E));
+		}
+	} else {
+		byte *pData = loadData(0x3B26);
+		if (!isSoundActive(pData)) {
+			command1();
+			_channels[0].load(pData);
+			_channels[1].load(loadData(0x3BD8));
+			_channels[2].load(loadData(0x3CF8));
+			_channels[3].load(loadData(0x3E46));
+		}
+	}
+	return 0;
+}
+
+int RSound3::command17() {
+	byte *pData = loadData(0x3F5C);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x4022));
+		_channels[2].load(loadData(0x41F0));
+		_channels[3].load(loadData(0x42F8));
+	}
+	return 0;
+}
+
+int RSound3::command18() {
+	command1();
+	_channels[0].load(loadData(0x4492));
+	_channels[1].load(loadData(0x45A4));
+	_channels[2].load(loadData(0x46A2));
+	_channels[3].load(loadData(0x47DC));
+	_channels[4].load(loadData(0x49C0));
+	_channels[5].load(loadData(0x4A4E));
+	return 0;
+}
+
+int RSound3::command19() {
+	if (!isSoundActive(loadData(0x1AE6)))
+		playSound(0x12B4);
+	return 0;
+}
+
+int RSound3::command20() {
+	if (!isSoundActive(loadData(0x1AE6)))
+		playSound(0x1246);
+	return 0;
+}
+
+int RSound3::command21() {
+	if (!isSoundActive(loadData(0x1AE6))) {
+		playSound(0x1232);
+		playSound(0x123C);
+	}
+	return 0;
+}
+
+int RSound3::command22() {
+	playSound(0x126E);
+	return 0;
+}
+
+int RSound3::command23() {
+	if (!isSoundActive(loadData(0x1AE6))) {
+		playSound(0x12D2);
+		playSound(0x12E0);
+	}
+	return 0;
+}
+
+int RSound3::command24() {
+	if (!isSoundActive(loadData(0x1AE6))) {
+		playSound(0x11E2);
+		playSound(0x120A);
+	}
+	return 0;
+}
+
+int RSound3::command25() {
+	method1(0x11A6, 42);
+	method1(0x11C4, 42);
+	return 0;
+}
+
+int RSound3::command26() {
+	playSound(0x1252);
+	return 0;
+}
+
+int RSound3::command27x42() {
+	playSound(0x14BE);
+	return 0;
+}
+
+int RSound3::command28() {
+	byte *pData = loadData(0x12EE);
+	pData[3] = 0x4D;
+	playSound(0x12EE);
+	return 0;
+}
+
+int RSound3::command29() {
+	byte *pData = loadData(0x12EE);
+	pData[3] = 0x7F;
+	playSound(0x12EE);
+	return 0;
+}
+
+int RSound3::command30() {
+	playSound(0x14B4);
+	playSound(0x14AA);
+	return 0;
+}
+
+int RSound3::command31() {
+	playSound(0x12F8);
+	playSound(0x130E);
+	return 0;
+}
+
+int RSound3::command32() {
+	playSound(0x1472);
+	return 0;
+}
+
+int RSound3::command33() {
+	playSound(0x147E);
+	return 0;
+}
+
+int RSound3::command34() {
+	playSound(0x1488);
+	return 0;
+}
+
+int RSound3::command35() {
+	playSound(0x1498);
+	return 0;
+}
+
+int RSound3::command36() {
+	playSound(0x14DA);
+	playSound(0x14EE);
+	return 0;
+}
+
+int RSound3::command37() {
+	playSound(0x14CC);
+	return 0;
+}
+
+int RSound3::command38() {
+	playSound(0x133A);
+	return 0;
+}
+
+int RSound3::command39() {
+	byte *pData = loadData(0x1346);
+	pData[3] = 77;
+	_command3940Toggle ^= 4;
+	pData[6] = _command3940Toggle + 0x28;
+	playSound(0x1346);
+	return 0;
+}
+
+int RSound3::command40() {
+	byte *pData = loadData(0x1346);
+	pData[3] = 47;
+	_command3940Toggle ^= 4;
+	pData[6] = _command3940Toggle + 0x28;
+	playSound(0x1346);
+	return 0;
+}
+
+int RSound3::command41() {
+	playSound(0x1184);
+	return 0;
+}
+
+int RSound3::command43() {
+	playSound(0x1350);
+	playSound(0x135A);
+	return 0;
+}
+
+int RSound3::command44() {
+	playSound(0x12A0);
+	return 0;
+}
+
+int RSound3::command45() {
+	playSound(0x12AA);
+	return 0;
+}
+
+int RSound3::command46() {
+	playSound(0x13AA);
+	playSound(0x13C6);
+	return 0;
+}
+
+int RSound3::command47x49() {
+	playSound(0x13E6);
+	playSound(0x13FE);
+	return 0;
+}
+
+int RSound3::command48() {
+	playSound(0x141E);
+	return 0;
+}
+
+int RSound3::command50() {
+	playSound(0x1436);
+	playSound(0x144C);
+	return 0;
+}
+
+int RSound3::command51() {
+	playSound(0x125C);
+	return 0;
+}
+
+int RSound3::command57() {
+	playSound(0x1466);
+	return 0;
+}
+
+int RSound3::command59() {
+	playSound(0x1324);
+	return 0;
+}
+
+int RSound3::command60() {
+	playSound(0x132E);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound4::CommandPtr RSound4::_commandList[60] = {
+	&RSound4::command0, &RSound4::command1, &RSound4::command2, &RSound4::command3,
+	&RSound4::command4, &RSound4::command5, &RSound4::command6, &RSound4::command7,
+	&RSound4::command8, &RSound4::command9, &RSound4::command10, &RSound4::nullCommand,
+	&RSound4::command12, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::command19,
+	&RSound4::command20, &RSound4::command21, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::command27,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::command30, &RSound4::nullCommand,
+	&RSound4::command32, &RSound4::command33, &RSound4::command34, &RSound4::command35,
+	&RSound4::command36, &RSound4::command37, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::command52, &RSound4::command53,
+	&RSound4::command54, &RSound4::command55, &RSound4::command56, &RSound4::command57,
+	&RSound4::command58, &RSound4::command59
+};
+
+RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.004", 0x1340, 0x2E20) {
+}
+
+int RSound4::command(int commandId, int param) {
+	if (commandId > 59)
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+void RSound4::tickCallback() {
+	if (!_callbackPeriod)
+		return;
+	if (--_callbackCounter)
+		return;
+
+	_callbackCounter = _callbackPeriod;
+	if (_callbackFnPtr)
+		(this->*_callbackFnPtr)();
+}
+
+int RSound4::command9() {
+	command1();
+	_byte10745 = (byte)_commandParam;
+	return 0;
+}
+
+byte RSound4::paramToVariant() {
+	return (byte)((_commandParam >> 1) + 36);
+}
+
+void RSound4::setCommand12Variant() {
+	byte value = paramToVariant();
+	loadData(0x1966)[1] = value;
+	loadData(0x1DC8)[1] = value;
+	loadData(0x1FBA)[1] = value;
+	loadData(0x211E)[1] = value;
+	loadData(0x24A8)[1] = value;
+}
+
+int RSound4::command12() {
+	if (_channels[4]._soundData == loadData(0x1966) && _channels[4]._activeCount) {
+		setCommand12Variant();
+		return 0;
+	}
+
+	setCommand12Variant();
+	command1();
+	_channels[4].load(loadData(0x1966));
+	_channels[5].load(loadData(0x1DC8));
+	_channels[6].load(loadData(0x1FBA));
+	_channels[7].load(loadData(0x211E));
+	_channels[8].load(loadData(0x24A8));
+	return 0;
+}
+
+void RSound4::loadIntroChannels() {
+	_channels[0].load(loadData(0x1274));
+	_channels[1].load(loadData(0x13A6));
+	_channels[2].load(loadData(0x15E4));
+}
+
+int RSound4::command10() {
+	command1();
+	_channels[3].load(loadData(0x17DE));
+	_channels[8].load(loadData(0x181C));
+	loadIntroChannels();
+	return 0;
+}
+
+int RSound4::command19() {
+	playSound(0x1196);
+	return 0;
+}
+
+int RSound4::command20() {
+	playSound(0x118A);
+	return 0;
+}
+
+int RSound4::command21() {
+	playSound(0x1260);
+	playSound(0x126A);
+	return 0;
+}
+
+int RSound4::command27() {
+	playSound(0x1220);
+	return 0;
+}
+
+int RSound4::command30() {
+	playSound(0x1216);
+	playSound(0x120C);
+	return 0;
+}
+
+int RSound4::command32() {
+	playSound(0x11D4);
+	return 0;
+}
+
+int RSound4::command33() {
+	playSound(0x11E0);
+	return 0;
+}
+
+int RSound4::command34() {
+	playSound(0x11EA);
+	return 0;
+}
+
+int RSound4::command35() {
+	playSound(0x11FA);
+	return 0;
+}
+
+int RSound4::command36() {
+	playSound(0x123C);
+	playSound(0x1250);
+	return 0;
+}
+
+int RSound4::command37() {
+	playSound(0x122E);
+	return 0;
+}
+
+int RSound4::command52() {
+	_channels[0]._pSrc = loadData(0x1188);
+	_channels[1]._pSrc = loadData(0x1188);
+	_channels[2]._pSrc = loadData(0x1188);
+	_channels[4].load(loadData(0x2A0C));
+	return 0;
+}
+
+int RSound4::command53() {
+	command1();
+	_callbackCounter = 56;
+	_callbackPeriod = 56;
+	playSoundAny(0x1888);
+	playSoundAny(0x18DE);
+	return 0;
+}
+
+void RSound4::loadCommand54() {
+	_callbackFnPtr = nullptr;
+	playSoundAny(0x18B2);
+	playSoundAny(0x1904);
+}
+
+int RSound4::command54() {
+	_callbackFnPtr = &RSound4::loadCommand54;
+	return 0;
+}
+
+void RSound4::loadCommand55() {
+	_callbackFnPtr = nullptr;
+	playSoundAny(0x191E);
+}
+
+int RSound4::command55() {
+	_callbackFnPtr = &RSound4::loadCommand55;
+	return 0;
+}
+
+void RSound4::loadCommand56() {
+	_callbackFnPtr = nullptr;
+	playSoundAny(0x185C);
+}
+
+int RSound4::command56() {
+	_callbackFnPtr = &RSound4::loadCommand56;
+	return 0;
+}
+
+int RSound4::command57() {
+	playSound(0x11C8);
+	return 0;
+}
+
+int RSound4::command58() {
+	_channels[4]._pSrc = loadData(0x1188);
+	loadIntroChannels();
+	return 0;
+}
+
+int RSound4::command59() {
+	playSound(0x11BE);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
 const RSound9::CommandPtr RSound9::_commandList[52] = {
 	&RSound9::command0, &RSound9::command1, &RSound9::command2, &RSound9::command3,
 	&RSound9::command4, &RSound9::command5, &RSound9::command6, &RSound9::command7,
diff --git a/engines/mads/nebular/rsound_nebular.h b/engines/mads/nebular/rsound_nebular.h
index b46f81422d0..468c0cedcdf 100644
--- a/engines/mads/nebular/rsound_nebular.h
+++ b/engines/mads/nebular/rsound_nebular.h
@@ -27,6 +27,349 @@
 namespace MADS {
 namespace RexNebular {
 
+class RSound1 : public RSound {
+private:
+	typedef int (RSound1:: *CommandPtr)();
+	static const CommandPtr _commandList[42];
+
+	/**
+	 * Shared loader for command11/12/13 - matches method1 in the
+	 * disassembly (isSoundActive-gated command1() + 4-channel load).
+	 */
+	void method1();
+
+	/**
+	 * Clamp helper: param > 0x40 ? param - 0x40 : 0. Matches the same
+	 * shape seen repeatedly across the Adlib drivers (e.g. ASound1's
+	 * command2627293032()).
+	 */
+	int clampParam();
+
+	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();
+public:
+	RSound1(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class RSound2 : public RSound {
+private:
+	typedef int (RSound2:: *CommandPtr)();
+	static const CommandPtr _commandList[44];
+
+	/**
+	 * Random-pick table used by command18 (TABLE1 in the disassembly).
+	 * Only entries 0-14 are ever reachable (command18 masks the random
+	 * index with decimal 30 = 0x1E), but all 16 confirmed clean entries
+	 * are kept for completeness. A further two rows of data follow
+	 * TABLE1 in seg001 but were never listed as reachable by any command
+	 * in this batch, and their values look like they may be text/other
+	 * data rather than further table entries - excluded until a command
+	 * turns up that actually reaches them.
+	 */
+	static const uint16 _table1[16];
+
+	/**
+	 * Persistent counter (byte_108F1 in the disassembly; initial value
+	 * 0x2F/47). Incremented by 16 (wrapping as a byte) each time
+	 * command12 runs; the low 7 bits are written into the sound data's
+	 * pitch/note byte before playback. command5 resets it back to 47.
+	 */
+	byte _pitchCycleCounter = 0x2F;
+
+	int command5();
+
+	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();
+public:
+	RSound2(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class RSound3 : public RSound {
+private:
+	typedef int (RSound3:: *CommandPtr)();
+	static const CommandPtr _commandList[61];
+
+	/**
+	 * Toggle used by command16 (byte_10A93 in the disassembly, initially
+	 * 0). Flips every call; alternates between two completely different
+	 * 4-channel music loads (one with a command1() fade first, one
+	 * without) - preserved exactly despite the asymmetry looking odd.
+	 */
+	bool _command16AltFlag = false;
+
+	/**
+	 * Toggle used by command39/40 (byte_10B77 in the disassembly,
+	 * initially 0). Shared between both commands: flips bit 2 (^= 4) on
+	 * every call to either one, and the post-toggle value + 0x28 is
+	 * written into the same sound data's byte 6, regardless of which of
+	 * the two commands triggered the toggle.
+	 */
+	byte _command3940Toggle = 0;
+
+	/**
+	 * byte_10742 in the disassembly. 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. Matches
+	 * loc_10C44 in the disassembly (called once from command25, offset
+	 * 0x11A6, with a truncated second call at offset 0x11C4 not yet
+	 * confirmed).
+	 */
+	Channel *method1(int offset, byte value);
+
+	/**
+	 * Matches the disassembly's OTHER "method1" (a same-named but
+	 * unrelated function at a different address, seg000:0947) - writes
+	 * the same byte into offset 1 of four fixed sound-data blocks
+	 * (0x204A/0x229C/0x2748/0x2C56), which are exactly the offsets
+	 * command11 loads into channels 4-7 right afterward. Renamed to
+	 * avoid colliding with the unrelated method1() above.
+	 */
+	void setVariantByte(byte value);
+
+	/**
+	 * Matches the disassembly's driver-specific "sendVolume" (distinct
+	 * from - and not related by inheritance to - the shared base
+	 * RSound::sendVolume(); renamed here to avoid the collision). Caches
+	 * the value into _channels[0]._volume (offset 0xC in the confirmed
+	 * Channel struct layout - see rsound.h), then sends it via the base
+	 * sendVolume() on MIDI channels 1 and 2 both. The double write to
+	 * _channels[0]._volume before each send is a preserved quirk from
+	 * the original (redundant but harmless).
+	 */
+	void sendDualVolume(byte volume);
+
+	/**
+	 * sub_1074E in the disassembly - 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
+	 * (funcs_108A2) 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 nullsub_1 in the original
+	 * (12, 52-56, 58).
+	 */
+	int notImplemented();
+
+	/**
+	 * Shared tail (loc_1083B in the disassembly) used by both command1
+	 * (falls through into it after calling command3()) and command5
+	 * (jumps straight into it after its isSoundActive gate). Enables
+	 * channels 5-8 (1-based; indices 4-7) - notably never reaches
+	 * channel 9.
+	 */
+	void resetUpperChannelsTail();
+
+	int command1();
+	int command5();
+	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 command27x42();
+	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 command43();
+	int command44();
+	int command45();
+	int command46();
+	int command47x49();
+	int command48();
+	int command50();
+	int command51();
+	int command57();
+	int command59();
+	int command60();
+public:
+	RSound3(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+class RSound4 : public RSound {
+private:
+	/**
+	 * Deferred callback state (checkCallback() in the disassembly,
+	 * called from this driver's own rsound_update() override) - confirmed
+	 * identical in shape to RSound9's mechanism: word_122C4 is the
+	 * reload period, word_122C2 the countdown, and _soundPtr the pointer
+	 * invoked (without self-clearing - each loadCommandNN() body clears
+	 * it itself, same as RSound9's loaders) once the countdown reaches 0.
+	 */
+	typedef void (RSound4:: *CallbackFunction)();
+	CallbackFunction _callbackFnPtr = nullptr;
+	int _callbackCounter = 0;
+	int _callbackPeriod = 0;
+
+	/**
+	 * byte_10745 in the disassembly - 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];
+
+	/**
+	 * method1 in the disassembly - called only from command12's shared
+	 * tail; computes (param >> 1) + 36.
+	 */
+	byte paramToVariant();
+
+	/**
+	 * loc_109D3 in the disassembly - writes the same variant byte into
+	 * offset 1 of the five sound blocks command12 (re)loads.
+	 */
+	void setCommand12Variant();
+
+	/**
+	 * loc_10967 in the disassembly - shared tail of both command10 and
+	 * command58, loading channels 1-3 (1-based; indices 0-2).
+	 */
+	void loadIntroChannels();
+
+	void tickCallback() override;
+
+	void loadCommand54();
+	void loadCommand55();
+	void loadCommand56();
+
+	int command9();
+	int command10();
+	int command12();
+	int command19();
+	int command20();
+	int command21();
+	int command27();
+	int command30();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command52();
+	int command53();
+	int command54();
+	int command55();
+	int command56();
+	int command57();
+	int command58();
+	int command59();
+public:
+	RSound4(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
 class RSound9 : public RSound {
 private:
 	/**




More information about the Scummvm-git-logs mailing list