[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