[Scummvm-git-logs] scummvm master -> c725b3ac2afcd801b81dc380ecf2b6c440ba152e
dreammaster
noreply at scummvm.org
Mon Jul 27 00:32:55 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:
c725b3ac2a MADS: PHANTOM: Added RSound Roland MT32 base class and RSound1
Commit: c725b3ac2afcd801b81dc380ecf2b6c440ba152e
https://github.com/scummvm/scummvm/commit/c725b3ac2afcd801b81dc380ecf2b6c440ba152e
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-27T10:32:39+10:00
Commit Message:
MADS: PHANTOM: Added RSound Roland MT32 base class and RSound1
Assisted-by: Claude Code:claude-opus-4.8
Changed paths:
A engines/mads/phantom/rsound.cpp
A engines/mads/phantom/rsound.h
A engines/mads/phantom/rsound_phantom.cpp
A engines/mads/phantom/rsound_phantom.h
engines/mads/module.mk
engines/mads/phantom/sound.cpp
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index eccf8ce6084..00c9320f5a9 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -261,6 +261,8 @@ MODULE_OBJS := \
phantom/phantom.o \
phantom/asound.o \
phantom/asound_phantom.o \
+ phantom/rsound.o \
+ phantom/rsound_phantom.o \
phantom/catacombs.o \
phantom/global.o \
phantom/main_menu.o \
diff --git a/engines/mads/phantom/rsound.cpp b/engines/mads/phantom/rsound.cpp
new file mode 100644
index 00000000000..9c3a3c60b20
--- /dev/null
+++ b/engines/mads/phantom/rsound.cpp
@@ -0,0 +1,1091 @@
+/* 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/phantom/rsound.h"
+
+namespace MADS {
+namespace Phantom {
+
+/*-----------------------------------------------------------------------*/
+
+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;
+ // Matches Channel_enable's "mov [bx+20h], ax" - invalidates the
+ // identity pointer so isSoundActive() no longer matches this
+ // channel. The original writes register garbage (AH was never
+ // explicitly cleared); nullptr is the correct semantic
+ // equivalent without inheriting undefined register state.
+ _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;
+ _isDisabled = false;
+ _randomSeed = 1234;
+ _lastMidiStatus = 0;
+ _sysexChecksum = 0;
+ _stateChangedFlag = 0;
+ _pollResult = 0;
+ _sysExOffset = sysExOffset;
+ _fadeCheckCounter = 0;
+ _fadeCheckPeriod = 0;
+
+ _tickCounter = 0;
+ _clockMedTarget = 0;
+ _clockCoarseTarget = 0;
+ _clockUnknown = 0;
+ _clockCoarse = 112;
+ _clockMed = 28;
+ _clockFine = 7;
+ _clockEnabled1 = 0;
+ _clockEnabled2 = 0;
+ _randomAmbianceTriggerFlag = 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() {
+ // Matches the "cmp word_12BB8, 0xFFFF" latch right after rsound_update.
+ 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::playSound(int offset) {
+ // Channels 6-8 (0-based 5-7), symmetric free/fallback scan.
+ return playSoundData(loadData(offset), 5, 7, 7);
+}
+
+Channel *RSound::playSoundAny(int offset) {
+ // Channels 1-5 (0-based 0-4) for the free scan, but the pending-stop
+ // fallback only reaches down to channel 4 (0-based 3) - channel 5
+ // can never be pre-empted here, confirmed from the disassembly.
+ return playSoundData(loadData(offset), 0, 4, 3);
+}
+
+Channel *RSound::playSoundChannels1To8(int offset) {
+ // Channels 1-8 (0-based 0-7), symmetric free/fallback scan.
+ return playSoundData(loadData(offset), 0, 7, 7);
+}
+
+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;
+}
+
+/*-----------------------------------------------------------------------*/
+// Low-level MIDI transmission. sendMidiByte() is the single point that
+// needs to change once the real MT-32/MIDI output interface is wired up;
+// everything else funnels through it.
+
+void RSound::sendMidiByte(byte value) {
+ warning("RSound: MIDI byte %02X", value);
+}
+
+void RSound::sendStatus(int midiChannel, byte statusNibble) {
+ byte status = statusNibble | midiChannel;
+ if (_lastMidiStatus != status) {
+ _lastMidiStatus = status;
+ sendMidiByte(status);
+ }
+}
+
+void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
+ sendStatus(midiChannel, 0x90);
+ sendMidiByte(note);
+ sendMidiByte(velocity);
+}
+
+void RSound::sendProgramChange(int midiChannel, int program) {
+ sendStatus(midiChannel, 0xC0);
+ sendMidiByte(program);
+}
+
+void RSound::sendVolume(int midiChannel, int volume) {
+ sendStatus(midiChannel, 0xB0);
+ sendMidiByte(7);
+ sendMidiByte(volume);
+}
+
+void RSound::sendVolumeCC(int midiChannel, int volume) {
+ // Matches sub_10423: unlike sendVolume()/sendStatus(), this sends the
+ // status byte UNCONDITIONALLY (no _lastMidiStatus dedup check) -
+ // used by command7 when restoring all 9 channels' volumes in a row.
+ byte status = 0xB0 | midiChannel;
+ _lastMidiStatus = status;
+ sendMidiByte(status);
+ sendMidiByte(7);
+ sendMidiByte(volume);
+}
+
+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) {
+ sendStatus(midiChannel, 0xB0);
+ sendMidiByte(0x0A); // CC#10: Pan
+ sendMidiByte(value);
+}
+
+void RSound::muteChannel(int midiChannel) {
+ // Matches muteChannel: unconditional status send, like sendVolumeCC().
+ byte status = 0xB0 | midiChannel;
+ _lastMidiStatus = status;
+ sendMidiByte(status);
+ sendMidiByte(7);
+ sendMidiByte(0);
+}
+
+void RSound::sendGmReset(int count) {
+ // Matches sub_1068A: 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; // matches "mov cs:byte_107BE, 0" at the top of every iteration
+
+ byte status = 0xB0 | midiChannel;
+ _lastMidiStatus = status;
+ sendMidiByte(status);
+ sendMidiByte(0x7B); // All Notes Off
+ sendMidiByte(0);
+ sendMidiByte(0x79); // Reset All Controllers
+ sendMidiByte(0);
+ sendMidiByte(7); // Channel Volume
+ sendMidiByte(100);
+ sendMidiByte(0x0A); // Pan
+ sendMidiByte(0x40);
+ }
+}
+
+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() {
+ // TENTATIVE - see header comment for sendPatchInitSequence(). Matches
+ // sub_102BE exactly: 4 outer iterations, each sending one SysEx
+ // message built from the fixed header at loadData(0xA3) plus a
+ // computed payload; byte_12BD2 (here: base) persists and accumulates
+ // across outer iterations.
+ byte base = 0;
+ for (int outer = 0; outer < 4; ++outer) {
+ byte *header = loadData(0xA3);
+ for (int i = 0; header[i] != 0xFF; ++i)
+ sendMidiByte(header[i]);
+
+ _sysexChecksum = 0;
+ byte b1 = 5;
+ _sysexChecksum += b1; sendMidiByte(b1);
+ byte b2 = (byte)(outer << 1);
+ _sysexChecksum += b2; sendMidiByte(b2);
+ byte b3 = 0;
+ _sysexChecksum += b3; sendMidiByte(b3);
+
+ 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) {
+ // CONFIRMED: unk_12BC9 (RSound1's dseg offset 0xA9) holds the fixed
+ // 3-byte Roland address 10 00 01h - the real MT-32 System Area Reverb
+ // parameter address, a hardware protocol constant rather than
+ // driver-specific sound data - immediately followed by the 3 mutable
+ // payload bytes (byte_12BCC/CD/CE) that this function fills in
+ // before sending. Hardcoded (not read via loadData()) since there's
+ // no reason to expect this address to live at the same offset in
+ // every driver's own resource file.
+ byte buffer[7] = { 0x10, 0x00, 0x01, (byte)(mode & 3), (byte)(time & 7), (byte)(level & 7), 0xFF };
+ sendSysExData(buffer);
+}
+
+/*-----------------------------------------------------------------------*/
+
+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) {
+ // unk_172E5 (RSound1's dseg offset 0x47C5) is 3 zero bytes - a
+ // generic silence/no-op stream, not driver-specific sound data.
+ // Hardcoded (not read via loadData()) for the same reason as
+ // sendReverbSysEx()'s fixed address - no reason to expect the
+ // same offset holds the same bytes in every driver's own
+ // resource file.
+ static byte silenceStream[3] = { 0, 0, 0 };
+ channel->_pSrc = silenceStream;
+ channel->_pendingStop = 0;
+ return;
+ }
+
+ channel->_volume -= 1;
+ sendVolume(midiChannel, channel->_volume);
+}
+
+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; // matches "mov [bx],ax; mov [bx+2],ax" zeroing the first 4 bytes
+ }
+}
+
+void RSound::resetAllChannels() {
+ bool wasDisabled = _isDisabled;
+ _isDisabled = true;
+ resetChannelRange(0, RSOUND_CHANNEL_COUNT - 1);
+ for (int i = 0; i <= RSOUND_CHANNEL_COUNT; ++i)
+ for (int j = 0; j < 4; ++j)
+ _heldNotes[i][j] = 0xFF;
+ _isDisabled = wasDisabled;
+}
+
+void RSound::resetChannels1to5() {
+ _isDisabled = true;
+ resetChannelRange(0, 4);
+ for (int i = 0; i <= RSOUND_CHANNEL_COUNT; ++i)
+ for (int j = 0; j < 4; ++j)
+ _heldNotes[i][j] = 0xFF;
+ _isDisabled = false;
+}
+
+void RSound::resetChannels6to9() {
+ _isDisabled = true;
+ resetChannelRange(5, 8);
+ _isDisabled = false;
+}
+
+/*-----------------------------------------------------------------------*/
+
+int RSound::command0() {
+ bool wasDisabled = _isDisabled;
+ _isDisabled = true;
+
+ resetAllChannels();
+ sendGmReset(RSOUND_CHANNEL_COUNT);
+ sendSysEx(_sysExOffset);
+
+ _isDisabled = wasDisabled;
+ return 0;
+}
+
+int RSound::command1() {
+ command3();
+ command5(); // shares command1/command5's tail (enables channels 5-8) - see command5()
+ return 0;
+}
+
+int RSound::command2() {
+ resetChannels1to5();
+ sendGmReset(4);
+ return 0;
+}
+
+int RSound::command3() {
+ // Matches rsound_command3: enables channels 1,2,3,4 AND 9 (not 5-8) -
+ // a genuine, confirmed asymmetry (channel 9 falls through into the
+ // tail-shared Channel_enable call with the others).
+ _fadeCheckPeriod = 1; // armFadeCheck
+ _channels[0].enable(0xFF);
+ _channels[1].enable(0xFF);
+ _channels[2].enable(0xFF);
+ _channels[3].enable(0xFF);
+ _channels[8].enable(0xFF);
+ return 0;
+}
+
+int RSound::command4() {
+ // Matches rsound_command4's chunk at loc_106DB: reset channels 6-9,
+ // then a full sendGmReset(9) (all 9 channels) - the "reset 6-9" and
+ // "GM-reset all 9" are both really executed, matching the original
+ // exactly despite the apparent redundancy.
+ resetChannels6to9();
+ sendGmReset(RSOUND_CHANNEL_COUNT);
+ return 0;
+}
+
+int RSound::command5() {
+ // Matches rsound_command5/loc_108A9: enables channels 5,6,7,8.
+ _fadeCheckPeriod = 1;
+ _channels[4].enable(0xFF);
+ _channels[5].enable(0xFF);
+ _channels[6].enable(0xFF);
+ _channels[7].enable(0xFF);
+ return 0;
+}
+
+int RSound::command6() {
+ _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::update() {
+ getRandomNumber();
+ if (_isDisabled)
+ return;
+
+ ++_frameCounter;
+ ++_tickCounter; // matches "inc word_12BE5" alongside _frameCounter
+ checkRandomAmbianceTrigger();
+ pollAllChannels();
+ checkFadingChannels();
+}
+
+void RSound::pollAllChannels() {
+ for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
+ pollActiveChannel(&_channels[i]);
+}
+
+/*-----------------------------------------------------------------------*/
+// Per-channel opcode interpreter. Bytes with the high bit clear are
+// two-byte [note][duration] events (or, if > 0xBD, an opcode 0xBE-0xFF).
+// Matches the sibling ASound driver's pollActiveChannel() structure: a
+// goto-based re-entrant dispatch, re-looping after every opcode that
+// doesn't consume a duration tick.
+
+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:
+ {
+ // NOTE: ch->_pSrc is left untouched by each case below until its
+ // own final "ch->_pSrc = ..." / "ch->_pSrc += N" assignment, so
+ // any earlier use of ch->_pSrc within the same case (e.g. to
+ // compute a resume/branch target) still refers to this opcode's
+ // start position - only the local pSrc advances as operand bytes
+ // are read via readScriptByte()/readScriptWord().
+ byte *pSrc = ch->_pSrc;
+ byte b = *pSrc;
+
+ if (!(b & 0x80)) {
+ // ---- Simple note event: [note][duration] (matches loc_11FCA;
+ // distinct from - and simpler than - the explicit chord opcode
+ // 0xED below, which is count-prefixed and can hold up to 4
+ // simultaneous notes) ----
+ 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; // matches the disassembly's fallthrough for 0x80-0xBD
+
+ switch (b) {
+ case 0xBE: {
+ // TODO: purpose unconfirmed - no reader found for
+ // _clockUnknown anywhere in the disassembly seen so far.
+ _clockUnknown = readScriptByte(pSrc);
+ ch->_pSrc += 2;
+ goto dispatch;
+ }
+ case 0xBF: {
+ // TODO: purpose unconfirmed - no reader found for
+ // _clockCoarse/_clockEnabled1/_clockEnabled2 anywhere in the
+ // disassembly seen so far. Only takes effect (via the
+ // _tickCounter==0 gate) if executed before the very first
+ // update() tick.
+ _clockCoarseTarget = readScriptWord(pSrc);
+ if (_tickCounter == 0)
+ _clockCoarse = _clockCoarseTarget;
+ _clockEnabled1 = 1;
+ _clockEnabled2 = 1;
+ ch->_pSrc += 3;
+ goto dispatch;
+ }
+ case 0xC0: {
+ // TODO: purpose unconfirmed - no reader found for _clockMed
+ // anywhere in the disassembly seen so far. Same one-time-only
+ // gate as 0xBF above.
+ _clockMedTarget = readScriptByte(pSrc);
+ if (_tickCounter == 0)
+ _clockMed = _clockMedTarget;
+ ch->_pSrc += 2;
+ goto dispatch;
+ }
+ case 0xC1: {
+ // TODO: purpose unconfirmed - no reader found for _clockFine
+ // anywhere in the disassembly seen so far. Unlike 0xBE/0xBF/
+ // 0xC0, this sets the value directly and unconditionally
+ // (no _tickCounter gate).
+ _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: {
+ int v = readScriptByte(pSrc);
+ noOpHandler(v);
+ ch->_pSrc += 2;
+ goto dispatch;
+ }
+ case 0xC4: {
+ // TODO: NOT PORTABLE AS-IS - the disassembly calls the word
+ // operand as a raw code-address function pointer
+ // ("mov bx,ax; call bx"). There's no equivalent in a C++
+ // port without knowing what specific handful of sub_
+ // routines this is meant to invoke. error() (not warning())
+ // so this is impossible to miss if real game data ever
+ // actually triggers it, rather than silently no-opping.
+ 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 ----
+ // (field_22/ _branchTarget), matching the call/return pair below.
+ 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); // 0xC5-C8, 0xCD-D0: var vs var; 0xC9-CC, 0xD1-D4: var vs immediate
+ 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; // 0:'>' 1:'<' 2:'!=' 3:'=='
+ 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: {
+ // SUSPECTED BUG in the original (MOD, "immediate" form): the
+ // disassembly reads a second operand byte but never uses it,
+ // instead computing scriptVar[idx1] % scriptVar[idx1] (always
+ // 0 when nonzero) - unlike every other immediate-mode
+ // arithmetic opcode (0xD6/D8/DA/E0/E2/E4), which all use the
+ // operand directly. Preserved exactly rather than "fixed" to
+ // use the operand as a real divisor, since that would be
+ // guessing at intended behavior.
+ int idx1 = readScriptByte(pSrc);
+ readScriptByte(pSrc); // operand read but unused, matching the disassembly
+ 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: {
+ // Same suspected bug shape as 0xDC (DIV "immediate" form:
+ // self-divide, operand read but unused - always yields 1
+ // when nonzero) - preserved exactly.
+ int idx1 = readScriptByte(pSrc);
+ readScriptByte(pSrc); // operand read but unused, matching the disassembly
+ 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: {
+ // Pokes scriptVar[idx1] into the sound-data stream itself at
+ // offset (currentPos + 1 + idx2) - a self-modifying-script op.
+ 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: {
+ // TODO: low confidence - a self-modifying op that reads
+ // table1[scriptVar[idx1]], then writes it into table2 at an
+ // offset determined by table2's own leading "size" byte.
+ // Translated as literally as possible; purpose unconfirmed.
+ 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: {
+ // Picks a uniform random value in [rangeLow, rangeHigh], reads
+ // a "tableByte" positioned right after the two range operands,
+ // and writes the random value at offset (pSrc+2+tableByte)
+ // relative to pSrc's position after reading both operands
+ // (an extra "inc word_174A0" in the disassembly, beyond the
+ // two operand reads, is what puts tableByte one byte further
+ // out than the 0xEC case below).
+ 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: {
+ // Same general shape as 0xEA: random-pick from a table1 of
+ // length idx1, then poke it into table2 at an offset given
+ // by table2's own leading "size" byte.
+ 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; // matches "jmp loc_11FAB" tail used by this cluster
+ }
+ 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(midiChannel, ch->_volume);
+ 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] ----
+ // Matches loc_11102 - distinct from (and richer than) the
+ // simple single-note "high bit clear" format at the top of
+ // dispatch: this one is count-prefixed and can hold up to
+ // 4 simultaneous notes (matching _heldNotes' 4 slots).
+ 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:
+ // Unknown/unhandled opcode - matches the disassembly falling
+ // through to re-dispatch on an unrecognised value.
+ 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(midiChannel, ch->_volume);
+ }
+ }
+
+ // ---- 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 Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/rsound.h b/engines/mads/phantom/rsound.h
new file mode 100644
index 00000000000..7241791c36a
--- /dev/null
+++ b/engines/mads/phantom/rsound.h
@@ -0,0 +1,439 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_PHANTOM_RSOUND_H
+#define MADS_PHANTOM_RSOUND_H
+
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace Phantom {
+
+class RSound;
+
+#define RSOUND_CHANNEL_COUNT 9
+
+/**
+ * Represents the data for a channel on the Return of the Phantom MT-32 /
+ * MPU-401 driver. Ported from the Channel struct identified in rsound.ph1's
+ * disassembly (sizeof 0x27, five bytes larger than the equivalent Rex
+ * Nebular RSound Channel struct).
+ *
+ * The word-sized fields at 0x14-0x20 (_loopStartPtr through _soundData) are
+ * at the IDENTICAL offsets/order/roles as Rex Nebular's Channel struct -
+ * that part of the layout is unchanged between games. The byte fields at
+ * 0x00-0x13 are the same overall SET as Rex Nebular (confirmed by matching
+ * each field against the exact MIDI status byte/controller number sent by
+ * its dedicated helper, e.g. sendProgramChange sends status 0xC0 using
+ * _program), just reordered, with _pendingStop moved down to the very last
+ * byte (0x26) to make room for a new field (_keyOnDelayOverride, 0x08) with
+ * no Rex Nebular equivalent. Two more new fields exist past the Rex Nebular
+ * struct's end: _branchTarget (0x22) and _transpose (0x25).
+ */
+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; new to Phantom)
+ 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 (new to Phantom)
+ int _transpose = 0; // added to note bytes before comparison/storage (new to Phantom)
+ int _pendingStop = 0; // non-zero while the channel is fading out to silence
+
+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). More aggressive than Rex Nebular's equivalent,
+ * which leaves volume/program/velocity/key-on state untouched.
+ */
+ 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 Return of the Phantom MT-32 / MPU-401 sound player
+ * resource files (rsound.ph1-.ph9). Mirrors the structure of the Rex
+ * Nebular RSound family (see nebular/rsound.h), adapted for this game's
+ * substantially richer Channel_pollActive script VM (~65 opcodes here vs.
+ * ~15 in Rex Nebular): general arithmetic on a script-variable table,
+ * conditional branches, and a call/return pair, in addition to the shared
+ * note/fade/loop mechanics.
+ *
+ * NOTE: The actual MIDI transmission (sendMidiByte()) currently just logs
+ * via warning() - it isn't hooked up to a real ScummVM MIDI/MT-32 output
+ * yet, matching the Rex Nebular RSound family. Every other MIDI-sending
+ * helper funnels through sendMidiByte().
+ *
+ * NOTE: DOS-specific driver ceremony from the original (timer IRQ hooking,
+ * MPU-401 hardware detection/reset, PIT-based SysEx delay calibration, the
+ * system-clock save/restore around it) has no ScummVM equivalent and is not
+ * ported - matching how Rex Nebular's RSound skips the same kind of
+ * hardware-detection dance.
+ */
+class RSound : public SoundDriver {
+ friend class Channel;
+private:
+ uint16 _randomSeed;
+ byte _lastMidiStatus; // running-status cache, avoids resending an unchanged status byte
+ byte _sysexChecksum;
+ int _stateChangedFlag; // word_12BB8 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). TODO/unconfirmed: the disassembly
+ * shows TWO seemingly-parallel tables using the identical
+ * "channel*4+slot" indexing and 0xFF-empty-slot convention -
+ * byte_1760C (read/written directly by the chord-note-storing logic
+ * in Channel_pollActive) and an unnamed array at dseg offset 0x4AEC
+ * (used by resetAllChannels's initialization and by the
+ * flush-held-notes helper). IDA never resolves 0x4AEC to a named
+ * symbol anywhere, so it's not confirmed whether these are the same
+ * underlying memory (most likely, and what's implemented here) or
+ * two genuinely separate tables - worth double-checking.
+ */
+ byte _heldNotes[RSOUND_CHANNEL_COUNT + 1][4];
+
+ /**
+ * Data-segment offset of this driver's own "command0_array" (the
+ * table sent by command0() via sendSysEx). Each driver has its own
+ * copy of this table at its own offset within its own resource file -
+ * matches the Rex Nebular RSound family's identical need. Not yet
+ * confirmed for rsound.ph1 - the disassembly shows only the symbolic
+ * "command0_array" label, not its numeric offset.
+ */
+ int _sysExOffset;
+
+ /**
+ * General-purpose script variable table (byte_17480 in the
+ * disassembly). Confirmed 32 bytes via the gap to the next declared
+ * global (_scriptReadPtr) - also matches the sibling ASound driver's
+ * analogous _scriptVars[32].
+ */
+ byte _scriptVariables[32];
+
+ /**
+ * Half-rate fade-check timer (byte_107BD/byte_107BE in the
+ * disassembly) - same counter/period/reload shape as the Rex Nebular
+ * RSound4/RSound6 callback mechanism, but drives checkFadingChannels()
+ * directly rather than an arbitrary function pointer.
+ */
+ int _fadeCheckCounter;
+ int _fadeCheckPeriod;
+
+ /**
+ * Cluster of globals written by opcodes 0xBE-0xC1 but with no
+ * confirmed reader anywhere in the disassembly seen so far (all
+ * TENTATIVE names - see individual comments). _clockFine/_clockMed/
+ * _clockCoarse default to 7/28/112, a clean 4x progression,
+ * suggesting a coarse/medium/fine clock-division hierarchy (a common
+ * shape for a MIDI-clock-like timing subdivision) rather than three
+ * unrelated parameters. _tickCounter gates a one-time-only override:
+ * opcodes 0xBF/0xC0 only take effect if executed before the first
+ * update() tick ever runs, since _tickCounter increments
+ * unconditionally every tick thereafter and the gate checks "== 0".
+ */
+ int _tickCounter; // word_12BE5
+ int _clockMedTarget; // word_12BE7 - pending value for _clockMed, set by opcode 0xC0
+ int _clockCoarseTarget; // word_12BE9 - pending value for _clockCoarse, set by opcode 0xBF
+ int _clockUnknown; // word_12BEB - default 0; doesn't fit the 4x pattern, standalone (opcode 0xBE)
+ int _clockCoarse; // word_12BEE - default 112 (=28*4)
+ int _clockMed; // word_12BF0 - default 28 (=7*4)
+ int _clockFine; // word_12BF2 - default 7
+ int _clockEnabled1; // word_12BD3 - set to 1 by opcode 0xBF
+ int _clockEnabled2; // word_12BE3 - set to 1 by opcode 0xBF
+
+ void update();
+ void pollAllChannels();
+
+ /**
+ * Per-channel opcode interpreter (Channel_pollActive in the
+ * disassembly). Implements a bytecode VM: bytes with the high bit
+ * clear are two-byte [note][duration] events; bytes > 0xBD are
+ * commands (0xBE-0xFF), dispatched via the switch in the .cpp.
+ * Re-enters its own dispatch point (via goto) after every opcode that
+ * doesn't consume a duration tick, matching the sibling ASound
+ * driver's pollActiveChannel() structure.
+ */
+ void pollActiveChannel(Channel *channel);
+
+ /**
+ * Reads one byte from the channel's current script position and
+ * sign-extends it, advancing pSrc by 1.
+ */
+ int readScriptByte(byte *&pSrc);
+
+ /**
+ * Reads two bytes (little-endian) from the channel's current script
+ * position as a raw offset into _soundData, advancing pSrc by 2.
+ * Distinct from readScriptByte() - used only for absolute
+ * jump/restart targets embedded in the script.
+ */
+ uint16 readScriptWord(byte *&pSrc);
+
+ void resetChannelRange(int first, int last);
+ void resetAllChannels();
+ void resetChannels1to5();
+ void resetChannels6to9();
+
+ void checkFadingChannels();
+ void Channel_checkFade(Channel *channel, int midiChannel);
+ void Channel_flushHeldNotes(Channel *channel);
+
+protected:
+ int _commandParam;
+
+ byte *loadData(int offset) {
+ return &_soundData[offset];
+ }
+
+ /**
+ * byte_1303E in the disassembly. Cleared to 0 by RSound1's command37
+ * (a "cancel any pending random-ambiance trigger" side effect of
+ * playing that specific sound). CONFIRMED: the only code that ever
+ * sets it to 0xFF (arming checkRandomAmbianceTrigger()) is itself
+ * unreachable/dead code - so in the real game this mechanism never
+ * actually fires. Implemented faithfully anyway (matching sub_1222E's
+ * shape exactly) in case that changes for a different driver.
+ * Protected (not private) so driver subclasses with their own
+ * commands touching it (like RSound1's command37) can reach it
+ * directly.
+ */
+ int _randomAmbianceTriggerFlag;
+
+ /**
+ * Hook called once per update() frame after the disabled check.
+ * Only drivers with a random-ambiance/music picker (e.g. RSound1's
+ * command16) override this; every other driver leaves it a no-op.
+ * Matches sub_1222E's confirmed shape: if _randomAmbianceTriggerFlag
+ * == 0xFF, clear it and fire the driver-specific picker.
+ */
+ virtual void checkRandomAmbianceTrigger() {
+ }
+
+ void resultCheck();
+
+ /**
+ * Plays the specified sound, using any free channel from 6 to 8.
+ * Matches the disassembly's playSound exactly (rsound_channel6-8).
+ */
+ Channel *playSound(int offset);
+
+ /**
+ * Plays the specified sound, using any free channel from 1 to 5.
+ * NOTE: unlike Rex Nebular's playSoundAny() (which reaches all 9
+ * channels), THIS driver's playSoundAny only scans channels 1-5 for a
+ * free slot - confirmed directly from the disassembly. Also confirmed:
+ * the pending-stop fallback scan only reaches down to channel 4, NOT
+ * channel 5 - a genuine asymmetry preserved exactly (channel 5 can
+ * never be pre-empted by this call, only picked while free).
+ */
+ Channel *playSoundAny(int offset);
+
+ /**
+ * Plays the specified sound, using any free channel from 1 to 8
+ * (everything except channel 9). Matches sub_104FF in the
+ * disassembly - a third, distinct scan range from playSound() and
+ * playSoundAny() above.
+ */
+ Channel *playSoundChannels1To8(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. The two end bounds are usually
+ * the same, but playSoundAny() is a confirmed exception (see above).
+ */
+ Channel *playSoundData(byte *pData, int startingChannel, int freeScanEnd, int fallbackScanEnd);
+
+ /**
+ * Checks whether the given block of data is already loaded into a channel.
+ */
+ bool isSoundActive(byte *pData);
+
+ int getRandomNumber();
+
+ // ---- Low-level MIDI send helpers -------------------------------
+ // All funnel through sendMidiByte(), the single hook point for
+ // wiring up real MT-32/MIDI output.
+ void sendMidiByte(byte value);
+ void sendStatus(int midiChannel, byte statusNibble);
+ void sendNoteOn(int midiChannel, int note, int velocity);
+ void sendProgramChange(int midiChannel, int program);
+ void sendVolume(int midiChannel, int volume);
+ void sendVolumeCC(int midiChannel, int volume);
+ void resetPitchBend(int midiChannel);
+ void sendPitchBend(int midiChannel, int value);
+ 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. Matches sub_1068A.
+ */
+ 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. The shared
+ * core of sendSysEx()/sendReverbSysEx() below - split out so
+ * hardcoded protocol buffers (not driver-specific loaded sound data)
+ * can be sent without going through loadData().
+ */
+ void sendSysExData(const byte *pData);
+
+ /**
+ * sendSysExData() for a block already in this driver's own loaded
+ * sound data. Matches sendSysEx exactly (same algorithm as the
+ * confirmed Rex Nebular RSound::sendSysEx()).
+ */
+ void sendSysEx(int offset);
+
+ /**
+ * TENTATIVE: matches sub_102BE - a nested loop (4 outer x 32 inner
+ * iterations) building and sending a SysEx message each inner pass.
+ * The overall shape (loop counters, accumulating base value, fixed
+ * bytes 0x18/0x32/0x0C) is clear from the disassembly, but the exact
+ * purpose (a bulk patch/rhythm-setup initialization sequence is the
+ * working hypothesis) is not confirmed.
+ */
+ void sendPatchInitSequence();
+
+ /**
+ * CONFIRMED: matches sub_108C7 - 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 real Roland MT-32 System Area Reverb SysEx address (10 00 01h -
+ * originally found at RSound1's dseg offset 0xA9, but hardcoded here
+ * rather than read via loadData(), since it's a fixed hardware
+ * protocol address, not driver-specific sound data).
+ */
+ void sendReverbSysEx(int mode, int time, int level);
+
+ /**
+ * Matches sub_108F9 - a confirmed no-op (reads one operand, does
+ * nothing with it).
+ */
+ void noOpHandler(int param) {
+ }
+
+ virtual int command0();
+ int command1();
+ int command2();
+ int command3();
+
+ /**
+ * NOTE: virtual, unlike command1-3/6-8. Confirmed (from RSound1) that
+ * this driver's own command4/command5 are gated by isSoundActive() on
+ * a driver-specific data offset (0x3D98 for RSound1) before doing
+ * anything else - a per-driver detail that doesn't belong hardcoded
+ * in the shared base. This base implementation is an UNGATED
+ * fallback for drivers whose own command4/5 haven't been confirmed
+ * yet; override once their disassembly is available.
+ */
+ virtual int command4();
+ virtual int command5();
+ int command6();
+ int command7();
+ int command8();
+
+ int nullCommand() {
+ return 0;
+ }
+
+public:
+ Channel _channels[RSOUND_CHANNEL_COUNT];
+ int _frameCounter;
+ 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 Phantom
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/phantom/rsound_phantom.cpp b/engines/mads/phantom/rsound_phantom.cpp
new file mode 100644
index 00000000000..563f4052b76
--- /dev/null
+++ b/engines/mads/phantom/rsound_phantom.cpp
@@ -0,0 +1,403 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mads/phantom/rsound_phantom.h"
+
+namespace MADS {
+namespace Phantom {
+
+RSound1::RSound1(Audio::Mixer *mixer) : RSound(mixer, "rsound.ph1", 0x2D20, 0x4B30, 0xDC) {
+}
+
+int RSound1::command(int commandId, int param) {
+ _commandParam = param;
+ _frameCounter = 0;
+
+ switch (commandId) {
+ case 0: return command0();
+ case 1: return command1();
+ case 2: return command2();
+ case 3: return command3();
+ case 4: return command4();
+ case 5: return command5();
+ case 6: return command6();
+ case 7: return command7();
+ case 8: return command8();
+ case 16: return command16();
+ case 24: return command24();
+ case 25: return command25();
+ case 26: return command26();
+ case 27: return command27();
+ case 32: return command32();
+ case 33: return command33();
+ case 34: return command34();
+ case 35: return command35();
+ case 36: return command36();
+ case 38: return command38();
+ case 39: return command39();
+ case 64: return command64();
+ case 65: return command65();
+ case 66: return command66();
+ case 67: return command67();
+ case 68: return command68();
+ case 69: return command69();
+ case 70: return command70();
+ case 71: return command71();
+ case 72: return command72();
+ case 73: return command73();
+ case 74: return command74();
+ case 75: return command75();
+ case 76: return command76();
+ default:
+ // TODO: command 37 not yet implemented - disassembly not yet
+ // provided. There is no command 17 (see the class comment).
+ return 0;
+ }
+}
+
+void RSound1::checkRandomAmbianceTrigger() {
+ // Matches sub_1222E exactly. CONFIRMED: the only code that ever arms
+ // this (setting _randomAmbianceTriggerFlag to 0xFF) is itself
+ // unreachable/dead code, so in practice this check always fails and
+ // command16() never fires via this path - only command37's clear
+ // (setting it to 0) is live. Kept faithful to the original regardless.
+ if (_randomAmbianceTriggerFlag != 0xFF)
+ return;
+ _randomAmbianceTriggerFlag = 0;
+ command16();
+}
+
+int RSound1::command16() {
+ // Matches rsound_command16. CONFIRMED: "ds:4982h" is
+ // rsound_channel1._activeCount, not a loadData()-relative data byte -
+ // i.e. this gate is "only bother checking whether one of the five
+ // known pieces is already playing if channel 1 is actually busy;
+ // if it's idle, nothing could conflict, so skip straight to picking."
+ if (_channels[0]._activeCount) {
+ if (isSoundActive(loadData(0x2172))) return 0;
+ if (isSoundActive(loadData(0x2448))) return 0;
+ if (isSoundActive(loadData(0x3826))) return 0;
+ if (isSoundActive(loadData(0x2780))) return 0;
+ if (isSoundActive(loadData(0x2E26))) return 0;
+ }
+
+ int idx;
+ do {
+ idx = getRandomNumber() & 7;
+ } while (idx > 4 || idx == _lastRandomAmbianceIndex);
+ _lastRandomAmbianceIndex = idx;
+
+ typedef int (RSound1:: *AmbiancePtr)();
+ static const AmbiancePtr targets[5] = {
+ &RSound1::sound1, &RSound1::sound2, &RSound1::sound3,
+ &RSound1::sound4, &RSound1::sound5
+ };
+ return (this->*targets[idx])();
+}
+
+int RSound1::command4() {
+ // Confirmed: rsound_command4 is gated by isSoundActive(0x3D98) -
+ // which is also command39's own target sound - before falling
+ // through to the base class's reset-channels-6-9 + full GM-reset.
+ if (isSoundActive(loadData(0x3D98)))
+ return 0;
+ return RSound::command4();
+}
+
+int RSound1::command5() {
+ // Confirmed: same isSoundActive(0x3D98) gate as command4() above.
+ if (isSoundActive(loadData(0x3D98)))
+ return 0;
+ return RSound::command5();
+}
+
+int RSound1::command24() {
+ playSound(0x4596);
+ playSound(0x45AA);
+ return 0;
+}
+
+int RSound1::command25() {
+ playSound(0x45BC);
+ playSound(0x45D0);
+ return 0;
+}
+
+int RSound1::command26() {
+ playSound(0x45E2);
+ return 0;
+}
+
+int RSound1::command27() {
+ playSound(0x45EC);
+ return 0;
+}
+
+int RSound1::command32() {
+ byte *pData = loadData(0x2044);
+ if (!isSoundActive(pData)) {
+ command1();
+ playSoundAny(0x2044);
+ playSoundAny(0x206B);
+ playSoundAny(0x208B);
+ playSoundAny(0x20AB);
+ playSoundAny(0x20C9);
+ playSoundAny(0x20E3);
+ }
+ return 0;
+}
+
+int RSound1::command33() {
+ byte *pData = loadData(0x530);
+ if (!isSoundActive(pData)) {
+ command1();
+ playSoundAny(0x530);
+ playSoundAny(0x766);
+ playSoundAny(0x929);
+ playSoundAny(0xB37);
+ playSoundAny(0xCBA);
+ _channels[5].load(loadData(0xE20));
+ }
+ return 0;
+}
+
+int RSound1::command34() {
+ byte *pData = loadData(0x1014);
+ if (!isSoundActive(pData)) {
+ command1();
+ playSoundAny(0x1014);
+ playSoundAny(0x126B);
+ playSoundAny(0x137C);
+ playSoundAny(0x149B);
+ playSoundAny(0x158C);
+ playSoundAny(0x164E);
+ }
+ return 0;
+}
+
+int RSound1::command35() {
+ // isSoundActive(0x1BD4) is checked twice in a row in the disassembly
+ // (a genuine, harmless duplicate - both checks are on the same data,
+ // so the second is always redundant with the first) - preserved
+ // exactly rather than collapsed to a single check.
+ byte *pData = loadData(0x1BD4);
+ if (!isSoundActive(pData) && !isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x1D46));
+ _channels[2].load(loadData(0x1DDA));
+ _channels[3].load(loadData(0x1E91));
+ _channels[4].load(loadData(0x1F1B));
+ }
+ return 0;
+}
+
+int RSound1::command36() {
+ byte *pData = loadData(0x1734);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x1882));
+ _channels[2].load(loadData(0x1949));
+ _channels[3].load(loadData(0x1A08));
+ _channels[4].load(loadData(0x1A9E));
+ }
+ return 0;
+}
+
+int RSound1::command38() {
+ // Also independently reachable via command16's random picker.
+ byte *pData = loadData(0x2172);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x2268));
+ _channels[2].load(loadData(0x22E1));
+ _channels[3].load(loadData(0x239A));
+ }
+ return 0;
+}
+
+int RSound1::command39() {
+ byte *pData = loadData(0x3D98);
+ if (!isSoundActive(pData)) {
+ command1();
+ playSoundChannels1To8(0x3D98);
+ playSoundChannels1To8(0x3F39);
+ playSoundChannels1To8(0x404D);
+ playSoundChannels1To8(0x413F);
+ playSoundChannels1To8(0x4247);
+ playSoundChannels1To8(0x43FD);
+ }
+ return 0;
+}
+
+int RSound1::sound1() {
+ // Not a real disassembly function - index 0 of funcs_122AD dispatches
+ // straight to command38 (see the class comment).
+ return command38();
+}
+
+int RSound1::sound2() {
+ byte *pData = loadData(0x2448);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x25B8));
+ _channels[2].load(loadData(0x26C9));
+ _channels[3].load(loadData(0x2772));
+ }
+ return 0;
+}
+
+int RSound1::sound3() {
+ byte *pData = loadData(0x3826);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x3AAA));
+ _channels[2].load(loadData(0x3CE6));
+ _channels[3].load(loadData(0x3CD7));
+ }
+ return 0;
+}
+
+int RSound1::sound4() {
+ byte *pData = loadData(0x2780);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x2972));
+ _channels[2].load(loadData(0x2AF5));
+ _channels[3].load(loadData(0x2CBA));
+ }
+ return 0;
+}
+
+int RSound1::sound5() {
+ byte *pData = loadData(0x35D8);
+ if (!isSoundActive(pData)) {
+ command1();
+ _channels[0].load(pData);
+ _channels[1].load(loadData(0x36A8));
+ _channels[2].load(loadData(0x3757));
+ _channels[3].load(loadData(0x3814));
+ }
+ return 0;
+}
+
+int RSound1::command37() {
+ // The disassembly's first "call isSoundActive" has no preceding
+ // "lea cx" - unlike every other command, which always reloads cx
+ // immediately before an isSoundActive check. The master dispatcher
+ // (rsound_command) never touches cx before invoking any command, so
+ // cx here holds whatever the game engine's own call into command()
+ // left it as - genuinely external state with no reproducible value,
+ // not something this port can derive from the disassembly. Treating
+ // this check as inert (unpredictable external data will essentially
+ // never match a real loaded Channel._soundData) rather than
+ // inventing a target offset for it, so command1() and the loads
+ // below run unconditionally.
+ _randomAmbianceTriggerFlag = 0;
+ command1();
+ _randomAmbianceTriggerFlag = 0;
+ _channels[2].load(loadData(0x3CF6));
+ _channels[3].load(loadData(0x3D3E));
+ _channels[4].load(loadData(0x3D75));
+ return 0;
+}
+
+int RSound1::command64() {
+ playSound(0x460B);
+ return 0;
+}
+
+int RSound1::command65() {
+ playSound(0x461D);
+ playSound(0x4631);
+ return 0;
+}
+
+int RSound1::command66() {
+ playSound(0x4647);
+ return 0;
+}
+
+int RSound1::command67() {
+ playSound(0x465D);
+ _channels[8].load(loadData(0x466E));
+ return 0;
+}
+
+int RSound1::command68() {
+ playSound(0x4725);
+ return 0;
+}
+
+int RSound1::command69() {
+ playSound(0x4676);
+ playSound(0x4693);
+ return 0;
+}
+
+int RSound1::command70() {
+ playSound(0x46AC);
+ playSound(0x46B8);
+ playSound(0x46C4);
+ playSound(0x47B7);
+ return 0;
+}
+
+int RSound1::command71() {
+ playSound(0x470B);
+ return 0;
+}
+
+int RSound1::command72() {
+ playSound(0x4747);
+ return 0;
+}
+
+int RSound1::command73() {
+ playSound(0x4761);
+ return 0;
+}
+
+int RSound1::command74() {
+ playSound(0x47A7);
+ playSound(0x47B7);
+ return 0;
+}
+
+int RSound1::command75() {
+ // Plays the same offset twice - a genuine quirk, preserved exactly.
+ playSound(0x4783);
+ playSound(0x4783);
+ return 0;
+}
+
+int RSound1::command76() {
+ playSound(0x46D0);
+ return 0;
+}
+
+} // namespace Phantom
+} // namespace MADS
diff --git a/engines/mads/phantom/rsound_phantom.h b/engines/mads/phantom/rsound_phantom.h
new file mode 100644
index 00000000000..b8df21a3980
--- /dev/null
+++ b/engines/mads/phantom/rsound_phantom.h
@@ -0,0 +1,114 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_PHANTOM_RSOUND_PHANTOM_H
+#define MADS_PHANTOM_RSOUND_PHANTOM_H
+
+#include "mads/phantom/rsound.h"
+
+namespace MADS {
+namespace Phantom {
+
+/**
+ * RSound1 (rsound.ph1)
+ *
+ * Dispatch table layout (funcs_10960/10976/1098C/109A2/109B8 in the
+ * disassembly - a 5-bucket sparse dispatch, mirroring the sibling ASound1's
+ * identical structure). There is no command 17 - bucket 2's dispatch
+ * bounds check only ever admits index 16 (confirmed: word_13047 = 16, an
+ * inclusive upper bound equal to the bucket's own lower bound).
+ * commands 0-8 (base class, except command4/5 - see RSound::command4/5)
+ * command 16 (random-ambiance picker, this class)
+ * commands 24-27 (this class)
+ * commands 32-39 (this class)
+ * commands 64-76 (this class - unlike ASound1's equivalent range, this
+ * one WAS reconstructable from the disassembly)
+ *
+ * command16 picks one of five alternatives at random (avoiding immediate
+ * repeats via _lastRandomAmbianceIndex), after first checking whether one
+ * of five known pieces is already playing via isSoundActive() gates.
+ * sound1() doesn't exist as a separate function in the disassembly -
+ * index 0 of funcs_122AD dispatches directly to command38 (also
+ * independently reachable as a direct command) - but is declared here as
+ * a thin wrapper for consistency with sound2()-sound5().
+ */
+class RSound1 : public RSound {
+private:
+ typedef int (RSound1:: *CommandPtr)();
+
+ // Mirrors word_1225D: avoids picking the same alternative twice in a row.
+ int _lastRandomAmbianceIndex = -1;
+
+ void checkRandomAmbianceTrigger() override;
+
+ int command16();
+
+ // Overrides confirming the isSoundActive(0x3D98) gate the base
+ // class's ungated command4()/command5() lack - see RSound::command4/5.
+ int command4() override;
+ int command5() override;
+
+ int command24();
+ int command25();
+ int command26();
+ int command27();
+
+ int command32();
+ // Targets of command16's random picker (funcs_122AD in the
+ // disassembly). sound1() is not a real disassembly function - see
+ // the class comment.
+ int command38();
+ int sound1();
+ int sound2();
+ int sound3();
+ int sound4();
+ int sound5();
+ int command33();
+ int command34();
+ int command35();
+ int command36();
+ int command37();
+ int command39();
+
+ int command64();
+ int command65();
+ int command66();
+ int command67();
+ int command68();
+ int command69();
+ int command70();
+ int command71();
+ int command72();
+ int command73();
+ int command74();
+ int command75();
+ int command76();
+
+public:
+ RSound1(Audio::Mixer *mixer);
+
+ int command(int commandId, int param) override;
+};
+
+} // namespace Phantom
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/phantom/sound.cpp b/engines/mads/phantom/sound.cpp
index 88555f62e23..b40b0083af8 100644
--- a/engines/mads/phantom/sound.cpp
+++ b/engines/mads/phantom/sound.cpp
@@ -21,6 +21,7 @@
#include "mads/phantom/sound.h"
#include "mads/phantom/asound_phantom.h"
+#include "mads/phantom/rsound_phantom.h"
namespace MADS {
namespace Phantom {
@@ -38,7 +39,8 @@ void PhantomSoundManager::loadDriver(int sectionNumber) {
if (_isMT32) {
// Roland MT32 drivers
- assert(0 == 1);
+ assert(sectionNumber == 1);
+ _driver = new RSound1(_mixer);
} else {
// Adlib drivers
More information about the Scummvm-git-logs
mailing list