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

dreammaster noreply at scummvm.org
Mon Jul 27 01:02:09 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:
ec3916f0cd MADS: NEBULAR: Added PC Speaker ISound provided by Tropp


Commit: ec3916f0cdc1dee39b88b5c63920baa597fd8a18
    https://github.com/scummvm/scummvm/commit/ec3916f0cdc1dee39b88b5c63920baa597fd8a18
Author: Paul Gilbert (dreammaster at scummvm.org)
Date: 2026-07-27T11:01:05+10:00

Commit Message:
MADS: NEBULAR: Added PC Speaker ISound provided by Tropp

Assisted-By: Codex

Changed paths:
  A engines/mads/nebular/isound.cpp
  A engines/mads/nebular/isound.h
  A engines/mads/nebular/isound_nebular.cpp
  A engines/mads/nebular/isound_nebular.h
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h
    engines/mads/dragonsphere/sound.cpp
    engines/mads/module.mk
    engines/mads/nebular/sound.cpp
    engines/mads/phantom/sound.cpp


diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 3ff2608bce8..52ca2789228 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -34,7 +34,17 @@ namespace MADS {
 SoundManager::SoundManager(Audio::Mixer *mixer, bool &soundFlag) : _mixer(mixer), _soundFlag(soundFlag) {
 	MidiDriver::DeviceHandle dev = MidiDriver::detectDevice(MDT_ADLIB | MDT_MIDI | MDT_PREFER_MT32);
 	MusicType musicType = MidiDriver::getMusicType(dev);
-	_isMT32 = musicType == MT_MT32;
+	switch (musicType) {
+	case MT_MT32:
+		_driverType = SOUND_MT32;
+		break;
+	case MT_PCSPK:
+		_driverType = SOUND_PCSPEAKER;
+		break;
+	default:
+		_driverType = SOUND_ADLIB;
+		break;
+	}
 }
 
 SoundManager::~SoundManager() {
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index 06125a4c79e..e7564245f5d 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -84,8 +84,9 @@ public:
 
 class SoundManager {
 protected:
+	enum DriverType { SOUND_ADLIB, SOUND_MT32, SOUND_PCSPEAKER };
 	Audio::Mixer *_mixer;
-	bool _isMT32;
+	DriverType _driverType;
 	bool &_soundFlag;
 	SoundDriver *_driver = nullptr;
 	bool _pollSoundEnabled = false;
diff --git a/engines/mads/dragonsphere/sound.cpp b/engines/mads/dragonsphere/sound.cpp
index 3640a3c3cb6..95421230835 100644
--- a/engines/mads/dragonsphere/sound.cpp
+++ b/engines/mads/dragonsphere/sound.cpp
@@ -26,21 +26,26 @@ namespace MADS {
 namespace Dragonsphere {
 
 void DragonSoundManager::validate() {
-	if (_isMT32) {
-		// TODO
-	} else {
+	switch (_driverType) {
+	case SOUND_MT32:
+		error("MT32 is not yet supported");
+		break;
+	default:
 		ASound::validate(_isDemo);
+		break;
 	}
 }
 
 void DragonSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_isMT32) {
+	switch (_driverType) {
+	case SOUND_MT32:
 		// Roland MT32 drivers
 		assert(0 == 1);
+		break;
 
-	} else {
+	default:
 		// Adlib drivers
 		switch (sectionNumber) {
 		case 1:
diff --git a/engines/mads/module.mk b/engines/mads/module.mk
index 00c9320f5a9..212da7d151b 100644
--- a/engines/mads/module.mk
+++ b/engines/mads/module.mk
@@ -68,6 +68,8 @@ MODULE_OBJS := \
 	nebular/nebular.o \
 	nebular/asound.o \
 	nebular/asound_nebular.o \
+	nebular/isound.o \
+	nebular/isound_nebular.o \
 	nebular/rsound.o \
 	nebular/rsound_nebular.o \
 	nebular/console.o \
diff --git a/engines/mads/nebular/isound.cpp b/engines/mads/nebular/isound.cpp
new file mode 100644
index 00000000000..8c148cb3a60
--- /dev/null
+++ b/engines/mads/nebular/isound.cpp
@@ -0,0 +1,702 @@
+/* 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/endian.h"
+#include "common/file.h"
+#include "common/textconsole.h"
+#include "common/util.h"
+#include "mads/nebular/isound.h"
+
+namespace MADS {
+namespace RexNebular {
+
+namespace {
+
+const uint32 kMinimumDataSegmentSize =
+ISound::kFrequencyTableOffset +
+ISound::kFrequencyTableEntries * 2;
+
+} // namespace
+
+ISound::OverlayLayout ISound::readOverlayLayout(
+	const Common::Path &filename) {
+	Common::File file;
+	if (!file.open(filename))
+		error("Could not open file - %s", filename.toString().c_str());
+
+	const int64 fileSize = file.size();
+	if (fileSize < 0x200)
+		error("ISOUND overlay is too small - %s",
+			filename.toString().c_str());
+
+	file.seek(0);
+	if (file.readUint16LE() != 0x5a4d)
+		error("ISOUND overlay is not an MZ executable - %s",
+			filename.toString().c_str());
+
+	const uint16 bytesOnLastPage = file.readUint16LE();
+	const uint16 pageCount = file.readUint16LE();
+	if (!pageCount || bytesOnLastPage >= 512)
+		error("Invalid ISOUND MZ file-size fields - %s",
+			filename.toString().c_str());
+
+	const uint32 declaredFileSize = (pageCount - 1) * 512U +
+		(bytesOnLastPage ? bytesOnLastPage : 512U);
+	if (declaredFileSize != (uint64)fileSize)
+		error("ISOUND MZ file-size fields do not match - %s",
+			filename.toString().c_str());
+
+	file.seek(8);
+	const uint32 imageOffset = file.readUint16LE() * 16U;
+	if (imageOffset < 0x1c || imageOffset + 0x48 >(uint64)fileSize)
+		error("Invalid ISOUND MZ header - %s",
+			filename.toString().c_str());
+
+	file.seek(imageOffset + 0x2a);
+	const uint32 dataSegmentOffset = file.readUint16LE() * 16U;
+	const uint16 dataSegmentSize = file.readUint16LE();
+	const uint16 timerHz = file.readUint16LE();
+	const uint16 exportCount = file.readUint16LE();
+	const uint32 dataOffset = imageOffset + dataSegmentOffset;
+
+	if (dataOffset >= (uint64)fileSize)
+		error("Invalid ISOUND data-segment offset - %s",
+			filename.toString().c_str());
+
+	OverlayLayout result;
+	result.dataOffset = dataOffset;
+	result.initializedDataSize = (uint32)(fileSize - dataOffset);
+	result.dataSegmentSize = dataSegmentSize;
+
+	if (result.dataSegmentSize < result.initializedDataSize ||
+		result.dataSegmentSize < kMinimumDataSegmentSize ||
+		timerHz != 100 || exportCount != 11)
+		error("Unsupported ISOUND overlay layout - %s",
+			filename.toString().c_str());
+
+	return result;
+}
+
+ISound::ISound(Audio::Mixer *mixer, const Common::Path &filename) :
+	ISound(mixer, filename, readOverlayLayout(filename)) {
+}
+
+ISound::ISound(Audio::Mixer *mixer, const Common::Path &filename, const OverlayLayout &layout) :
+		SoundDriver(mixer, filename, (int)layout.dataOffset, (int)layout.initializedDataSize),
+	_speakerGate(false),
+	_noiseEnabled(false),
+	_updatesEnabled(false),
+	_masterVolume(255),
+	_outputRate(mixer->getOutputRate()),
+	_sequenceAccumulator(0),
+	_noiseAccumulator(0),
+	_oscillatorPhase(0),
+	_frameCounter(0),
+	_randomSeed(0),
+	_commandParam(0),
+	_pollResult(0),
+	_resultState(0),
+	_priority(0),
+	_sequenceStart(0),
+	_position(0),
+	_innerLoopStart(0),
+	_outerLoopStart(0),
+	_restartOverride(0),
+	_innerLoopCount(0),
+	_outerLoopCount(0),
+	_note(0),
+	_activeTicks(0),
+	_releaseCounter(0),
+	_gateOffset(0),
+	_transpose(0),
+	_fineOffset(0),
+	_noiseMask(0),
+	_currentDivisor(0),
+	_lastOutputDivisor(0),
+	_pendingDivisor(0),
+	_hasPendingDivisor(false),
+	_speakerHigh(true),
+	_pitchStep(0),
+	_directDivisor(0),
+	_alternationReload(0),
+	_alternationOffset(0),
+	_alternationCounter(0),
+	_alternationToggle(false),
+	_sweepInitialized(false),
+	_sweepUpper(0),
+	_sweepLower(0),
+	_sweepDirection(1) {
+	// The descriptor size includes a zero-initialized tail absent from the
+	// executable image. Preserve the original data/BSS layout.
+	_soundData.resize(layout.dataSegmentSize);
+	_randomSeed = readSequenceUint16(0x00f9);
+
+	initializeDriver();
+	_mixer->playStream(Audio::Mixer::kSFXSoundType, &_speakerHandle,
+		this, -1, Audio::Mixer::kMaxChannelVolume, 0,
+		DisposeAfterUse::NO, true);
+}
+
+ISound::~ISound() {
+	_mixer->stopHandle(_speakerHandle);
+}
+
+byte ISound::readSequenceByte(uint16 offset) const {
+	if (offset >= _soundData.size())
+		error("ISOUND sequence offset 0x%04x is out of range", offset);
+	return _soundData[offset];
+}
+
+uint16 ISound::readSequenceUint16(uint16 offset) const {
+	if ((uint32)offset + 1 >= _soundData.size())
+		error("ISOUND word offset 0x%04x is out of range", offset);
+	return READ_LE_UINT16(&_soundData[offset]);
+}
+
+void ISound::writeSequenceByte(uint16 offset, byte value) {
+	if (offset >= _soundData.size())
+		error("ISOUND mutation offset 0x%04x is out of range", offset);
+	_soundData[offset] = value;
+}
+
+void ISound::initializeDriver() {
+	// Export 0 resets the driver, installs the null sequence at DS:00F0 with
+	// priority 0x64, and only then enables update processing.
+	resetDriver();
+	playSequence(kInitialNullSequenceOffset, 0x64);
+	_updatesEnabled = true;
+}
+
+void ISound::resetDriver() {
+	// The native reset at image 0x0146 is intentionally partial.
+	_sweepInitialized = false;
+	_alternationCounter = 0;
+	_priority = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+	_noiseMask = 0;
+	_currentDivisor = 0;
+	_pendingDivisor = 0;
+	_hasPendingDivisor = false;
+	_pitchStep = 0;
+	_gateOffset = 0;
+	_fineOffset = 0;
+	_activeTicks = 0;
+	_alternationReload = 0;
+	_alternationOffset = 0;
+
+	stopSpeaker();
+}
+
+void ISound::beginCommand(int param) {
+	// The dispatcher zeroes AH and stores only the parameter's low byte.
+	_commandParam = (byte)param;
+}
+
+int ISound::executeCommonCommand(int commandId) {
+	switch (commandId) {
+	case 0:
+	case 1:
+	case 2:
+	case 3:
+	case 4:
+	case 5:
+		resetDriver();
+		return 0;
+	case 6:
+		_updatesEnabled = false;
+		return 0;
+	case 7:
+		_updatesEnabled = true;
+		return 0;
+	case 8:
+		return _activeTicks;
+	default:
+		return 0;
+	}
+}
+
+void ISound::playSequence(uint16 sequenceOffset, byte priority) {
+	const bool wasEnabled = _updatesEnabled;
+	_updatesEnabled = false;
+
+	// Native code uses CMP followed by JS. Preserve the sign bit of the
+	// wrapped eight-bit subtraction rather than using a wider comparison.
+	if ((int8)(byte)(priority - _priority) < 0) {
+		_updatesEnabled = wasEnabled;
+		return;
+	}
+
+	_priority = priority;
+	_sequenceStart = sequenceOffset;
+	_position = sequenceOffset;
+	_innerLoopStart = sequenceOffset;
+	_outerLoopStart = sequenceOffset;
+	_restartOverride = 0;
+	_innerLoopCount = 0;
+	_outerLoopCount = 0;
+
+	_noiseMask = 0;
+	_currentDivisor = 0;
+	_pitchStep = 0;
+	_directDivisor = 0;
+	_gateOffset = 0;
+	_fineOffset = 0;
+	_transpose = 0;
+	_alternationReload = 0;
+	_alternationOffset = 0;
+	_alternationCounter = 0;
+	_activeTicks = 1;
+
+	_updatesEnabled = wasEnabled;
+}
+
+uint16 ISound::nextRandom() {
+	const uint16 value = (uint16)(0x9248 + _randomSeed);
+	_randomSeed = (uint16)((value >> 3) | (value << 13));
+	return _randomSeed;
+}
+
+void ISound::setResultState(int8 state) {
+	if (_resultState == state)
+		return;
+
+	_resultState = state;
+	_pollResult = state;
+}
+
+void ISound::processInnerLoop() {
+	const uint16 commandPosition = _position;
+
+	if (!_innerLoopCount) {
+		const byte count = readSequenceByte((uint16)(commandPosition + 1));
+		if (!count) {
+			_position = (uint16)(commandPosition + 2);
+			_innerLoopStart = _position;
+			_innerLoopCount = 0;
+		} else {
+			_innerLoopCount = count;
+			_position = _innerLoopStart;
+		}
+	} else if (--_innerLoopCount) {
+		_position = _innerLoopStart;
+	} else {
+		_position = (uint16)(commandPosition + 2);
+		_innerLoopStart = _position;
+	}
+}
+
+void ISound::processOuterLoop() {
+	const uint16 commandPosition = _position;
+
+	if (!_outerLoopCount) {
+		const byte count = readSequenceByte((uint16)(commandPosition + 1));
+		if (!count) {
+			_position = (uint16)(commandPosition + 2);
+			_innerLoopStart = _position;
+			_outerLoopStart = _position;
+			_innerLoopCount = 0;
+			_outerLoopCount = 0;
+		} else {
+			_outerLoopCount = count;
+			_position = _outerLoopStart;
+		}
+	} else if (--_outerLoopCount) {
+		// The original outer-loop repeat uses the current inner-loop anchor.
+		_position = _innerLoopStart;
+	} else {
+		_position = (uint16)(commandPosition + 2);
+		_outerLoopStart = _position;
+		_innerLoopStart = _position;
+	}
+}
+
+void ISound::processRestart() {
+	if (!_restartOverride) {
+		_position = _sequenceStart;
+		return;
+	}
+
+	_sequenceStart = _restartOverride;
+	_position = _restartOverride;
+	_innerLoopStart = _restartOverride;
+	_outerLoopStart = _restartOverride;
+}
+
+void ISound::processRandomMutation() {
+	const uint16 commandPosition = _position;
+	const byte count = readSequenceByte((uint16)(commandPosition + 1));
+	if (!count)
+		error("Invalid zero-length ISOUND F8 mutation at 0x%04x",
+			commandPosition);
+
+	const uint16 choices = (uint16)(commandPosition + 2);
+	const byte index = (byte)(nextRandom() & (byte)(count - 1));
+	const byte selected = readSequenceByte((uint16)(choices + index));
+	const byte destination = readSequenceByte((uint16)(choices + count));
+
+	// The target expression is evaluated in AL, so the relative byte wraps
+	// before it is added to the 16-bit choice-table address.
+	const byte relative = (byte)(
+		destination + (byte)(count - 1) + 2);
+	writeSequenceByte((uint16)(choices + relative), selected);
+
+	_position = (uint16)(commandPosition + count + 3);
+}
+
+uint16 ISound::calculateNoteDivisor(byte note) const {
+	const byte tableIndex = (byte)(note + _transpose);
+	if (tableIndex >= kFrequencyTableEntries)
+		error("ISOUND note-table index %u is out of range", tableIndex);
+
+	const uint32 tableOffset =
+		kFrequencyTableOffset + (uint32)tableIndex * 2;
+	const uint16 divisor = READ_LE_UINT16(&_soundData[tableOffset]);
+	return (uint16)(divisor + _fineOffset);
+}
+
+byte ISound::outputVolume() const {
+	return (byte)((kDefaultOutputVolume * _masterVolume) / 255);
+}
+
+void ISound::outputDivisor(uint16 divisor) {
+	// Reprogramming an 8254 counter in mode 3 does not restart the current
+	// half-cycle. The new count is loaded at the next output transition.
+	// This is especially important while the noise service rewrites the
+	// divisor: restarting the waveform on every write creates false tones.
+	if (_speakerGate && _lastOutputDivisor) {
+		_pendingDivisor = divisor;
+		_hasPendingDivisor = true;
+	} else {
+		_lastOutputDivisor = divisor;
+		_pendingDivisor = 0;
+		_hasPendingDivisor = false;
+		_oscillatorPhase = 0;
+		_speakerHigh = true;
+	}
+}
+
+void ISound::startSpeaker() {
+	_currentDivisor = _directDivisor ?
+		_directDivisor : calculateNoteDivisor(_note);
+	_directDivisor = 0;
+
+	_sweepInitialized = false;
+	_alternationToggle = false;
+	outputDivisor(_currentDivisor);
+	_speakerGate = true;
+}
+
+void ISound::stopSpeaker() {
+	_speakerGate = false;
+	_sweepInitialized = false;
+	_hasPendingDivisor = false;
+	_oscillatorPhase = 0;
+	_speakerHigh = true;
+}
+
+void ISound::processOrdinaryEvent() {
+	_note = readSequenceByte(_position);
+	_activeTicks = readSequenceByte((uint16)(_position + 1));
+	_position = (uint16)(_position + 2);
+
+	if (!_note || !_activeTicks)
+		stopSpeaker();
+
+	if (!_activeTicks) {
+		_priority = 0;
+		_pitchStep = 0;
+		_sweepInitialized = false;
+		_alternationCounter = 0;
+		setResultState(-1);
+		return;
+	}
+
+	// Note zero with nonzero duration is a timed rest.
+	if (!_note)
+		return;
+
+	_releaseCounter = (byte)(_activeTicks - _gateOffset);
+	startSpeaker();
+}
+
+void ISound::processSequenceTick() {
+	if (!_activeTicks)
+		return;
+
+	if (_releaseCounter && !--_releaseCounter)
+		stopSpeaker();
+
+	if (--_activeTicks)
+		return;
+
+	for (uint operation = 0; operation < kMaxOperationsPerTick; ++operation) {
+		const byte opcode = readSequenceByte(_position);
+		if (opcode < 0xf0) {
+			processOrdinaryEvent();
+			return;
+		}
+
+		switch (opcode) {
+		case 0xff:
+			processInnerLoop();
+			break;
+		case 0xfe:
+			processOuterLoop();
+			break;
+		case 0xfd:
+			processRestart();
+			break;
+		case 0xfc:
+			_noiseMask = readSequenceUint16((uint16)(_position + 1));
+			setResultState(_noiseMask ? 1 : -1);
+			_pitchStep = readSequenceUint16((uint16)(_position + 3));
+			_position = (uint16)(_position + 5);
+			break;
+		case 0xfb:
+			_gateOffset = readSequenceByte((uint16)(_position + 1));
+			_position = (uint16)(_position + 2);
+			break;
+		case 0xfa:
+			_pitchStep = readSequenceUint16((uint16)(_position + 1));
+			_position = (uint16)(_position + 3);
+			break;
+		case 0xf9:
+			_fineOffset = (int8)readSequenceByte(
+				(uint16)(_position + 1));
+			_position = (uint16)(_position + 2);
+			break;
+		case 0xf8:
+			processRandomMutation();
+			break;
+		case 0xf7:
+			_transpose = readSequenceByte((uint16)(_position + 1));
+			_position = (uint16)(_position + 2);
+			break;
+		case 0xf6:
+			_alternationReload = readSequenceByte(
+				(uint16)(_position + 1));
+			_alternationCounter = _alternationReload;
+			_alternationToggle = false;
+			_alternationOffset = readSequenceByte(
+				(uint16)(_position + 2));
+			_position = (uint16)(_position + 3);
+			break;
+		case 0xf5:
+			_directDivisor = readSequenceUint16(
+				(uint16)(_position + 1));
+			_position = (uint16)(_position + 3);
+			break;
+		case 0xf4:
+		case 0xf3:
+		case 0xf1:
+			_position = (uint16)(_position + 2);
+			break;
+		case 0xf2:
+		case 0xf0:
+			_position = (uint16)(_position + 3);
+			break;
+		}
+	}
+
+	error("ISOUND sequence exceeded %u operations in one tick",
+		kMaxOperationsPerTick);
+}
+
+void ISound::updateAlternation() {
+	_alternationCounter = _alternationReload;
+
+	byte alternatingOffset = 0;
+	if (!_alternationToggle) {
+		_alternationToggle = true;
+		alternatingOffset = _alternationOffset;
+	} else {
+		_alternationToggle = false;
+	}
+
+	const byte tableIndex = (byte)(
+		_note + _transpose + alternatingOffset);
+	if (tableIndex >= kFrequencyTableEntries)
+		error("ISOUND alternating note-table index %u is out of range",
+			tableIndex);
+
+	const uint32 tableOffset =
+		kFrequencyTableOffset + (uint32)tableIndex * 2;
+	_currentDivisor = (uint16)(
+		READ_LE_UINT16(&_soundData[tableOffset]) + _fineOffset);
+}
+
+void ISound::updatePitch() {
+	bool alternationChanged = false;
+	if (_alternationCounter && !--_alternationCounter) {
+		updateAlternation();
+		alternationChanged = true;
+	}
+
+	if (!_pitchStep) {
+		if (alternationChanged)
+			outputDivisor(_currentDivisor);
+		return;
+	}
+
+	if ((_pitchStep & 0xf000) == 0x8000) {
+		const uint16 range = _pitchStep & 0x00ff;
+		const uint16 step = (_pitchStep >> 8) & 0x000f;
+
+		if (!_sweepInitialized) {
+			_sweepUpper = (uint16)(_currentDivisor + range);
+			_sweepLower = (uint16)(_currentDivisor - range);
+			_sweepInitialized = true;
+		}
+
+		if ((int16)_currentDivisor > (int16)_sweepUpper)
+			_sweepDirection = -1;
+		else if ((int16)_currentDivisor < (int16)_sweepLower)
+			_sweepDirection = 1;
+
+		_currentDivisor = (uint16)(
+			_currentDivisor + step * _sweepDirection);
+	} else {
+		_currentDivisor = (uint16)(_currentDivisor + _pitchStep);
+	}
+
+	outputDivisor(_currentDivisor);
+}
+
+void ISound::update() {
+	++_frameCounter;
+	nextRandom();
+
+	// Command 6 freezes both stream and pitch/gate processing. The frame
+	// counter and random state continue to advance.
+	if (!_updatesEnabled)
+		return;
+
+	processSequenceTick();
+	updatePitch();
+}
+
+void ISound::timerTick() {
+	update();
+
+	if (_pollResult) {
+		_noiseEnabled = _pollResult > 0;
+		_pollResult = 0;
+	}
+}
+
+void ISound::noiseTick() {
+	if (!_noiseMask)
+		return;
+
+	// Export 4 changes the emitted divisor without replacing DS:00E0.
+	const uint16 divisor = (uint16)(
+		(nextRandom() & _noiseMask) + _currentDivisor);
+	outputDivisor(divisor);
+}
+
+int ISound::poll() {
+	// Playback advances from the audio stream at the overlay's 100 Hz rate.
+	return 0;
+}
+
+void ISound::noise() {
+	Common::StackLock lock(_driverMutex);
+	noiseTick();
+}
+
+int ISound::stop() {
+	Common::StackLock lock(_driverMutex);
+	resetDriver();
+
+	const int result = _pollResult;
+	_pollResult = 0;
+	return result;
+}
+
+void ISound::setVolume(int volume) {
+	Common::StackLock lock(_driverMutex);
+	_masterVolume = CLIP(volume, 0, 255);
+}
+
+int16 ISound::generateSample() {
+	if (!_speakerGate)
+		return 0;
+
+	// A PIT divisor of zero represents 65536.
+	_oscillatorPhase += kPitClockHz;
+
+	for (;;) {
+		const uint32 effectiveDivisor =
+			_lastOutputDivisor ? _lastOutputDivisor : 0x10000;
+		const uint32 halfCount = _speakerHigh ?
+			(effectiveDivisor + 1) / 2 : effectiveDivisor / 2;
+		const uint64 halfPeriod = (uint64)MAX<uint32>(halfCount, 1) *
+			_outputRate;
+		if (_oscillatorPhase < halfPeriod)
+			break;
+
+		_oscillatorPhase -= halfPeriod;
+		_speakerHigh = !_speakerHigh;
+		if (_hasPendingDivisor) {
+			_lastOutputDivisor = _pendingDivisor;
+			_pendingDivisor = 0;
+			_hasPendingDivisor = false;
+		}
+	}
+
+	if (!_masterVolume)
+		return 0;
+
+	const int amplitude = 127 * outputVolume();
+	return _speakerHigh ? amplitude : -amplitude;
+}
+
+int ISound::readBuffer(int16 *buffer, int numSamples) {
+	Common::StackLock lock(_driverMutex);
+
+	for (int sample = 0; sample < numSamples; ++sample) {
+		_sequenceAccumulator += kSequenceRateHz;
+		if (_sequenceAccumulator >= (uint32)_outputRate) {
+			_sequenceAccumulator -= _outputRate;
+			timerTick();
+		}
+
+		_noiseAccumulator += kNoiseRateHz;
+		if (_noiseAccumulator >= (uint32)_outputRate) {
+			_noiseAccumulator -= _outputRate;
+
+			// The overlay proves that noise is serviced independently from
+			// its 100 Hz sequencer, but not the host interval. The original
+			// capture is consistent with the surviving MADS 60 Hz service
+			// cadence; the 600 Hz counter is a timing clock.
+			if (_noiseEnabled)
+				noiseTick();
+		}
+
+		buffer[sample] = generateSample();
+	}
+
+	return numSamples;
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/isound.h b/engines/mads/nebular/isound.h
new file mode 100644
index 00000000000..0d4508f43b3
--- /dev/null
+++ b/engines/mads/nebular/isound.h
@@ -0,0 +1,182 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_NEBULAR_ISOUND_H
+#define MADS_NEBULAR_ISOUND_H
+
+#include "audio/audiostream.h"
+#include "audio/mixer.h"
+#include "mads/core/sound_manager.h"
+
+namespace MADS {
+namespace RexNebular {
+
+/**
+ * Shared native implementation of the Rex Nebular IBM PC Speaker runtime.
+ *
+ * The original ISOUND files are relocatable DOS MZ driver overlays. They
+ * contain an 11-entry driver descriptor, a data segment with a monophonic
+ * sequence interpreter's state and streams, and PIT channel 2 divisor data.
+ * ScummVM implements the interpreter natively and reads the original data
+ * segment; it does not execute the 16-bit overlay code.
+ */
+class ISound : public SoundDriver, public Audio::AudioStream {
+public:
+	enum {
+		kPitClockHz = 1193182,
+		kSequenceRateHz = 100,
+		kNoiseRateHz = 60,
+		kDefaultOutputVolume = 20,
+		kFrequencyTableOffset = 0x0114,
+		kFrequencyTableEntries = 90,
+		kInitialNullSequenceOffset = 0x00f0,
+		kMaxOperationsPerTick = 1024
+	};
+
+protected:
+	struct OverlayLayout {
+		uint32 dataOffset;
+		uint32 initializedDataSize;
+		uint16 dataSegmentSize;
+	};
+
+	Audio::SoundHandle _speakerHandle;
+	bool _speakerGate;
+	bool _noiseEnabled;
+	bool _updatesEnabled;
+	int _masterVolume;
+	int _outputRate;
+	uint32 _sequenceAccumulator;
+	uint32 _noiseAccumulator;
+	uint64 _oscillatorPhase;
+
+	uint16 _frameCounter;
+	uint16 _randomSeed;
+	uint16 _commandParam;
+	int16 _pollResult;
+	int8 _resultState;
+
+	byte _priority;
+	uint16 _sequenceStart;
+	uint16 _position;
+	uint16 _innerLoopStart;
+	uint16 _outerLoopStart;
+	uint16 _restartOverride;
+	byte _innerLoopCount;
+	byte _outerLoopCount;
+
+	byte _note;
+	byte _activeTicks;
+	byte _releaseCounter;
+	byte _gateOffset;
+	byte _transpose;
+	int8 _fineOffset;
+
+	uint16 _noiseMask;
+	uint16 _currentDivisor;
+	uint16 _lastOutputDivisor;
+	uint16 _pendingDivisor;
+	bool _hasPendingDivisor;
+	bool _speakerHigh;
+	uint16 _pitchStep;
+	uint16 _directDivisor;
+
+	// F6 alternates between the current note and an offset note.
+	byte _alternationReload;
+	byte _alternationOffset;
+	byte _alternationCounter;
+	bool _alternationToggle;
+
+	// Pitch-step values with high nibble 8 implement a bounded triangle sweep.
+	bool _sweepInitialized;
+	uint16 _sweepUpper;
+	uint16 _sweepLower;
+	int16 _sweepDirection;
+
+	static OverlayLayout readOverlayLayout(const Common::Path &filename);
+
+	ISound(Audio::Mixer *mixer,const Common::Path &filename, const OverlayLayout &layout);
+
+	byte readSequenceByte(uint16 offset) const;
+	uint16 readSequenceUint16(uint16 offset) const;
+	void writeSequenceByte(uint16 offset, byte value);
+
+	void resetDriver();
+	void initializeDriver();
+	void beginCommand(int param);
+	int executeCommonCommand(int commandId);
+	void playSequence(uint16 sequenceOffset, byte priority);
+
+	void update();
+	void timerTick();
+	void noiseTick();
+	void processSequenceTick();
+	void processInnerLoop();
+	void processOuterLoop();
+	void processRestart();
+	void processRandomMutation();
+	void processOrdinaryEvent();
+
+	void updatePitch();
+	void updateAlternation();
+	uint16 calculateNoteDivisor(byte note) const;
+
+	uint16 nextRandom();
+	void setResultState(int8 state);
+
+	void outputDivisor(uint16 divisor);
+	void startSpeaker();
+	void stopSpeaker();
+	byte outputVolume() const;
+	int16 generateSample();
+
+public:
+	ISound(Audio::Mixer *mixer, const Common::Path &filename);
+	~ISound() override;
+
+	int stop() override;
+	int poll() override;
+	void noise() override;
+	void setVolume(int volume) override;
+
+	int readBuffer(int16 *buffer, int numSamples) override;
+	bool isStereo() const override {
+		return false;
+	}
+	bool endOfData() const override {
+		return false;
+	}
+	bool endOfStream() const override {
+		return false;
+	}
+	int getRate() const override {
+		return _outputRate;
+	}
+
+	uint16 frameCounter() const {
+		return _frameCounter;
+	}
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/isound_nebular.cpp b/engines/mads/nebular/isound_nebular.cpp
new file mode 100644
index 00000000000..d5def52a8c5
--- /dev/null
+++ b/engines/mads/nebular/isound_nebular.cpp
@@ -0,0 +1,476 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "mads/nebular/isound_nebular.h"
+
+namespace MADS {
+namespace RexNebular {
+
+namespace {
+
+const ISoundCommandSequence kSection1Commands[42] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x01c8, 0x40, false },
+	{ 0x0000, 0x00, false }, // 10: no-op
+	{ 0x0000, 0x00, false }, // 11: no-op
+	{ 0x0000, 0x00, false }, // 12: no-op
+	{ 0x0000, 0x00, false }, // 13: no-op
+	{ 0x01dc, 0x40, false },
+	{ 0x0000, 0x00, false }, // 15: no-op
+	{ 0x0226, 0x40, false },
+	{ 0x0236, 0x40, false },
+	{ 0x0242, 0x40, false },
+	{ 0x024e, 0x40, false },
+	{ 0x025a, 0x40, false },
+	{ 0x0266, 0x40, false },
+	{ 0x0272, 0x40, false },
+	{ 0x027e, 0x40, false },
+	{ 0x028a, 0x40, false },
+	{ 0x0296, 0x40, true  },
+	{ 0x02a2, 0x40, true  },
+	{ 0x02ae, 0x44, true  },
+	{ 0x02ba, 0x44, true  },
+	{ 0x02c6, 0x40, true  },
+	{ 0x02d0, 0x40, true  },
+	{ 0x02dc, 0x44, false },
+	{ 0x02e8, 0x40, true  },
+	{ 0x02f4, 0x40, false },
+	{ 0x0300, 0x40, false },
+	{ 0x030c, 0x40, false },
+	{ 0x031a, 0x40, false },
+	{ 0x0326, 0x40, false },
+	{ 0x0332, 0x40, false },
+	{ 0x033e, 0x40, false },
+	{ 0x0340, 0x40, false },
+	{ 0x034c, 0x40, false }
+};
+
+const ISoundCommandSequence kSection2Commands[44] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x0000, 0x00, false }, // 9: no-op
+	{ 0x0000, 0x00, false }, // 10: no-op
+	{ 0x0000, 0x00, false }, // 11: no-op
+	{ 0x01ce, 0x40, false },
+	{ 0x01da, 0x40, false },
+	{ 0x01e6, 0x40, false },
+	{ 0x0000, 0x00, false }, // 15: no-op
+	{ 0x0000, 0x00, false }, // 16: no-op
+	{ 0x0000, 0x00, false }, // 17: no-op
+	{ 0x020c, 0x40, false },
+	{ 0x0222, 0x40, false },
+	{ 0x0238, 0x40, false },
+	{ 0x0244, 0x40, false },
+	{ 0x0274, 0x40, false },
+	{ 0x0280, 0x40, false },
+	{ 0x028c, 0x40, false },
+	{ 0x0298, 0x40, false },
+	{ 0x02a4, 0x40, false },
+	{ 0x02b0, 0x40, false },
+	{ 0x02bc, 0x40, false },
+	{ 0x02cc, 0x40, false },
+	{ 0x02d8, 0x40, false },
+	{ 0x0334, 0x40, false },
+	{ 0x0390, 0x40, false },
+	{ 0x039c, 0x40, false },
+	{ 0x03a8, 0x40, false },
+	{ 0x03be, 0x40, false },
+	{ 0x0000, 0x00, false }, // 36: no-op
+	{ 0x03e4, 0x40, false },
+	{ 0x03f0, 0x40, false },
+	{ 0x0400, 0x40, false },
+	{ 0x040c, 0x40, false },
+	{ 0x0418, 0x40, false },
+	{ 0x03d8, 0x40, false },
+	{ 0x0424, 0x40, false }
+};
+
+const ISoundCommandSequence kSection3Commands[61] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x01c8, 0x40, false },
+	{ 0x01ca, 0x40, false },
+	{ 0x01cc, 0x40, false },
+	{ 0x01ce, 0x40, false },
+	{ 0x01d0, 0x40, false },
+	{ 0x022c, 0x40, false },
+	{ 0x0288, 0x40, false },
+	{ 0x028a, 0x40, false },
+	{ 0x028c, 0x40, false },
+	{ 0x0000, 0x00, false }, // 18: no-op
+	{ 0x0290, 0x40, false },
+	{ 0x029c, 0x40, false },
+	{ 0x02a8, 0x40, false },
+	{ 0x02b8, 0x40, false },
+	{ 0x02c2, 0x40, false },
+	{ 0x02cc, 0x40, false },
+	{ 0x02d8, 0x40, false },
+	{ 0x02ee, 0x40, false },
+	{ 0x0484, 0x40, false },
+	{ 0x02fc, 0x40, false },
+	{ 0x0308, 0x40, false },
+	{ 0x0314, 0x40, false },
+	{ 0x0370, 0x40, false },
+	{ 0x037c, 0x40, false },
+	{ 0x0388, 0x40, false },
+	{ 0x0394, 0x40, false },
+	{ 0x03aa, 0x40, false },
+	{ 0x0000, 0x00, false }, // 36: no-op
+	{ 0x03d0, 0x40, false },
+	{ 0x03dc, 0x40, false },
+	{ 0x03e8, 0x40, false },
+	{ 0x03f4, 0x40, false },
+	{ 0x0400, 0x40, false },
+	{ 0x0410, 0x40, false },
+	{ 0x041c, 0x40, false },
+	{ 0x0428, 0x40, false },
+	{ 0x0434, 0x40, false },
+	{ 0x0440, 0x40, false },
+	{ 0x0442, 0x40, false },
+	{ 0x0444, 0x40, false },
+	{ 0x0446, 0x40, false },
+	{ 0x0448, 0x40, false },
+	{ 0x044a, 0x40, false },
+	{ 0x0456, 0x40, false },
+	{ 0x0458, 0x40, false },
+	{ 0x045a, 0x40, false },
+	{ 0x045c, 0x40, false },
+	{ 0x045e, 0x40, false },
+	{ 0x0460, 0x40, false },
+	{ 0x046c, 0x40, false },
+	{ 0x03c4, 0x40, false },
+	{ 0x0478, 0x40, false }
+};
+
+const ISoundCommandSequence kSection5Commands[42] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x01c8, 0x40, false },
+	{ 0x01d4, 0x40, false },
+	{ 0x01e0, 0x40, false },
+	{ 0x01ec, 0x40, false },
+	{ 0x01f8, 0x40, false },
+	{ 0x0204, 0x40, false },
+	{ 0x0214, 0x40, false },
+	{ 0x0224, 0x40, false },
+	{ 0x0234, 0x40, false },
+	{ 0x0258, 0x40, false },
+	{ 0x0268, 0x40, false },
+	{ 0x0274, 0x40, false },
+	{ 0x0280, 0x40, false },
+	{ 0x028c, 0x40, false },
+	{ 0x0298, 0x40, false },
+	{ 0x02c8, 0x40, false },
+	{ 0x02d4, 0x40, false },
+	{ 0x02e0, 0x40, false },
+	{ 0x02ec, 0x40, false },
+	{ 0x02f8, 0x40, false },
+	{ 0x0000, 0x00, false }, // 29: no-op
+	{ 0x0306, 0x40, false },
+	{ 0x0362, 0x40, false },
+	{ 0x036e, 0x40, false },
+	{ 0x037a, 0x40, false },
+	{ 0x0386, 0x40, false },
+	{ 0x039c, 0x40, false },
+	{ 0x0000, 0x00, false }, // 36: no-op
+	{ 0x03b8, 0x40, false },
+	{ 0x0000, 0x00, false }, // 38: no-op
+	{ 0x03c6, 0x40, false },
+	{ 0x03d6, 0x40, false },
+	{ 0x0000, 0x00, false }  // 41: no-op
+};
+
+const ISoundCommandSequence kSection6Commands[30] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x01c8, 0x40, false },
+	{ 0x01d4, 0x40, false },
+	{ 0x01e0, 0x40, false },
+	{ 0x01f0, 0x40, false },
+	{ 0x01fc, 0x40, false },
+	{ 0x0208, 0x40, false },
+	{ 0x0214, 0x40, false },
+	{ 0x0220, 0x40, false },
+	{ 0x022c, 0x40, false },
+	{ 0x0238, 0x40, false },
+	{ 0x0244, 0x40, false },
+	{ 0x0250, 0x40, false },
+	{ 0x0266, 0x40, false },
+	{ 0x0276, 0x40, false },
+	{ 0x0282, 0x40, false },
+	{ 0x0000, 0x00, false }, // 24: no-op
+	{ 0x0290, 0x40, false },
+	{ 0x0000, 0x00, false }, // 26: no-op
+	{ 0x0000, 0x00, false }, // 27: no-op
+	{ 0x0000, 0x00, false }, // 28: no-op
+	{ 0x0000, 0x00, false }  // 29: no-op
+};
+
+const ISoundCommandSequence kSection7Commands[38] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x024a, 0x40, false },
+	{ 0x024a, 0x40, false },
+	{ 0x024a, 0x40, false },
+	{ 0x024a, 0x40, false },
+	{ 0x024a, 0x40, false },
+	{ 0x024a, 0x40, false },
+	{ 0x01c8, 0x40, false },
+	{ 0x01d4, 0x40, false },
+	{ 0x01e0, 0x40, false },
+	{ 0x01ec, 0x40, false },
+	{ 0x01fc, 0x40, false },
+	{ 0x020c, 0x40, false },
+	{ 0x0220, 0x40, false },
+	{ 0x022c, 0x40, false },
+	{ 0x0238, 0x40, false },
+	{ 0x0244, 0x40, false },
+	{ 0x0246, 0x40, false },
+	{ 0x0248, 0x40, false },
+	{ 0x0304, 0x40, false },
+	{ 0x0312, 0x40, false },
+	{ 0x024e, 0x40, false },
+	{ 0x0250, 0x40, false },
+	{ 0x02ac, 0x40, false },
+	{ 0x02ae, 0x40, false },
+	{ 0x02ba, 0x40, false },
+	{ 0x02c6, 0x40, false },
+	{ 0x02dc, 0x40, false },
+	{ 0x0000, 0x00, false }, // 36: no-op
+	{ 0x02f8, 0x40, false }
+};
+
+const ISoundCommandSequence kSection8Commands[38] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x01c8, 0x40, false },
+	{ 0x0000, 0x00, false }, // 10: no-op
+	{ 0x01d6, 0x40, false },
+	{ 0x01e2, 0x40, false },
+	{ 0x01ee, 0x40, false },
+	{ 0x01fa, 0x40, false },
+	{ 0x020a, 0x40, false },
+	{ 0x0216, 0x40, false },
+	{ 0x0222, 0x40, false },
+	{ 0x022e, 0x40, false },
+	{ 0x023a, 0x40, false },
+	{ 0x0246, 0x40, false },
+	{ 0x0252, 0x40, false },
+	{ 0x025e, 0x40, false },
+	{ 0x026a, 0x40, false },
+	{ 0x026c, 0x40, false },
+	{ 0x0278, 0x40, false },
+	{ 0x0290, 0x40, false },
+	{ 0x029c, 0x40, false },
+	{ 0x0000, 0x00, false }, // 28: no-op
+	{ 0x0000, 0x00, false }, // 29: no-op
+	{ 0x02ac, 0x40, false },
+	{ 0x0360, 0x40, false },
+	{ 0x030a, 0x40, false },
+	{ 0x0316, 0x40, false },
+	{ 0x0322, 0x40, false },
+	{ 0x0338, 0x40, false },
+	{ 0x0000, 0x00, false }, // 36: no-op
+	{ 0x0354, 0x40, false }
+};
+
+const ISoundCommandSequence kSection9Commands[51] = {
+	{ 0x0000, 0x00, false }, // 0: reset
+	{ 0x0000, 0x00, false }, // 1: reset
+	{ 0x0000, 0x00, false }, // 2: reset
+	{ 0x0000, 0x00, false }, // 3: reset
+	{ 0x0000, 0x00, false }, // 4: reset
+	{ 0x0000, 0x00, false }, // 5: reset
+	{ 0x0000, 0x00, false }, // 6: pause
+	{ 0x0000, 0x00, false }, // 7: resume
+	{ 0x0000, 0x00, false }, // 8: active duration
+	{ 0x0000, 0x00, false }, // 9: no-op
+	{ 0x0000, 0x00, false }, // 10: no-op
+	{ 0x01cc, 0x40, false },
+	{ 0x01d8, 0x40, false },
+	{ 0x01e4, 0x40, false },
+	{ 0x01f0, 0x40, false },
+	{ 0x01fc, 0x40, false },
+	{ 0x0210, 0x40, false },
+	{ 0x0220, 0x40, false },
+	{ 0x0230, 0x40, false },
+	{ 0x0256, 0x40, false },
+	{ 0x0274, 0x40, false },
+	{ 0x0000, 0x00, false }, // 21: no-op
+	{ 0x0000, 0x00, false }, // 22: no-op
+	{ 0x02b6, 0x40, false },
+	{ 0x02d2, 0x40, false },
+	{ 0x02de, 0x40, false },
+	{ 0x02ea, 0x40, false },
+	{ 0x02f6, 0x40, false },
+	{ 0x0302, 0x40, false },
+	{ 0x030e, 0x40, false },
+	{ 0x031a, 0x40, false },
+	{ 0x0336, 0x40, false },
+	{ 0x034c, 0x40, false },
+	{ 0x0358, 0x40, false },
+	{ 0x0000, 0x00, false }, // 34: no-op
+	{ 0x036a, 0x40, false },
+	{ 0x037e, 0x40, false },
+	{ 0x038a, 0x40, false },
+	{ 0x0000, 0x00, false }, // 38: no-op
+	{ 0x0000, 0x00, false }, // 39: no-op
+	{ 0x0000, 0x00, false }, // 40: no-op
+	{ 0x0000, 0x00, false }, // 41: no-op
+	{ 0x0000, 0x00, false }, // 42: no-op
+	{ 0x0000, 0x00, false }, // 43: no-op
+	{ 0x0000, 0x00, false }, // 44: no-op
+	{ 0x0000, 0x00, false }, // 45: no-op
+	{ 0x0000, 0x00, false }, // 46: no-op
+	{ 0x0000, 0x00, false }, // 47: no-op
+	{ 0x039a, 0x40, false },
+	{ 0x0000, 0x00, false }, // 49: no-op
+	{ 0x0000, 0x00, false }  // 50: no-op
+};
+
+} // namespace
+
+ISoundSection::ISoundSection(Audio::Mixer *mixer, const char *filename,
+		const ISoundCommandSequence *commands, uint commandCount) :
+		ISound(mixer, filename),
+	_commands(commands),
+	_commandCount(commandCount) {
+}
+
+int ISoundSection::command(int commandId, int param) {
+	Common::StackLock lock(_driverMutex);
+	if (commandId < 0 || (uint)commandId >= _commandCount)
+		return 0;
+
+	beginCommand(param);
+	if (commandId <= 8)
+		return executeCommonCommand(commandId);
+
+	const ISoundCommandSequence &entry = _commands[commandId];
+	if (entry.parameterAtLeast120 && _commandParam < 0x78)
+		return 0;
+
+	if (entry.sequenceOffset)
+		playSequence(entry.sequenceOffset, entry.priority);
+
+	return 0;
+}
+
+ISound1::ISound1(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.001", kSection1Commands,
+		ARRAYSIZE(kSection1Commands)) {
+}
+
+ISound2::ISound2(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.002", kSection2Commands,
+		ARRAYSIZE(kSection2Commands)) {
+}
+
+ISound3::ISound3(Audio::Mixer *mixer) :
+	ISound3(mixer, "ISOUND.003") {
+}
+
+ISound3::ISound3(Audio::Mixer *mixer, const char *filename) :
+	ISoundSection(mixer, filename, kSection3Commands,
+		ARRAYSIZE(kSection3Commands)) {
+}
+
+ISound4::ISound4(Audio::Mixer *mixer) :
+	ISound3(mixer, "ISOUND.004") {
+}
+
+ISound5::ISound5(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.005", kSection5Commands,
+		ARRAYSIZE(kSection5Commands)) {
+}
+
+ISound6::ISound6(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.006", kSection6Commands,
+		ARRAYSIZE(kSection6Commands)) {
+}
+
+ISound7::ISound7(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.007", kSection7Commands,
+		ARRAYSIZE(kSection7Commands)) {
+}
+
+ISound8::ISound8(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.008", kSection8Commands,
+		ARRAYSIZE(kSection8Commands)) {
+}
+
+ISound9::ISound9(Audio::Mixer *mixer) :
+	ISoundSection(mixer, "ISOUND.009", kSection9Commands,
+		ARRAYSIZE(kSection9Commands)) {
+}
+
+} // namespace RexNebular
+} // namespace MADS
diff --git a/engines/mads/nebular/isound_nebular.h b/engines/mads/nebular/isound_nebular.h
new file mode 100644
index 00000000000..c768b436ca3
--- /dev/null
+++ b/engines/mads/nebular/isound_nebular.h
@@ -0,0 +1,109 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef MADS_NEBULAR_ISOUND_NEBULAR_H
+#define MADS_NEBULAR_ISOUND_NEBULAR_H
+
+#include "mads/nebular/isound.h"
+
+namespace MADS {
+namespace RexNebular {
+
+struct ISoundCommandSequence {
+	uint16 sequenceOffset;
+	byte priority;
+	bool parameterAtLeast120;
+};
+
+class ISoundSection : public ISound {
+private:
+	const ISoundCommandSequence *_commands;
+	uint _commandCount;
+
+protected:
+	ISoundSection(Audio::Mixer *mixer, const char *filename,
+		const ISoundCommandSequence *commands, uint commandCount);
+
+public:
+	int command(int commandId, int param) override;
+};
+
+/** ISOUND.001: "Rex IBM Mod1 9-11-92", command IDs 0-41. */
+class ISound1 : public ISoundSection {
+public:
+	ISound1(Audio::Mixer *mixer);
+};
+
+/** ISOUND.002: "Rex IBM Mod2 9-11-92", command IDs 0-43. */
+class ISound2 : public ISoundSection {
+public:
+	ISound2(Audio::Mixer *mixer);
+};
+
+/** ISOUND.003: "Rex IBM Mod3 9-11-92", command IDs 0-60. */
+class ISound3 : public ISoundSection {
+protected:
+	ISound3(Audio::Mixer *mixer, const char *filename);
+
+public:
+	ISound3(Audio::Mixer *mixer);
+};
+
+/** ISOUND.004 has the same command and stream layout as ISOUND.003. */
+class ISound4 : public ISound3 {
+public:
+	ISound4(Audio::Mixer *mixer);
+};
+
+/** ISOUND.005: "Rex IBM Mod5 9-11-92", command IDs 0-41. */
+class ISound5 : public ISoundSection {
+public:
+	ISound5(Audio::Mixer *mixer);
+};
+
+/** ISOUND.006: "Rex IBM Mod6 9-11-92", command IDs 0-29. */
+class ISound6 : public ISoundSection {
+public:
+	ISound6(Audio::Mixer *mixer);
+};
+
+/** ISOUND.007: "Rex IBM Mod7 9-11-92", command IDs 0-37. */
+class ISound7 : public ISoundSection {
+public:
+	ISound7(Audio::Mixer *mixer);
+};
+
+/** ISOUND.008: "Rex IBM Mod8 9-11-92", command IDs 0-37. */
+class ISound8 : public ISoundSection {
+public:
+	ISound8(Audio::Mixer *mixer);
+};
+
+/** ISOUND.009: "Rex IBM Mod9 9-11-92", command IDs 0-50. */
+class ISound9 : public ISoundSection {
+public:
+	ISound9(Audio::Mixer *mixer);
+};
+
+} // namespace RexNebular
+} // namespace MADS
+
+#endif
diff --git a/engines/mads/nebular/sound.cpp b/engines/mads/nebular/sound.cpp
index 5f5abc72114..2a84ef94bd7 100644
--- a/engines/mads/nebular/sound.cpp
+++ b/engines/mads/nebular/sound.cpp
@@ -21,22 +21,33 @@
 
 #include "mads/nebular/sound.h"
 #include "mads/nebular/asound_nebular.h"
+#include "mads/nebular/isound_nebular.h"
 #include "mads/nebular/rsound_nebular.h"
 
 namespace MADS {
 namespace RexNebular {
 
 void RexSoundManager::validate() {
-	if (_isMT32)
+	switch (_driverType) {
+	case SOUND_MT32:
 		RSound::validate();
-	else
+		break;
+
+	case SOUND_PCSPEAKER:
+		// No validation needed
+		break;
+
+	default:
 		ASound::validate();
+		break;
+	}
 }
 
 void RexSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_isMT32) {
+	switch (_driverType) {
+	case SOUND_MT32:
 		// Roland MT32 drivers
 		switch (sectionNumber) {
 		case 1:
@@ -69,7 +80,43 @@ void RexSoundManager::loadDriver(int sectionNumber) {
 		default:
 			return;
 		}
-	} else {
+		break;
+
+	case SOUND_PCSPEAKER:
+		switch (sectionNumber) {
+		case 1:
+			_driver = new RexNebular::ISound1(_mixer);
+			break;
+		case 2:
+			_driver = new RexNebular::ISound2(_mixer);
+			break;
+		case 3:
+			_driver = new RexNebular::ISound3(_mixer);
+			break;
+		case 4:
+			_driver = new RexNebular::ISound4(_mixer);
+			break;
+		case 5:
+			_driver = new RexNebular::ISound5(_mixer);
+			break;
+		case 6:
+			_driver = new RexNebular::ISound6(_mixer);
+			break;
+		case 7:
+			_driver = new RexNebular::ISound7(_mixer);
+			break;
+		case 8:
+			_driver = new RexNebular::ISound8(_mixer);
+			break;
+		case 9:
+			_driver = new RexNebular::ISound9(_mixer);
+			break;
+		default:
+			return;
+		}
+		break;
+
+	default:
 		// Adlib drivers
 		switch (sectionNumber) {
 		case 1:
diff --git a/engines/mads/phantom/sound.cpp b/engines/mads/phantom/sound.cpp
index b40b0083af8..beb5d94ff46 100644
--- a/engines/mads/phantom/sound.cpp
+++ b/engines/mads/phantom/sound.cpp
@@ -27,22 +27,30 @@ namespace MADS {
 namespace Phantom {
 
 void PhantomSoundManager::validate() {
-	if (_isMT32) {
+	switch (_driverType) {
+	case SOUND_MT32:
+		// TODO
 		assert(0 == 1);
-	} else {
+		break;
+
+	default:
+		// Adlib
 		ASound::validate(_isDemo);
+		break;
 	}
 }
 
 void PhantomSoundManager::loadDriver(int sectionNumber) {
 	removeDriver();
 
-	if (_isMT32) {
+	switch (_driverType) {
+	case SOUND_MT32:
 		// Roland MT32 drivers
 		assert(sectionNumber == 1);
 		_driver = new RSound1(_mixer);
+		break;
 
-	} else {
+	default:	
 		// Adlib drivers
 		if (_isDemo) {
 			_driver = new ASoundDemo(_mixer);




More information about the Scummvm-git-logs mailing list