[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