[Scummvm-git-logs] scummvm master -> 124b69922ecf589f68384607838ba336372e6bf0

dreammaster noreply at scummvm.org
Mon Jul 27 12:28:36 UTC 2026


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

Summary:
124b69922e MADS: DRAGONSPHERE: Implemented RSound1 through RSound4


Commit: 124b69922ecf589f68384607838ba336372e6bf0
    https://github.com/scummvm/scummvm/commit/124b69922ecf589f68384607838ba336372e6bf0
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-27T22:28:29+10:00

Commit Message:
MADS: DRAGONSPHERE: Implemented RSound1 through RSound4

Assisted-by: Claude Code:claude-opus-4.8

Changed paths:
  A engines/mads/dragonsphere/rsound.cpp
  A engines/mads/dragonsphere/rsound.h
  A engines/mads/dragonsphere/rsound_dragonsphere.cpp
  A engines/mads/dragonsphere/rsound_dragonsphere.h
    engines/mads/dragonsphere/sound.cpp
    engines/mads/module.mk


diff --git a/engines/mads/dragonsphere/rsound.cpp b/engines/mads/dragonsphere/rsound.cpp
new file mode 100644
index 00000000000..bf5729c2f0d
--- /dev/null
+++ b/engines/mads/dragonsphere/rsound.cpp
@@ -0,0 +1,1070 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/util.h"
+#include "mads/dragonsphere/rsound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+/*-----------------------------------------------------------------------*/
+
+void Channel::reset(byte *startPtr) {
+	_activeCount = 0;
+	_pitchBendFadeStep = 0;
+	_volumeFadeStep = 0;
+	_panFadeStep = 0;
+	_note = 0;
+	_program = 0;
+	_velocity = 0;
+	_noteOffset = 0;
+	_keyOnDelayOverride = 0;
+	_keyOnDelay = 0;
+	_volumeFadeCounter = 0;
+	_volumeFadeReload = 0;
+	_pitchBendFadeCounter = 0;
+	_panFadeCounter = 0;
+	_panFadeReload = 0;
+	_pitchBendFadeReload = 0;
+	_pitchBendFadeCount = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_branchTarget = nullptr;
+	_transpose = 0;
+	_pendingStop = 0;
+	_volume = 0;
+
+	_loopStartPtr = startPtr;
+	_pSrc = startPtr;
+	_innerLoopPtr = startPtr;
+	_outerLoopPtr = startPtr;
+	_soundData = startPtr;
+	_pan = 0x40;
+	_pitchBend = 0x40;
+}
+
+void Channel::enable(int flag) {
+	if (_activeCount) {
+		_pendingStop = flag;
+		_soundData = nullptr;
+	}
+}
+
+void Channel::load(byte *pData) {
+	reset(pData);
+	_activeCount = 1;
+	_owner->resetPitchBend(_midiChannel);
+}
+
+/*-----------------------------------------------------------------------*/
+
+RSound::RSound(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize, int sysExOffset) : SoundDriver(mixer, filename, dataOffset, dataSize) {
+	_commandParam = 0;
+	_frameCounter = 0;
+	_tickCounter = 0;
+	_isDisabled = false;
+	_randomSeed = 1234;
+	_lastMidiStatus = 0;
+	_sysexChecksum = 0;
+	_stateChangedFlag = 0;
+	_pollResult = 0;
+	_sysExOffset = sysExOffset;
+	_fadeCheckCounter = 0;
+	_fadeCheckPeriod = 0;
+
+	_clockMedTarget = 0;
+	_clockCoarseTarget = 0;
+	_clockUnknown = 0;
+	_clockCoarse = 112;
+	_clockMed = 28;
+	_clockFine = 7;
+	_clockEnabled1 = 0;
+	_clockEnabled2 = 0;
+
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+	_musicIndex = 0;
+
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
+		_channels[i]._owner = this;
+		_channels[i]._midiChannel = i + 1;
+	}
+
+	for (int i = 0; i <= RSOUND_CHANNEL_COUNT; ++i)
+		for (int j = 0; j < 4; ++j)
+			_heldNotes[i][j] = 0xFF;
+
+	for (int i = 0; i < ARRAYSIZE(_scriptVariables); ++i)
+		_scriptVariables[i] = 0;
+
+	command0();
+}
+
+int RSound::stop() {
+	command0();
+	int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+int RSound::poll() {
+	update();
+	int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void RSound::setVolume(int volume) {
+	// TODO: no confirmed handler for this in the disassembly seen so far.
+}
+
+void RSound::resultCheck() {
+	if (_stateChangedFlag != 0xFFFF) {
+		_stateChangedFlag = 0xFFFF;
+		_pollResult = 0xFFFF;
+	}
+}
+
+/*-----------------------------------------------------------------------*/
+
+int RSound::getRandomNumber() {
+	uint16 val = _randomSeed + 0x9249;
+	val = (val >> 3) | (val << 13); // matches three "ror ax,1" in a row
+	_randomSeed = val;
+	return val;
+}
+
+Channel *RSound::playSoundData(byte *pData, int startingChannel, int freeScanEnd, int fallbackScanEnd) {
+	for (int i = startingChannel; i <= freeScanEnd; ++i) {
+		if (!_channels[i]._activeCount) {
+			_channels[i].load(pData);
+			return &_channels[i];
+		}
+	}
+
+	for (int i = fallbackScanEnd; i >= startingChannel; --i) {
+		if (_channels[i]._pendingStop == 0xFF) {
+			_channels[i].load(pData);
+			return &_channels[i];
+		}
+	}
+
+	return nullptr;
+}
+
+Channel *RSound::playSoundChannels6to8(int offset) {
+	// Channels 6-8 (0-based 5-7), symmetric free/fallback scan. Matches
+	// the disassembly's own playSoundChannels6to8 exactly.
+	return playSoundData(loadData(offset), 5, 7, 7);
+}
+
+Channel *RSound::playSoundChannels1To5(int offset) {
+	// Matches the disassembly's own playSoundAny exactly: channels 1-5
+	// (0-based 0-4), fully symmetric free/fallback scan. Renamed from
+	// playSoundAny for clarity - NOT Phantom's same-named function's 1-8
+	// range.
+	return playSoundData(loadData(offset), 0, 4, 4);
+}
+
+bool RSound::isSoundActive(byte *pData) {
+	// Matches the disassembly exactly: only channels 1-8 are checked,
+	// channel 9 is never included.
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT - 1; ++i) {
+		if (_channels[i]._activeCount && _channels[i]._soundData == pData)
+			return true;
+	}
+	return false;
+}
+
+int RSound::isMusicChannelsActive() {
+	// Matches sub_10477: channels 1-5 AND 9 (this driver's "lower"/music
+	// group) - NOT Phantom/ASound's fixed channel range.
+	return _channels[0]._activeCount || _channels[1]._activeCount ||
+		_channels[2]._activeCount || _channels[3]._activeCount ||
+		_channels[4]._activeCount || _channels[8]._activeCount;
+}
+
+/*-----------------------------------------------------------------------*/
+// 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) {
+	// Matches sub_10AC2. The disassembly derives midiChannel/velocity from
+	// the currently-active channel context rather than taking them as
+	// explicit call-site parameters, but the transmitted bytes are
+	// identical either way - kept parameterized here for API consistency
+	// with the rest of the class (and with the Phantom RSound family).
+	sendStatus(midiChannel, 0x90);
+	sendMidiByte(note);
+	sendMidiByte(velocity);
+}
+
+void RSound::sendProgramChange(int midiChannel, int program) {
+	sendStatus(midiChannel, 0xC0);
+	sendMidiByte(program);
+}
+
+void RSound::sendVolume(Channel *ch) {
+	// CORRECTED naming and NEW gate - see rsound.h class comment. The
+	// disassembly's real volume-sender (sub_10B54, unnamed) only
+	// transmits when the channel is not pending-stop; Channel_checkFade's
+	// own fade-out mechanism takes over otherwise.
+	if (ch->_pendingStop)
+		return;
+	sendStatus(ch->_midiChannel, 0xB0);
+	sendMidiByte(7);
+	sendMidiByte(ch->_volume);
+}
+
+void RSound::sendVolumeCC(int midiChannel, int volume) {
+	// Unlike sendVolume(), this sends the status byte UNCONDITIONALLY (no
+	// _lastMidiStatus dedup check) - used by command7 when restoring all
+	// 9 channels' volumes in a row.
+	byte status = 0xB0 | midiChannel;
+	_lastMidiStatus = status;
+	sendMidiByte(status);
+	sendMidiByte(7);
+	sendMidiByte(volume);
+}
+
+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::resetPitchBend(int midiChannel) {
+	sendPitchBend(midiChannel, 0x40);
+}
+
+void RSound::sendPan(int midiChannel, int value) {
+	// CORRECTED naming - see rsound.h class comment: this is the function
+	// the disassembly auto-named "sendVolume", which actually sends CC#10.
+	sendStatus(midiChannel, 0xB0);
+	sendMidiByte(0x0A); // CC#10: Pan
+	sendMidiByte(value);
+}
+
+void RSound::muteChannel(int midiChannel) {
+	byte status = 0xB0 | midiChannel;
+	_lastMidiStatus = status;
+	sendMidiByte(status);
+	sendMidiByte(7);
+	sendMidiByte(0);
+}
+
+void RSound::sendGmReset(int count) {
+	// Matches sendGmReset: counts DOWN from count to 1, using the counter
+	// itself as the MIDI channel number each iteration.
+	for (int midiChannel = count; midiChannel >= 1; --midiChannel) {
+		_fadeCheckPeriod = 0;
+
+		byte status = 0xB0 | midiChannel;
+		_lastMidiStatus = status;
+		sendMidiByte(status);
+		sendMidiByte(0x7B); // All Notes Off
+		sendMidiByte(0);
+		sendMidiByte(0x79); // Reset All Controllers
+		sendMidiByte(0);
+		sendMidiByte(7);    // Channel Volume
+		sendMidiByte(100);
+		sendMidiByte(0x0A); // Pan
+		sendMidiByte(0x40);
+	}
+}
+
+void RSound::sendSysExData(const byte *pData) {
+	static const byte header[] = { 0xF0, 0x41, 0x10, 0x16, 0x12 };
+	for (int i = 0; i < ARRAYSIZE(header); ++i)
+		sendMidiByte(header[i]);
+
+	_sysexChecksum = 0;
+	for (int i = 0; pData[i] != 0xFF; ++i) {
+		sendMidiByte(pData[i]);
+		_sysexChecksum += pData[i];
+	}
+
+	sendMidiByte((~_sysexChecksum + 1) & 0x7F);
+	sendMidiByte(0xF7);
+}
+
+void RSound::sendSysEx(int offset) {
+	sendSysExData(loadData(offset));
+}
+
+void RSound::sendPatchInitSequence() {
+	// Matches sendPatchInitSequence exactly: 4 outer iterations, each
+	// sending one SysEx message built from the fixed header at
+	// loadData(0x61) plus a computed payload.
+	byte base = 0;
+	for (int outer = 0; outer < 4; ++outer) {
+		byte *header = loadData(0x61);
+		for (int i = 0; header[i] != 0xFF; ++i)
+			sendMidiByte(header[i]);
+
+		_sysexChecksum = 0;
+		byte b1 = 5;
+		_sysexChecksum += b1; sendMidiByte(b1);
+		byte b2 = (byte)(outer << 1);
+		_sysexChecksum += b2; sendMidiByte(b2);
+		byte b3 = 0;
+		_sysexChecksum += b3; sendMidiByte(b3);
+
+		for (int inner = 0; inner < 0x20; ++inner) {
+			byte v1 = (byte)(outer >> 1);
+			_sysexChecksum += v1; sendMidiByte(v1);
+			byte v2 = (byte)(inner + base);
+			_sysexChecksum += v2; sendMidiByte(v2);
+			static const byte tail[] = { 0x18, 0x32, 0x0C, 0, 1, 0 };
+			for (int t = 0; t < ARRAYSIZE(tail); ++t) {
+				_sysexChecksum += tail[t];
+				sendMidiByte(tail[t]);
+			}
+		}
+
+		sendMidiByte((~_sysexChecksum + 1) & 0x7F);
+		sendMidiByte(0xF7);
+
+		base = (byte)((base + 0x20) & 0x3F);
+	}
+}
+
+void RSound::sendReverbSysEx(int mode, int time, int level) {
+	// INFERRED by structural analogy to Phantom's confirmed
+	// sendReverbSysEx() - see rsound.h class comment: not independently
+	// confirmed against rsound.dr1's literal offset-0x67 sysex template.
+	byte buffer[7] = { 0x10, 0x00, 0x01, (byte)(mode & 3), (byte)(time & 7), (byte)(level & 7), 0xFF };
+	sendSysExData(buffer);
+}
+
+/*-----------------------------------------------------------------------*/
+
+void RSound::Channel_flushHeldNotes(Channel *channel) {
+	byte *slots = _heldNotes[channel->_midiChannel];
+	for (int i = 0; i < 4; ++i) {
+		if (slots[i] != 0xFF) {
+			sendStatus(channel->_midiChannel, 0x90);
+			sendMidiByte(slots[i]);
+			sendMidiByte(0); // velocity 0 = note off
+			slots[i] = 0xFF;
+		}
+	}
+}
+
+void RSound::Channel_checkFade(Channel *channel, int midiChannel) {
+	if (!channel->_activeCount)
+		return;
+	if (!channel->_pendingStop)
+		return;
+
+	if (channel->_volume == 0) {
+		// enable_channel_data: 2 zero bytes - a generic silence/no-op
+		// stream, not driver-specific sound data (2 bytes here, unlike
+		// Phantom's 3 - confirmed directly from this disassembly).
+		static byte silenceStream[2] = { 0, 0 };
+		channel->_pSrc = silenceStream;
+		channel->_pendingStop = 0;
+		return;
+	}
+
+	channel->_volume -= 1;
+	sendVolume(channel);
+}
+
+void RSound::checkFadingChannels() {
+	if (!_fadeCheckPeriod)
+		return;
+	if (--_fadeCheckCounter > 0)
+		return;
+
+	_fadeCheckCounter = _fadeCheckPeriod;
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		Channel_checkFade(&_channels[i], i + 1);
+}
+
+/*-----------------------------------------------------------------------*/
+
+void RSound::resetChannelRange(int first, int last) {
+	for (int i = first; i <= last; ++i) {
+		_channels[i]._activeCount = 0;
+		_channels[i]._pitchBendFadeStep = 0;
+		_channels[i]._volumeFadeStep = 0;
+		_channels[i]._panFadeStep = 0;
+	}
+}
+
+void RSound::resetHeldNotes() {
+	// Zeroes (0xFF-fills) the logically-used region of _heldNotes; the
+	// real table has extra unused padding rows beyond channel 9 that
+	// this doesn't need to replicate - see the field comment.
+	for (int i = 0; i <= RSOUND_CHANNEL_COUNT; ++i)
+		for (int j = 0; j < 4; ++j)
+			_heldNotes[i][j] = 0xFF;
+}
+
+void RSound::resetAllChannels() {
+	bool wasDisabled = _isDisabled;
+	_isDisabled = true;
+	resetChannelRange(0, RSOUND_CHANNEL_COUNT - 1);
+	resetHeldNotes();
+	_isDisabled = wasDisabled;
+}
+
+void RSound::resetChannels1to5() {
+	// CORRECTED: also resets channel 9 (confirmed directly from
+	// resetChannels1to5's disassembly - channels 1,2,3,4,5,9, matching
+	// command3's own 6-channel "lower" group exactly), not just 1-5.
+	_isDisabled = true;
+	resetChannelRange(0, 4);
+	_channels[8]._activeCount = 0;
+	_channels[8]._pitchBendFadeStep = 0;
+	_channels[8]._volumeFadeStep = 0;
+	_channels[8]._panFadeStep = 0;
+	resetHeldNotes();
+	_isDisabled = false;
+}
+
+void RSound::resetChannels6to8() {
+	// Matches resetChannels6to8: channels 6,7,8 (0-based indices 5-7).
+	_isDisabled = true;
+	resetChannelRange(5, 7);
+	_isDisabled = false;
+}
+
+/*-----------------------------------------------------------------------*/
+
+int RSound::command0() {
+	// Matches rsound_command0: clears the deferred-callback state, then
+	// falls into the shared reset() (resetAllChannels + sendGmReset(9) +
+	// sendSysEx(_sysExOffset)).
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+
+	resetAllChannels();
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	sendSysEx(_sysExOffset);
+	return 0;
+}
+
+int RSound::command1() {
+	// Matches rsound_command1: calls command3() then falls straight
+	// through into command5() - both unconditional, no gating.
+	command3();
+	command5();
+	return 0;
+}
+
+int RSound::command2() {
+	resetChannels1to5();
+	sendGmReset(4);
+	return 0;
+}
+
+int RSound::command3() {
+	// Matches rsound_command3: enables channels 1,2,3,4,5 AND 9 (SIX
+	// channels) - this driver's "lower" group, wider than Phantom's
+	// default 1-4,9.
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[0].enable(0xFF);
+	_channels[1].enable(0xFF);
+	_channels[2].enable(0xFF);
+	_channels[3].enable(0xFF);
+	_channels[4].enable(0xFF);
+	_channels[8].enable(0xFF);
+	return 0;
+}
+
+int RSound::command4() {
+	// Matches rsound_command4: CONCRETE, no isSoundActive() gate at all
+	// (unlike Phantom's pure-virtual, always-gated command4/5) - see
+	// class comment.
+	resetChannels6to8();
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound::command5() {
+	// Matches rsound_command5: enables channels 6,7,8 - this driver's
+	// "upper" group, narrower than Phantom's default 5-8.
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[5].enable(0xFF);
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+	return 0;
+}
+
+int RSound::command6() {
+	_isDisabled = true;
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		muteChannel(i);
+	return 0;
+}
+
+int RSound::command7() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		sendVolumeCC(i + 1, _channels[i]._volume);
+	_isDisabled = false;
+	return 0;
+}
+
+int RSound::command8() {
+	int result = 0;
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		result |= _channels[i]._activeCount;
+	return result;
+}
+
+/*-----------------------------------------------------------------------*/
+
+int RSound::readScriptByte(byte *&pSrc) {
+	++pSrc;
+	return (int8)*pSrc;
+}
+
+uint16 RSound::readScriptWord(byte *&pSrc) {
+	byte lo = *++pSrc;
+	byte hi = *++pSrc;
+	return lo | (hi << 8);
+}
+
+void RSound::tickCallback() {
+	// Matches sub_122DA.
+	if (!_callbackPeriod)
+		return;
+	if (--_callbackCounter != 0)
+		return;
+
+	_callbackCounter = _callbackPeriod;
+	if (!_callbackFnPtr)
+		return;
+
+	CallbackFunction fn = _callbackFnPtr;
+	_callbackFnPtr = nullptr;
+	(this->*fn)();
+}
+
+void RSound::update() {
+	getRandomNumber();
+	if (_isDisabled)
+		return;
+
+	++_frameCounter;
+	++_tickCounter;
+	pollAllChannels();
+	tickCallback();
+	checkFadingChannels();
+}
+
+void RSound::pollAllChannels() {
+	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+		pollActiveChannel(&_channels[i]);
+}
+
+/*-----------------------------------------------------------------------*/
+// Per-channel opcode interpreter. Ported by structural analogy to the
+// already-confirmed Phantom RSound::pollActiveChannel() - see the class
+// comment in rsound.h for exactly which parts were independently
+// re-checked against THIS game's disassembly (the dispatch range and the
+// loop/restart/branch opcode cluster) versus carried over unchanged.
+
+void RSound::pollActiveChannel(Channel *ch) {
+	int midiChannel = ch->_midiChannel;
+
+	if (!ch->_activeCount)
+		return;
+
+	if (ch->_keyOnDelay) {
+		if (--ch->_keyOnDelay == 0)
+			Channel_flushHeldNotes(ch);
+	}
+
+	if (--ch->_activeCount != 0)
+		goto post_keyon;
+
+dispatch:
+	{
+		byte *pSrc = ch->_pSrc;
+		byte b = *pSrc;
+
+		if (!(b & 0x80)) {
+			// ---- Simple note event: [note][duration] ----
+			int note = (int8)pSrc[0] + ch->_transpose;
+			int duration = pSrc[1];
+			ch->_activeCount = duration;
+			ch->_pSrc += 2;
+
+			if (note != ch->_note)
+				Channel_flushHeldNotes(ch);
+
+			if (note != 0 && duration != 0) {
+				if (ch->_keyOnDelayOverride)
+					ch->_keyOnDelay = ch->_keyOnDelayOverride;
+				else
+					ch->_keyOnDelay = ch->_activeCount - ch->_noteOffset;
+
+				bool skip = false;
+				if (ch->_noteOffset < 0 && _heldNotes[midiChannel][0] == (byte)note)
+					skip = true;
+
+				if (!skip) {
+					ch->_note = note;
+					_heldNotes[midiChannel][0] = (byte)note;
+					sendNoteOn(midiChannel, note, ch->_velocity);
+				}
+			}
+			goto post_keyon;
+		}
+
+		if (b <= 0xBD)
+			goto post_keyon;
+
+		switch (b) {
+		case 0xBE: {
+			_clockUnknown = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			goto dispatch;
+		}
+		case 0xBF: {
+			_clockCoarseTarget = readScriptWord(pSrc);
+			if (_tickCounter == 0)
+				_clockCoarse = _clockCoarseTarget;
+			_clockEnabled1 = 1;
+			_clockEnabled2 = 1;
+			ch->_pSrc += 3;
+			goto dispatch;
+		}
+		case 0xC0: {
+			_clockMedTarget = readScriptByte(pSrc);
+			if (_tickCounter == 0)
+				_clockMed = _clockMedTarget;
+			ch->_pSrc += 2;
+			goto dispatch;
+		}
+		case 0xC1: {
+			_clockFine = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			goto dispatch;
+		}
+		case 0xC2: {
+			int v1 = readScriptByte(pSrc);
+			int v2 = readScriptByte(pSrc);
+			int v3 = readScriptByte(pSrc);
+			sendReverbSysEx(v1, v2, v3);
+			ch->_pSrc += 4;
+			goto dispatch;
+		}
+		case 0xC3: {
+			readScriptByte(pSrc); // matches sub_108D1: reads one operand, does nothing with it
+			ch->_pSrc += 2;
+			goto dispatch;
+		}
+		case 0xC4: {
+			// TODO: NOT PORTABLE AS-IS - see Phantom's identical case for
+			// rationale (raw code-address function-pointer call in the
+			// original). error() so this is impossible to miss if real
+			// game data ever actually triggers it.
+			readScriptWord(pSrc);
+			error("RSound::pollActiveChannel: opcode 0xC4 (function-pointer call) not portable as-is");
+			ch->_pSrc += 3;
+			goto dispatch;
+		}
+
+		// ---- Conditional branches, WITH resume-point bookkeeping ----
+		case 0xC5: case 0xC6: case 0xC7: case 0xC8:
+		case 0xC9: case 0xCA: case 0xCB: case 0xCC:
+		// ---- Same 8 conditions, WITHOUT bookkeeping (plain goto-if) ----
+		case 0xCD: case 0xCE: case 0xCF: case 0xD0:
+		case 0xD1: case 0xD2: case 0xD3: case 0xD4: {
+			bool withSave = (b <= 0xCC);
+			bool varMode = (b <= 0xC8) || (b >= 0xCD && b <= 0xD0);
+			int idx1 = readScriptByte(pSrc);
+			int idx2 = readScriptByte(pSrc);
+			int lhs = _scriptVariables[idx1 & 0xFF];
+			int rhs = varMode ? _scriptVariables[idx2 & 0xFF] : idx2;
+
+			int cond = (b - (withSave ? 0xC5 : 0xCD)) % 4;
+			bool take = false;
+			switch (cond) {
+			case 0: take = (lhs > rhs); break;
+			case 1: take = (lhs < rhs); break;
+			case 2: take = (lhs != rhs); break;
+			case 3: take = (lhs == rhs); break;
+			}
+
+			if (take) {
+				if (withSave)
+					ch->_branchTarget = ch->_pSrc + 5;
+				ch->_pSrc = loadData(readScriptWord(pSrc));
+			} else {
+				ch->_pSrc += 5;
+			}
+			goto dispatch;
+		}
+
+		// ---- Bitwise/arithmetic ops on script variables ----
+		case 0xD5: { // XOR, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] ^= _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xD6: { // XOR, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] ^= (byte)imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xD7: { // OR, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] |= _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xD8: { // OR, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] |= (byte)imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xD9: { // AND, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] &= _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDA: { // AND, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] &= (byte)imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDB: { // MOD, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			byte divisor = _scriptVariables[idx2 & 0xFF];
+			if (divisor)
+				_scriptVariables[idx1 & 0xFF] = _scriptVariables[idx1 & 0xFF] % divisor;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDC: {
+			int idx1 = readScriptByte(pSrc);
+			readScriptByte(pSrc); // operand read but unused, matching Phantom's confirmed same-shaped bug
+			byte self = _scriptVariables[idx1 & 0xFF];
+			if (self)
+				_scriptVariables[idx1 & 0xFF] = self % self;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDD: { // DIV, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			byte divisor = _scriptVariables[idx2 & 0xFF];
+			if (divisor)
+				_scriptVariables[idx1 & 0xFF] = _scriptVariables[idx1 & 0xFF] / divisor;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDE: {
+			int idx1 = readScriptByte(pSrc);
+			readScriptByte(pSrc); // operand read but unused, matching Phantom's confirmed same-shaped bug
+			byte self = _scriptVariables[idx1 & 0xFF];
+			if (self)
+				_scriptVariables[idx1 & 0xFF] = self / self;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xDF: { // MUL, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] = (byte)(_scriptVariables[idx1 & 0xFF] * _scriptVariables[idx2 & 0xFF]);
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE0: { // MUL, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] = (byte)(_scriptVariables[idx1 & 0xFF] * imm);
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE1: { // SUB, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] -= _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE2: { // SUB, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] -= (byte)imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE3: { // ADD, variable
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] += _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE4: { // ADD, immediate
+			int idx1 = readScriptByte(pSrc), imm = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] += (byte)imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE5: { // DEC
+			int idx1 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF]--;
+			ch->_pSrc += 2; goto dispatch;
+		}
+		case 0xE6: { // INC
+			int idx1 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF]++;
+			ch->_pSrc += 2; goto dispatch;
+		}
+		case 0xE7: {
+			int idx1 = readScriptByte(pSrc);
+			int idx2 = readScriptByte(pSrc);
+			byte v = _scriptVariables[idx1 & 0xFF];
+			(pSrc + 1 + idx2)[0] = v;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE8: { // copy: scriptVar[idx1] = scriptVar[idx2]
+			int idx1 = readScriptByte(pSrc), idx2 = readScriptByte(pSrc);
+			_scriptVariables[idx1 & 0xFF] = _scriptVariables[idx2 & 0xFF];
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xE9: { // set: scriptVar[idx1] = immediate (raw, unsigned)
+			int idx1 = readScriptByte(pSrc);
+			byte imm = *++pSrc;
+			_scriptVariables[idx1 & 0xFF] = imm;
+			ch->_pSrc += 3; goto dispatch;
+		}
+		case 0xEA: {
+			int idx1 = readScriptByte(pSrc);
+			int len2 = readScriptByte(pSrc);
+			byte *table1Base = pSrc + 1;
+			byte v1 = table1Base[_scriptVariables[idx1 & 0xFF]];
+			byte *table2 = pSrc + 1 + len2 + 1;
+			int8 sizeByte = (int8)*(pSrc + 1 + len2);
+			table2[sizeByte] = v1;
+			ch->_pSrc += len2 + 4;
+			goto dispatch;
+		}
+		case 0xEB: {
+			int rangeLow = readScriptByte(pSrc);
+			int rangeHigh = readScriptByte(pSrc);
+			int range = rangeHigh - rangeLow + 1;
+			int r = range ? (getRandomNumber() % range) : 0;
+			byte value = (byte)(rangeLow + r);
+			int tableByte = (int8)*(pSrc + 1);
+			(pSrc + 2 + tableByte)[0] = value;
+			ch->_pSrc += 4;
+			goto dispatch;
+		}
+		case 0xEC: {
+			int len1 = readScriptByte(pSrc);
+			int r = len1 ? (getRandomNumber() % len1) : 0;
+			byte *table1 = pSrc + 1;
+			byte v1 = table1[r];
+			byte *table2 = pSrc + 1 + len1 + 1;
+			int8 sizeByte = (int8)*(pSrc + 1 + len1);
+			table2[sizeByte] = v1;
+			ch->_pSrc += len1 + 3;
+			goto dispatch;
+		}
+
+		// ---- Loop / restart-pointer opcodes ----
+		case 0xFD: {
+			if (ch->_soundData == nullptr) {
+				ch->_pSrc = ch->_loopStartPtr;
+			} else {
+				ch->_loopStartPtr = ch->_soundData;
+				ch->_pSrc = ch->_soundData;
+				ch->_innerLoopPtr = ch->_soundData;
+				ch->_outerLoopPtr = ch->_soundData;
+			}
+			goto post_keyon;
+		}
+		case 0xFC: {
+			byte *ptr = loadData(readScriptWord(pSrc));
+			ch->_loopStartPtr = ptr;
+			ch->_pSrc = ptr;
+			ch->_innerLoopPtr = ptr;
+			ch->_outerLoopPtr = ptr;
+			ch->_soundData = ptr;
+			goto post_keyon;
+		}
+		case 0xFB: { // unconditional jump, no bookkeeping
+			ch->_pSrc = loadData(readScriptWord(pSrc));
+			goto post_keyon;
+		}
+		case 0xFA: { // "call": jump, saving a resume point
+			byte *target = loadData(readScriptWord(pSrc));
+			ch->_branchTarget = ch->_pSrc + 3;
+			ch->_pSrc = target;
+			goto post_keyon;
+		}
+		case 0xF9: { // "return"
+			if (ch->_branchTarget) {
+				ch->_pSrc = ch->_branchTarget;
+				ch->_branchTarget = nullptr;
+			} else {
+				ch->_pSrc++;
+			}
+			goto post_keyon;
+		}
+		case 0xF8: {
+			ch->_program = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			sendProgramChange(midiChannel, ch->_program);
+			goto post_keyon;
+		}
+		case 0xF7: {
+			ch->_noteOffset = readScriptByte(pSrc);
+			ch->_keyOnDelayOverride = 0;
+			ch->_pSrc += 2;
+			goto post_keyon;
+		}
+		case 0xF6: {
+			ch->_keyOnDelayOverride = readScriptByte(pSrc);
+			ch->_noteOffset = 0;
+			ch->_pSrc += 2;
+			goto post_keyon;
+		}
+		case 0xF5: {
+			ch->_pitchBendFadeReload = readScriptByte(pSrc);
+			ch->_pitchBendFadeStep = readScriptByte(pSrc);
+			ch->_pitchBendFadeCount = readScriptByte(pSrc);
+			ch->_pitchBendFadeCounter = 1;
+			ch->_pSrc += 4;
+			goto post_keyon;
+		}
+		case 0xF4: {
+			ch->_volume = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			sendVolume(ch);
+			goto post_keyon;
+		}
+		case 0xF3: {
+			ch->_velocity = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			goto post_keyon;
+		}
+		case 0xF2: {
+			ch->_volumeFadeReload = readScriptByte(pSrc);
+			ch->_volumeFadeStep = readScriptByte(pSrc);
+			ch->_volumeFadeCounter = 1;
+			ch->_pSrc += 3;
+			goto post_keyon;
+		}
+		case 0xF1: {
+			ch->_pitchBend = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			sendPitchBend(midiChannel, ch->_pitchBend);
+			goto post_keyon;
+		}
+		case 0xF0: {
+			ch->_pan = readScriptByte(pSrc);
+			sendPan(midiChannel, ch->_pan);
+			ch->_pSrc += 2;
+			goto post_keyon;
+		}
+		case 0xEF: {
+			ch->_panFadeReload = readScriptByte(pSrc);
+			ch->_panFadeStep = readScriptByte(pSrc);
+			ch->_panFadeCounter = 1;
+			ch->_pSrc += 3;
+			goto post_keyon;
+		}
+		case 0xEE: {
+			ch->_transpose = readScriptByte(pSrc);
+			ch->_pSrc += 2;
+			goto post_keyon;
+		}
+		case 0xED: {
+			// ---- Chord event: [count][note1..noteN][duration] ----
+			int count = readScriptByte(pSrc);
+			int firstNote = (int8)*(pSrc + 1) + ch->_transpose;
+			if (firstNote != _heldNotes[midiChannel][0])
+				Channel_flushHeldNotes(ch);
+
+			int i;
+			for (i = 0; i < count; ++i) {
+				int note = (int8)*(pSrc + 1 + i) + ch->_transpose;
+				bool skip = false;
+				if (ch->_noteOffset < 0 && _heldNotes[midiChannel][i] == (byte)note)
+					skip = true;
+				if (!skip) {
+					_heldNotes[midiChannel][i] = (byte)note;
+					sendNoteOn(midiChannel, note, ch->_velocity);
+				}
+			}
+			for (; i < 4; ++i)
+				_heldNotes[midiChannel][i] = 0xFF;
+
+			ch->_activeCount = *(pSrc + 1 + count);
+			if (ch->_keyOnDelayOverride)
+				ch->_keyOnDelay = ch->_keyOnDelayOverride;
+			else
+				ch->_keyOnDelay = ch->_activeCount - ch->_noteOffset;
+
+			ch->_pSrc += count + 3;
+			goto post_keyon;
+		}
+		default:
+			goto dispatch;
+		}
+	}
+
+post_keyon:
+	// ---- Volume fade tail ----
+	if (ch->_volumeFadeStep) {
+		if (--ch->_volumeFadeCounter == 0) {
+			ch->_volumeFadeCounter = ch->_volumeFadeReload;
+			ch->_volume += ch->_volumeFadeStep;
+			if ((byte)ch->_volume > 0x7F) {
+				ch->_volumeFadeStep = 0;
+				ch->_volume = ((byte)ch->_volume > 0xAF) ? 0 : 0x7F;
+			}
+			sendVolume(ch);
+		}
+	}
+
+	// ---- Pitch bend fade tail ----
+	if (ch->_pitchBendFadeStep) {
+		if (--ch->_pitchBendFadeCounter == 0) {
+			ch->_pitchBendFadeCounter = ch->_pitchBendFadeReload;
+			ch->_pitchBend += ch->_pitchBendFadeStep;
+			sendPitchBend(midiChannel, ch->_pitchBend);
+		}
+		if (--ch->_pitchBendFadeCount == 0)
+			ch->_pitchBendFadeStep = 0;
+	}
+
+	// ---- Pan fade tail ----
+	if (ch->_panFadeStep) {
+		if (--ch->_panFadeCounter == 0) {
+			ch->_panFadeCounter = ch->_panFadeReload;
+			ch->_pan += ch->_panFadeStep;
+			sendPan(midiChannel, ch->_pan);
+		}
+	}
+}
+
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/rsound.h b/engines/mads/dragonsphere/rsound.h
new file mode 100644
index 00000000000..cee728e3f9a
--- /dev/null
+++ b/engines/mads/dragonsphere/rsound.h
@@ -0,0 +1,507 @@
+/* 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_RSOUND_H
+#define MADS_DRAGONSPHERE_RSOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+class RSound;
+
+#define RSOUND_CHANNEL_COUNT 9
+
+/**
+ * Represents the data for a channel on the Dragonsphere MT-32 / MPU-401
+ * driver. Confirmed identical in layout (sizeof 0x27, same field offsets/
+ * order/roles) to the equivalent Return of the Phantom RSound Channel
+ * struct (engines/mads/phantom/rsound.h) - spot-checked directly against
+ * this game's own rsound.dr1 disassembly at every anchor point that has an
+ * IDA-resolved name or a distinctive access pattern: _activeCount(0x00),
+ * the 0x00-0x01/0x02-0x03 word-pair zeroing in resetAllChannels, _program
+ * (0x05, sendProgramChange), _velocity (0x06, the note-on helper),
+ * field_9/_keyOnDelay (0x09, the countdown+flush at the top of
+ * Channel_pollActive), _pan (0x0F), _volume (0x10), _pitchBend (0x11),
+ * _pSrc (0x16, the working script-read pointer), _soundData (0x20),
+ * _pendingStop (0x26). The remaining byte fields (0x00-0x13) were NOT
+ * individually re-derived from Dragonsphere-specific code - they're
+ * carried over from Phantom's confirmed layout on the strength of every
+ * checked anchor matching exactly; flag if a future command's behavior
+ * contradicts one of the un-rechecked fields.
+ */
+class Channel {
+public:
+	RSound *_owner = nullptr;
+	int _midiChannel = 0;         // 1-9: the MIDI channel this struct drives (C++ convenience, not a real field)
+
+	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;                // last-played note value (note + _transpose), used to detect a note change
+	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 (relative mode)
+	int _keyOnDelayOverride = 0;  // when non-zero, used directly as _keyOnDelay instead of the relative computation (absolute mode)
+	int _keyOnDelay = 0;          // countdown to RSound::Channel_flushHeldNotes()
+	int _volumeFadeCounter = 0;   // counts down to 0 before applying _volumeFadeStep
+	int _volumeFadeReload = 0;    // reload value for _volumeFadeCounter
+	int _pitchBendFadeCounter = 0; // counts down to 0 before applying _pitchBendFadeStep
+	int _panFadeCounter = 0;      // counts down to 0 before applying _panFadeStep
+	int _panFadeReload = 0;       // reload value for _panFadeCounter
+	int _pan = 0;                 // current pan value (MIDI CC#10); 64 = center
+	int _volume = 0;              // current channel volume (MIDI CC#7)
+	int _pitchBend = 0;           // current pitch bend value (status 0xEn, coarse/MSB only)
+	int _pitchBendFadeReload = 0; // reload value for _pitchBendFadeCounter
+	int _pitchBendFadeCount = 0;  // total ramp steps remaining before the pitch-bend ramp halts
+	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()
+	byte *_branchTarget = nullptr; // resume-after-branch pointer, used by the call/return opcode pair
+	int _transpose = 0;            // added to note bytes before comparison/storage
+	int _pendingStop = 0;          // non-zero while the channel is fading out to silence; also gates RSound::sendVolume()
+
+public:
+	Channel() {}
+
+	/**
+	 * Zeroes the entire 40-byte channel struct, then re-initialises the
+	 * loop/source/sound-data pointers to startPtr and centers pan and
+	 * pitch bend (0x40). Matches Channel_reset exactly.
+	 */
+	void reset(byte *startPtr);
+
+	/**
+	 * Marks the channel as pending-stop (fading toward silence) and
+	 * invalidates _soundData so isSoundActive() no longer matches it.
+	 */
+	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 Dragonsphere MT-32 / MPU-401 sound player resource
+ * files (rsound.dr1-.dr6, .dr9). Ported from rsound.dr1's disassembly,
+ * cross-checked wherever possible against the already-confirmed Return of
+ * the Phantom RSound family (engines/mads/phantom/rsound.h/.cpp) - the two
+ * games' RSound engines share an essentially identical Channel struct and
+ * Channel_pollActive opcode VM (same 0xBE-0xFF opcode range; every
+ * spot-checked opcode - the loop/restart/branch cluster, the dispatch
+ * range itself - matched Phantom's confirmed implementation exactly).
+ * Genuine, CONFIRMED differences from Phantom's RSound base:
+ *
+ *  - command1/command3/command5 use a 6-channel "lower" group (1-5 AND 9)
+ *    and a 3-channel "upper" group (6,7,8), not Phantom's default 5+4
+ *    split (this shape matched Phantom's RSound5 specifically, but here
+ *    it's the shared base behavior, not a per-driver override).
+ *  - command4/command5 are concrete (non-virtual) in this base, not pure
+ *    virtual like Phantom's - rsound.dr1's command4/command5 have NO
+ *    isSoundActive() gate at all, unlike every Phantom driver. This may
+ *    need revisiting once RSound2+ confirm whether that's universal here
+ *    too, or driver-specific.
+ *  - A new deferred-callback subsystem (_callbackCounter/_callbackPeriod/
+ *    _callbackFnPtr, tickCallback(), scheduleCallback(),
+ *    resetCallbackTimer()) mirrors the sibling Dragonsphere ASound
+ *    driver's identically-shaped mechanism (see asound.h) - ABSENT from
+ *    Phantom's RSound family entirely. Used by RSound1's music-loading
+ *    commands (16, 32-48) for the same "immediate load, or defer until
+ *    the music channels free up" idiom documented for ASound1's Pattern B.
+ *  - isMusicChannelsActive() (matches sub_10477) checks this driver's own
+ *    6-channel "lower" group (1-5, 9), not Phantom's fixed 6-channel(0-6)
+ *    equivalent from ASound.
+ *  - No checkRandomAmbianceTrigger()/_randomAmbianceTriggerFlag hook -
+ *    rsound_update() has no equivalent call at all; that Phantom RSound1
+ *    mechanism is not present here (replaced by the deferred-callback
+ *    subsystem above).
+ *  - sendVolume()/sendPan() naming CORRECTED from a mixed-up disassembly
+ *    symbol: the function IDA auto-named "sendVolume" actually sends CC#10
+ *    (Pan) using the channel's _pan field; the real volume-sender (CC#7,
+ *    _volume field) was an unnamed sub_10B54. Also, CONFIRMED NEW: the
+ *    real sendVolume() only actually transmits when the channel's
+ *    _pendingStop is zero - Channel_checkFade's own separate fade-out
+ *    mechanism otherwise takes precedence. No equivalent gate exists on
+ *    Phantom's sendVolume().
+ *  - sendReverbSysEx()'s exact byte layout (fixed 10 00 01h Roland System
+ *    Area Reverb address) is INFERRED by strong structural analogy to
+ *    Phantom's confirmed implementation (same 2/3/3-bit parameter
+ *    masking, same "mutate 3 bytes then sendSysEx" shape found at
+ *    sub_108A1) - NOT independently confirmed by inspecting the literal
+ *    bytes at rsound.dr1's offset 0x67 sysex template.
+ *  - The silence/no-op stream used by Channel_checkFade is 2 zero bytes
+ *    here (enable_channel_data), not Phantom's 3.
+ *
+ * NOTE: The actual MIDI transmission (sendMidiByte()) currently just logs
+ * via warning() - it isn't hooked up to a real ScummVM MIDI/MT-32 output
+ * yet, matching every other RSound/ASound driver in this codebase.
+ *
+ * NOTE: DOS-specific driver ceremony from the original (timer IRQ hooking,
+ * MPU-401 hardware detection/reset, PIT-based SysEx delay calibration, the
+ * system-clock save/restore around it) has no ScummVM equivalent and is
+ * not ported, matching every sibling driver.
+ */
+class RSound : public SoundDriver {
+	friend class Channel;
+public:
+	/**
+	 * Member-function pointer type for deferred sound-loader callbacks.
+	 * Public so driver subclasses can build a MAKE_CALLBACK-style cast
+	 * (reinterpret_cast<RSound::CallbackFunction>(&DerivedClass::fn)) to
+	 * pass to scheduleCallback().
+	 */
+	typedef void (RSound::*CallbackFunction)();
+
+private:
+	uint16 _randomSeed;
+	byte _lastMidiStatus;         // running-status cache, avoids resending an unchanged status byte
+	byte _sysexChecksum;
+	int _stateChangedFlag;        // word_12A56 in the disassembly; latches _pollResult=0xFFFF once per state change
+
+	/**
+	 * Per-MIDI-channel held-note slots (index 0 unused; channels are
+	 * 1-9; 4 = max chord polyphony). CONFIRMED byte-addressed, 4 bytes
+	 * per channel (channel*4 byte offset, single-byte reads/writes in
+	 * Channel_flushHeldNotes and the chord opcode). The real table
+	 * (word_161FC, "dw 20h dup(?)") is 64 bytes/16 rows - 6 rows larger
+	 * than this [10][4] array - but rows 10-15 are never addressed since
+	 * channel numbers only run 1-9; that's unused padding in the
+	 * original; this smaller array is behaviorally equivalent.
+	 */
+	byte _heldNotes[RSOUND_CHANNEL_COUNT + 1][4];
+
+	/**
+	 * Data-segment offset of this driver's own "command0_array" (the
+	 * table sent by command0()/reset() via sendSysEx). Each driver has
+	 * its own copy of this table at its own offset within its own
+	 * resource file. Confirmed 0x9C for rsound.dr1.
+	 */
+	int _sysExOffset;
+
+	/**
+	 * General-purpose script variable table (matches the equivalent
+	 * Phantom mechanism, 32 bytes).
+	 */
+	byte _scriptVariables[32];
+
+	/**
+	 * Half-rate fade-check timer (byte_10796 in the disassembly) -
+	 * drives checkFadingChannels() directly.
+	 */
+	int _fadeCheckCounter;
+
+	/**
+	 * Cluster of globals written by opcodes 0xBE-0xC1, matching the
+	 * identically-shaped (and identically unconfirmed-purpose) cluster in
+	 * Phantom's RSound - see that class's comment for details. Ported by
+	 * structural analogy: the opcode dispatch range confirmed these same
+	 * four opcodes exist here, but their bodies were not independently
+	 * re-read for Dragonsphere.
+	 */
+	int _clockMedTarget;
+	int _clockCoarseTarget;
+	int _clockUnknown;
+	int _clockCoarse;
+	int _clockMed;
+	int _clockFine;
+	int _clockEnabled1;
+	int _clockEnabled2;
+
+	// ---- Deferred-callback subsystem (word_131AA/AC/AE) - NEW vs. the
+	// Phantom RSound family, mirrors the sibling ASound driver's
+	// identically-shaped mechanism. ----
+	uint16 _callbackCounter = 0;
+	uint16 _callbackPeriod = 0;
+	CallbackFunction _callbackFnPtr = nullptr;
+
+	/** Tracks which bucket-4 (32-48) music piece was last launched, for command18's re-entry. Matches word_12A8B. */
+	uint16 _musicIndex = 0;
+
+	void update();
+	void pollAllChannels();
+
+	/**
+	 * Per-channel opcode interpreter (Channel_pollActive in the
+	 * disassembly). Implements the same bytecode VM as the Phantom
+	 * RSound family - see the class comment for what's been independently
+	 * re-confirmed vs. carried over by structural analogy.
+	 */
+	void pollActiveChannel(Channel *channel);
+
+	int readScriptByte(byte *&pSrc);
+	uint16 readScriptWord(byte *&pSrc);
+
+	void resetAllChannels();
+	void resetChannels1to5();
+
+	/** Resets channels 6,7,8 (0-based indices 5-7). Matches resetChannels6to8. */
+	void resetChannels6to8();
+
+	void checkFadingChannels();
+	void Channel_checkFade(Channel *channel, int midiChannel);
+	void Channel_flushHeldNotes(Channel *channel);
+
+protected:
+	int _commandParam;
+
+	byte *loadData(int offset) {
+		return &_soundData[offset];
+	}
+
+	/**
+	 * Zeroes _activeCount/_pitchBendFadeStep/_volumeFadeStep/_panFadeStep
+	 * for channels [first, last] (0-based indices). Protected (not
+	 * private) so per-driver reset helpers with a different range (e.g.
+	 * RSound2's resetChannels1to6()) can call it directly.
+	 */
+	void resetChannelRange(int first, int last);
+
+	/**
+	 * Resets the _heldNotes table (see its field comment). Protected so
+	 * per-driver reset helpers with a different channel range can still
+	 * clear this shared table without needing direct access to the
+	 * private member.
+	 */
+	void resetHeldNotes();
+
+	/**
+	 * Half-rate fade-check period reload value (see _fadeCheckCounter
+	 * above). Protected (not private) so per-driver command1()/command3()/
+	 * command5() overrides that need to arm it directly (matching an
+	 * inline "mov cs:_fadeCheckPeriod, 1" / armFadeCheck in the
+	 * disassembly) can do so without going through the base class's own
+	 * command3()/command5().
+	 */
+	int _fadeCheckPeriod;
+
+	void resultCheck();
+
+	/**
+	 * Schedule fn as the next deferred-load callback. Does NOT touch
+	 * _callbackCounter/_callbackPeriod - those are preserved from the
+	 * previous loader so the callback fires on the right beat. Cast the
+	 * derived-class member-function pointer with reinterpret_cast.
+	 */
+	void scheduleCallback(CallbackFunction fn) {
+		_callbackFnPtr = fn;
+	}
+
+	/**
+	 * Returns true if a deferred-load callback is currently scheduled.
+	 * Used by RSound2's command32/command35 to work around a confirmed
+	 * bug in the original (see their .cpp comments).
+	 */
+	bool isCallbackScheduled() const {
+		return _callbackFnPtr != nullptr;
+	}
+
+	/**
+	 * Arm the periodic timer and clear any pending callback pointer.
+	 * Call at the head of every immediate-load function.
+	 */
+	void resetCallbackTimer(uint16 period) {
+		_callbackFnPtr = nullptr;
+		_callbackCounter = period;
+		_callbackPeriod = period;
+	}
+
+	/**
+	 * Arm the periodic timer with separate counter and period values.
+	 * Used by RSound1's command17 (counter=0xC0, period=0x60) and
+	 * command44 (counter=0x60, period=0xE0).
+	 */
+	void resetCallbackTimerEx(uint16 counter, uint16 period) {
+		_callbackFnPtr = nullptr;
+		_callbackCounter = counter;
+		_callbackPeriod = period;
+	}
+
+	/** Set the music-piece index (word_12A8B) read by command18. */
+	void setMusicIndex(uint16 idx) {
+		_musicIndex = idx;
+	}
+	uint16 getMusicIndex() const {
+		return _musicIndex;
+	}
+
+	/**
+	 * Deferred-callback tick: decrements _callbackCounter; when it
+	 * reaches zero, reloads it from _callbackPeriod and calls
+	 * _callbackFnPtr (if non-null), then clears _callbackFnPtr so it
+	 * fires exactly once. Matches sub_122DA.
+	 */
+	void tickCallback();
+
+	/**
+	 * Checks whether channels 1-5 or 9 (this driver's "lower"/music
+	 * group) have any non-zero _activeCount. Matches sub_10477 - the
+	 * Dragonsphere-specific equivalent of the sibling ASound driver's
+	 * isMusicChannelsActive(), but scanning THIS driver's own channel
+	 * grouping rather than ASound's fixed channels 0-6.
+	 */
+	int isMusicChannelsActive();
+
+	/**
+	 * Plays the specified sound, using any free channel from 6 to 8.
+	 * Matches the disassembly's own playSoundChannels6to8 exactly
+	 * (symmetric free/fallback scan: free scan ch6,7,8; fallback scan
+	 * ch8,7,6). Renamed from playSound for clarity, matching the
+	 * disassembly's own name.
+	 */
+	Channel *playSoundChannels6to8(int offset);
+
+	/**
+	 * Plays the specified sound, using any free channel from 1 to 5, with
+	 * a fully symmetric free/fallback scan. Matches the disassembly's own
+	 * playSoundAny exactly - renamed here to playSoundChannels1To5 for
+	 * clarity, since (unlike Phantom's identically-named playSoundAny(),
+	 * which reaches channels 1-8) this driver's version only ever reaches
+	 * channels 1-5. Also distinct from Phantom's playSoundChannels1To5(),
+	 * whose fallback scan is asymmetric (never reaches channel 5) - this
+	 * one's fallback does.
+	 */
+	Channel *playSoundChannels1To5(int offset);
+
+	/**
+	 * Scans [startingChannel, freeScanEnd] for a free channel; if none
+	 * found, scans [startingChannel, fallbackScanEnd] in reverse for a
+	 * pending-stop channel to pre-empt.
+	 */
+	Channel *playSoundData(byte *pData, int startingChannel, int freeScanEnd, int fallbackScanEnd);
+
+	/** Checks whether the given block of data is already loaded into a channel (channels 1-8 only, matching the disassembly). */
+	bool isSoundActive(byte *pData);
+
+	int getRandomNumber();
+
+	// ---- Low-level MIDI send helpers -------------------------------
+	void sendMidiByte(byte value);
+	void sendStatus(int midiChannel, byte statusNibble);
+	void sendNoteOn(int midiChannel, int note, int velocity);
+	void sendProgramChange(int midiChannel, int program);
+
+	/**
+	 * CORRECTED naming (see class comment): sends CC#7 (Volume) - but
+	 * ONLY if the channel is not currently pending-stop (matches
+	 * sub_10B54's "cmp [bx+_pendingStop],0" gate, confirmed new vs.
+	 * Phantom's unconditional sendVolume()).
+	 */
+	void sendVolume(Channel *ch);
+
+	void sendVolumeCC(int midiChannel, int volume);
+	void resetPitchBend(int midiChannel);
+	void sendPitchBend(int midiChannel, int value);
+
+	/** CORRECTED naming (see class comment): sends CC#10 (Pan). Matches the disassembly's mislabeled "sendVolume". */
+	void sendPan(int midiChannel, int value);
+
+	void muteChannel(int midiChannel);
+
+	/**
+	 * Sends the GM-reset Control Change sequence (all notes off, reset all
+	 * controllers, volume=100, pan=center) to `count` MIDI channels,
+	 * counting down from `count` to 1.
+	 */
+	void sendGmReset(int count);
+
+	/**
+	 * Sends a single Roland DT1-style SysEx message from a raw buffer:
+	 * the fixed header, then bytes from pData up to (not including) a
+	 * 0xFF terminator - each byte sent and folded into a running
+	 * checksum - then the checksum byte and a closing F7.
+	 */
+	void sendSysExData(const byte *pData);
+
+	/** sendSysExData() for a block already in this driver's own loaded sound data. */
+	void sendSysEx(int offset);
+
+	void sendPatchInitSequence();
+
+	/**
+	 * Masks the 3 caller-supplied values to 2/3/3 bits (mode 0-3, time
+	 * 0-7, level 0-7) and sends them via the Roland MT-32 System Area
+	 * Reverb SysEx address (10 00 01h) - see class comment re: this
+	 * being inferred by analogy rather than independently confirmed for
+	 * Dragonsphere. Matches sub_108A1.
+	 */
+	void sendReverbSysEx(int mode, int time, int level);
+
+	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;
+	int _tickCounter; // word_12A83 - incremented alongside _frameCounter every update() tick
+	bool _isDisabled;
+	int _pollResult;
+
+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
+	 * @param sysExOffset	Offset of this driver's own command0_array
+	 */
+	RSound(Audio::Mixer *mixer, const Common::Path &filename,
+		int dataOffset, int dataSize, int sysExOffset);
+
+	~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;
+	}
+};
+
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/dragonsphere/rsound_dragonsphere.cpp b/engines/mads/dragonsphere/rsound_dragonsphere.cpp
new file mode 100644
index 00000000000..53c4814f087
--- /dev/null
+++ b/engines/mads/dragonsphere/rsound_dragonsphere.cpp
@@ -0,0 +1,1716 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/util.h"
+#include "mads/dragonsphere/rsound_dragonsphere.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+#define MAKE_CALLBACK(cls, fn) reinterpret_cast<RSound::CallbackFunction>(&cls::fn)
+
+RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr1", 0x2C00, 0x3840, 0x9C) {
+}
+
+const RSound1::CommandPtr RSound1::_commandList[102] = {
+	&RSound1::command0, &RSound1::command1, &RSound1::command2, &RSound1::command3,
+	&RSound1::command4, &RSound1::command5, &RSound1::command6, &RSound1::command7,
+	&RSound1::command8, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::command16, &RSound1::command17, &RSound1::command18, &RSound1::nullCommand,
+	&RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::command24, &RSound1::command25, &RSound1::command26, &RSound1::command27,
+	&RSound1::command28, &RSound1::nullCommand, &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::command42, &RSound1::command43,
+	&RSound1::command44, &RSound1::command45, &RSound1::command46, &RSound1::command47,
+	&RSound1::command48, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand, &RSound1::nullCommand,
+	&RSound1::command64, &RSound1::command65, &RSound1::command66, &RSound1::command67,
+	&RSound1::command68, &RSound1::command69, &RSound1::command70, &RSound1::command71,
+	&RSound1::nullCommand, &RSound1::command73, &RSound1::command74, &RSound1::command75,
+	&RSound1::command76, &RSound1::command77, &RSound1::command78, &RSound1::command79,
+	&RSound1::command80, &RSound1::command81, &RSound1::command82, &RSound1::command83,
+	&RSound1::command84, &RSound1::command85, &RSound1::command86, &RSound1::command87,
+	&RSound1::command88, &RSound1::command89, &RSound1::command90, &RSound1::command91,
+	&RSound1::nullCommand, &RSound1::command93, &RSound1::command94, &RSound1::command95,
+	&RSound1::command96, &RSound1::command97, &RSound1::nullCommand, &RSound1::command99,
+	&RSound1::command100, &RSound1::command101
+};
+
+int RSound1::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	return (this->*_commandList[commandId])();
+}
+
+/*-----------------------------------------------------------------------*/
+// Bucket 2 (16-18): background-music dispatcher / direct play / re-entrant
+// launcher. Matches the sibling ASound1 driver's identically-purposed
+// command16/17/18 trio.
+
+int RSound1::command16() {
+	byte *pData = loadData(0x7BA);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand16();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand16));
+	return 0;
+}
+
+void RSound1::loadCommand16() {
+	resetCallbackTimer(0x90);
+	setMusicIndex(0x10);
+	command3();
+	_channels[0].load(loadData(0x7BA));
+	_channels[1].load(loadData(0x8C3));
+	_channels[2].load(loadData(0x939));
+	_channels[3].load(loadData(0x9AF));
+}
+
+int RSound1::command17() {
+	// Ungated scheduling (Pattern A): no isMusicChannelsActive() check,
+	// always loads immediately once the isSoundActive() gate passes.
+	if (isSoundActive(loadData(0x2F54)))
+		return 0;
+	command3();
+	resetCallbackTimerEx(0xC0, 0x60);
+	_channels[0].load(loadData(0x2F54));
+	_channels[1].load(loadData(0x2F84));
+	_channels[2].load(loadData(0x2FB5));
+	_channels[3].load(loadData(0x2FCC));
+	return 0;
+}
+
+int RSound1::command18() {
+	// Matches rsound_command18: command3() then re-dispatch by
+	// _musicIndex. The original branches into one of two separate
+	// physical dispatch tables (bucket 2 or bucket 4) depending on
+	// whether the index is <= 0x12; since this port uses a single flat
+	// _commandList[] covering every index, both branches collapse to the
+	// same array lookup.
+	command3();
+	return (this->*_commandList[getMusicIndex()])();
+}
+
+/*-----------------------------------------------------------------------*/
+// Bucket 3 (24-31): plain SFX, channels 6-8.
+
+int RSound1::command24() {
+	playSoundChannels6to8(0x3474);
+	playSoundChannels6to8(0x348B);
+	return 0;
+}
+
+int RSound1::command25() {
+	playSoundChannels6to8(0x349F);
+	playSoundChannels6to8(0x34B6);
+	return 0;
+}
+
+int RSound1::command26() {
+	playSoundChannels6to8(0x34CA);
+	return 0;
+}
+
+int RSound1::command27() {
+	playSoundChannels6to8(0x34D4);
+	return 0;
+}
+
+int RSound1::command28() {
+	playSoundChannels6to8(0x358D);
+	playSoundChannels6to8(0x3597);
+	return 0;
+}
+
+int RSound1::command30() {
+	playSoundChannels6to8(0x34ED);
+	return 0;
+}
+
+int RSound1::command31() {
+	playSoundChannels6to8(0x35DF);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+// Bucket 4 (32-48): music-piece loaders, mostly gated by isSoundActive()
+// then either an immediate load or a deferred one (scheduled once the
+// lower/music channel group frees up), matching the sibling ASound1
+// driver's Pattern B idiom.
+
+int RSound1::command32() {
+	byte *pData = loadData(0xA14);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand32();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand32));
+	return 0;
+}
+
+void RSound1::loadCommand32() {
+	resetCallbackTimer(0xB0);
+	setMusicIndex(0x20);
+	command3();
+	_channels[0].load(loadData(0xA14));
+	_channels[1].load(loadData(0xA5A));
+	_channels[2].load(loadData(0xAB9));
+	_channels[3].load(loadData(0xAD6));
+}
+
+int RSound1::command33() {
+	byte *pData = loadData(0xAFC);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand33();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand33));
+	return 0;
+}
+
+void RSound1::loadCommand33() {
+	resetCallbackTimer(0xB0);
+	command3();
+	_channels[0].load(loadData(0xAFC));
+	_channels[1].load(loadData(0xB9E));
+	_channels[2].load(loadData(0xBFD));
+	_channels[3].load(loadData(0xC20));
+}
+
+int RSound1::command34() {
+	// Ungated scheduling (Pattern A), unlike every other bucket-4
+	// command: no isMusicChannelsActive() check, and uses
+	// playSoundChannels1To5() (free-channel search) rather than direct
+	// Channel_loadN targets. Also the only asymmetric timer in this
+	// bucket (counter=0x50, period=5).
+	if (isSoundActive(loadData(0x25F2)))
+		return 0;
+	resetCallbackTimerEx(0x50, 5);
+	command3();
+	playSoundChannels1To5(0x25F2);
+	playSoundChannels1To5(0x2660);
+	playSoundChannels1To5(0x2676);
+	return 0;
+}
+
+int RSound1::command35() {
+	byte *pData = loadData(0xDDA);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand35();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand35));
+	return 0;
+}
+
+void RSound1::loadCommand35() {
+	resetCallbackTimer(0x60);
+	command3();
+	_channels[0].load(loadData(0xDDA));
+	_channels[1].load(loadData(0xE6F));
+	_channels[2].load(loadData(0xEF4));
+	_channels[3].load(loadData(0xF3F));
+}
+
+int RSound1::command36() {
+	byte *pData = loadData(0xF66);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand36();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand36));
+	return 0;
+}
+
+void RSound1::loadCommand36() {
+	resetCallbackTimer(0x80);
+	command3();
+	_channels[0].load(loadData(0xF66));
+	_channels[1].load(loadData(0x1028));
+	_channels[2].load(loadData(0x1113));
+	_channels[3].load(loadData(0x11A6));
+}
+
+int RSound1::command37() {
+	byte *pData = loadData(0x120C);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand37();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand37));
+	return 0;
+}
+
+void RSound1::loadCommand37() {
+	resetCallbackTimer(0xC0);
+	command3();
+	_channels[0].load(loadData(0x120C));
+	_channels[1].load(loadData(0x1267));
+}
+
+int RSound1::command38() {
+	// NOTE: channel-load order is 5,3,4,2 - not sequential. Preserved
+	// exactly.
+	byte *pData = loadData(0x12CE);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand38();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand38));
+	return 0;
+}
+
+void RSound1::loadCommand38() {
+	resetCallbackTimer(0x60);
+	command3();
+	_channels[4].load(loadData(0x12CE));
+	_channels[2].load(loadData(0x1373));
+	_channels[3].load(loadData(0x1449));
+	_channels[1].load(loadData(0x1534));
+}
+
+int RSound1::command39() {
+	byte *pData = loadData(0x1622);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand39();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand39));
+	return 0;
+}
+
+void RSound1::loadCommand39() {
+	resetCallbackTimer(0xB0);
+	command3();
+	_channels[0].load(loadData(0x1622));
+	_channels[1].load(loadData(0x1678));
+	_channels[2].load(loadData(0x16D7));
+}
+
+int RSound1::command40() {
+	byte *pData = loadData(0x172E);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand40();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand40));
+	return 0;
+}
+
+void RSound1::loadCommand40() {
+	resetCallbackTimer(0xA8);
+	command3();
+	_channels[0].load(loadData(0x172E));
+	_channels[1].load(loadData(0x189C));
+	_channels[2].load(loadData(0x1A47));
+	_channels[3].load(loadData(0x1D0A));
+	_channels[4].load(loadData(0x1F05));
+}
+
+int RSound1::command41() {
+	// NOTE: uses channel 9 in place of channel 3 - confirmed directly
+	// from the disassembly (Channel_load9, not Channel_load3).
+	byte *pData = loadData(0x219E);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand41();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand41));
+	return 0;
+}
+
+void RSound1::loadCommand41() {
+	resetCallbackTimer(0x90);
+	command3();
+	_channels[0].load(loadData(0x219E));
+	_channels[1].load(loadData(0x220C));
+	_channels[8].load(loadData(0x2283));
+	_channels[3].load(loadData(0x22D8));
+	_channels[4].load(loadData(0x23B1));
+}
+
+int RSound1::command42() {
+	byte *pData = loadData(0x2400);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand42();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand42));
+	return 0;
+}
+
+void RSound1::loadCommand42() {
+	resetCallbackTimer(0x90);
+	setMusicIndex(0x29);
+	command3();
+	_channels[0].load(loadData(0x2400));
+	_channels[1].load(loadData(0x242B));
+	_channels[2].load(loadData(0x245C));
+	_channels[3].load(loadData(0x2499));
+	_channels[4].load(loadData(0x24D0));
+}
+
+void RSound1::command43_48Tail(byte variant) {
+	// Matches the shared command43/48 tail: writes `variant` into the
+	// sound data one byte into the block command2's load targets
+	// (offset 0x2539), then gates on isSoundActive(0x24EC) with NO
+	// isMusicChannelsActive() branching at all (unlike every other
+	// bucket-4 loader) - always proceeds unconditionally once the gate
+	// passes.
+	*loadData(0x2539) = variant;
+	if (isSoundActive(loadData(0x24EC)))
+		return;
+	loadCommand43_48();
+}
+
+int RSound1::command43() {
+	command43_48Tail(0x5D);
+	return 0;
+}
+
+int RSound1::command48() {
+	command43_48Tail(0x31);
+	return 0;
+}
+
+void RSound1::loadCommand43_48() {
+	resetCallbackTimer(0x54);
+	setMusicIndex(0x28);
+	command3();
+	_channels[0].load(loadData(0x24EC));
+	_channels[1].load(loadData(0x2538));
+	_channels[2].load(loadData(0x257D));
+	_channels[3].load(loadData(0x25A4));
+	_channels[4].load(loadData(0x25CD));
+}
+
+int RSound1::command44() {
+	byte *pData = loadData(0x268C);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand44();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand44));
+	return 0;
+}
+
+void RSound1::loadCommand44() {
+	// Asymmetric timer: counter=0x60, period=0xE0.
+	resetCallbackTimerEx(0x60, 0xE0);
+	command3();
+	_channels[0].load(loadData(0x268C));
+	_channels[1].load(loadData(0x26D2));
+	_channels[2].load(loadData(0x2717));
+	_channels[3].load(loadData(0x273F));
+	_channels[4].load(loadData(0x2801));
+}
+
+int RSound1::command45() {
+	byte *pData = loadData(0x28F6);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand45();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand45));
+	return 0;
+}
+
+void RSound1::loadCommand45() {
+	resetCallbackTimer(0x60);
+	command3();
+	_channels[0].load(loadData(0x28F6));
+	_channels[1].load(loadData(0x2A60));
+	_channels[2].load(loadData(0x2B46));
+	_channels[3].load(loadData(0x2C41));
+	_channels[4].load(loadData(0x2D2F));
+}
+
+int RSound1::command46() {
+	byte *pData = loadData(0x2EC6);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand46();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand46));
+	return 0;
+}
+
+void RSound1::loadCommand46() {
+	resetCallbackTimer(0x90);
+	command3();
+	_channels[0].load(loadData(0x2EC6));
+	_channels[1].load(loadData(0x2F1B));
+}
+
+int RSound1::command47() {
+	// NOTE: channel-load order is 5,3,4,2 - not sequential, same shape
+	// as command38. Preserved exactly.
+	byte *pData = loadData(0x1342);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand47();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound1, loadCommand47));
+	return 0;
+}
+
+void RSound1::loadCommand47() {
+	resetCallbackTimer(0x60);
+	command3();
+	_channels[4].load(loadData(0x1342));
+	_channels[2].load(loadData(0x13EC));
+	_channels[3].load(loadData(0x1503));
+	_channels[1].load(loadData(0x15BE));
+}
+
+/*-----------------------------------------------------------------------*/
+// Bucket 5 (64-101): plain SFX, channels 6-8 (playSound), except
+// command101 which loads channel 9 directly.
+
+int RSound1::command64() {
+	playSoundChannels6to8(0x303A);
+	playSoundChannels6to8(0x304C);
+	playSoundChannels6to8(0x3052);
+	return 0;
+}
+
+int RSound1::command65() {
+	playSoundChannels6to8(0x3077);
+	return 0;
+}
+
+int RSound1::command66() {
+	playSoundChannels6to8(0x3093);
+	return 0;
+}
+
+int RSound1::command67() {
+	playSoundChannels6to8(0x30D9);
+	return 0;
+}
+
+int RSound1::command68() {
+	playSoundChannels6to8(0x3105);
+	return 0;
+}
+
+int RSound1::command69() {
+	playSoundChannels6to8(0x3111);
+	return 0;
+}
+
+int RSound1::command70() {
+	playSoundChannels6to8(0x3133);
+	return 0;
+}
+
+int RSound1::command71() {
+	playSoundChannels6to8(0x3149);
+	return 0;
+}
+
+int RSound1::command73() {
+	playSoundChannels6to8(0x3199);
+	return 0;
+}
+
+int RSound1::command74() {
+	playSoundChannels6to8(0x31CF);
+	return 0;
+}
+
+int RSound1::command75() {
+	playSoundChannels6to8(0x31F3);
+	return 0;
+}
+
+int RSound1::command76() {
+	playSoundChannels6to8(0x320F);
+	return 0;
+}
+
+int RSound1::command77() {
+	playSoundChannels6to8(0x321B);
+	playSoundChannels6to8(0x3227);
+	playSoundChannels6to8(0x3233);
+	return 0;
+}
+
+int RSound1::command78() {
+	playSoundChannels6to8(0x323F);
+	return 0;
+}
+
+int RSound1::command79() {
+	playSoundChannels6to8(0x3286);
+	return 0;
+}
+
+int RSound1::command80() {
+	playSoundChannels6to8(0x32A7);
+	return 0;
+}
+
+int RSound1::command81() {
+	// 0x32DC is called three times in a row - a genuine quirk, preserved
+	// exactly rather than collapsed to a single call.
+	playSoundChannels6to8(0x32DC);
+	playSoundChannels6to8(0x32DC);
+	playSoundChannels6to8(0x32DC);
+	return 0;
+}
+
+int RSound1::command82() {
+	playSoundChannels6to8(0x3303);
+	return 0;
+}
+
+int RSound1::command83() {
+	playSoundChannels6to8(0x3367);
+	return 0;
+}
+
+int RSound1::command84() {
+	playSoundChannels6to8(0x3383);
+	return 0;
+}
+
+int RSound1::command85() {
+	playSoundChannels6to8(0x338D);
+	return 0;
+}
+
+int RSound1::command86() {
+	playSoundChannels6to8(0x33A4);
+	return 0;
+}
+
+int RSound1::command87() {
+	playSoundChannels6to8(0x33C9);
+	return 0;
+}
+
+int RSound1::command88() {
+	playSoundChannels6to8(0x33E3);
+	return 0;
+}
+
+int RSound1::command89() {
+	playSoundChannels6to8(0x33FC);
+	return 0;
+}
+
+int RSound1::command90() {
+	playSoundChannels6to8(0x340E);
+	return 0;
+}
+
+int RSound1::command91() {
+	playSoundChannels6to8(0x343B);
+	return 0;
+}
+
+int RSound1::command93() {
+	playSoundChannels6to8(0x3004);
+	playSoundChannels6to8(0x301F);
+	return 0;
+}
+
+int RSound1::command94() {
+	playSoundChannels6to8(0x30B7);
+	return 0;
+}
+
+int RSound1::command95() {
+	playSoundChannels6to8(0x3517);
+	return 0;
+}
+
+int RSound1::command96() {
+	// 0x357C is called twice in a row - a genuine quirk, preserved
+	// exactly rather than collapsed to a single call.
+	playSoundChannels6to8(0x352F);
+	playSoundChannels6to8(0x357C);
+	playSoundChannels6to8(0x357C);
+	return 0;
+}
+
+int RSound1::command97() {
+	playSoundChannels6to8(0x3581);
+	return 0;
+}
+
+int RSound1::command99() {
+	playSoundChannels6to8(0x35A1);
+	return 0;
+}
+
+int RSound1::command100() {
+	playSoundChannels6to8(0x35B3);
+	playSoundChannels6to8(0x35C0);
+	playSoundChannels6to8(0x35CD);
+	return 0;
+}
+
+int RSound1::command101() {
+	_channels[8].load(loadData(0x35D7));
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+RSound2::RSound2(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr2", 0x27B0, 0x1A60, 0xAC) {
+}
+
+void RSound2::resetChannels1to6() {
+	// Matches resetChannels1to6: zeroes channels 1-6 (0-based 0-5).
+	_isDisabled = true;
+	resetChannelRange(0, 5);
+	resetHeldNotes();
+	_isDisabled = false;
+}
+
+int RSound2::command2() {
+	resetChannels1to6();
+	sendGmReset(4);
+	return 0;
+}
+
+int RSound2::command1() {
+	// Must call THIS driver's own command3() (not virtual in the base -
+	// see class comment).
+	command3();
+	command5();
+	return 0;
+}
+
+int RSound2::command3() {
+	// Confirmed: enables channels 1-6 (six channels) - channel 9 is
+	// untouched here, unlike the base class's default (1-5,9).
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[0].enable(0xFF);
+	_channels[1].enable(0xFF);
+	_channels[2].enable(0xFF);
+	_channels[3].enable(0xFF);
+	_channels[4].enable(0xFF);
+	_channels[5].enable(0xFF);
+	return 0;
+}
+
+int RSound2::command5() {
+	// Confirmed: enables channels 7,8 only (two channels) - the base
+	// class's default enables 3 (6,7,8).
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+	return 0;
+}
+
+Channel *RSound2::playSoundChannels7to8(int offset) {
+	// CONFIRMED: channels 7,8 (0-based 6-7), symmetric free/fallback
+	// scan (free scan ch7 then ch8; fallback scan ch8 then ch7). A
+	// trailing "load channel 9" block immediately after this function's
+	// retn is unreachable dead code (no jump targets it) - not ported.
+	return playSoundData(loadData(offset), 6, 7, 7);
+}
+
+/*-----------------------------------------------------------------------*/
+
+int RSound2::command16() {
+	byte *pData = loadData(0xEE0);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand16();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound2, loadCommand16));
+	return 0;
+}
+
+void RSound2::loadCommand16() {
+	resetCallbackTimer(96);
+	command3();
+	_channels[0].load(loadData(0xEE0));
+	_channels[1].load(loadData(0xF2F));
+}
+
+int RSound2::command17() {
+	// Ungated scheduling (Pattern A), matching RSound1's command17 shape.
+	if (isSoundActive(loadData(0x155C)))
+		return 0;
+	command3();
+	resetCallbackTimerEx(192, 96);
+	_channels[0].load(loadData(0x155C));
+	_channels[1].load(loadData(0x158C));
+	_channels[2].load(loadData(0x15BD));
+	_channels[3].load(loadData(0x15D4));
+	return 0;
+}
+
+int RSound2::command18() {
+	// See RSound1::command18 - the flat _commandList[] collapses the
+	// original's two-table branch into a single array lookup.
+	command3();
+	return (this->*_commandList[getMusicIndex()])();
+}
+
+int RSound2::command24() {
+	playSoundChannels7to8(0x160C);
+	playSoundChannels7to8(0x1620);
+	return 0;
+}
+
+int RSound2::command25() {
+	playSoundChannels7to8(0x1632);
+	playSoundChannels7to8(0x1646);
+	return 0;
+}
+
+int RSound2::command26() {
+	playSoundChannels7to8(0x1658);
+	return 0;
+}
+
+int RSound2::command27() {
+	playSoundChannels7to8(0x1662);
+	return 0;
+}
+
+int RSound2::command28() {
+	playSoundChannels7to8(0x167B);
+	playSoundChannels7to8(0x1685);
+	return 0;
+}
+
+int RSound2::command30() {
+	playSoundChannels7to8(0x16FC);
+	return 0;
+}
+
+int RSound2::command31() {
+	playSoundChannels7to8(0x17F9);
+	return 0;
+}
+
+int RSound2::command32() {
+	// WORKAROUND: this fixes a bug in the original that incorrectly
+	// called isSoundActive instead - it passed _callbackFnPtr's own
+	// data-segment offset (0x2379) to isSoundActive() as if it were
+	// sound data, when it almost certainly meant to check whether a
+	// callback was already scheduled before proceeding.
+	if (isCallbackScheduled())
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand32();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound2, loadCommand32));
+	return 0;
+}
+
+void RSound2::loadCommand32() {
+	resetCallbackTimer(96);
+	command3();
+	_channels[0].load(loadData(0x7BA));
+	_channels[1].load(loadData(0x80B));
+	_channels[2].load(loadData(0x8A3));
+	_channels[3].load(loadData(0x95F));
+	_channels[4].load(loadData(0x9E3));
+	_channels[5].load(loadData(0xA2D));
+}
+
+int RSound2::command33() {
+	byte *pData = loadData(0xA54);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand33();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound2, loadCommand33));
+	return 0;
+}
+
+void RSound2::loadCommand33() {
+	resetCallbackTimer(96);
+	command3();
+	_channels[0].load(loadData(0xA54));
+	_channels[1].load(loadData(0xAF3));
+	_channels[2].load(loadData(0xB93));
+	_channels[3].load(loadData(0xC3F));
+	_channels[4].load(loadData(0xD17));
+	_channels[5].load(loadData(0xD9D));
+}
+
+int RSound2::command34() {
+	byte *pData = loadData(0xE4A);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand34();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound2, loadCommand34));
+	return 0;
+}
+
+void RSound2::loadCommand34() {
+	resetCallbackTimer(96);
+	command3();
+	_channels[0].load(loadData(0xE4A));
+	_channels[1].load(loadData(0xEE2));
+	_channels[2].load(loadData(0xF2F));
+	_channels[3].load(loadData(0xF82));
+	_channels[4].load(loadData(0xFCD));
+	_channels[5].load(loadData(0x1024));
+}
+
+int RSound2::command35() {
+	// WORKAROUND: this fixes a bug in the original that incorrectly
+	// called isSoundActive instead - same shape as command32's bug (see
+	// its comment).
+	if (isCallbackScheduled())
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand35();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound2, loadCommand35));
+	return 0;
+}
+
+void RSound2::loadCommand35() {
+	// Asymmetric timer: counter=192, period=80.
+	resetCallbackTimerEx(192, 80);
+	command3();
+	_channels[0].load(loadData(0x107A));
+	_channels[1].load(loadData(0x11BA));
+	_channels[2].load(loadData(0x1246));
+	_channels[3].load(loadData(0x1320));
+	_channels[4].load(loadData(0x13FA));
+	_channels[5].load(loadData(0x149C));
+}
+
+int RSound2::command64() {
+	playSoundChannels7to8(0x168F);
+	return 0;
+}
+
+int RSound2::command65() {
+	playSoundChannels7to8(0x1699);
+	playSoundChannels7to8(0x16B9);
+	playSoundChannels7to8(0x16D2);
+	return 0;
+}
+
+int RSound2::command66() {
+	playSoundChannels7to8(0x1726);
+	return 0;
+}
+
+int RSound2::command67() {
+	playSoundChannels7to8(0x1745);
+	return 0;
+}
+
+int RSound2::command68() {
+	playSoundChannels7to8(0x175B);
+	return 0;
+}
+
+int RSound2::command69() {
+	playSoundChannels7to8(0x176B);
+	playSoundChannels7to8(0x1775);
+	return 0;
+}
+
+int RSound2::command70() {
+	playSoundChannels7to8(0x1781);
+	playSoundChannels7to8(0x1791);
+	return 0;
+}
+
+int RSound2::command71() {
+	playSoundChannels7to8(0x17A1);
+	playSoundChannels7to8(0x17E0);
+	return 0;
+}
+
+int RSound2::command72() {
+	playSoundChannels1To5(0x184B);
+	playSoundChannels1To5(0x185F);
+	return 0;
+}
+
+const RSound2::CommandPtr RSound2::_commandList[73] = {
+	&RSound2::command0, &RSound2::command1, &RSound2::command2, &RSound2::command3,
+	&RSound2::command4, &RSound2::command5, &RSound2::command6, &RSound2::command7,
+	&RSound2::command8, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::command16, &RSound2::command17, &RSound2::command18, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::command24, &RSound2::command25, &RSound2::command26, &RSound2::command27,
+	&RSound2::command28, &RSound2::nullCommand, &RSound2::command30, &RSound2::command31,
+	&RSound2::command32, &RSound2::command33, &RSound2::command34, &RSound2::command35,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::command64, &RSound2::command65, &RSound2::command66, &RSound2::command67,
+	&RSound2::command68, &RSound2::command69, &RSound2::command70, &RSound2::command71,
+	&RSound2::command72
+};
+
+int RSound2::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	return (this->*_commandList[commandId])();
+}
+
+/*-----------------------------------------------------------------------*/
+
+RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr3", 0x2750, 0x1780, 0xAC) {
+}
+
+int RSound3::command1() {
+	// Must call THIS driver's own command3() (not virtual in the base -
+	// see class comment).
+	command3();
+	command5();
+	return 0;
+}
+
+int RSound3::command3() {
+	// Confirmed: enables channels 1-6 AND 9 (seven channels) - overlaps
+	// with command5's channels 5,6.
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[0].enable(0xFF);
+	_channels[1].enable(0xFF);
+	_channels[2].enable(0xFF);
+	_channels[3].enable(0xFF);
+	_channels[4].enable(0xFF);
+	_channels[5].enable(0xFF);
+	_channels[8].enable(0xFF);
+	return 0;
+}
+
+int RSound3::command5() {
+	// Confirmed: enables channels 5,6,7,8 (four channels) - overlaps
+	// with command3's channels 5,6.
+	_fadeCheckPeriod = 1; // armFadeCheck
+	_channels[4].enable(0xFF);
+	_channels[5].enable(0xFF);
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+	return 0;
+}
+
+int RSound3::command16() {
+	// Gate uses the disassembly's own "isSoundPlaying" name - treated as
+	// equivalent to isSoundActive() (confirmed identical for Phantom's
+	// RSound4; not independently re-confirmed here).
+	byte *pData = loadData(0x7BF);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand16();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound3, loadCommand16));
+	return 0;
+}
+
+void RSound3::loadCommand16() {
+	// Asymmetric timer: counter=28, period=84.
+	resetCallbackTimerEx(28, 84);
+	command3();
+	_channels[0].load(loadData(0x7BF));
+	_channels[1].load(loadData(0x88C));
+	_channels[2].load(loadData(0x8EE));
+	_channels[3].load(loadData(0xA72));
+	_channels[4].load(loadData(0xAE6));
+	_channels[8].load(loadData(0xBBF));
+}
+
+int RSound3::command17() {
+	// Ungated scheduling (Pattern A), matching RSound1/2's command17 shape.
+	if (isSoundActive(loadData(0x12CC)))
+		return 0;
+	command3();
+	resetCallbackTimerEx(0xC0, 0x60);
+	_channels[0].load(loadData(0x12CC));
+	_channels[1].load(loadData(0x12FC));
+	_channels[2].load(loadData(0x132D));
+	_channels[3].load(loadData(0x1344));
+	return 0;
+}
+
+int RSound3::command18() {
+	// See RSound1::command18 - the flat _commandList[] collapses the
+	// original's two-table branch into a single array lookup.
+	command3();
+	return (this->*_commandList[getMusicIndex()])();
+}
+
+int RSound3::command24() {
+	playSoundChannels6to8(0x137C);
+	playSoundChannels6to8(0x1390);
+	return 0;
+}
+
+int RSound3::command25() {
+	playSoundChannels6to8(0x13A2);
+	playSoundChannels6to8(0x13B6);
+	return 0;
+}
+
+int RSound3::command26() {
+	playSoundChannels6to8(0x13C8);
+	return 0;
+}
+
+int RSound3::command27() {
+	playSoundChannels6to8(0x13D2);
+	return 0;
+}
+
+int RSound3::command28() {
+	playSoundChannels6to8(0x13EB);
+	playSoundChannels6to8(0x13F5);
+	return 0;
+}
+
+int RSound3::command30() {
+	playSoundChannels6to8(0x149A);
+	return 0;
+}
+
+void RSound3::command31_64Tail(byte variant) {
+	*loadData(0x1402) = variant;
+	playSoundChannels6to8(0x13FF);
+}
+
+int RSound3::command31() {
+	command31_64Tail(0x5A);
+	return 0;
+}
+
+int RSound3::command64() {
+	command31_64Tail(0x78);
+	return 0;
+}
+
+int RSound3::command32() {
+	byte *pData = loadData(0xCAE);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand32();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound3, loadCommand32));
+	return 0;
+}
+
+void RSound3::loadCommand32() {
+	resetCallbackTimerEx(0x1C, 0x54);
+	command3();
+	_channels[0].load(loadData(0xCAE));
+	_channels[1].load(loadData(0xD68));
+	_channels[2].load(loadData(0xE17));
+	_channels[3].load(loadData(0xEC8));
+	_channels[4].load(loadData(0xEED));
+	_channels[5].load(loadData(0xFAE));
+}
+
+int RSound3::command33() {
+	byte *pData = loadData(0x102A);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand33();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound3, loadCommand33));
+	return 0;
+}
+
+void RSound3::loadCommand33() {
+	resetCallbackTimerEx(0x1C, 0x54);
+	command3();
+	_channels[0].load(loadData(0x102A));
+	_channels[1].load(loadData(0x10FE));
+	_channels[2].load(loadData(0x1160));
+	_channels[3].load(loadData(0x116F));
+	_channels[4].load(loadData(0x1264));
+	_channels[8].load(loadData(0x1284));
+}
+
+int RSound3::command65() {
+	playSoundChannels6to8(0x1451);
+	return 0;
+}
+
+int RSound3::command67() {
+	playSoundChannels6to8(0x14C4);
+	return 0;
+}
+
+int RSound3::command68() {
+	playSoundChannels6to8(0x14E0);
+	return 0;
+}
+
+int RSound3::command69() {
+	playSoundChannels6to8(0x1552);
+	return 0;
+}
+
+int RSound3::command70() {
+	playSoundChannels6to8(0x1560);
+	playSoundChannels6to8(0x157E);
+	return 0;
+}
+
+int RSound3::command71() {
+	playSoundChannels6to8(0x148D);
+	return 0;
+}
+
+int RSound3::command72() {
+	playSoundChannels6to8(0x14FB);
+	playSoundChannels6to8(0x1518);
+	playSoundChannels6to8(0x1535);
+	return 0;
+}
+
+int RSound3::command73() {
+	playSoundChannels6to8(0x1598);
+	return 0;
+}
+
+const RSound3::CommandPtr RSound3::_commandList[74] = {
+	&RSound3::command0, &RSound3::command1, &RSound3::command2, &RSound3::command3,
+	&RSound3::command4, &RSound3::command5, &RSound3::command6, &RSound3::command7,
+	&RSound3::command8, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::command16, &RSound3::command17, &RSound3::command18, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::command24, &RSound3::command25, &RSound3::command26, &RSound3::command27,
+	&RSound3::command28, &RSound3::nullCommand, &RSound3::command30, &RSound3::command31,
+	&RSound3::command32, &RSound3::command33, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::command64, &RSound3::command65, &RSound3::nullCommand, &RSound3::command67,
+	&RSound3::command68, &RSound3::command69, &RSound3::command70, &RSound3::command71,
+	&RSound3::command72, &RSound3::command73
+};
+
+int RSound3::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	return (this->*_commandList[commandId])();
+}
+
+/*-----------------------------------------------------------------------*/
+
+RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.dr4", 0x2930, 0x2370, 0xAC) {
+}
+
+void RSound4::disableChannel(int channelIndex, byte flag) {
+	Channel &ch = _channels[channelIndex];
+	if (!ch._activeCount)
+		return;
+	if (ch._activeCount == 1)
+		ch._pSrc = loadData(0x1F8B);
+	ch._pendingStop = flag;
+	ch._soundData = loadData(0x1F8B);
+}
+
+int RSound4::command1() {
+	// Must call THIS driver's own command3() (not virtual in the base -
+	// see class comment).
+	command3();
+	if (isSoundActive(loadData(0x1F4F)))
+		return 0;
+	_fadeCheckPeriod = 1; // armFadeCheck
+	disableChannel(5, 0xFF); // channel 6 (redundant re-disable if command3's own gate already did it - harmless)
+	disableChannel(6, 0xFF); // channel 7
+	disableChannel(7, 0xFF); // channel 8
+	return 0;
+}
+
+int RSound4::command2() {
+	resetChannels1to5(); // this driver's own 7-channel version (1-6,9)
+	sendGmReset(4);
+	return 0;
+}
+
+int RSound4::command3() {
+	// Confirmed: unconditionally disables channels 1,2,3,4,5,9 (the
+	// familiar "lower" group), then conditionally disables channel 6 too -
+	// only if loadData(0x1F4F) isn't already playing.
+	_fadeCheckPeriod = 1; // armFadeCheck
+	disableChannel(0, 0xFF); // ch1
+	disableChannel(1, 0xFF); // ch2
+	disableChannel(2, 0xFF); // ch3
+	disableChannel(3, 0xFF); // ch4
+	disableChannel(4, 0xFF); // ch5
+	disableChannel(8, 0xFF); // ch9
+	if (isSoundActive(loadData(0x1F4F)))
+		return 0;
+	disableChannel(5, 0xFF); // ch6 (conditional)
+	return 0;
+}
+
+int RSound4::command4() {
+	resetChannels6to8(); // this driver's own conditional version
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+void RSound4::resetChannels1to5() {
+	// Matches this driver's own resetChannels1to5: channels 1-6 AND 9
+	// (seven channels) - one more than the base class's default (1-5,9).
+	_isDisabled = true;
+	resetChannelRange(0, 5);
+	_channels[8]._activeCount = 0;
+	_channels[8]._pitchBendFadeStep = 0;
+	_channels[8]._volumeFadeStep = 0;
+	_channels[8]._panFadeStep = 0;
+	resetHeldNotes();
+	_isDisabled = false;
+}
+
+void RSound4::resetChannels6to8() {
+	// Matches this driver's own resetChannels6to8: channels 7,8
+	// unconditionally, channel 6 only if it's currently active AND
+	// playing loadData(0x1F4F).
+	_isDisabled = true;
+	resetChannelRange(6, 7);
+	if (_channels[5]._activeCount && _channels[5]._soundData == loadData(0x1F4F))
+		resetChannelRange(5, 5);
+	_isDisabled = false;
+}
+
+const RSound4::CommandPtr RSound4::_commandList[79] = {
+	&RSound4::command0, &RSound4::command1, &RSound4::command2, &RSound4::command3,
+	&RSound4::command4, &RSound4::nullCommand, &RSound4::command6, &RSound4::command7,
+	&RSound4::command8, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::command16, &RSound4::command17, &RSound4::command18, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::command24, &RSound4::command25, &RSound4::command26, &RSound4::command27,
+	&RSound4::command28, &RSound4::command29, &RSound4::command30, &RSound4::command31,
+	&RSound4::command32, &RSound4::command33, &RSound4::nullCommand, &RSound4::command35,
+	&RSound4::command36, &RSound4::command37, &RSound4::command38, &RSound4::command39,
+	&RSound4::command40, &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::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::command64, &RSound4::command65, &RSound4::command66, &RSound4::command67,
+	&RSound4::command68, &RSound4::command69, &RSound4::command70, &RSound4::command71,
+	&RSound4::command72, &RSound4::command73, &RSound4::command74, &RSound4::command75,
+	&RSound4::command76, &RSound4::command77, &RSound4::command78
+};
+
+int RSound4::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound4::command16() {
+	// Genuinely different gate order - see class comment: checks
+	// isMusicChannelsActive() first, with no isSoundActive() gate at all
+	// on the immediate path.
+	if (!isMusicChannelsActive()) {
+		loadCommand16();
+	} else {
+		if (isSoundActive(loadData(0x7C4)))
+			return 0;
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand16));
+	}
+	return 0;
+}
+
+void RSound4::loadCommand16() {
+	resetCallbackTimer(192);
+	command3();
+	_channels[0].load(loadData(0x7C4));
+	_channels[1].load(loadData(0x804));
+	_channels[2].load(loadData(0x838));
+	_channels[8].load(loadData(0x887));
+}
+
+int RSound4::command17() {
+	// Ungated scheduling (Pattern A), matching every other driver's
+	// command17 shape.
+	if (isSoundActive(loadData(0x1E1C)))
+		return 0;
+	command3();
+	resetCallbackTimerEx(0xC0, 0x60);
+	_channels[0].load(loadData(0x1E1C));
+	_channels[1].load(loadData(0x1E4C));
+	_channels[2].load(loadData(0x1E7D));
+	_channels[3].load(loadData(0x1E94));
+	return 0;
+}
+
+int RSound4::command18() {
+	// See RSound1::command18 - the flat _commandList[] collapses the
+	// original's two-table branch into a single array lookup.
+	command3();
+	return (this->*_commandList[getMusicIndex()])();
+}
+
+int RSound4::command24() {
+	playSoundChannels6to8(0x1ECC);
+	playSoundChannels6to8(0x1EE0);
+	return 0;
+}
+
+int RSound4::command25() {
+	playSoundChannels6to8(0x1EF2);
+	playSoundChannels6to8(0x1F06);
+	return 0;
+}
+
+int RSound4::command26() {
+	playSoundChannels6to8(0x1F18);
+	return 0;
+}
+
+int RSound4::command27() {
+	playSoundChannels6to8(0x1F22);
+	return 0;
+}
+
+int RSound4::command28() {
+	playSoundChannels6to8(0x1F3B);
+	playSoundChannels6to8(0x1F45);
+	return 0;
+}
+
+int RSound4::command29() {
+	// Reuses 0x1F4F both as the isSoundActive gate and the play target -
+	// confirmed directly from the disassembly.
+	if (isSoundActive(loadData(0x1F4F)))
+		return 0;
+	playSoundChannels6to8(0x1F4F);
+	return 0;
+}
+
+int RSound4::command30() {
+	playSoundChannels6to8(0x205A);
+	return 0;
+}
+
+int RSound4::command31() {
+	playSoundChannels6to8(0x20FD);
+	return 0;
+}
+
+int RSound4::command32() {
+	byte *pData = loadData(0x964);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand32();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand32));
+	return 0;
+}
+
+void RSound4::loadCommand32() {
+	resetCallbackTimer(96);
+	command3();
+	_channels[0].load(loadData(0x964));
+	_channels[1].load(loadData(0xA44));
+	_channels[2].load(loadData(0xC50));
+	_channels[3].load(loadData(0xCFC));
+	_channels[4].load(loadData(0xD18));
+	_channels[5].load(loadData(0xD92));
+}
+
+int RSound4::command33() {
+	byte *pData = loadData(0xE10);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand33();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand33));
+	return 0;
+}
+
+void RSound4::loadCommand33() {
+	resetCallbackTimer(192);
+	command3();
+	_channels[0].load(loadData(0xE10));
+	_channels[1].load(loadData(0xE2E));
+	_channels[2].load(loadData(0xE56));
+	_channels[3].load(loadData(0xE97));
+	_channels[8].load(loadData(0xF5E));
+}
+
+int RSound4::command35() {
+	byte *pData = loadData(0xFFA);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand35();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand35));
+	return 0;
+}
+
+void RSound4::loadCommand35() {
+	resetCallbackTimer(84);
+	// NOTE: calls command1(), not command3() - unlike every other
+	// bucket-4 command in this driver, confirmed directly from the
+	// disassembly.
+	command1();
+	_channels[0].load(loadData(0xFFA));
+}
+
+int RSound4::command36() {
+	// NOTE: the isSoundActive() gate (0x25F1) doesn't match the first
+	// load offset (0x1950) - same kind of confirmed gate/load mismatch
+	// seen in RSound3's command32/command35.
+	byte *pData = loadData(0x25F1);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand36();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand36));
+	return 0;
+}
+
+void RSound4::loadCommand36() {
+	resetCallbackTimer(84);
+	command3();
+	_channels[0].load(loadData(0x1950));
+	_channels[1].load(loadData(0x1BB6));
+	_channels[2].load(loadData(0x1BFF));
+	_channels[3].load(loadData(0x1C1A));
+	_channels[4].load(loadData(0x1E15));
+}
+
+int RSound4::command37() {
+	byte *pData = loadData(0x1062);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand37();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand37));
+	return 0;
+}
+
+void RSound4::loadCommand37() {
+	resetCallbackTimer(64);
+	command3();
+	_channels[0].load(loadData(0x1062));
+	_channels[1].load(loadData(0x10A0));
+	_channels[2].load(loadData(0x10CB));
+	_channels[3].load(loadData(0x1100));
+	_channels[4].load(loadData(0x1141));
+}
+
+int RSound4::command38() {
+	byte *pData = loadData(0x1172);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand38();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand38));
+	return 0;
+}
+
+void RSound4::loadCommand38() {
+	resetCallbackTimer(64);
+	command3();
+	_channels[0].load(loadData(0x1172));
+	_channels[1].load(loadData(0x11C9));
+	_channels[2].load(loadData(0x1202));
+	_channels[3].load(loadData(0x122F));
+	_channels[8].load(loadData(0x126E));
+}
+
+int RSound4::command39() {
+	byte *pData = loadData(0x14DE);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand39();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand39));
+	return 0;
+}
+
+void RSound4::loadCommand39() {
+	resetCallbackTimer(72);
+	command3();
+	_channels[0].load(loadData(0x14DE));
+	_channels[1].load(loadData(0x1550));
+	_channels[2].load(loadData(0x15BB));
+	_channels[3].load(loadData(0x1672));
+	_channels[4].load(loadData(0x170D));
+	_channels[8].load(loadData(0x17C8));
+}
+
+int RSound4::command40() {
+	byte *pData = loadData(0x7BF);
+	if (isSoundActive(pData))
+		return 0;
+	if (!isMusicChannelsActive())
+		loadCommand40();
+	else
+		scheduleCallback(MAKE_CALLBACK(RSound4, loadCommand40));
+	return 0;
+}
+
+void RSound4::loadCommand40() {
+	resetCallbackTimer(192);
+	command3();
+	_channels[0].load(loadData(0x7BF));
+	_channels[1].load(loadData(0x7FF));
+	_channels[2].load(loadData(0x833));
+	_channels[3].load(loadData(0x7BA));
+	_channels[8].load(loadData(0x887));
+}
+
+int RSound4::command64() {
+	playSoundChannels6to8(0x1F8D);
+	return 0;
+}
+
+int RSound4::command65() {
+	playSoundChannels6to8(0x1FD9);
+	return 0;
+}
+
+int RSound4::command66() {
+	playSoundChannels6to8(0x1FE3);
+	return 0;
+}
+
+int RSound4::command67() {
+	playSoundChannels6to8(0x2037);
+	return 0;
+}
+
+int RSound4::command68() {
+	// 0x1FF8 is called twice in a row - a genuine quirk, preserved
+	// exactly rather than collapsed to a single call.
+	playSoundChannels6to8(0x1FF8);
+	playSoundChannels6to8(0x1FF8);
+	return 0;
+}
+
+int RSound4::command69() {
+	playSoundChannels6to8(0x1FB3);
+	return 0;
+}
+
+int RSound4::command70() {
+	playSoundChannels6to8(0x201B);
+	return 0;
+}
+
+int RSound4::command71() {
+	playSoundChannels6to8(0x202D);
+	return 0;
+}
+
+int RSound4::command72() {
+	playSoundChannels6to8(0x2084);
+	return 0;
+}
+
+int RSound4::command73() {
+	playSoundChannels6to8(0x20CB);
+	return 0;
+}
+
+int RSound4::command74() {
+	playSoundChannels6to8(0x2099);
+	return 0;
+}
+
+int RSound4::command75() {
+	playSoundChannels6to8(0x20B0);
+	return 0;
+}
+
+int RSound4::command76() {
+	playSoundChannels6to8(0x20DD);
+	playSoundChannels6to8(0x20ED);
+	return 0;
+}
+
+int RSound4::command77() {
+	playSoundChannels6to8(0x214F);
+	return 0;
+}
+
+int RSound4::command78() {
+	playSoundChannels6to8(0x216A);
+	return 0;
+}
+
+} // namespace Dragonsphere
+} // namespace MADS
diff --git a/engines/mads/dragonsphere/rsound_dragonsphere.h b/engines/mads/dragonsphere/rsound_dragonsphere.h
new file mode 100644
index 00000000000..35463eff180
--- /dev/null
+++ b/engines/mads/dragonsphere/rsound_dragonsphere.h
@@ -0,0 +1,449 @@
+/* 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_RSOUND_DRAGONSPHERE_H
+#define MADS_DRAGONSPHERE_RSOUND_DRAGONSPHERE_H
+
+#include "mads/dragonsphere/rsound.h"
+
+namespace MADS {
+namespace Dragonsphere {
+
+/**
+ * RSound1 (rsound.dr1)
+ *
+ * Dispatch table layout (funcs_10936/1094C/10962/1097C/10992 in the
+ * disassembly - same 5-bucket sparse dispatch as the Phantom RSound
+ * family, represented here as a single flat array):
+ *   commands  0-8   (base class)
+ *   commands 16-18  (this class - command16 is a gated deferred music
+ *                    loader, command17 an ungated-scheduling immediate
+ *                    loader, command18 the re-entrant launcher that
+ *                    re-dispatches into bucket 2 or bucket 4 by
+ *                    _musicIndex)
+ *   commands 24-31  (this class); 29 is confirmed nullsub_2
+ *   commands 32-48  (this class - music-piece loaders, mostly the
+ *                    "immediate load, or defer until the lower/music
+ *                    channels free up" idiom via isMusicChannelsActive()
+ *                    + scheduleCallback())
+ *   commands 64-101 (this class); 72, 92, 98 confirmed nullsub_2
+ *
+ * command43/command48 share a tail (a variant byte written into the
+ * loaded sound data before playing, matching the sibling ASound1
+ * driver's command43/48 idiom) - see command43_48Tail().
+ */
+class RSound1 : public RSound {
+private:
+	typedef int (RSound1:: *CommandPtr)();
+	static const CommandPtr _commandList[102];
+
+	int command16();
+	void loadCommand16();
+
+	int command17();
+
+	int command18();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command30();
+	int command31();
+
+	int command32();
+	void loadCommand32();
+	int command33();
+	void loadCommand33();
+	int command34();
+	int command35();
+	void loadCommand35();
+	int command36();
+	void loadCommand36();
+	int command37();
+	void loadCommand37();
+	int command38();
+	void loadCommand38();
+	int command39();
+	void loadCommand39();
+	int command40();
+	void loadCommand40();
+	int command41();
+	void loadCommand41();
+	int command42();
+	void loadCommand42();
+
+	/**
+	 * Shared tail of command43()/command48(): writes a variant byte
+	 * (0x5D for command43, 0x31 for command48) into the loaded sound
+	 * data at offset 0x2539 (one byte into the block that command2's
+	 * load targets) before the gated load, matching the sibling ASound1
+	 * driver's command43/48 idiom (see CLAUDE.md).
+	 */
+	void command43_48Tail(byte variant);
+	int command43();
+	int command48();
+	void loadCommand43_48();
+
+	int command44();
+	void loadCommand44();
+	int command45();
+	void loadCommand45();
+	int command46();
+	void loadCommand46();
+	int command47();
+	void loadCommand47();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command73();
+	int command74();
+	int command75();
+	int command76();
+	int command77();
+	int command78();
+	int command79();
+	int command80();
+	int command81();
+	int command82();
+	int command83();
+	int command84();
+	int command85();
+	int command86();
+	int command87();
+	int command88();
+	int command89();
+	int command90();
+	int command91();
+	int command93();
+	int command94();
+	int command95();
+	int command96();
+	int command97();
+	int command99();
+	int command100();
+	int command101();
+
+public:
+	RSound1(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound2 (rsound.dr2)
+ *
+ * command0/reset()/command4/command6/command7/command8 all confirmed to
+ * match the shared RSound base exactly (no overrides needed).
+ *
+ * command3/command5 are a genuinely different channel split from the
+ * base class's default (1-5,9 lower / 6,7,8 upper): here command3
+ * enables channels 1-6 (six channels, confirmed via symbolic names -
+ * channel 9 is untouched), and command5 enables channels 7,8 (confirmed
+ * via raw offsets 0x19CA/0x19F2, spaced by the channel stride).
+ *
+ * command2 calls resetChannels1to6() (matches sub_1079A) + sendGmReset(4)
+ * - a genuinely different 6-channel range from the base class's
+ * resetChannels1to5(), matching command3's own shifted grouping.
+ *
+ * command1/2/3 are not virtual in the base class, so this driver's own
+ * command1() must be overridden too (calling THIS class's command3()) -
+ * otherwise the inherited RSound::command1() would still call the base
+ * class's command3() rather than the driver-specific one, same pitfall
+ * as Phantom's RSound5.
+ */
+class RSound2 : public RSound {
+private:
+	typedef int (RSound2:: *CommandPtr)();
+	static const CommandPtr _commandList[73];
+
+	int command1();
+	int command2();
+	int command3();
+	int command5();
+
+	/** Matches resetChannels1to6: zeroes channels 1-6 (0-based 0-5), plus the held-notes table. */
+	void resetChannels1to6();
+
+	int command16();
+	void loadCommand16();
+	int command17();
+	int command18();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command30();
+	int command31();
+
+	/**
+	 * Plays the specified sound, using any free channel from 7 to 8.
+	 * Matches playSoundChannels7to8: symmetric free/fallback scan (free
+	 * scan ch7 then ch8; fallback scan ch8 then ch7).
+	 */
+	Channel *playSoundChannels7to8(int offset);
+
+	int command32();
+	void loadCommand32();
+	int command33();
+	void loadCommand33();
+	int command34();
+	void loadCommand34();
+
+	int command35();
+	void loadCommand35();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+
+public:
+	RSound2(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound3 (rsound.dr3)
+ *
+ * command0/command2/command4/command6/command7/command8 all confirmed to
+ * match the shared RSound base exactly (no overrides needed) -
+ * including command4 (whose reset helper sub_107E7 matches the base
+ * class's own resetChannels6to8() exactly) and command2 (whose
+ * resetChannels1to5 matches the base class's own method exactly,
+ * including the channel-9 fix).
+ *
+ * command3/command5 are a genuinely new, wider split with actual
+ * overlap: command3 enables channels 1-6 AND 9 (seven channels, via
+ * explicit calls through channel 6, falling through to channel 9), and
+ * command5 enables channels 5,6,7,8 (four channels) - so channels 5 and
+ * 6 are enabled by BOTH commands, confirmed directly from the
+ * disassembly.
+ *
+ * command1/2/3 are not virtual in the base class, so this driver's own
+ * command1() must be overridden too (calling THIS class's command3()) -
+ * same pitfall as Phantom's RSound5 and this game's RSound2.
+ *
+ * Dispatch table layout (funcs_109BB/109D1/109E7/10A01/10A17):
+ *   commands  0-8   (base class, except command1/3/5 - see above)
+ *   commands 16-18  (this class - same isSoundActive+isMusicChannelsActive
+ *                    deferred-load idiom as RSound1/2; the disassembly's
+ *                    gate uses the name "isSoundPlaying", confirmed
+ *                    equivalent to isSoundActive())
+ *   commands 24-31  (this class); 29 confirmed nullsub_1
+ *   commands 32-34  (this class); 34 confirmed nullsub_1
+ *   commands 64-73  (this class); 66 confirmed nullsub_1. command68 was
+ *                    initially suspected to be unreferenced dead code
+ *                    (an earlier dispatch table read appeared to skip
+ *                    straight from 67 to 69) - confirmed by the user to
+ *                    be a real, live dispatch entry after correcting the
+ *                    function names in the IDB; no index shift needed.
+ *                    There is no command74 - 73 is the last real entry.
+ */
+class RSound3 : public RSound {
+private:
+	typedef int (RSound3:: *CommandPtr)();
+	static const CommandPtr _commandList[74];
+
+	int command1();
+	int command3();
+	int command5();
+
+	int command16();
+	void loadCommand16();
+	int command17();
+	int command18();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command30();
+	int command31();
+
+	/**
+	 * Shared tail of command31()/command64() (matches loc_124D8): writes
+	 * a variant byte (0x5A for command31, 0x78 for command64) into the
+	 * sound data at offset 0x1402 before playing 0x13FF.
+	 */
+	void command31_64Tail(byte variant);
+	int command64();
+
+	int command32();
+	void loadCommand32();
+	int command33();
+	void loadCommand33();
+
+	int command65();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+	int command73();
+
+public:
+	RSound3(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound4 (rsound.dr4)
+ *
+ * command0/command6/command7/command8 all confirmed to match the shared
+ * RSound base exactly (no overrides needed).
+ *
+ * There is no separate command5 here - that dispatch slot is nullsub_1;
+ * its role is folded directly into command1().
+ *
+ * command1/command2/command3/command4 are all genuinely different from
+ * the base class, in two ways:
+ *   - They use a NEW driver-specific channel-disable mechanic
+ *     (disableChannel(), matching sub_1092A) instead of Channel::enable() -
+ *     it redirects _soundData (and, if the channel is about to expire
+ *     this tick, _pSrc too) to loadData(0x1F8B) instead of nullptr.
+ *   - Channel 6 (the boundary between the "lower" 1-5,9 group and
+ *     "upper" 6,7,8 group) is conditionally included/excluded based on
+ *     isSoundActive(loadData(0x1F4F)) in command1/command3, and based on
+ *     a direct _activeCount/_soundData check against the same offset in
+ *     resetChannels6to8(). resetChannels1to5() here also always resets
+ *     channel 6 in addition to 1-5,9 (seven channels total) - one more
+ *     than the base class's six.
+ *
+ * command1/2/3 are not virtual in the base class, so all four overrides
+ * are needed to ensure they call each other correctly (same pitfall as
+ * every other Dragonsphere RSound driver so far) - resetChannels1to5()
+ * and resetChannels6to8() are likewise redeclared here (matching the
+ * disassembly's own names) to hide the base class's versions for calls
+ * made from this class's own command2()/command4().
+ */
+class RSound4 : public RSound {
+private:
+	/**
+	 * Matches sub_1092A: a driver-specific variant of Channel::enable()
+	 * that redirects _soundData (and _pSrc, if the channel is about to
+	 * expire this tick) to loadData(0x1F8B) instead of nullptr.
+	 */
+	void disableChannel(int channelIndex, byte flag);
+
+	int command1();
+	int command2();
+	int command3();
+	int command4();
+
+	/** Hides the base class's method - see class comment. Resets channels 1-6 AND 9 (seven channels). */
+	void resetChannels1to5();
+
+	/** Hides the base class's method - see class comment. Channels 7,8 unconditionally; channel 6 only if it's playing loadData(0x1F4F). */
+	void resetChannels6to8();
+
+	typedef int (RSound4:: *CommandPtr)();
+	static const CommandPtr _commandList[79];
+
+	/**
+	 * Genuinely different gate order from every other bucket-4/16-18
+	 * command in this driver: checks isMusicChannelsActive() FIRST
+	 * (unconditionally, no isSoundActive() gate at all on the immediate
+	 * path); only if music channels ARE active does it then check
+	 * isSoundActive(0x7C4) as a secondary gate before scheduling the
+	 * deferred callback.
+	 */
+	int command16();
+	void loadCommand16();
+
+	int command17();
+	int command18();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+
+	/** Matches rsound_command29 - reuses offset 0x1F4F both as the isSoundActive gate AND the play target. */
+	int command29();
+	int command30();
+	int command31();
+
+	int command32();
+	void loadCommand32();
+	int command33();
+	void loadCommand33();
+
+	/** Calls command1() (not command3(), unlike every other bucket-4 command here) before loading a single channel. */
+	int command35();
+	void loadCommand35();
+
+	int command36();
+	void loadCommand36();
+	int command37();
+	void loadCommand37();
+	int command38();
+	void loadCommand38();
+	int command39();
+	void loadCommand39();
+	int command40();
+	void loadCommand40();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+	int command73();
+	int command74();
+	int command75();
+	int command76();
+	int command77();
+	int command78();
+
+public:
+	RSound4(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+} // namespace Dragonsphere
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/dragonsphere/sound.cpp b/engines/mads/dragonsphere/sound.cpp
index 95421230835..0f23fff6069 100644
--- a/engines/mads/dragonsphere/sound.cpp
+++ b/engines/mads/dragonsphere/sound.cpp
@@ -21,6 +21,7 @@
 
 #include "mads/dragonsphere/sound.h"
 #include "mads/dragonsphere/asound_dragonsphere.h"
+#include "mads/dragonsphere/rsound_dragonsphere.h"
 
 namespace MADS {
 namespace Dragonsphere {
@@ -42,7 +43,23 @@ void DragonSoundManager::loadDriver(int sectionNumber) {
 	switch (_driverType) {
 	case SOUND_MT32:
 		// Roland MT32 drivers
-		assert(0 == 1);
+		switch (sectionNumber) {
+		case 1:
+			_driver = new RSound1(_mixer);
+			break;
+		case 2:
+			_driver = new RSound2(_mixer);
+			break;
+		case 3:
+			_driver = new RSound3(_mixer);
+			break;
+		case 4:
+			_driver = new RSound4(_mixer);
+			break;
+		default:
+			// TODO
+			break;
+		}
 		break;
 
 	default:
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 212da7d151b..492431cad0d 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -347,6 +347,8 @@ MODULE_OBJS := \
 	dragonsphere/dragonsphere.o \
 	dragonsphere/asound.o \
 	dragonsphere/asound_dragonsphere.o \
+	dragonsphere/rsound.o \
+	dragonsphere/rsound_dragonsphere.o \
 	dragonsphere/global.o \
 	dragonsphere/main.o \
 	dragonsphere/main_menu.o \




More information about the Scummvm-git-logs mailing list