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

dreammaster noreply at scummvm.org
Mon Jul 27 08:57:38 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:
b5b0299435 MADS: PHANTOM: Remainder of RSound classes


Commit: b5b029943530d4136da05ba48e31c6c02a82b894
    https://github.com/scummvm/scummvm/commit/b5b029943530d4136da05ba48e31c6c02a82b894
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-27T18:57:31+10:00

Commit Message:
MADS: PHANTOM: Remainder of RSound classes

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

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


diff --git a/engines/mads/phantom/rsound.cpp b/engines/mads/phantom/rsound.cpp
index 9c3a3c60b20..c2702c6f129 100644
--- a/engines/mads/phantom/rsound.cpp
+++ b/engines/mads/phantom/rsound.cpp
@@ -179,18 +179,28 @@ Channel *RSound::playSound(int offset) {
 	return playSoundData(loadData(offset), 5, 7, 7);
 }
 
-Channel *RSound::playSoundAny(int offset) {
+Channel *RSound::playSoundChannels1To5(int offset) {
 	// Channels 1-5 (0-based 0-4) for the free scan, but the pending-stop
 	// fallback only reaches down to channel 4 (0-based 3) - channel 5
 	// can never be pre-empted here, confirmed from the disassembly.
 	return playSoundData(loadData(offset), 0, 4, 3);
 }
 
-Channel *RSound::playSoundChannels1To8(int offset) {
+Channel *RSound::playSoundAny(int offset) {
 	// Channels 1-8 (0-based 0-7), symmetric free/fallback scan.
 	return playSoundData(loadData(offset), 0, 7, 7);
 }
 
+Channel *RSound::playSoundChannels5To8(int offset) {
+	// Channels 5-8 (0-based 4-7), symmetric free/fallback scan.
+	return playSoundData(loadData(offset), 4, 7, 7);
+}
+
+Channel *RSound::playSoundChannels1To6(int offset) {
+	// Channels 1-6 (0-based 0-5), symmetric free/fallback scan.
+	return playSoundData(loadData(offset), 0, 5, 5);
+}
+
 bool RSound::isSoundActive(byte *pData) {
 	// Matches the disassembly exactly: only channels 1-8 are checked,
 	// channel 9 is never included.
@@ -413,9 +423,15 @@ void RSound::checkFadingChannels() {
 /*-----------------------------------------------------------------------*/
 
 void RSound::resetChannelRange(int first, int last) {
+	// Matches the confirmed struct-field disassembly exactly: two
+	// word-sized writes, each zeroing a pair of adjacent byte fields -
+	// [bx+_activeCount] (covers _activeCount + _pitchBendFadeStep) and
+	// [bx+_volumeFadeStep] (covers _volumeFadeStep + _panFadeStep).
 	for (int i = first; i <= last; ++i) {
 		_channels[i]._activeCount = 0;
-		_channels[i]._pitchBendFadeStep = 0; // matches "mov [bx],ax; mov [bx+2],ax" zeroing the first 4 bytes
+		_channels[i]._pitchBendFadeStep = 0;
+		_channels[i]._volumeFadeStep = 0;
+		_channels[i]._panFadeStep = 0;
 	}
 }
 
@@ -438,9 +454,11 @@ void RSound::resetChannels1to5() {
 	_isDisabled = false;
 }
 
-void RSound::resetChannels6to9() {
+void RSound::resetChannels4to9() {
+	// CORRECTED (was wrongly named/ranged resetChannels6to9): channels
+	// 4-9, 0-based indices 3-8 - confirmed directly from disassembly.
 	_isDisabled = true;
-	resetChannelRange(5, 8);
+	resetChannelRange(3, 8);
 	_isDisabled = false;
 }
 
@@ -459,8 +477,13 @@ int RSound::command0() {
 }
 
 int RSound::command1() {
+	// IMPORTANT: falls through to the SAME tail as command5() (loc_108A9)
+	// directly and ungated - it must NOT call the virtual command5()
+	// here, since that would wrongly apply whatever driver-specific
+	// isSoundActive() gate command5() has. command1() itself is never
+	// gated in any driver confirmed so far.
 	command3();
-	command5(); // shares command1/command5's tail (enables channels 5-8) - see command5()
+	enableUpperChannels();
 	return 0;
 }
 
@@ -483,24 +506,23 @@ int RSound::command3() {
 	return 0;
 }
 
-int RSound::command4() {
-	// Matches rsound_command4's chunk at loc_106DB: reset channels 6-9,
-	// then a full sendGmReset(9) (all 9 channels) - the "reset 6-9" and
-	// "GM-reset all 9" are both really executed, matching the original
+void RSound::resetAndGmResetUpperChannels() {
+	// Matches loc_106DB (command4()'s shared tail in every driver
+	// confirmed so far): reset channels 4-9, then a full sendGmReset(9)
+	// (all 9 channels) - both really execute, matching the original
 	// exactly despite the apparent redundancy.
-	resetChannels6to9();
+	resetChannels4to9();
 	sendGmReset(RSOUND_CHANNEL_COUNT);
-	return 0;
 }
 
-int RSound::command5() {
-	// Matches rsound_command5/loc_108A9: enables channels 5,6,7,8.
+void RSound::enableUpperChannels() {
+	// Matches loc_108A9 (command1()'s and command5()'s shared tail in
+	// every driver confirmed so far): enables channels 5,6,7,8.
 	_fadeCheckPeriod = 1;
 	_channels[4].enable(0xFF);
 	_channels[5].enable(0xFF);
 	_channels[6].enable(0xFF);
 	_channels[7].enable(0xFF);
-	return 0;
 }
 
 int RSound::command6() {
diff --git a/engines/mads/phantom/rsound.h b/engines/mads/phantom/rsound.h
index 7241791c36a..f069c1c9ab1 100644
--- a/engines/mads/phantom/rsound.h
+++ b/engines/mads/phantom/rsound.h
@@ -176,7 +176,6 @@ private:
 	 * directly rather than an arbitrary function pointer.
 	 */
 	int _fadeCheckCounter;
-	int _fadeCheckPeriod;
 
 	/**
 	 * Cluster of globals written by opcodes 0xBE-0xC1 but with no
@@ -228,10 +227,15 @@ private:
 	 */
 	uint16 readScriptWord(byte *&pSrc);
 
-	void resetChannelRange(int first, int last);
 	void resetAllChannels();
 	void resetChannels1to5();
-	void resetChannels6to9();
+
+	/**
+	 * CORRECTED (was wrongly named/ranged resetChannels6to9): resets
+	 * channels 4-9 (0-based indices 3-8), confirmed directly from
+	 * disassembly - "CODE XREF: rsound_command4".
+	 */
+	void resetChannels4to9();
 
 	void checkFadingChannels();
 	void Channel_checkFade(Channel *channel, int midiChannel);
@@ -258,6 +262,28 @@ protected:
 	 */
 	int _randomAmbianceTriggerFlag;
 
+	/**
+	 * 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" in the disassembly, e.g.
+	 * RSound5's driver-specific 6-channel command3() and 3-channel
+	 * enableChannels678()) can do so without going through the base
+	 * class's own command3()/enableUpperChannels().
+	 */
+	int _fadeCheckPeriod;
+
+	/**
+	 * Zeroes _activeCount/_pitchBendFadeStep/_volumeFadeStep/_panFadeStep
+	 * for channels [first, last] (0-based indices). Protected (not
+	 * private) so per-driver command4()/command5() overrides that need a
+	 * narrower or differently-shaped reset than resetChannels4to9() /
+	 * resetAndGmResetUpperChannels() below can call it directly - e.g.
+	 * RSound3's command4(), confirmed to reset only channels 5-9
+	 * (indices 4-8), not 4-9.
+	 */
+	void resetChannelRange(int first, int last);
+
 	/**
 	 * Hook called once per update() frame after the disabled check.
 	 * Only drivers with a random-ambiance/music picker (e.g. RSound1's
@@ -285,7 +311,7 @@ protected:
 	 * channel 5 - a genuine asymmetry preserved exactly (channel 5 can
 	 * never be pre-empted by this call, only picked while free).
 	 */
-	Channel *playSoundAny(int offset);
+	Channel *playSoundChannels1To5(int offset);
 
 	/**
 	 * Plays the specified sound, using any free channel from 1 to 8
@@ -293,7 +319,22 @@ protected:
 	 * disassembly - a third, distinct scan range from playSound() and
 	 * playSoundAny() above.
 	 */
-	Channel *playSoundChannels1To8(int offset);
+	Channel *playSoundAny(int offset);
+
+	/**
+	 * Plays the specified sound, using any free channel from 5 to 8.
+	 * Matches playChannels5to8 in the disassembly - a fourth, distinct
+	 * scan range (symmetric free/fallback scan, unlike playSoundAny()'s
+	 * asymmetry, and one channel narrower than playSoundAny()'s 1-8).
+	 */
+	Channel *playSoundChannels5To8(int offset);
+
+	/**
+	 * Plays the specified sound, using any free channel from 1 to 6.
+	 * Matches playSoundChannels1to6 in the disassembly - a fifth,
+	 * distinct scan range (symmetric free/fallback scan).
+	 */
+	Channel *playSoundChannels1To6(int offset);
 
 	/**
 	 * Scans [startingChannel, freeScanEnd] for a free channel; if none
@@ -382,16 +423,32 @@ protected:
 	int command3();
 
 	/**
-	 * NOTE: virtual, unlike command1-3/6-8. Confirmed (from RSound1) that
-	 * this driver's own command4/command5 are gated by isSoundActive() on
-	 * a driver-specific data offset (0x3D98 for RSound1) before doing
-	 * anything else - a per-driver detail that doesn't belong hardcoded
-	 * in the shared base. This base implementation is an UNGATED
-	 * fallback for drivers whose own command4/5 haven't been confirmed
-	 * yet; override once their disassembly is available.
+	 * Shared tail of command1() (falls through into it after command3())
+	 * and command5() (jumps straight into it, ungated, in every driver
+	 * confirmed so far): enables channels 5,6,7,8. Matches loc_108A9.
+	 */
+	void enableUpperChannels();
+
+	/**
+	 * Shared tail of command4() in every driver confirmed so far:
+	 * resetChannels4to9() + sendGmReset(9). Matches loc_106DB.
+	 */
+	void resetAndGmResetUpperChannels();
+
+	/**
+	 * PURE VIRTUAL, unlike command1-3/6-8. Confirmed (from RSound1 AND
+	 * RSound2, independently) that every driver's command4/command5 are
+	 * gated by isSoundActive() on a driver-specific data offset before
+	 * calling resetAndGmResetUpperChannels()/enableUpperChannels() above
+	 * - a per-driver detail that must not live in the shared base.
+	 * Deliberately NO default implementation, so a new driver subclass
+	 * can't compile without explicitly providing its own gate - silently
+	 * falling back to an ungated version would be wrong (and was, in an
+	 * earlier version of this port, until RSound1/RSound2 confirmed the
+	 * gate is universal even though its offset isn't).
 	 */
-	virtual int command4();
-	virtual int command5();
+	virtual int command4() = 0;
+	virtual int command5() = 0;
 	int command6();
 	int command7();
 	int command8();
diff --git a/engines/mads/phantom/rsound_phantom.cpp b/engines/mads/phantom/rsound_phantom.cpp
index 563f4052b76..877b8c8bb00 100644
--- a/engines/mads/phantom/rsound_phantom.cpp
+++ b/engines/mads/phantom/rsound_phantom.cpp
@@ -19,6 +19,7 @@
  *
  */
 
+#include "common/util.h"
 #include "mads/phantom/rsound_phantom.h"
 
 namespace MADS {
@@ -116,17 +117,19 @@ int RSound1::command16() {
 int RSound1::command4() {
 	// Confirmed: rsound_command4 is gated by isSoundActive(0x3D98) -
 	// which is also command39's own target sound - before falling
-	// through to the base class's reset-channels-6-9 + full GM-reset.
+	// through to the shared reset-channels-4-9 + full GM-reset tail.
 	if (isSoundActive(loadData(0x3D98)))
 		return 0;
-	return RSound::command4();
+	resetAndGmResetUpperChannels();
+	return 0;
 }
 
 int RSound1::command5() {
 	// Confirmed: same isSoundActive(0x3D98) gate as command4() above.
 	if (isSoundActive(loadData(0x3D98)))
 		return 0;
-	return RSound::command5();
+	enableUpperChannels();
+	return 0;
 }
 
 int RSound1::command24() {
@@ -155,12 +158,12 @@ int RSound1::command32() {
 	byte *pData = loadData(0x2044);
 	if (!isSoundActive(pData)) {
 		command1();
-		playSoundAny(0x2044);
-		playSoundAny(0x206B);
-		playSoundAny(0x208B);
-		playSoundAny(0x20AB);
-		playSoundAny(0x20C9);
-		playSoundAny(0x20E3);
+		playSoundChannels1To5(0x2044);
+		playSoundChannels1To5(0x206B);
+		playSoundChannels1To5(0x208B);
+		playSoundChannels1To5(0x20AB);
+		playSoundChannels1To5(0x20C9);
+		playSoundChannels1To5(0x20E3);
 	}
 	return 0;
 }
@@ -169,11 +172,11 @@ int RSound1::command33() {
 	byte *pData = loadData(0x530);
 	if (!isSoundActive(pData)) {
 		command1();
-		playSoundAny(0x530);
-		playSoundAny(0x766);
-		playSoundAny(0x929);
-		playSoundAny(0xB37);
-		playSoundAny(0xCBA);
+		playSoundChannels1To5(0x530);
+		playSoundChannels1To5(0x766);
+		playSoundChannels1To5(0x929);
+		playSoundChannels1To5(0xB37);
+		playSoundChannels1To5(0xCBA);
 		_channels[5].load(loadData(0xE20));
 	}
 	return 0;
@@ -183,12 +186,12 @@ int RSound1::command34() {
 	byte *pData = loadData(0x1014);
 	if (!isSoundActive(pData)) {
 		command1();
-		playSoundAny(0x1014);
-		playSoundAny(0x126B);
-		playSoundAny(0x137C);
-		playSoundAny(0x149B);
-		playSoundAny(0x158C);
-		playSoundAny(0x164E);
+		playSoundChannels1To5(0x1014);
+		playSoundChannels1To5(0x126B);
+		playSoundChannels1To5(0x137C);
+		playSoundChannels1To5(0x149B);
+		playSoundChannels1To5(0x158C);
+		playSoundChannels1To5(0x164E);
 	}
 	return 0;
 }
@@ -240,12 +243,12 @@ int RSound1::command39() {
 	byte *pData = loadData(0x3D98);
 	if (!isSoundActive(pData)) {
 		command1();
-		playSoundChannels1To8(0x3D98);
-		playSoundChannels1To8(0x3F39);
-		playSoundChannels1To8(0x404D);
-		playSoundChannels1To8(0x413F);
-		playSoundChannels1To8(0x4247);
-		playSoundChannels1To8(0x43FD);
+		playSoundAny(0x3D98);
+		playSoundAny(0x3F39);
+		playSoundAny(0x404D);
+		playSoundAny(0x413F);
+		playSoundAny(0x4247);
+		playSoundAny(0x43FD);
 	}
 	return 0;
 }
@@ -399,5 +402,1071 @@ int RSound1::command76() {
 	return 0;
 }
 
+/*-----------------------------------------------------------------------*/
+
+const RSound2::CommandPtr RSound2::_commandList[74] = {
+	&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::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&RSound2::command24, &RSound2::command25, &RSound2::command26, &RSound2::command27,
+	&RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand, &RSound2::nullCommand,
+	&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, &RSound2::nullCommand
+};
+
+RSound2::RSound2(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph2", 0x2B40, 0x1C40, 0xDC) {
+}
+
+int RSound2::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound2::command4() {
+	// Confirmed: same isSoundActive(0x3D98)-gated shape as RSound1's
+	// command4(), though this driver has no equivalent to RSound1's
+	// command39 (its own bucket structure stops at 35, then jumps to
+	// 64+), so this offset can't be "reused ambient-piece target" here -
+	// just a coincidentally identical offset NUMBER, not assumed to hold
+	// the same data.
+	if (isSoundActive(loadData(0x3D98)))
+		return 0;
+	resetAndGmResetUpperChannels();
+	return 0;
+}
+
+int RSound2::command5() {
+	// Confirmed: same isSoundActive(0x3D98) gate as command4() above.
+	if (isSoundActive(loadData(0x3D98)))
+		return 0;
+	enableUpperChannels();
+	return 0;
+}
+
+int RSound2::command16() {
+	// Unlike RSound1's command16 (a random-ambiance picker), this is a
+	// plain gated 3-channel load - confirms command16 is genuinely
+	// driver-specific, not shared behavior.
+	byte *pData = loadData(0x1188);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x11E1));
+		_channels[2].load(loadData(0x1246));
+	}
+	return 0;
+}
+
+int RSound2::command24() {
+	playSound(0x16AC);
+	playSound(0x16C0);
+	return 0;
+}
+
+int RSound2::command25() {
+	playSound(0x16D2);
+	playSound(0x16E6);
+	return 0;
+}
+
+int RSound2::command26() {
+	playSound(0x16F8);
+	return 0;
+}
+
+int RSound2::command27() {
+	playSound(0x1702);
+	return 0;
+}
+
+int RSound2::command32() {
+	// No isSoundActive gate at all - unlike every other multi-load
+	// command seen so far.
+	command1();
+	playSoundChannels1To5(0x129E);
+	playSoundChannels1To5(0x136C);
+	playSoundChannels1To5(0x14D1);
+	return 0;
+}
+
+int RSound2::command33() {
+	byte *pData = loadData(0x52E);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x52E);
+		playSoundAny(0x561);
+		playSoundAny(0x56F);
+		playSoundAny(0x57D);
+		playSoundAny(0x58B);
+		playSoundAny(0x597);
+	}
+	return 0;
+}
+
+int RSound2::command34() {
+	byte *pData = loadData(0x5A6);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x5A6);
+		playSoundAny(0x776);
+		playSoundAny(0xA3B);
+		playSoundAny(0xBC5);
+		playSoundAny(0xD87);
+		playSoundAny(0x1086);
+		playSoundAny(0x112E);
+	}
+	return 0;
+}
+
+int RSound2::command35() {
+	byte *pData = loadData(0x157C);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x157C);
+		playSoundAny(0x15EA);
+		playSoundAny(0x1615);
+		playSoundAny(0x1640);
+	}
+	return 0;
+}
+
+int RSound2::command64() {
+	_channels[8].load(loadData(0x17B6));
+	return 0;
+}
+
+int RSound2::command65() {
+	_channels[8].load(loadData(0x17AE));
+	return 0;
+}
+
+int RSound2::command66() {
+	playSound(0x17A0);
+	return 0;
+}
+
+int RSound2::command67() {
+	playSound(0x1732);
+	playSound(0x1748);
+	playSound(0x175E);
+	playSound(0x1774);
+	playSound(0x178A);
+	return 0;
+}
+
+int RSound2::command68() {
+	// Same 4 offsets called twice each - a genuine quirk, preserved
+	// exactly rather than collapsed to 4 calls.
+	playSoundAny(0x17C2);
+	playSoundAny(0x17F2);
+	playSoundAny(0x1828);
+	playSoundAny(0x185E);
+	playSoundAny(0x17C2);
+	playSoundAny(0x17F2);
+	playSoundAny(0x1828);
+	playSoundAny(0x185E);
+	return 0;
+}
+
+int RSound2::command69() {
+	_channels[8].load(loadData(0x1721));
+	return 0;
+}
+
+int RSound2::command70() {
+	playSound(0x1894);
+	playSound(0x18B1);
+	return 0;
+}
+
+int RSound2::command71() {
+	playSound(0x18CA);
+	return 0;
+}
+
+int RSound2::command72() {
+	playSound(0x168A);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound3::CommandPtr RSound3::_commandList[77] = {
+	&RSound3::command0, &RSound3::command1, &RSound3::command2,
+	// Index 3 is a plain "jmp rsound_command1" in the disassembly - the
+	// dispatch slot for command index 3 aliases directly to command1's
+	// FULL handler (lower channels 1-4,9 AND upper channels 5-8), not
+	// to the "inner" lower-only logic that the base class's own
+	// command3() implements. Mapping it to &RSound3::command1 (rather
+	// than &RSound3::command3) preserves that exactly.
+	&RSound3::command1,
+	&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::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::command24, &RSound3::command25, &RSound3::command26, &RSound3::command27,
+	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
+	&RSound3::command32, &RSound3::command33, &RSound3::command34, &RSound3::command35,
+	&RSound3::command36, &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::command66, &RSound3::command67,
+	&RSound3::command68, &RSound3::command69, &RSound3::command70, &RSound3::command71,
+	&RSound3::command72, &RSound3::command73, &RSound3::command74, &RSound3::command75,
+	&RSound3::nullCommand
+};
+
+RSound3::RSound3(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph3", 0x2BA0, 0x37A0, 0xEC) {
+}
+
+int RSound3::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound3::command4() {
+	// Confirmed: TWO chained isSoundActive() gates (unlike RSound1/
+	// RSound2's single 0x3D98 gate), then a tail (sub_10832) that
+	// resets only channels 5-9 (indices 4-8) - narrower than the
+	// shared resetChannels4to9()'s 4-9 (indices 3-8) - so it can't
+	// reuse resetAndGmResetUpperChannels() and calls resetChannelRange()
+	// directly instead.
+	if (isSoundActive(loadData(0x2AA6)))
+		return 0;
+	if (isSoundActive(loadData(0x1E30)))
+		return 0;
+
+	_isDisabled = true;
+	resetChannelRange(4, 8);
+	_isDisabled = false;
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound3::command5() {
+	// Same two-gate shape as command4() above, but the plain
+	// enableUpperChannels() tail (loc_10955, shared with command1()).
+	if (isSoundActive(loadData(0x2AA6)))
+		return 0;
+	if (isSoundActive(loadData(0x1E30)))
+		return 0;
+	enableUpperChannels();
+	return 0;
+}
+
+int RSound3::command16() {
+	byte *pData = loadData(0x1BFA);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x1CAF));
+		_channels[2].load(loadData(0x1D24));
+		_channels[3].load(loadData(0x1D8B));
+	}
+	return 0;
+}
+
+int RSound3::command24() {
+	playSoundChannels5To8(0x32E9);
+	playSoundChannels5To8(0x32FD);
+	return 0;
+}
+
+int RSound3::command25() {
+	playSoundChannels5To8(0x330F);
+	playSoundChannels5To8(0x3323);
+	return 0;
+}
+
+int RSound3::command26() {
+	playSoundChannels5To8(0x3335);
+	return 0;
+}
+
+int RSound3::command27() {
+	playSoundChannels5To8(0x333F);
+	return 0;
+}
+
+int RSound3::command32() {
+	byte *pData = loadData(0x14F2);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x14F2);
+		playSoundAny(0x154A);
+		playSoundAny(0x1589);
+		playSoundAny(0x15C8);
+		playSoundAny(0x1638);
+		playSoundAny(0x16BF);
+		playSoundAny(0x1704);
+	}
+	return 0;
+}
+
+int RSound3::command33() {
+	byte *pData = loadData(0x1E30);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x1E30);
+		playSoundAny(0x2094);
+		playSoundAny(0x2326);
+		playSoundAny(0x2432);
+		playSoundAny(0x2632);
+		playSoundAny(0x28FF);
+	}
+	return 0;
+}
+
+int RSound3::command34() {
+	// No isSoundActive gate at all - unconditional, unlike 32/33/35/36.
+	command1();
+	playSoundAny(0x6AE);
+	playSoundAny(0x95F);
+	playSoundAny(0xA67);
+	playSoundAny(0xC49);
+	playSoundAny(0xE43);
+	playSoundAny(0x10AA);
+	playSoundAny(0x11E0);
+	playSoundAny(0x138C);
+	return 0;
+}
+
+int RSound3::command35() {
+	byte *pData = loadData(0x2AA6);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x2AA6);
+		playSoundAny(0x2C43);
+		playSoundAny(0x2D53);
+		playSoundAny(0x2E43);
+		playSoundAny(0x2F4B);
+		playSoundAny(0x30FD);
+	}
+	return 0;
+}
+
+int RSound3::command36() {
+	byte *pData = loadData(0x1778);
+	if (!isSoundActive(pData)) {
+		command1();
+		playSoundAny(0x1778);
+		playSoundAny(0x1832);
+		playSoundAny(0x18EC);
+		playSoundAny(0x1B9D);
+	}
+	return 0;
+}
+
+int RSound3::command64() {
+	playSoundChannels5To8(0x335E);
+	return 0;
+}
+
+int RSound3::command65() {
+	playSoundChannels5To8(0x337E);
+	playSoundChannels5To8(0x3391);
+	return 0;
+}
+
+int RSound3::command66() {
+	// 0x33AE is called three times in a row - a genuine quirk, preserved
+	// exactly rather than collapsed to a single call.
+	playSoundChannels5To8(0x33A4);
+	playSoundChannels5To8(0x33AE);
+	playSoundChannels5To8(0x33AE);
+	playSoundChannels5To8(0x33AE);
+	return 0;
+}
+
+int RSound3::command67() {
+	playSoundChannels5To8(0x33BC);
+	return 0;
+}
+
+int RSound3::command68() {
+	playSoundChannels5To8(0x33C9);
+	return 0;
+}
+
+int RSound3::command69() {
+	playSoundChannels5To8(0x33DA);
+	return 0;
+}
+
+int RSound3::command70() {
+	playSoundChannels5To8(0x32CF);
+	return 0;
+}
+
+int RSound3::command71() {
+	playSoundChannels5To8(0x3294);
+	return 0;
+}
+
+int RSound3::command72() {
+	playSoundChannels5To8(0x3406);
+	return 0;
+}
+
+int RSound3::command73() {
+	playSoundChannels5To8(0x3414);
+	return 0;
+}
+
+int RSound3::command74() {
+	_channels[8].load(loadData(0x3429));
+	return 0;
+}
+
+int RSound3::command75() {
+	playSoundChannels5To8(0x33EB);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound4::CommandPtr RSound4::_commandList[72] = {
+	&RSound4::command0, &RSound4::command1, &RSound4::command2, &RSound4::command3,
+	&RSound4::command4, &RSound4::command5, &RSound4::command6, &RSound4::command7,
+	&RSound4::command8, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::command16, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand, &RSound4::nullCommand,
+	&RSound4::command24, &RSound4::command25, &RSound4::command26, &RSound4::command27,
+	&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::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::nullCommand
+};
+
+RSound4::RSound4(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph4", 0x2A40, 0xE00, 0xEC) {
+}
+
+int RSound4::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound4::command4() {
+	// Confirmed: NO isSoundActive() gate in this driver, unlike every
+	// other driver seen so far - matches sub_107FC + sendGmReset(9)
+	// unconditionally.
+	_isDisabled = true;
+	resetChannelRange(5, 8);
+	_isDisabled = false;
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound4::command5() {
+	// Confirmed: sub_10838 (_fadeCheckPeriod = 1) + channels 5-8 -
+	// identical to the shared enableUpperChannels(), no gate at all.
+	enableUpperChannels();
+	return 0;
+}
+
+int RSound4::command16() {
+	// sub_101E7 ("isSoundPlaying") confirmed functionally identical to
+	// RSound::isSoundActive() - same channel 1-8 scan, same
+	// _activeCount && _soundData==pData test, same pop-return-address
+	// early-exit trick.
+	if (!isSoundActive(loadData(0x8F0))) {
+		command1();
+		_channels[0].load(loadData(0x63A));
+		_channels[1].load(loadData(0x8F0));
+		_channels[2].load(loadData(0x916));
+		_channels[3].load(loadData(0x93C));
+		_channels[4].load(loadData(0x962));
+	}
+	return 0;
+}
+
+int RSound4::command24() {
+	playSound(0x9C6);
+	playSound(0x9D6);
+	return 0;
+}
+
+int RSound4::command25() {
+	playSound(0x9EC);
+	playSound(0xA00);
+	return 0;
+}
+
+int RSound4::command26() {
+	playSound(0xA12);
+	return 0;
+}
+
+int RSound4::command27() {
+	playSound(0xA1C);
+	return 0;
+}
+
+int RSound4::command64() {
+	playSound(0xA38);
+	return 0;
+}
+
+int RSound4::command65() {
+	playSound(0xA4E);
+	return 0;
+}
+
+int RSound4::command66() {
+	playSound(0xA60);
+	return 0;
+}
+
+int RSound4::command67() {
+	playSound(0xA76);
+	return 0;
+}
+
+int RSound4::command68() {
+	playSound(0xA80);
+	return 0;
+}
+
+int RSound4::command69() {
+	playSound(0xA8C);
+	return 0;
+}
+
+int RSound4::command70() {
+	playSound(0x9A4);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound5::CommandPtr RSound5::_commandList[80] = {
+	&RSound5::command0, &RSound5::command1, &RSound5::command2, &RSound5::command3,
+	&RSound5::command4, &RSound5::command5, &RSound5::command6, &RSound5::command7,
+	&RSound5::command8, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::command16, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::command24, &RSound5::command25, &RSound5::command26, &RSound5::command27,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::command33, &RSound5::command34, &RSound5::command35,
+	&RSound5::command36, &RSound5::command37, &RSound5::command38, &RSound5::command39,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand, &RSound5::nullCommand,
+	&RSound5::command64, &RSound5::command65, &RSound5::command66, &RSound5::command67,
+	&RSound5::command68, &RSound5::command69, &RSound5::command70, &RSound5::command71,
+	&RSound5::command72, &RSound5::command73, &RSound5::command74, &RSound5::command75,
+	&RSound5::command76, &RSound5::command77, &RSound5::command78, &RSound5::nullCommand
+};
+
+RSound5::RSound5(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph5", 0x2BD0, 0x4870, 0xEC) {
+}
+
+int RSound5::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound5::command3() {
+	// Confirmed: enables channels 1-5 AND 9 (six channels) - channel 5
+	// has moved into the "lower" group here, unlike every prior driver's
+	// command3() (channels 1-4,9 only, matching RSound::command3()).
+	_fadeCheckPeriod = 1;
+	_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;
+}
+
+void RSound5::enableChannels678() {
+	_fadeCheckPeriod = 1;
+	_channels[5].enable(0xFF);
+	_channels[6].enable(0xFF);
+	_channels[7].enable(0xFF);
+}
+
+int RSound5::command1() {
+	// Must call THIS driver's own command3() (see class comment - not
+	// virtual in the base, so RSound::command1() would otherwise call
+	// the base's command3() even with this override present).
+	command3();
+	enableChannels678();
+	return 0;
+}
+
+int RSound5::command4() {
+	// No isSoundActive() gate, matching RSound4's command4() shape.
+	_isDisabled = true;
+	resetChannelRange(5, 8);
+	_isDisabled = false;
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound5::command5() {
+	enableChannels678();
+	return 0;
+}
+
+int RSound5::command16() {
+	byte *pData = loadData(0xBBE);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0xC60));
+		_channels[2].load(loadData(0xD04));
+		_channels[3].load(loadData(0xD97));
+		_channels[4].load(loadData(0xDDA));
+	}
+	return 0;
+}
+
+int RSound5::command24() {
+	playSound(0x437C);
+	playSound(0x4390);
+	return 0;
+}
+
+int RSound5::command25() {
+	playSound(0x43A2);
+	playSound(0x43B6);
+	return 0;
+}
+
+int RSound5::command26() {
+	playSound(0x43C8);
+	return 0;
+}
+
+int RSound5::command27() {
+	playSound(0x43D2);
+	return 0;
+}
+
+int RSound5::command33() {
+	byte *pData = loadData(0xE66);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x110E));
+		_channels[2].load(loadData(0x13A4));
+		_channels[3].load(loadData(0x14B4));
+		_channels[4].load(loadData(0x16E4));
+		_channels[5].load(loadData(0x19CB));
+	}
+	return 0;
+}
+
+int RSound5::command34() {
+	byte *pData = loadData(0x3190);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x3330));
+		_channels[2].load(loadData(0x34BA));
+		_channels[3].load(loadData(0x34E8));
+		_channels[4].load(loadData(0x3518));
+		_channels[5].load(loadData(0x3546));
+		_channels[6].load(loadData(0x3574));
+		_channels[7].load(loadData(0x35A6));
+	}
+	return 0;
+}
+
+int RSound5::command35() {
+	byte *pData = loadData(0x1B9A);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x218B));
+		_channels[2].load(loadData(0x271D));
+	}
+	return 0;
+}
+
+int RSound5::command36() {
+	byte *pData = loadData(0x2D04);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x2E43));
+		_channels[2].load(loadData(0x2F73));
+	}
+	return 0;
+}
+
+int RSound5::command37() {
+	byte *pData = loadData(0x35D8);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x3884));
+		_channels[2].load(loadData(0x39A6));
+		_channels[3].load(loadData(0x3BC9));
+	}
+	return 0;
+}
+
+int RSound5::command38() {
+	byte *pData = loadData(0x73A);
+	if (!isSoundActive(pData)) {
+		command1();
+		_channels[0].load(pData);
+		_channels[1].load(loadData(0x87A));
+		_channels[2].load(loadData(0x938));
+		_channels[3].load(loadData(0x9F9));
+		_channels[4].load(loadData(0xA8A));
+	}
+	return 0;
+}
+
+int RSound5::command39() {
+	// Gate leads to command3() (the lower-group enable), not command1() -
+	// see class comment. Only playSoundChannels1To5() is used here, never
+	// the upper 3 channels.
+	byte *pData = loadData(0x3C5C);
+	if (!isSoundActive(pData)) {
+		command3();
+		playSoundChannels1To5(0x3C5C);
+		playSoundChannels1To5(0x3EB3);
+		playSoundChannels1To5(0x3FC4);
+		playSoundChannels1To5(0x40E3);
+		playSoundChannels1To5(0x41D4);
+		playSoundChannels1To5(0x4296);
+	}
+	return 0;
+}
+
+int RSound5::command64() {
+	playSound(0x447B);
+	return 0;
+}
+
+int RSound5::command65() {
+	playSound(0x43EE);
+	return 0;
+}
+
+int RSound5::command66() {
+	playSound(0x43FC);
+	return 0;
+}
+
+int RSound5::command67() {
+	_channels[6].load(loadData(0x4422));
+	return 0;
+}
+
+int RSound5::command68() {
+	playSound(0x4406);
+	return 0;
+}
+
+int RSound5::command69() {
+	playSound(0x442C);
+	return 0;
+}
+
+int RSound5::command70() {
+	playSound(0x4418);
+	return 0;
+}
+
+int RSound5::command71() {
+	playSound(0x444A);
+	playSound(0x4459);
+	return 0;
+}
+
+int RSound5::command72() {
+	playSound(0x4467);
+	return 0;
+}
+
+int RSound5::command73() {
+	_channels[6].load(loadData(0x4471));
+	return 0;
+}
+
+int RSound5::command74() {
+	playSound(0x4488);
+	return 0;
+}
+
+int RSound5::command75() {
+	playSound(0x449D);
+	return 0;
+}
+
+int RSound5::command76() {
+	playSound(0x44B9);
+	return 0;
+}
+
+int RSound5::command77() {
+	playSound(0x44D1);
+	return 0;
+}
+
+int RSound5::command78() {
+	playSound(0x44EB);
+	return 0;
+}
+
+/*-----------------------------------------------------------------------*/
+
+const RSound9::CommandPtr RSound9::_commandList[73] = {
+	&RSound9::command0, &RSound9::command1, &RSound9::command2, &RSound9::command3,
+	&RSound9::command4, &RSound9::command5, &RSound9::command6, &RSound9::command7,
+	&RSound9::command8, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::command16Or24, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::command16Or24, &RSound9::command25, &RSound9::command26, &RSound9::command27,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::command32, &RSound9::nullCommand, &RSound9::command34, &RSound9::command35,
+	&RSound9::command36, &RSound9::command37, &RSound9::command38, &RSound9::command39,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand, &RSound9::nullCommand,
+	&RSound9::command64, &RSound9::command65Or66, &RSound9::command65Or66, &RSound9::command67,
+	&RSound9::command68, &RSound9::command69, &RSound9::command70, &RSound9::command71,
+	&RSound9::nullCommand
+};
+
+RSound9::RSound9(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph9", 0x2B70, 0x2D40, 0xEC) {
+}
+
+int RSound9::command(int commandId, int param) {
+	if (commandId < 0 || commandId >= ARRAYSIZE(_commandList))
+		return 0;
+
+	_commandParam = param;
+	_frameCounter = 0;
+	return (this->*_commandList[commandId])();
+}
+
+int RSound9::command4() {
+	// sub_1082B: resets channels 7,8,9 (0-based indices 6-8) - a fourth
+	// distinct range for this reset tail (RSound1/2: 4-9, RSound3: 5-9,
+	// RSound4/5: 6-9). No isSoundActive() gate, matching RSound4/5.
+	_isDisabled = true;
+	resetChannelRange(6, 8);
+	_isDisabled = false;
+	sendGmReset(RSOUND_CHANNEL_COUNT);
+	return 0;
+}
+
+int RSound9::command5() {
+	// Confirmed to match the shared enableUpperChannels() exactly
+	// (channels 5-8, armFadeCheck).
+	enableUpperChannels();
+	return 0;
+}
+
+int RSound9::command16Or24() {
+	playSoundChannels5To8(0x295A);
+	return 0;
+}
+
+int RSound9::command25() {
+	playSoundChannels5To8(0x295C);
+	return 0;
+}
+
+int RSound9::command26() {
+	playSoundChannels5To8(0x295E);
+	return 0;
+}
+
+int RSound9::command27() {
+	playSoundChannels5To8(0x2960);
+	return 0;
+}
+
+int RSound9::command32() {
+	// No isSoundActive gate at all - unconditional.
+	command1();
+	playSoundAny(0x628);
+	playSoundAny(0x682);
+	playSoundAny(0x6CA);
+	playSoundAny(0x99C);
+	return 0;
+}
+
+int RSound9::command34() {
+	// True dispatch index 34 - this is the disassembly's
+	// "rsound_command33" body (see class comment on the naming shift).
+	command1();
+	playSoundAny(0x9A8);
+	playSoundAny(0x9E4);
+	playSoundAny(0xA53);
+	playSoundAny(0xA96);
+	playSoundAny(0xAFB);
+	playSoundAny(0xB34);
+	return 0;
+}
+
+int RSound9::command35() {
+	// True dispatch index 35 - disassembly's "rsound_command34" body.
+	command1();
+	playSoundAny(0x1008);
+	playSoundAny(0x103C);
+	playSoundAny(0x105D);
+	playSoundAny(0x107E);
+	playSoundAny(0x10D6);
+	playSoundAny(0x1131);
+	playSoundAny(0x116C);
+	return 0;
+}
+
+int RSound9::command36() {
+	// True dispatch index 36 - disassembly's "rsound_command35" body.
+	command1();
+	playSoundAny(0x1198);
+	playSoundAny(0x11C9);
+	playSoundAny(0x11F5);
+	playSoundAny(0x1229);
+	playSoundAny(0x125D);
+	playSoundAny(0x1283);
+	return 0;
+}
+
+int RSound9::command37() {
+	// True dispatch index 37 - disassembly's "rsound_command36" body.
+	command1();
+	playSoundAny(0x12B8);
+	playSoundAny(0x1306);
+	playSoundAny(0x1408);
+	playSoundAny(0x1486);
+	playSoundAny(0x14E0);
+	playSoundAny(0x1733);
+	playSoundAny(0x1A44);
+	return 0;
+}
+
+int RSound9::command38() {
+	// True dispatch index 38 - disassembly's "rsound_command37" body.
+	command1();
+	playSoundAny(0x1B30);
+	playSoundAny(0x1DE1);
+	playSoundAny(0x1EE9);
+	playSoundAny(0x20CB);
+	playSoundAny(0x22C5);
+	playSoundAny(0x250F);
+	playSoundAny(0x2645);
+	playSoundAny(0x27F1);
+	return 0;
+}
+
+int RSound9::command39() {
+	// True dispatch index 39 - disassembly's "rsound_command38" body.
+	command1();
+	playSoundChannels1To6(0xB76);
+	playSoundChannels1To6(0xCB4);
+	playSoundChannels1To6(0xD7B);
+	playSoundChannels1To6(0xE3E);
+	playSoundChannels1To6(0xED2);
+	return 0;
+}
+
+int RSound9::command64() {
+	_channels[7].load(loadData(0x296A));
+	return 0;
+}
+
+int RSound9::command65Or66() {
+	_channels[8].load(loadData(0x2962));
+	return 0;
+}
+
+int RSound9::command67() {
+	// 0x298A is called three times in a row - a genuine quirk, preserved
+	// exactly rather than collapsed to a single call.
+	playSoundChannels5To8(0x2980);
+	playSoundChannels5To8(0x298A);
+	playSoundChannels5To8(0x298A);
+	playSoundChannels5To8(0x298A);
+	return 0;
+}
+
+int RSound9::command68() {
+	_channels[7].load(loadData(0x29C4));
+	return 0;
+}
+
+int RSound9::command69() {
+	_channels[8].load(loadData(0x29B3));
+	return 0;
+}
+
+int RSound9::command70() {
+	// Shares a fallthrough tail with command71 (loc_12439 in the
+	// disassembly): writes a variant byte into the sound data at 0x299B
+	// before playing 0x2998 - twice, matching the disassembly's call +
+	// tail-jmp pair exactly.
+	*loadData(0x299B) = 80;
+	playSoundChannels5To8(0x2998);
+	playSoundChannels5To8(0x2998);
+	return 0;
+}
+
+int RSound9::command71() {
+	*loadData(0x299B) = 99;
+	playSoundChannels5To8(0x2998);
+	playSoundChannels5To8(0x2998);
+	return 0;
+}
+
 } // namespace Phantom
 } // namespace MADS
diff --git a/engines/mads/phantom/rsound_phantom.h b/engines/mads/phantom/rsound_phantom.h
index b8df21a3980..f73a4869a82 100644
--- a/engines/mads/phantom/rsound_phantom.h
+++ b/engines/mads/phantom/rsound_phantom.h
@@ -62,7 +62,8 @@ private:
 	int command16();
 
 	// Overrides confirming the isSoundActive(0x3D98) gate the base
-	// class's ungated command4()/command5() lack - see RSound::command4/5.
+	// class's pure-virtual command4()/command5() require - see
+	// RSound::command4/5.
 	int command4() override;
 	int command5() override;
 
@@ -108,6 +109,346 @@ public:
 	int command(int commandId, int param) override;
 };
 
+/**
+ * RSound2 (rsound.ph2)
+ *
+ * Dispatch table layout (funcs_10964/1097A/10990/109A6/109BC in the
+ * disassembly - same 5-bucket sparse-dispatch shape as RSound1, but here
+ * represented as a single flat array, matching the Rex Nebular RSound
+ * convention, since the bucket boundaries don't otherwise affect behavior):
+ *   commands  0-8   (base class, except command4/5 - see RSound::command4/5)
+ *   command   16    (this class - a plain gated 3-channel load, NOT a
+ *                    random picker like RSound1's command16)
+ *   commands 24-27  (this class)
+ *   commands 32-35  (this class)
+ *   commands 64-72  (this class); 73 is confirmed nullsub_1
+ * All other indices in [0, 73] are confirmed or inferred unreachable
+ * (nullCommand).
+ */
+class RSound2 : public RSound {
+private:
+	typedef int (RSound2:: *CommandPtr)();
+	static const CommandPtr _commandList[74];
+
+	// Overrides confirming the isSoundActive(0x3D98) gate the base
+	// class's pure-virtual command4()/command5() require - see
+	// RSound::command4/5. Same offset NUMBER as RSound1's, but not
+	// assumed to be the same underlying data.
+	int command4() override;
+	int command5() override;
+
+	int command16();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	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.ph3)
+ *
+ * Dispatch table layout (funcs_10A0C/10A22/10A38/10A4E/10A64 in the
+ * disassembly - same 5-bucket sparse-dispatch shape as RSound1/RSound2,
+ * represented here as a single flat array):
+ *   commands  0-8   (base class, except command4/5 - see RSound::command4/5;
+ *                    index 3 aliases directly to command1's full handler -
+ *                    see below)
+ *   command   16    (this class - a plain gated 4-channel load)
+ *   commands 24-27  (this class)
+ *   commands 32-36  (this class); 37 is confirmed nullsub_1
+ *   commands 64-75  (this class); 76 is confirmed nullsub_1
+ *
+ * command0/1/2/3/6/7/8 all confirmed to match the shared RSound base
+ * exactly (no overrides needed) - including a notable dispatch-table
+ * detail: command index 3 in this driver's dispatch table is a plain
+ * jmp straight into command1's full body (lower channels 1-4,9 AND
+ * upper channels 5-8), NOT the "inner" lower-only logic that the base
+ * class's own command3() implements - so the command list must map
+ * index 3 to &RSound::command1, not &RSound::command3. command0's
+ * disassembly is confirmed to genuinely omit the _isDisabled
+ * save/restore wrap that RSound::command0() has, but with no
+ * observable behavioral difference, so no override is needed there.
+ *
+ * command4()/command5() are also a genuinely different shape from
+ * RSound1/RSound2: both gate on TWO chained isSoundActive() checks
+ * (0x2AA6 then 0x1E30, each an independent early-out per the
+ * pop-return-address mechanic) rather than one, and command4()'s tail
+ * (sub_10832) resets only channels 5-9 (indices 4-8) - NOT channels
+ * 4-9 (indices 3-8) like the shared resetAndGmResetUpperChannels() -
+ * so it can't reuse that helper and calls resetChannelRange()
+ * directly instead.
+ */
+class RSound3 : public RSound {
+private:
+	typedef int (RSound3:: *CommandPtr)();
+	static const CommandPtr _commandList[77];
+
+	int command4() override;
+	int command5() override;
+
+	int command16();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+	int command73();
+	int command74();
+	int command75();
+
+public:
+	RSound3(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound4 (rsound.ph4)
+ *
+ * Dispatch table layout (funcs_109CE/109E4/109FA/10A10/10A26 in the
+ * disassembly - same 5-bucket sparse-dispatch shape as RSound1-3,
+ * represented here as a single flat array):
+ *   commands  0-8   (base class, except command4/5 - see RSound::command4/5)
+ *   command   16    (this class)
+ *   commands 24-27  (this class)
+ *   command   32    (confirmed nullsub_1 - this driver has no command32-39
+ *                    at all, unlike RSound1-3, which all had a populated
+ *                    32+ bucket)
+ *   commands 64-70  (this class); 71 is confirmed nullsub_1
+ *
+ * command0/1/2/3/6/7/8 confirmed to match the shared RSound base exactly
+ * (no overrides needed) - including command3, which (unlike RSound3) is
+ * NOT aliased to command1's dispatch slot; this driver's dispatch table
+ * has a genuine, separate rsound_command3 entry matching the base
+ * class's command3() exactly.
+ *
+ * command4() is a genuinely new shape: unlike every driver confirmed so
+ * far, it has NO isSoundActive() gate at all - it unconditionally resets
+ * channels 6-9 (0-based indices 5-8, confirmed via explicit
+ * Channel._activeCount/_volumeFadeStep-labeled writes in sub_107FC) via
+ * resetChannelRange(), then sends a full sendGmReset(9). A third distinct
+ * channel range for this reset (RSound1/RSound2 used 4-9, RSound3 used
+ * 5-9), reinforcing that this range is always driver-specific.
+ *
+ * command1/command3 confirmed to match the shared RSound base exactly
+ * (no overrides needed) - sub_10838 (called at the top of both) is
+ * confirmed to be exactly "_fadeCheckPeriod = 1", and command5's four
+ * channel-enable targets are confirmed to be channels 5-8, matching
+ * enableUpperChannels() exactly.
+ */
+class RSound4 : public RSound {
+private:
+	typedef int (RSound4:: *CommandPtr)();
+	static const CommandPtr _commandList[72];
+
+	int command4() override;
+	int command5() override;
+
+	int command16();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+
+public:
+	RSound4(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound5 (rsound.ph5)
+ *
+ * Dispatch table layout (funcs_109F0/10A06/10A1C/10A32/10A48 in the
+ * disassembly - same 5-bucket sparse-dispatch shape as RSound1-4):
+ *   commands  0-8   (base class, except command1/3/4/5 - see below)
+ *   command   16    (this class)
+ *   commands 24-27  (this class)
+ *   commands 32-39  (this class); 32 is confirmed nullsub_2
+ *   commands 64-78  (this class); 79 is confirmed nullsub_1
+ *
+ * command0/2/6/7/8 confirmed to match the shared RSound base exactly (no
+ * overrides needed).
+ *
+ * command1/command3 are a genuinely different shape from every prior
+ * driver: command3's "lower" enable group is channels 1-5 AND 9 (SIX
+ * channels), not the base class's 1-4,9 (five channels) - channel 5 has
+ * moved from the "upper" group into the "lower" one. Since command1/2/3
+ * are not virtual in the base class, RSound::command1() would still call
+ * the base's own command3() even with a derived override present, so
+ * command1() needs its own override here too (calling this driver's own
+ * command3()) rather than relying on the inherited one. command4/5's
+ * upper group is correspondingly narrowed to 3 channels (6,7,8) instead
+ * of the usual 4 (5,6,7,8) - both confirmed directly from disassembly.
+ *
+ * command39 is notable: its gate leads to this driver's own command3()
+ * (the 6-channel lower-group enable), NOT command1() like every other
+ * gated multi-load/multi-play command in this driver - confirmed
+ * directly from the disassembly ("call rsound_command3"), not a typo.
+ */
+class RSound5 : public RSound {
+private:
+	typedef int (RSound5:: *CommandPtr)();
+	static const CommandPtr _commandList[80];
+
+	int command1();
+	int command3();
+	int command4() override;
+	int command5() override;
+
+	/**
+	 * Shared tail of command1() and command5() in this driver: enables
+	 * channels 6,7,8 (three channels - this driver's narrowed "upper"
+	 * group, see class comment). Matches rsound_command5's body.
+	 */
+	void enableChannels678();
+
+	int command16();
+
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+
+	int command64();
+	int command65();
+	int command66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+	int command72();
+	int command73();
+	int command74();
+	int command75();
+	int command76();
+	int command77();
+	int command78();
+
+public:
+	RSound5(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
+/**
+ * RSound9 (rsound.ph9)
+ *
+ * Dispatch table layout (funcs_109F5/10A0B/10A37/10A4D in the
+ * disassembly - same sparse-dispatch shape as RSound1-5, represented
+ * here as a single flat array):
+ *   commands  0-8   (base class, except command4/5 - see RSound::command4/5)
+ *   commands 16,24  (SHARED handler - both indices point to the exact
+ *                    same function in the dispatch table)
+ *   commands 25-27  (this class)
+ *   commands 32-39  (this class); 33 is confirmed nullsub_1. IMPORTANT:
+ *                    the disassembly's own symbol names for 34-39 are
+ *                    each off by one from their true dispatch index
+ *                    (e.g. the function IDA calls "rsound_command33" is
+ *                    actually invoked at index 34, "rsound_command34" at
+ *                    35, etc., through "rsound_command38" at 39) -
+ *                    confirmed directly from the funcs_10A37 table
+ *                    layout, not a transcription error. The C++ method
+ *                    names below use the TRUE (table-position) command
+ *                    ID, not the stale IDA symbol suffix.
+ *   commands 64-71  (this class); 65 and 66 SHARE a handler (both point
+ *                    to the same function); 72 is confirmed nullsub_1
+ *
+ * command0/1/2/3/6/7/8 confirmed to match the shared RSound base exactly
+ * (no overrides needed, including command1/command3, unlike RSound5).
+ * command5 also matches the base's enableUpperChannels() exactly
+ * (channels 5-8), so its override just delegates to that helper.
+ */
+class RSound9 : public RSound {
+private:
+	typedef int (RSound9:: *CommandPtr)();
+	static const CommandPtr _commandList[73];
+
+	int command4() override;
+	int command5() override;
+
+	// Shared by dispatch indices 16 AND 24 (identical handler).
+	int command16Or24();
+
+	int command25();
+	int command26();
+	int command27();
+
+	int command32();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+
+	int command64();
+	// Shared by dispatch indices 65 AND 66 (identical handler).
+	int command65Or66();
+	int command67();
+	int command68();
+	int command69();
+	int command70();
+	int command71();
+
+public:
+	RSound9(Audio::Mixer *mixer);
+
+	int command(int commandId, int param) override;
+};
+
 } // namespace Phantom
 } // namespace MADS
 




More information about the Scummvm-git-logs mailing list