[Scummvm-git-logs] scummvm master -> 03e90922c932bc5ed41eb0e95a9baf6b1e7c8bfa

NMIError noreply at scummvm.org
Fri Oct 9 16:55:27 UTC 2026


This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .

Summary:
d755c2d919 AUDIO/MIDI: Add music_mute / sfx_mute volume scaling
c7fc48767d MADS: Rex MT-32 fixes
140dfaa9ed MADS: More Rex MT-32 refactoring
75aee5d537 MADS: Refactor Rex demo MT-32 code
1acb7ec2a1 MADS: Rex MT-32 music / SFX volume control
03e90922c9 MADS: Fix Rex MT-32 volume fade-out bug


Commit: d755c2d91949c8f27a50f205cf9da8ae47b867e6
    https://github.com/scummvm/scummvm/commit/d755c2d91949c8f27a50f205cf9da8ae47b867e6
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
AUDIO/MIDI: Add music_mute / sfx_mute volume scaling

Changed paths:
    audio/adlib_ms.cpp
    audio/casio.cpp
    audio/mididrv_ms.cpp
    audio/mididrv_ms.h
    audio/miles_adlib.cpp
    audio/mt32gm.cpp


diff --git a/audio/adlib_ms.cpp b/audio/adlib_ms.cpp
index d13015b0762..80c512bf442 100644
--- a/audio/adlib_ms.cpp
+++ b/audio/adlib_ms.cpp
@@ -1738,11 +1738,11 @@ uint8 MidiDriver_ADLIB_Multisource::calculateVolume(uint8 channel, uint8 source,
 	// Scale by source volume.
 	invertedVolume = (invertedVolume * _sources[source].volume) / _sources[source].neutralVolume;
 	if (_userVolumeScaling) {
-		if (_userMute) {
+		if (_userMute || (_sources[source].type == SOURCE_TYPE_SFX ? _userSfxMute : _userMusicMute)) {
 			invertedVolume = 0;
 		} else {
 			// Scale by user volume.
-			uint16 userVolume = (_sources[source].type == SOURCE_TYPE_SFX ? _userSfxVolume : _userMusicVolume); // Treat SOURCE_TYPE_UNDEFINED as music
+			uint16 userVolume = _sources[source].type == SOURCE_TYPE_SFX ? _userSfxVolume : _userMusicVolume; // Treat SOURCE_TYPE_UNDEFINED as music
 			invertedVolume = (invertedVolume * userVolume) >> 8;
 		}
 	}
diff --git a/audio/casio.cpp b/audio/casio.cpp
index e9c7c5ea6f4..0ee8beab6cf 100644
--- a/audio/casio.cpp
+++ b/audio/casio.cpp
@@ -285,11 +285,11 @@ byte MidiDriver_Casio::calculateVelocity(int8 source, byte velocity) {
 			calculatedVelocity = 0;
 	}
 	if (_userVolumeScaling) {
-		if (_userMute) {
+		if (_userMute || ((source >= 0 && _sources[source].type == SOURCE_TYPE_SFX) ? _userSfxMute : _userMusicMute)) {
 			calculatedVelocity = 0;
 		} else {
 			// Scale to user volume.
-			uint16 userVolume = _sources[source].type == SOURCE_TYPE_SFX ? _userSfxVolume : _userMusicVolume;
+			uint16 userVolume = ((source >= 0 && _sources[source].type == SOURCE_TYPE_SFX) ? _userSfxVolume : _userMusicVolume);
 			if (userVolume == 0)
 				calculatedVelocity = 0;
 		}
diff --git a/audio/mididrv_ms.cpp b/audio/mididrv_ms.cpp
index a214c007883..13cc984baf1 100644
--- a/audio/mididrv_ms.cpp
+++ b/audio/mididrv_ms.cpp
@@ -60,6 +60,8 @@ MidiDriver_Multisource::MidiDriver_Multisource() :
 		_userMusicVolume(192),
 		_userSfxVolume(192),
 		_userMute(false),
+		_userMusicMute(false),
+		_userSfxMute(false),
 		_timerRate(0),
 		_fadeDelay(0),
 		_timer_param(nullptr),
@@ -382,6 +384,8 @@ void MidiDriver_Multisource::syncSoundSettings() {
 	_userMusicVolume = MIN(256, ConfMan.getInt("music_volume"));
 	_userSfxVolume = MIN(256, ConfMan.getInt("sfx_volume"));
 	_userMute = ConfMan.getBool("mute");
+	_userMusicMute = ConfMan.getBool("music_mute");
+	_userSfxMute = ConfMan.getBool("sfx_mute");
 
 	// Calling applySourceVolume will apply the user volume.
 	applySourceVolume(0xFF);
diff --git a/audio/mididrv_ms.h b/audio/mididrv_ms.h
index 07b665df992..a9f8cac5280 100644
--- a/audio/mididrv_ms.h
+++ b/audio/mididrv_ms.h
@@ -467,6 +467,8 @@ protected:
 	uint16 _userMusicVolume;
 	uint16 _userSfxVolume;
 	bool _userMute;
+	bool _userMusicMute;
+	bool _userSfxMute;
 
 	Common::Mutex _fadingMutex; // For operations on fades
 
diff --git a/audio/miles_adlib.cpp b/audio/miles_adlib.cpp
index 4cb52c70086..ff383c844a0 100644
--- a/audio/miles_adlib.cpp
+++ b/audio/miles_adlib.cpp
@@ -822,7 +822,7 @@ void MidiDriver_Miles_AdLib::updatePhysicalFmVoice(byte virtualFmVoice, bool key
 		// Scale by source volume.
 		compositeVolume = compositeVolume * _sources[0].volume / _sources[0].neutralVolume;
 		if (_userVolumeScaling) {
-			if (_userMute) {
+			if (_userMute || (_sources[0].type == SOURCE_TYPE_SFX ? _userSfxMute : _userMusicMute)) {
 				compositeVolume = 0;
 			} else {
 				// Scale by user volume.
diff --git a/audio/mt32gm.cpp b/audio/mt32gm.cpp
index 8ee474f6e47..78887039c74 100644
--- a/audio/mt32gm.cpp
+++ b/audio/mt32gm.cpp
@@ -596,7 +596,7 @@ void MidiDriver_MT32GM::controlChange(byte outputChannel, byte controllerNumber,
 			controllerValue = (controllerValue * _sources[source].volume) / _sources[source].neutralVolume;
 		}
 		if (_userVolumeScaling) {
-			if (_userMute) {
+			if (_userMute || ((source >= 0 && _sources[source].type == SOURCE_TYPE_SFX) ? _userSfxMute : _userMusicMute)) {
 				controllerValue = 0;
 			} else {
 				// Scale to user volume


Commit: c7fc48767d2e27641aab2dd4f17976f7da78e177
    https://github.com/scummvm/scummvm/commit/c7fc48767d2e27641aab2dd4f17976f7da78e177
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
MADS: Rex MT-32 fixes

- RSound code readability
- User volume settings applied
- SysEx delays awaited during event loop

Changed paths:
    engines/mads/core/config.cpp
    engines/mads/core/config.h
    engines/mads/core/sound_manager.cpp
    engines/mads/core/sound_manager.h
    engines/mads/mads.cpp
    engines/mads/mads.h
    engines/mads/nebular/menus.cpp
    engines/mads/nebular/sound/rsound.cpp
    engines/mads/nebular/sound/rsound.h
    engines/mads/nebular/sound/rsound_nebular.cpp
    engines/mads/nebular/sound/sound.cpp


diff --git a/engines/mads/core/config.cpp b/engines/mads/core/config.cpp
index 16849f14e31..e070e441341 100644
--- a/engines/mads/core/config.cpp
+++ b/engines/mads/core/config.cpp
@@ -64,9 +64,7 @@ void read_config_file() {
 	ConfMan.registerDefault("difficulty", -1);
 	ConfMan.registerDefault("animview_minimum_black_ticks", -1);
 
-	config_file.music_flag = !ConfMan.getBool("music_mute") && !ConfMan.getBool("mute");
-	config_file.sound_flag = !ConfMan.getBool("sfx_mute") && !ConfMan.getBool("mute");
-	config_file.speech_flag = !ConfMan.getBool("speech_mute") && !ConfMan.getBool("mute");
+	read_sound_settings();
 
 	config_file.interface_hotspots = ConfMan.getBool("interface_hotspots") ? INTERFACE_BRAINDEAD : INTERFACE_MACINTOSH;
 	config_file.inventory_mode = ConfMan.getBool("inventory_mode") ? INVENTORY_SPINNING : INVENTORY_SQUAT;
@@ -88,6 +86,12 @@ void read_config_file() {
 		game.difficulty = ConfMan.getInt("difficulty");
 }
 
+void read_sound_settings() {
+	config_file.music_flag = !ConfMan.getBool("music_mute") && !ConfMan.getBool("mute");
+	config_file.sound_flag = !ConfMan.getBool("sfx_mute") && !ConfMan.getBool("mute");
+	config_file.speech_flag = !ConfMan.getBool("speech_mute") && !ConfMan.getBool("mute");
+}
+
 void write_config_file() {
 	ConfMan.setBool("music_mute", !config_file.music_flag);
 	ConfMan.setBool("sfx_mute", !config_file.sound_flag);
diff --git a/engines/mads/core/config.h b/engines/mads/core/config.h
index af92acd6cf0..dd1999a2270 100644
--- a/engines/mads/core/config.h
+++ b/engines/mads/core/config.h
@@ -104,6 +104,7 @@ extern ConfigFile config_file;
 
 extern void init_config();
 extern void read_config_file();
+extern void read_sound_settings();
 extern void write_config_file();
 
 } // namespace MADS
diff --git a/engines/mads/core/sound_manager.cpp b/engines/mads/core/sound_manager.cpp
index 1517a4f2281..e7f9b977034 100644
--- a/engines/mads/core/sound_manager.cpp
+++ b/engines/mads/core/sound_manager.cpp
@@ -25,6 +25,7 @@
 #include "common/file.h"
 #include "common/memstream.h"
 #include "common/textconsole.h"
+#include "mads/mads.h"
 #include "mads/core/sound_manager.h"
 
 namespace Audio {
@@ -93,8 +94,24 @@ void SoundManager::init(int sectionNumber) {
 		return;
 	}
 
-	// Set volume for newly loaded driver
-	_driver->setVolume(_masterVolume);
+	// Wait for driver to be ready to start playback
+	// (i.e. wait for MT-32 SysExes)
+	while (!isReady()) {
+		g_engine->flushKeys();
+
+		if (g_engine->shouldQuit()) {
+			if (_midiDriver != nullptr)
+				_midiDriver->clearSysExQueue();
+			return;
+		}
+	}
+}
+
+bool SoundManager::isReady() {
+	if (_midiDriver == nullptr)
+		return true;
+
+	return _midiDriver->isReady();
 }
 
 bool SoundManager::isDriverActive() {
@@ -128,11 +145,9 @@ void SoundManager::startQueuedCommands() {
 	}
 }
 
-void SoundManager::setVolume(int volume) {
-	_masterVolume = volume;
-
-	if (_driver)
-		_driver->setVolume(volume);
+void SoundManager::syncSoundSettings() {
+	if (_midiDriver != nullptr)
+		_midiDriver->syncSoundSettings();
 }
 
 int SoundManager::command(int commandId, int param) {
@@ -157,6 +172,11 @@ void SoundManager::stop() {
 		_driver->stop();
 }
 
+void SoundManager::pause(bool pause) {
+	if (_driver != nullptr)
+		_driver->pause(pause);
+}
+
 void SoundManager::noise() {
 	if (_driver)
 		_driver->noise();
@@ -191,7 +211,7 @@ void SoundManager::timerCallback(void *data) {
 //====================================================================
 
 SoundDriver::SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
-		int dataOffset, int dataSize) : _mixer(mixer) {
+		int dataOffset, int dataSize) : _mixer(mixer), _paused(false) {
 	// Open up the appropriate sound file
 	Common::File soundFile;
 	if (!soundFile.open(filename))
@@ -202,4 +222,14 @@ SoundDriver::SoundDriver(Audio::Mixer *mixer, const Common::Path &filename,
 	soundFile.read(&_soundData[0], dataSize);
 }
 
+void SoundDriver::pause(bool paused) {
+	if (_paused == paused)
+		return;
+
+	_paused = paused;
+
+	if (_paused)
+		stopAllNotes();
+}
+
 } // namespace MADS
diff --git a/engines/mads/core/sound_manager.h b/engines/mads/core/sound_manager.h
index f03c36546bb..16680a84d42 100644
--- a/engines/mads/core/sound_manager.h
+++ b/engines/mads/core/sound_manager.h
@@ -42,6 +42,7 @@ protected:
 	Audio::Mixer *_mixer;
 	Common::Array<byte> _soundData;
 	Common::Mutex _driverMutex;
+	bool _paused;
 
 	/**
 	 * Gets a stream starting at a given offset in the loaded sound data
@@ -50,7 +51,7 @@ protected:
 		return Common::MemoryReadStream(&_soundData[offset], _soundData.size() - offset);
 	}
 
-	explicit SoundDriver(Audio::Mixer *mixer) : _mixer(mixer) {}
+	explicit SoundDriver(Audio::Mixer *mixer) : _mixer(mixer), _paused(false) {}
 
 public:
 	/**
@@ -77,6 +78,11 @@ public:
 	 */
 	virtual int stop() = 0;
 
+	/**
+	 * Pause or unpause sound playback
+	 */
+	virtual void pause(bool paused);
+
 	/**
 	 * Main poll method to allow sounds to progress
 	 */
@@ -87,10 +93,13 @@ public:
 	 */
 	virtual void noise() = 0;
 
+	// TODO Remove
+	virtual void setVolume(int volume) { };
+
 	/**
-	 * Set the volume
+	 * Stop all active notes
 	 */
-	virtual void setVolume(int volume) = 0;
+	virtual void stopAllNotes() { }
 };
 
 class SoundManager {
@@ -114,7 +123,6 @@ protected:
 	SoundDriver *_driver = nullptr;
 	bool _newSoundsPaused = false;
 	Common::Queue<QueuedCommand> _queuedCommands;
-	int _masterVolume = 255;
 
 	NativeSoundTimer _hostTimer;
 
@@ -147,6 +155,13 @@ public:
 		return _driver != nullptr;
 	}
 
+	/**
+	 * Sound manager is ready to start sound playback.
+	 * This will return false while the MT-32 driver is sending SysEx messages
+	 * to the unit. Do not start sound playback while this returns false.
+	 */
+	bool isReady();
+
 	/**
 	 * Returns whether the current driver reports an active sound.
 	 */
@@ -180,9 +195,9 @@ public:
 	virtual void startQueuedCommands();
 
 	/**
-	 * Set the master volume
+	 * Applies sound settings from the ScummVM configuration manager.
 	 */
-	void setVolume(int volume);
+	void syncSoundSettings();
 
 	//@{
 	/**
@@ -197,6 +212,11 @@ public:
 	 */
 	void stop();
 
+	/**
+	 * Pauses or unpauses playback.
+	 */
+	void pause(bool pause);
+
 	/**
 	 * Noise
 	 * Some sort of random noise generation?
diff --git a/engines/mads/mads.cpp b/engines/mads/mads.cpp
index a0da8e8d368..e3ca98d940a 100644
--- a/engines/mads/mads.cpp
+++ b/engines/mads/mads.cpp
@@ -25,7 +25,6 @@
 #include "engines/util.h"
 #include "graphics/scaler.h"
 #include "mads/mads.h"
-#include "mads/mads.h"
 #include "mads/core/anim.h"
 #include "mads/core/attr.h"
 #include "mads/core/camera.h"
@@ -428,6 +427,13 @@ void MADSEngine::updateDisplay() {
 	presentScreen(0);
 }
 
+void MADSEngine::pauseEngineIntern(bool pause) {
+	Engine::pauseEngineIntern(pause);
+
+	if (_soundManager != nullptr)
+		_soundManager->pause(pause);
+}
+
 uint32 MADSEngine::getMillis() {
 	pollEvents();
 	return g_system->getMillis();
@@ -445,6 +451,14 @@ bool MADSEngine::isSpeechPlaying() const {
 	return _mixer->isSoundHandleActive(_speechHandle);
 }
 
+void MADSEngine::syncSoundSettings() {
+	Engine::syncSoundSettings();
+	read_sound_settings();
+
+	if (_soundManager != nullptr)
+		_soundManager->syncSoundSettings();
+}
+
 SaveStateList MADSEngine::listSaves() const {
 	return getMetaEngine()->listSaves(_targetName.c_str());
 }
diff --git a/engines/mads/mads.h b/engines/mads/mads.h
index a1301426aaf..20fc018c9f6 100644
--- a/engines/mads/mads.h
+++ b/engines/mads/mads.h
@@ -137,6 +137,8 @@ public:
 	void warpMouse(int x, int y);
 	void updateDisplay();
 
+	void pauseEngineIntern(bool pause) override;
+
 	const Graphics::Surface &getSavegameThumbnail() const {
 		return _savegameThumbnail;
 	}
@@ -209,6 +211,7 @@ public:
 	void playSpeech(Audio::AudioStream *stream);
 	void stopSpeech();
 	bool isSpeechPlaying() const;
+	void syncSoundSettings() override;
 
 	/**
 	 * Sets the timer function to call at 60Hz
diff --git a/engines/mads/nebular/menus.cpp b/engines/mads/nebular/menus.cpp
index 6a5e0a1138c..d6c697123fb 100644
--- a/engines/mads/nebular/menus.cpp
+++ b/engines/mads/nebular/menus.cpp
@@ -1060,6 +1060,7 @@ static void game_menu_options() {
 		save_config = config_file;
 	} else {
 		write_config_file();
+		g_engine->syncSoundSettings();
 	}
 }
 
diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index 9bcf5bb16df..8672b4a0cc2 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -37,8 +37,8 @@ void Channel::loadData(byte *soundData) {
 	_noteDurationOffset = 0;
 	_fadeOutActive = 0;
 	_volumeFadeSpeed = 0xFF;
-	_pitchBend = 64;
-	_panning = 64;
+	_pitchBend = 0x40;
+	_panning = 0x40;
 	_soundDataStart = soundData;
 	_pSrc = soundData;
 	_innerLoopStart = soundData;
@@ -47,7 +47,7 @@ void Channel::loadData(byte *soundData) {
 }
 
 void Channel::setFadeOut(bool fadeOut) {
-	if (_deltaCounter) {
+	if (_deltaCounter > 0) {
 		_fadeOutActive = fadeOut;
 
 		// WORKAROUND: matches the same apparent original-code quirk
@@ -72,24 +72,21 @@ void Channel::playData(byte *soundData) {
 /*-----------------------------------------------------------------------*/
 
 RSound::RSound(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset, RSoundFadeCheckMode fadeCheckMode) : 
+		int dataOffset, int dataSize, int sysExOffset, RSoundFadeCheckMode fadeOutCheckMode) : 
 		SoundDriver(mixer, filename, dataOffset, dataSize) {
 	_commandParam = 0;
 	_ticksSinceLastCommand = 0;
 	_ticksProcessingDisabled = false;
 	_mute = false;
-	_masterVolume = 255;
-	// FIXME RSOUND.008 initializes this to 0x005C; is the initial value different per driver?
-	_randomSeed = 1234;
+	_randomSeed = 0x005C;
 	_runningStatus = 0;
 	_pollResult = 0;
 	_resultFlag = 0;
 	_sysExOffset = sysExOffset;
 	_midiDriver = midiDriver;
-	_fadeCheckMode = fadeCheckMode;
-	_fadeCheckAlternate = false;
-	_fadeCheckCounter = 0;
-	_fadeCheckPeriod = 0;
+	_fadeOutCheckMode = fadeOutCheckMode;
+	_fadeOutSpeed = (_fadeOutCheckMode == kRSoundFadeCheckAlternating ? 2 : 0);
+	_fadeOutCounter = _fadeOutSpeed;
 
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
 		_channels[i]._owner = this;
@@ -158,6 +155,9 @@ int RSound::stop() {
 int RSound::poll() {
 	Common::StackLock slock(_driverMutex);
 
+	if (_paused)
+		return 0;
+
 	processTick();
 
 	int result = _pollResult;
@@ -214,48 +214,45 @@ bool RSound::isSoundPlaying(byte *pData) {
 	return false;
 }
 
-int RSound::generateRandomNumber() {
-	int v = 0x9249 + (int)_randomSeed;
-	_randomSeed = ((v >> 3) | (v << 13)) & 0xFFFF;
+uint16 RSound::generateRandomNumber() {
+	uint16 newValue = 0x9249 + _randomSeed;
+	_randomSeed = ((newValue >> 3) | (newValue << 13)) & 0xFFFF;
 	return _randomSeed;
 }
 
-void RSound::setVolume(int volume) {
-	_masterVolume = CLIP(volume, 0, 255);
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		sendVolume(i + 1, _mute ? 0 : _channels[i]._volume);
+int RSound::getTicksSinceLastCommand() {
+	return _ticksSinceLastCommand;
 }
 
 /*-----------------------------------------------------------------------*/
 
-void RSound::sendNoteOn(int midiChannel, int note, int velocity) {
+void RSound::sendNoteOn(byte midiChannel, byte note, byte velocity) {
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel, note, velocity);
 }
 
-void RSound::sendProgramChange(int midiChannel, int program) {
+void RSound::sendProgramChange(byte midiChannel, byte program) {
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | midiChannel, program, 0);
 }
 
-void RSound::sendVolume(int midiChannel, int volume) {
-	const int scaledVolume = CLIP(volume, 0, 127) * _masterVolume / 255;
+void RSound::sendVolume(byte midiChannel, byte volume) {
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
-			MidiDriver::MIDI_CONTROLLER_VOLUME, scaledVolume);
+					  MidiDriver::MIDI_CONTROLLER_VOLUME, volume);
 }
 
-void RSound::sendPitchBend(int midiChannel, int value) {
+void RSound::sendPitchBend(byte midiChannel, byte value) {
 	// LSB always 0 - only coarse (MSB) control is used
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_PITCH_BEND | midiChannel, 0, value);
 }
 
-void RSound::sendPanning(int midiChannel, int value) {
+void RSound::sendPanning(byte midiChannel, byte value) {
 	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel, MidiDriver::MIDI_CONTROLLER_PANNING, value);
 }
 
-void RSound::muteChannel(int midiChannel) {
+void RSound::muteChannel(byte midiChannel) {
 	sendVolume(midiChannel, 0);
 }
 
-void RSound::unmuteChannel(int midiChannel, int volume) {
+void RSound::unmuteChannel(byte midiChannel, byte volume) {
 	sendVolume(midiChannel, volume);
 }
 
@@ -269,12 +266,7 @@ byte *RSound::sendSysExData(byte *pData, uint maxLength) {
 		return nullptr;
 	}
 
-	// FIXME This call adds the necessary delay for the MT-32 to process the
-	// SysEx message, which will make the engine unresponsive.
-	// This can be fixed using the SysEx queue (specify true as 3rd param).
-	// Driver status can then be checked from the main event loop using
-	// _midiDriver->isReady().
-	_midiDriver->sysExMT32(pData, length);
+	_midiDriver->sysExMT32(pData, length, true);
 
 	return &pData[length];
 }
@@ -315,12 +307,16 @@ void RSound::sendSysExSequence() {
 	}
 }
 
+void RSound::stopAllNotes() {
+	_midiDriver->stopAllNotes();
+}
+
 /*-----------------------------------------------------------------------*/
 
 void RSound::Channel_turnOffActiveNotes(Channel *channel) {
 	byte *channelActiveNotes = _activeNotes[channel->_midiChannel];
 
-	for (int i = 0; i < 4; ++i) {
+	for (int i = 0; i < RSOUND_ACTIVE_NOTES_COUNT; ++i) {
 		if (channelActiveNotes[i] == 0xFF)
 			break;
 
@@ -330,9 +326,7 @@ void RSound::Channel_turnOffActiveNotes(Channel *channel) {
 }
 
 void RSound::Channel_processFadeOut(Channel *channel) {
-	if (!channel->_deltaCounter)
-		return;
-	if (!channel->_fadeOutActive)
+	if (channel->_deltaCounter == 0 || !channel->_fadeOutActive)
 		return;
 
 	if (channel->_volume != 0) {
@@ -352,28 +346,32 @@ void RSound::Channel_processFadeOut(Channel *channel) {
 }
 
 void RSound::processChannelFadeOuts() {
-	if (_fadeCheckMode == kRSoundFadeCheckAlternating) {
-		_fadeCheckAlternate = !_fadeCheckAlternate;
-		if (_fadeCheckAlternate)
-			return;
-	} else {
-		if (!_fadeCheckPeriod)
-			return;
-		if (--_fadeCheckCounter > 0)
-			return;
+	if (_fadeOutSpeed == 0 || --_fadeOutCounter > 0)
+		return;
 
-		_fadeCheckCounter = _fadeCheckPeriod;
-	}
+	_fadeOutCounter = _fadeOutSpeed;
 
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
 		Channel_processFadeOut(&_channels[i]);
 }
 
+void RSound::setFadeOutSpeed(int fadeOutSpeed) {
+	if (_fadeOutCheckMode == kRSoundFadeCheckProgrammable)
+		_fadeOutSpeed = fadeOutSpeed;
+}
+
 void RSound::Channel_processTick(Channel *channel) {
-	if (channel->_deltaCounter == 0)
+	if (channel->_deltaCounter == 0) {
+		channel->_pitchSlideStepSize = 0;
+		channel->_volumeFadeStepSize = 0;
+		channel->_panningSweepStepSize = 0;
+		// Original code continues with the effects processing code after the
+		// event processing loop, but because all effects have been disabled
+		// by setting step size to 0, this does nothing.
 		return;
+	}
 
-	int midiChannel = channel->_midiChannel;
+	byte midiChannel = channel->_midiChannel;
 
 	if (channel->_noteDurationCounter > 0 && --channel->_noteDurationCounter == 0)
 		Channel_turnOffActiveNotes(channel);
@@ -383,207 +381,313 @@ void RSound::Channel_processTick(Channel *channel) {
 		while (!chordEventProcessed) {
 			byte *pSrc = channel->_pSrc;
 
-			if (*pSrc < 0x80) {
-				// Plain (note, duration) pair
+			if (pSrc[0] < 0x80) {
+				// Plain (note, delta) pair
 				byte note = pSrc[0];
-				byte duration = pSrc[1];
+				byte delta = pSrc[1];
 				channel->_note = note;
-				channel->_deltaCounter = duration;
-				channel->_pSrc = pSrc + 2;
+				channel->_deltaCounter = delta;
+				channel->_pSrc += 2;
 
-				if (!note || !duration) {
+				// Opcode 0x00: all notes off
+				// Delta 0: stop channel playback
+				if (note == 0 || delta == 0) {
 					Channel_turnOffActiveNotes(channel);
 				} else {
-					channel->_noteDurationCounter = channel->_deltaCounter - channel->_noteDurationOffset;
-
-					bool skipRetrigger = false;
-					if ((int8)channel->_noteDurationOffset < 0 && _activeNotes[midiChannel][0] == note)
-						skipRetrigger = true;
-
-					if (!skipRetrigger) {
+					int duration = delta - channel->_noteDurationOffset;
+					if (duration < 0) {
+						// In the original code this calculation can result in underflow
+						// of the duration, which is a byte value. Not sure if this
+						// actually happens in any of the sound data.
+						warning("Calculated note duration is negative");
+						// Assignment to the noteDurationCounter byte field should
+						// result in the same behavior as the original code, i.e.
+						// duration -1 becomes counter value 0xFF.
+					}
+					channel->_noteDurationCounter = duration;
+
+					// If the note duration offset is 0 or positive, the notes played by
+					// the previous note or chord event had a duration that was equal to
+					// or shorter than the event delta and are already turned off, so the
+					// new note should always be played. Otherwise, if this note is
+					// already the first active note on the channel, the note should
+					// remain active and will not be retriggered.
+					// Note that a chord event could also have already played the new
+					// note as the second to fourth active note; this is not checked.
+					if (channel->_noteDurationOffset >= 0 || _activeNotes[midiChannel][0] != note) {
 						Channel_turnOffActiveNotes(channel);
 						sendNoteOn(midiChannel, note, channel->_velocity);
 					}
 					_activeNotes[midiChannel][0] = note;
 				}
 
+				// An event delta has been set, so break out of the event processing loop.
 				break;
 			}
 
 			// Opcode dispatch (bytes 0xF1-0xFF)
-			int opcode = *pSrc - 0xF1;
+			byte opcode = pSrc[0];
 			switch (opcode) {
-			case 0: // 0xF1: no-op (consume and ignore)
-				channel->_pSrc = pSrc + 2;
+			case 0xF1: // No-op (consume and ignore)
+				channel->_pSrc += 2;
 				break;
 
-			case 1: { // 0xF2: self-modifying randomize (byte swap in the data stream)
-				int v1 = *++pSrc;
-				++pSrc;
-				int v2 = (v1 - 1) & generateRandomNumber();
-				int v3 = pSrc[v2];
-				int v4 = pSrc[v1];
-				pSrc[v4 + v1 + 1] = v3;
-				channel->_pSrc += v1 + 3;
+			case 0xF2: { // Randomize
+				// Event data byte 1: number of values.
+				byte numValues = pSrc[1];
+				// Event data byte 2+: values.
+				// Generate a random index to pick one of the values.
+				byte randomIndex = (numValues - 1) & generateRandomNumber();
+				byte randomValue = pSrc[2 + randomIndex];
+				// Last event data byte: data offset where the random value should be written.
+				byte dataOffset = pSrc[2 + numValues];
+				// Advance sound data pointer to after this event.
+				channel->_pSrc += numValues + 3;
+				// Write the random value at the data offset after this event.
+				channel->_pSrc[dataOffset] = randomValue;
 				break;
 			}
 
-			case 2: // 0xF3: setup pan fade
+			case 0xF3: // Start panning sweep
 				channel->_panningSweepSpeed = pSrc[1];
-				channel->_panningSweepStepSize = pSrc[2];
+				channel->_panningSweepStepSize = (int8) pSrc[2];
 				channel->_panningSweepCounter = 1;
-				channel->_pSrc = pSrc + 3;
+				channel->_pSrc += 3;
 				break;
 
-			case 3: // 0xF4: set pan directly and send
+			case 0xF4: // Panning
 				channel->_panning = pSrc[1];
 				sendPanning(midiChannel, channel->_panning);
-				channel->_pSrc = pSrc + 2;
+				channel->_pSrc += 2;
 				break;
 
-			case 4: { // 0xF5: chord note-on trigger (up to 4 simultaneous notes)
+			case 0xF5: { // Chord
+				// Event data byte 1: number of notes in the chord
 				byte noteCount = pSrc[1];
+				byte noteBytesCount = pSrc[1];
+				if (noteCount > RSOUND_ACTIVE_NOTES_COUNT) {
+					// The original code does not check the note count.
+					// It will play more than 4 notes and write them
+					// into the active note data of the next channel
+					// or out of bounds. Some tracks contain chords
+					// with more than 4 notes.
+					warning("Encountered chord event with note count %i; limiting to %i notes", noteCount, RSOUND_ACTIVE_NOTES_COUNT);
+					noteCount = RSOUND_ACTIVE_NOTES_COUNT;
+				}
+				// Event data byte 2+: note values
 				byte *notes = pSrc + 2;
-				byte *slots = _activeNotes[midiChannel];
+				byte *activeNotes = _activeNotes[midiChannel];
 
 				int i;
 				for (i = 0; i < noteCount; ++i) {
 					byte note = notes[i];
-					if (slots[i] != note) {
+					// Do not play the note again if it is already registered
+					// at this index in the active notes data. The note could
+					// also be registered at another index, but this is not
+					// checked.
+					if (activeNotes[i] != note) {
 						sendNoteOn(midiChannel, note, channel->_velocity);
-						slots[i] = note;
+						activeNotes[i] = note;
 					}
 				}
-				for (; i < 4; ++i)
-					slots[i] = 0xFF;
-
-				byte duration = notes[noteCount];
-				channel->_deltaCounter = duration;
-				if (channel->_noteDurationOffset != 0xFF) {
-					channel->_noteDurationCounter = (duration < channel->_noteDurationOffset) ?
-						duration : duration - channel->_noteDurationOffset;
+				// Clear the remaining active note entries for this channel.
+				for (; i < RSOUND_ACTIVE_NOTES_COUNT; ++i)
+					activeNotes[i] = 0xFF;
+
+				// Last event data byte: delta
+				byte delta = pSrc[noteBytesCount + 2];
+				channel->_deltaCounter = delta;
+				// Subtract note duration offset from the delta to get the
+				// chord duration.
+				// Note that the duration is determined here in a different
+				// way than for a note event. The offset is checked for
+				// the value -1 instead of < 0, suggesting that -1 is the
+				// minimum value. Also, a check is added to make sure the
+				// duration does not underflow.
+				byte duration;
+				if (channel->_noteDurationOffset == -1) {
+					duration = delta + 1;
 				} else {
-					channel->_noteDurationCounter = duration + 1;
+					duration = (delta < channel->_noteDurationOffset) ? delta : delta - channel->_noteDurationOffset;
 				}
+				channel->_noteDurationCounter = duration;
 
-				channel->_pSrc = pSrc + noteCount + 3;
+				channel->_pSrc += noteBytesCount + 3;
+				// An event delta has been set, so break out of the event
+				// processing loop.
 				chordEventProcessed = true;
 				break;
 			}
 
-			case 5: // 0xF6: set volume directly and send (priority-gated while pending-stop)
+			case 0xF6: // Volume
+				// If the channel is fading out to stop and the new volume
+				// value is higher than the current value, do not set
+				// the new value.
 				if (!channel->_fadeOutActive || channel->_volume >= pSrc[1]) {
 					channel->_volume = pSrc[1];
 					sendVolume(midiChannel, channel->_volume);
 				}
-				channel->_pSrc = pSrc + 2;
+				channel->_pSrc += 2;
 				break;
 
-			case 6: // 0xF7: set pitch bend directly and send
+			case 0xF7: // Pitch bend
+				// Only the pitch bend MSB is used.
 				channel->_pitchBend = pSrc[1];
 				sendPitchBend(midiChannel, channel->_pitchBend);
-				channel->_pSrc = pSrc + 2;
+				channel->_pSrc += 2;
 				break;
 
-			case 7: // 0xF8: setup volume fade (gated by pending-stop)
+			case 0xF8: // Start volume fade
+				// If the channel is already fading out to stop, do not start
+				// a volume fade.
 				if (!channel->_fadeOutActive) {
 					channel->_volumeFadeSpeed = pSrc[1];
 					channel->_volumeFadeCounter = pSrc[1];
-					channel->_volumeFadeStepSize = pSrc[2];
+					channel->_volumeFadeStepSize = (int8) pSrc[2];
 				}
-				channel->_pSrc = pSrc + 3;
+				channel->_pSrc += 3;
 				break;
 
-			case 8: // 0xF9: set velocity
+			case 0xF9: // Note velocity
+				// Velocity is not part of a note event; it is set using this
+				// event as a channel setting.
 				channel->_velocity = pSrc[1];
-				channel->_pSrc = pSrc + 2;
+				channel->_pSrc += 2;
 				break;
 
-			case 9: // 0xFA: setup pitch-bend ramp
+			case 0xFA: // Start pitch slide
 				channel->_pitchSlideSpeed = pSrc[1];
 				channel->_pitchSlideStepSize = (int8) pSrc[2];
 				channel->_pitchSlideDurationCounter = pSrc[3];
 				channel->_pitchSlideCounter = 1;
-				channel->_pSrc = pSrc + 4;
+				channel->_pSrc += 4;
 				break;
 
-			case 10: // 0xFB: set note offset
-				channel->_noteDurationOffset = pSrc[1];
-				channel->_pSrc = pSrc + 2;
+			case 0xFB: // Note duration offset
+				channel->_noteDurationOffset = (int8) pSrc[1];
+				channel->_pSrc += 2;
 				break;
 
-			case 11: // 0xFC: program change
+			case 0xFC: // Program change
 				channel->_program = pSrc[1];
 				sendProgramChange(midiChannel, channel->_program);
-				channel->_pSrc = pSrc + 2;
+				channel->_pSrc += 2;
 				break;
 
-			case 12: { // 0xFD: full reset / loop restart, preserving pending-stop
+			case 0xFD: { // Loop to start
+				// Reload data to completely reset channel state, but keep
+				// fade-out to stop state.
 				bool fadeOutActive = channel->_fadeOutActive;
 				channel->loadData(channel->_soundDataStart);
 				channel->_fadeOutActive = fadeOutActive;
 				break;
 			}
 
-			case 13: // 0xFE: outer loop
-				if (!channel->_outerLoopCounter) {
-					if (*++pSrc == 0) {
+			case 0xFE: { // Outer loop
+				byte repeats = pSrc[1];
+				if (channel->_outerLoopCounter == 0) {
+					// Outer loop is inactive. Set loop start point or start looping.
+					if (repeats == 0) {
+						// Event data byte is 0: set loop start point after this event.
+						// Also set inner loop start point to keep it contained within
+						// the outer loop.
 						channel->_pSrc += 2;
 						channel->_outerLoopStart = channel->_pSrc;
 						channel->_innerLoopStart = channel->_pSrc;
-						channel->_innerLoopCounter = 0;
 						channel->_outerLoopCounter = 0;
+						channel->_innerLoopCounter = 0;
 					} else {
-						channel->_outerLoopCounter =
-							(uint16)(int16)(int8)*pSrc;
+						// Event data byte is > 0: set number of repeats and loop back
+						// to the loop start point. Also reset the inner loop start
+						// point to the outer loop start point.
+						channel->_outerLoopCounter = repeats;
 						channel->_pSrc = channel->_outerLoopStart;
 						channel->_innerLoopStart = channel->_outerLoopStart;
 					}
-				} else if (--channel->_outerLoopCounter) {
+				} else if (--channel->_outerLoopCounter > 0) {
+					// Outer loop is active and counter is >= 1: loop back to the loop
+					// start point. Also reset the inner loop start point to the outer
+					// loop start point.
 					channel->_pSrc = channel->_outerLoopStart;
 					channel->_innerLoopStart = channel->_outerLoopStart;
 				} else {
+					// Outer loop is active and was counted down to 0. Continue playback
+					// with the first event after the loop end point.
+					// This also sets both loop start points to that event.
 					channel->_pSrc += 2;
 					channel->_outerLoopStart = channel->_pSrc;
 					channel->_innerLoopStart = channel->_pSrc;
 				}
 				break;
+			}
 
-			case 14: // 0xFF: inner loop
-				if (!channel->_innerLoopCounter) {
-					if (*++pSrc == 0) {
+			case 0xFF: { // Inner loop
+				byte repeats = pSrc[1];
+				if (channel->_innerLoopCounter == 0) {
+					// Inner loop is inactive. Set loop start point or start looping.
+					if (repeats == 0) {
+						// Event data byte is 0: set loop start point after this event.
 						channel->_pSrc += 2;
 						channel->_innerLoopStart = channel->_pSrc;
 						channel->_innerLoopCounter = 0;
 					} else {
-						channel->_innerLoopCounter =
-							(uint16)(int16)(int8)*pSrc;
+						// Event data byte is > 0: set number of repeats and loop back
+						// to the loop start point.
+						channel->_innerLoopCounter = repeats;
 						channel->_pSrc = channel->_innerLoopStart;
 					}
-				} else if (--channel->_innerLoopCounter) {
+				} else if (--channel->_innerLoopCounter > 0) {
+					// Inner loop is active and counter is >= 1: loop back to the loop
+					// start point.
 					channel->_pSrc = channel->_innerLoopStart;
 				} else {
+					// Inner loop is active and was counted down to 0. Continue playback
+					// with the first event after the loop end point.
+					// This also sets the loop start point to that event.
 					channel->_pSrc += 2;
 					channel->_innerLoopStart = channel->_pSrc;
 				}
 				break;
-
+			}
+			
 			default:
-				break;
+				// When encountering an invalid opcode, the original code would
+				// not advance the sound data pointer or break out of the event
+				// processing loop. This causes the code to hang on this opcode.
+				// Instead, playback for this channel is stopped here.
+				warning("Invalid sound opcode %02X encountered; stopping channel", opcode);
+				channel->_deltaCounter = 0;
+				return;
 			}
 		}
 	}
 
-	// Fade tail: pitch bend, volume, pan
+	// Process effects
+
+	// Volume fade
+	if (channel->_volumeFadeStepSize > 0 && --channel->_volumeFadeCounter == 0) {
+		channel->_volumeFadeCounter = channel->_volumeFadeSpeed;
+		int newVolume = channel->_volume + channel->_volumeFadeStepSize;
+		if (newVolume < 0 || newVolume > 0x7F) {
+			// Volume has reached minimum or maximum volume. Stop the fade.
+			channel->_volumeFadeStepSize = 0;
+			newVolume = CLIP(newVolume, 0, 0x7F);
+		}
+		channel->_volume = newVolume;
+		sendVolume(midiChannel, newVolume);
+	}
+
+	// Pitch slide
 	if (channel->_pitchSlideStepSize != 0) {
 		if (--channel->_pitchSlideCounter == 0) {
 			channel->_pitchSlideCounter = channel->_pitchSlideSpeed;
 			int newPitchBend = channel->_pitchBend + channel->_pitchSlideStepSize;
 			if (newPitchBend < 0 || newPitchBend > 0x7F) {
-				// Overflow will cause the driver to output invalid MIDI events.
+				// There is no bounds checking in the original code. Overflow or
+				// underflow will cause the driver to output invalid MIDI events.
 				// This actually happens in the original code, in the intro,
 				// when Rex lands his ship.
-				warning("Pitch bend overflow; terminating pitch slide");
+				warning("Pitch bend overflow or underflow; terminating pitch slide");
 				channel->_pitchSlideStepSize = 0;
 				newPitchBend = CLIP(newPitchBend, 0, 0x7F);
 			}
@@ -594,34 +698,22 @@ void RSound::Channel_processTick(Channel *channel) {
 			channel->_pitchSlideStepSize = 0;
 	}
 
-	if (channel->_volumeFadeStepSize) {
-		if (!--channel->_volumeFadeCounter) {
-			channel->_volumeFadeCounter = channel->_volumeFadeSpeed;
-			int volumeSum = (int8)channel->_volume + (int8)channel->_volumeFadeStepSize;
-			if (volumeSum < 0) {
-				channel->_volumeFadeStepSize = 0;
-				volumeSum = 0;
-			} else if (volumeSum >= 0x7F) {
-				channel->_volumeFadeStepSize = 0;
-				volumeSum = 0x7F;
-			}
-			int8 newVolume = (int8)volumeSum;
-			channel->_volume = newVolume;
-			sendVolume(midiChannel, newVolume);
-		}
-	}
-
-	if (channel->_panningSweepStepSize) {
-		if (!--channel->_panningSweepCounter) {
-			channel->_panningSweepCounter = channel->_panningSweepSpeed;
-			byte newPan = channel->_panning + channel->_panningSweepStepSize;
-			if (newPan > 0x7F) {
-				newPan = (newPan ^ 0x7F) & 0x7F;
-				channel->_panningSweepCounter = 0;
-			}
-			channel->_panning = newPan;
-			sendPanning(midiChannel, newPan);
+	// Panning sweep
+	if (channel->_panningSweepStepSize != 0 && --channel->_panningSweepCounter == 0) {
+		channel->_panningSweepCounter = channel->_panningSweepSpeed;
+		byte newPanning = channel->_panning + channel->_panningSweepStepSize;
+		if (newPanning > 0x7F) {
+			// This inverts overflow or underflow back into the valid range
+			// of 0x00-0x7F; f.e. -3 becomes 2 and 0x83 becomes 0x7C.
+			newPanning = (newPanning ^ 0x7F) & 0x7F;
+			// This causes the next sweep step to be executed in 256 ticks.
+			// This is accurate to the original code, but maybe the
+			// intention was to stop the sweep by setting the step size
+			// to 0?
+			channel->_panningSweepCounter = 0;
 		}
+		channel->_panning = newPanning;
+		sendPanning(midiChannel, newPanning);
 	}
 }
 
@@ -672,7 +764,7 @@ void RSound::resetChannelRange(int firstChannel, int lastChannel, bool includeCh
 
 void RSound::clearActiveNotes() {
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT + 1; ++i)
-		for (int j = 0; j < 4; ++j)
+		for (int j = 0; j < RSOUND_ACTIVE_NOTES_COUNT; ++j)
 			_activeNotes[i][j] = 0xFF;
 }
 
@@ -680,10 +772,14 @@ void RSound::clearActiveNotesRange(int firstChannel, int lastChannel) {
 	assert(firstChannel >= 1 && lastChannel <= RSOUND_CHANNEL_COUNT &&
 			firstChannel <= lastChannel);
 	for (int channel = firstChannel; channel <= lastChannel; ++channel)
-		for (int slot = 0; slot < 4; ++slot)
+		for (int slot = 0; slot < RSOUND_ACTIVE_NOTES_COUNT; ++slot)
 			_activeNotes[channel][slot] = 0xFF;
 }
 
+byte *RSound::loadData(int offset) {
+	return &_soundData[offset];
+}
+
 /**
  * Resets all 9 channels and the active notes table.
  * Called both from the constructor (mirroring rsound_init) and from
@@ -710,7 +806,7 @@ void RSound::sendMidiChannelReset(int first, int last) {
 
 int RSound::command0() {
 	resetAllChannels();
-	setFadeCheckPeriod(0);
+	setFadeOutSpeed(0);
 	sendMidiChannelReset(1, RSOUND_CHANNEL_COUNT);
 
 	// Matches the tail of the original rsound_command0.
@@ -724,7 +820,7 @@ int RSound::command0() {
 }
 
 int RSound::command1() {
-	setFadeCheckPeriod(1);
+	setFadeOutSpeed(1);
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
 		_channels[i].setFadeOut(true);
 	return 0;
@@ -735,7 +831,7 @@ int RSound::command2() {
 	// table) plus the MIDI channel reset for those same channels.
 	resetChannelRange(1, 4, true);
 	clearActiveNotes();
-	setFadeCheckPeriod(0);
+	setFadeOutSpeed(0);
 	// The original code does not reset MIDI channel 9, which is
 	// probably an oversight.
 	sendMidiChannelReset(1, 4);
@@ -745,7 +841,7 @@ int RSound::command2() {
 
 int RSound::command3() {
 	// Start fade-out to stop for channels 1-4 and 9.
-	setFadeCheckPeriod(1);
+	setFadeOutSpeed(1);
 	for (int i = 0; i < 4; ++i)
 		_channels[i].setFadeOut(true);
 	_channels[8].setFadeOut(true);
@@ -756,7 +852,7 @@ int RSound::command4() {
 	// Channels 5-8 (does NOT touch the held-notes
 	// table) plus the MIDI channel reset for those same channels.
 	resetChannelRange(5, 8);
-	setFadeCheckPeriod(0);
+	setFadeOutSpeed(0);
 	// The original code also resets MIDI channel 9, which is
 	// probably incorrect.
 	sendMidiChannelReset(5, 8);
@@ -765,7 +861,7 @@ int RSound::command4() {
 
 int RSound::command5() {
 	// Start fade-out to stop for channels 5-8.
-	setFadeCheckPeriod(1);
+	setFadeOutSpeed(1);
 	for (int i = 4; i < 8; ++i)
 		_channels[i].setFadeOut(true);
 	return 0;
@@ -795,6 +891,10 @@ int RSound::command8() {
 	return result;
 }
 
+int RSound::nullCommand() {
+	return 0;
+}
+
 } // namespace Sound
 } // namespace RexNebular
 } // namespace MADS
diff --git a/engines/mads/nebular/sound/rsound.h b/engines/mads/nebular/sound/rsound.h
index b6257b02443..cee08d24970 100644
--- a/engines/mads/nebular/sound/rsound.h
+++ b/engines/mads/nebular/sound/rsound.h
@@ -34,7 +34,14 @@ namespace Sound {
 class RSound;
 
 #define RSOUND_CHANNEL_COUNT 9
-
+// The number of active notes registered per channel.
+// The original code used 4, but some chord events contain more notes and
+// overwrite in the active notes for the next channel.
+#define RSOUND_ACTIVE_NOTES_COUNT 8
+
+// Mode for fade-out to stop checks.
+// kRSoundFadeCheckAlternating: fixed speed of 2 (every other tick)
+// kRSoundFadeCheckProgrammable: speed is programmable using setFadeOutSpeed
 enum RSoundFadeCheckMode {
 	kRSoundFadeCheckAlternating,
 	kRSoundFadeCheckProgrammable
@@ -72,37 +79,37 @@ enum RSoundFadeCheckMode {
 class Channel {
 public:
 	RSound *_owner = nullptr;
-	int _midiChannel = 0;				// 1-9: the MIDI channel to which the data in this struct pertains
-
-	int _deltaCounter = 0;				// number of ticks until the next event occurs; loaded from the delta byte of a note or chord event
-										// 0: channel is not active
-	int _pitchSlideStepSize = 0;		// delta added to _pitchBend each pitch-slide step; 0: pitch slide is not active
-	int _volumeFadeStepSize = 0;		// delta added to _volume each volume fade step; 0: volume fade is not active
-	int _panningSweepStepSize = 0;		// delta added to _panning each panning sweep step; 0: panning sweep is not active
-	int _note = 0;						// MIDI note number, read from the note or chord event
-	int _program = 0;					// patch/instrument number, sent as a Program Change
-	int _velocity = 0;					// note velocity, used by RSound::sendNoteOn()
-	int _noteDurationOffset = 0;		// subtracted from the event delta to derive the note duration; positive offset: note is turned off
-										// before the next event is processed. Data might only use up to -1 in the negative direction.
-	int _noteDurationCounter = 0;		// number of ticks until the currently active note(s) is/are turned off
-	int _volumeFadeCounter = 0;			// number of ticks until the next volume fade step is processed
-	int _pitchSlideCounter = 0;			// number of ticks until the next pitch slide step is processed
-	int _panningSweepCounter = 0;		// number of ticks until the next panning sweep step is processed
-	int _volume = 0;					// current channel volume (MIDI CC#7)
-	int _pitchBend = 0;					// current pitch bend value (status 0xEn, coarse/MSB only); 0x40 = center
-	int _panning = 0;					// current pan value (MIDI CC#10); 0x40 = center
-	int _volumeFadeSpeed = 0;			// number of ticks between volume fade steps
-	int _pitchSlideSpeed = 0;			// number of ticks between pitch slide steps
-	int _panningSweepSpeed = 0;			// number of ticks between panning sweep steps
-	int _pitchSlideDurationCounter = 0;	// number of ticks until the pitch slide ends
-	bool _fadeOutActive = false;		// true while the channel is fading out to silence (not to be confused with a volume fade)
-	byte *_soundDataStart = nullptr;	// start of the sound data stream playing on this channel
-	byte *_pSrc = nullptr;				// current read pointer into the sound data stream
-	byte *_innerLoopStart = nullptr;	// inner loop restart address
-	byte *_outerLoopStart = nullptr;	// outer loop restart address
-	int _innerLoopCounter = 0;			// number of repeats of the inner loop remaining
-	int _outerLoopCounter = 0;			// number of repeats of the outer loop remaining
-	byte *_soundData = nullptr;			// identifies the sound data played by this channel; effectively the same as _soundDataStart
+	byte _midiChannel = 0;					// 1-9: the MIDI channel to which the data in this struct pertains
+
+	byte _deltaCounter = 0;					// number of ticks until the next event occurs; loaded from the delta byte of a note or chord event
+											// 0: channel is not active
+	int8 _pitchSlideStepSize = 0;			// delta added to _pitchBend each pitch-slide step; 0: pitch slide is not active
+	int8 _volumeFadeStepSize = 0;			// delta added to _volume each volume fade step; 0: volume fade is not active
+	int8 _panningSweepStepSize = 0;			// delta added to _panning each panning sweep step; 0: panning sweep is not active
+	byte _note = 0;							// MIDI note number, read from the note or chord event
+	byte _program = 0;						// patch/instrument number, sent as a Program Change
+	byte _velocity = 0;						// note velocity, used by RSound::sendNoteOn()
+	int8 _noteDurationOffset = 0;			// subtracted from the event delta to derive the note duration; positive offset: note is turned off
+											// before the next event is processed. Data might only use up to -1 in the negative direction.
+	byte _noteDurationCounter = 0;			// number of ticks until the currently active note(s) is/are turned off
+	byte _volumeFadeCounter = 0;			// number of ticks until the next volume fade step is processed
+	byte _pitchSlideCounter = 0;			// number of ticks until the next pitch slide step is processed
+	byte _panningSweepCounter = 0;			// number of ticks until the next panning sweep step is processed
+	byte _volume = 0;						// current channel volume (MIDI CC#7)
+	byte _pitchBend = 0;					// current pitch bend value (status 0xEn, coarse/MSB only); 0x40 = center
+	byte _panning = 0;						// current pan value (MIDI CC#10); 0x40 = center
+	byte _volumeFadeSpeed = 0;				// number of ticks between volume fade steps
+	byte _pitchSlideSpeed = 0;				// number of ticks between pitch slide steps
+	byte _panningSweepSpeed = 0;			// number of ticks between panning sweep steps
+	byte _pitchSlideDurationCounter = 0;	// number of ticks until the pitch slide ends
+	bool _fadeOutActive = false;			// true while the channel is fading out to silence (not to be confused with a volume fade)
+	byte *_soundDataStart = nullptr;		// start of the sound data stream playing on this channel
+	byte *_pSrc = nullptr;					// current read pointer into the sound data stream
+	byte *_innerLoopStart = nullptr;		// inner loop restart address
+	byte *_outerLoopStart = nullptr;		// outer loop restart address
+	byte _innerLoopCounter = 0;				// number of repeats of the inner loop remaining
+	byte _outerLoopCounter = 0;				// number of repeats of the outer loop remaining
+	byte *_soundData = nullptr;				// identifies the sound data played by this channel; effectively the same as _soundDataStart
 
 public:
 	Channel() {}
@@ -139,14 +146,15 @@ class RSound : public SoundDriver {
 	friend class Channel;
 private:
 	uint16 _randomSeed;
-	int _masterVolume;
-	byte _runningStatus;							// running-status cache, avoids resending an unchanged status byte
-													// Note that the ScummVM MIDI drivers do not use this; they always send the status byte
-	byte _activeNotes[RSOUND_CHANNEL_COUNT + 1][4];	// The note(s) currently playing on each MIDI channel (index 0 unused; channels are 1-9)
-	RSoundFadeCheckMode _fadeCheckMode;
-	bool _fadeCheckAlternate;
-	int _fadeCheckCounter;
-	int _fadeCheckPeriod;
+	// running-status cache, avoids resending an unchanged status byte
+	// Note that the ScummVM MIDI drivers do not use this; they always send the status byte
+	byte _runningStatus;
+	// The note(s) currently playing on each MIDI channel (index 0 unused; channels are 1-9)
+	byte _activeNotes[RSOUND_CHANNEL_COUNT + 1][RSOUND_ACTIVE_NOTES_COUNT];
+
+	RSoundFadeCheckMode _fadeOutCheckMode;
+	int _fadeOutCounter;
+	int _fadeOutSpeed;
 
 	/**
 	 * Data-segment offset of this driver's own "command0_array" (the
@@ -163,6 +171,26 @@ private:
 
 	void processTick();
 	void processTickAllChannels();
+	/**
+	 * Brief description of the event loop:
+	 * This function reads an opcode byte plus a number of data bytes (depending on the operation)
+	 * from the sound data stream. Opcodes 0x00 - 0x7F are note events, playing the MIDI note
+	 * indicated by the opcode. Opcodes 0xF1-0xFF do things like setting volume, program change
+	 * and starting a panning sweep. Opcode 0xF5 plays a chord i.e. multiple notes at the same
+	 * time (up to 4). The note events and the chord event are the only events that specify a
+	 * delta as (one of) the data byte(s). This delta is the number of ticks until the next event
+	 * should be processed. The other events are processed one after the other in the same tick,
+	 * until an event with a delta is encountered.
+	 * The note and chord events also have a duration, which is the number of ticks until the
+	 * played note(s) is/are turned off. By default this duration is equal to the event delta,
+	 * but this can be changed by setting the note duration offset (opcode 0xFB). This offset is
+	 * subtracted from the event delta to determine the note duration. So a positive offset will
+	 * cause notes to end before the next event is processed, while a negative offset will overlap
+	 * the notes with the next events. The MIDI convention of pairing note on events with note off
+	 * events is not used, but opcode 0x00 can be used to turn off all active notes.
+	 * Specifying an event delta of 0 will stop playback. Because of this, when starting playback
+	 * of sound data for a channel, a positive delta (usually 1) must be set on the channel.
+	 */
 	void Channel_processTick(Channel *channel);
 
 	/**
@@ -189,10 +217,7 @@ private:
 protected:
 	int _commandParam;
 
-	void setFadeCheckPeriod(int period) {
-		if (_fadeCheckMode == kRSoundFadeCheckProgrammable)
-			_fadeCheckPeriod = period;
-	}
+	void setFadeOutSpeed(int fadeOutSpeed);
 
 	/**
 	 * Clear the active and fade state for MIDI channels in [first, last].
@@ -206,9 +231,7 @@ protected:
 	/** Reset active note slots for the inclusive MIDI-channel range. */
 	void clearActiveNotesRange(int firstChannel, int lastChannel);
 
-	byte *loadData(int offset) {
-		return &_soundData[offset];
-	}
+	byte *loadData(int offset);
 
 	/**
 	 * Hook called once per processTick() frame, immediately after the disabled
@@ -250,17 +273,17 @@ protected:
 	 */
 	bool isSoundPlaying(byte *pData);
 
-	int generateRandomNumber();
+	uint16 generateRandomNumber();
 
 	// ---- Low-level MIDI send helpers -------------------------------
 	// All send through the ScummVM MT-32 / General MIDI driver.
-	void sendNoteOn(int midiChannel, int note, int velocity);
-	void sendProgramChange(int midiChannel, int program);
-	void sendVolume(int midiChannel, int volume);
-	void sendPitchBend(int midiChannel, int value);
-	void sendPanning(int midiChannel, int value);
-	void muteChannel(int midiChannel);
-	void unmuteChannel(int midiChannel, int volume);
+	void sendNoteOn(byte midiChannel, byte note, byte velocity);
+	void sendProgramChange(byte midiChannel, byte program);
+	void sendVolume(byte midiChannel, byte volume);
+	void sendPitchBend(byte midiChannel, byte value);
+	void sendPanning(byte midiChannel, byte value);
+	void muteChannel(byte midiChannel);
+	void unmuteChannel(byte midiChannel, byte volume);
 
 	/**
 	 * Resets the MIDI channel state (all notes off, reset all
@@ -292,6 +315,12 @@ protected:
 	 */
 	void sendSysExSequence();
 
+	/**
+	 * Stop all notes playing on the device. Used when pausing or
+	 * quitting the engine.
+	 */
+	void stopAllNotes() override;
+
 	virtual int command0();
 	int command1();
 	int command2();
@@ -302,9 +331,7 @@ protected:
 	int command7();
 	int command8();
 
-	int nullCommand() {
-		return 0;
-	}
+	int nullCommand();
 
 public:
 	Channel _channels[RSOUND_CHANNEL_COUNT];
@@ -338,11 +365,7 @@ public:
 	void noise() override {
 		// No equivalent in the Roland driver - noise() is an Adlib/OPL-only concept
 	}
-	void setVolume(int volume) override;
-
-	int getTicksSinceLastCommand() {
-		return _ticksSinceLastCommand;
-	}
+	int getTicksSinceLastCommand();
 };
 
 } // namespace Sound
diff --git a/engines/mads/nebular/sound/rsound_nebular.cpp b/engines/mads/nebular/sound/rsound_nebular.cpp
index ce7152af906..024157a727b 100644
--- a/engines/mads/nebular/sound/rsound_nebular.cpp
+++ b/engines/mads/nebular/sound/rsound_nebular.cpp
@@ -939,7 +939,7 @@ Channel *RSound3::method1(int offset, byte value) {
 }
 
 void RSound3::resetUpperChannelsTail() {
-	setFadeCheckPeriod(1);
+	setFadeOutSpeed(1);
 	_channels[4].setFadeOut(true);
 	_channels[5].setFadeOut(true);
 	_channels[6].setFadeOut(true);
@@ -965,7 +965,7 @@ int RSound3::command5() {
 
 int RSound3::command9() {
 	command1();
-	setFadeCheckPeriod((byte)_commandParam);
+	setFadeOutSpeed((byte)_commandParam);
 	return 0;
 }
 
@@ -1043,7 +1043,7 @@ int RSound3::command15() {
 		_channels[5]._fadeOutActive = true;
 		_channels[6]._fadeOutActive = true;
 		_channels[7]._fadeOutActive = true;
-		setFadeCheckPeriod(1);
+		setFadeOutSpeed(1);
 		return 0;
 	}
 
@@ -1349,7 +1349,7 @@ void RSound4::tickCallback() {
 
 int RSound4::command9() {
 	command1();
-	setFadeCheckPeriod((byte)_commandParam);
+	setFadeOutSpeed((byte)_commandParam);
 	return 0;
 }
 
diff --git a/engines/mads/nebular/sound/sound.cpp b/engines/mads/nebular/sound/sound.cpp
index ad34b4f2be4..2b72556ff2f 100644
--- a/engines/mads/nebular/sound/sound.cpp
+++ b/engines/mads/nebular/sound/sound.cpp
@@ -51,6 +51,8 @@ RexSoundManager::RexSoundManager(Audio::Mixer *mixer, bool &soundFlag,
 		if (returnCode != 0)
 			error("SoundManager - Failed to open MIDI music driver - error code %d.", returnCode);
 
+		_midiDriver->property(MidiDriver::PROP_USER_VOLUME_SCALING, true);
+
 		_driverCallbackDelta = _midiDriver->getBaseTempo();
 		_midiDriver->setTimerCallback(this, &timerCallback);
 	}


Commit: 140dfaa9ed7134eaecbcc2f63ac9f9e2c7f66e84
    https://github.com/scummvm/scummvm/commit/140dfaa9ed7134eaecbcc2f63ac9f9e2c7f66e84
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
MADS: More Rex MT-32 refactoring

Changed paths:
    engines/mads/nebular/sound/rsound.cpp
    engines/mads/nebular/sound/rsound.h
    engines/mads/nebular/sound/rsound_nebular.cpp
    engines/mads/nebular/sound/rsound_nebular.h


diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index 8672b4a0cc2..3d01b5c5651 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -58,17 +58,6 @@ void Channel::setFadeOut(bool fadeOut) {
 	}
 }
 
-void Channel::playData(byte *soundData) {
-	bool ticksProcessingDisabled = _owner->_ticksProcessingDisabled;
-	_owner->_ticksProcessingDisabled = true;
-
-	loadData(soundData);
-	_deltaCounter = 1;
-	_owner->sendPitchBend(_midiChannel, 0x40);
-
-	_owner->_ticksProcessingDisabled = ticksProcessingDisabled;
-}
-
 /*-----------------------------------------------------------------------*/
 
 RSound::RSound(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common::Path &filename,
@@ -89,7 +78,6 @@ RSound::RSound(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common:
 	_fadeOutCounter = _fadeOutSpeed;
 
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
-		_channels[i]._owner = this;
 		_channels[i]._midiChannel = i + 1;
 	}
 
@@ -172,48 +160,75 @@ void RSound::resultCheck() {
 	}
 }
 
-Channel *RSound::playSoundCh5To8(int offset) {
+Channel *RSound::getChannel(byte channel) {
+	return &_channels[channel - 1];
+}
+
+Channel *RSound::playSoundStatic(byte *soundData, byte channel) {
+	assert(channel >= 1 && channel <= RSOUND_CHANNEL_COUNT);
+
+	Channel *chan = getChannel(channel);
+	Channel_playData(chan, soundData);
+
+	return chan;
+}
+
+Channel *RSound::playSoundStatic(int offset, byte channel) {
+	return playSoundStatic(loadData(offset), channel);
+}
+
+Channel *RSound::playSoundDynamic(int offset) {
 	return allocateAndPlay(loadData(offset));
 }
 
-Channel *RSound::playSoundCh1To8(int offset) {
-	return allocateAndPlay(loadData(offset), 0);
+Channel *RSound::playSoundAnyChannel(int offset) {
+	return allocateAndPlay(loadData(offset), 1);
 }
 
 Channel *RSound::allocateAndPlay(byte *pData, int startingChannel) {
 	int endChannel = RSOUND_CHANNEL_COUNT - 1;
 
+	Channel *foundChannel = nullptr;
+
 	// Scan for a free channel
-	for (int i = startingChannel; i < endChannel; ++i) {
-		if (_channels[i]._deltaCounter == 0) {
-			_channels[i].playData(pData);
-			return &_channels[i];
+	for (int i = startingChannel; i <= endChannel; ++i) {
+		if (getChannel(i)->_deltaCounter == 0) {
+			foundChannel = getChannel(i);
+			break;
 		}
 	}
 
-	// None found; fall back to a channel that is fading out to stop
-	for (int i = endChannel - 1; i >= startingChannel; --i) {
-		if (_channels[i]._fadeOutActive) {
-			_channels[i].playData(pData);
-			return &_channels[i];
+	if (foundChannel == nullptr) {
+		// None found; fall back to a channel that is fading out to stop
+		for (int i = endChannel; i >= startingChannel; --i) {
+			if (getChannel(i)->_fadeOutActive) {
+				foundChannel = getChannel(i);
+				break;
+			}
 		}
 	}
 
-	// No available channel
-	return nullptr;
+	if (foundChannel != nullptr)
+		Channel_playData(foundChannel, pData);
+
+	return foundChannel;
 }
 
 bool RSound::isSoundPlaying(byte *pData) {
-	// Deliberately excludes channel 9 (index 8), matching the disassembly -
+	// Deliberately excludes channel 9, matching the disassembly -
 	// same as allocateAndPlay()'s scan never reaching channel 9 either.
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT - 1; ++i) {
-		if (_channels[i]._deltaCounter > 0 && _channels[i]._soundData == pData)
+	for (int i = 1; i < RSOUND_CHANNEL_COUNT; ++i) {
+		if (isSoundPlaying(i, pData))
 			return true;
 	}
 
 	return false;
 }
 
+bool RSound::isSoundPlaying(byte channel, byte *pData) {
+	return getChannel(channel)->_deltaCounter > 0 && getChannel(channel)->_soundData == pData;
+}
+
 uint16 RSound::generateRandomNumber() {
 	uint16 newValue = 0x9249 + _randomSeed;
 	_randomSeed = ((newValue >> 3) | (newValue << 13)) & 0xFFFF;
@@ -256,6 +271,11 @@ void RSound::unmuteChannel(byte midiChannel, byte volume) {
 	sendVolume(midiChannel, volume);
 }
 
+void RSound::setChannelVolume(byte channel, byte volume) {
+	getChannel(channel)->_volume = volume;
+	sendVolume(channel, volume);
+}
+
 byte *RSound::sendSysExData(byte *pData, uint maxLength) {
 	uint length = 0;
 	while (length < maxLength && pData[length] != 0xFF)
@@ -313,6 +333,17 @@ void RSound::stopAllNotes() {
 
 /*-----------------------------------------------------------------------*/
 
+void RSound::Channel_playData(Channel *channel, byte *soundData) {
+	bool ticksProcessingDisabled = _ticksProcessingDisabled;
+	_ticksProcessingDisabled = true;
+
+	channel->loadData(soundData);
+	channel->_deltaCounter = 1;
+	sendPitchBend(channel->_midiChannel, 0x40);
+
+	_ticksProcessingDisabled = ticksProcessingDisabled;
+}
+
 void RSound::Channel_turnOffActiveNotes(Channel *channel) {
 	byte *channelActiveNotes = _activeNotes[channel->_midiChannel];
 
@@ -351,8 +382,8 @@ void RSound::processChannelFadeOuts() {
 
 	_fadeOutCounter = _fadeOutSpeed;
 
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		Channel_processFadeOut(&_channels[i]);
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		Channel_processFadeOut(getChannel(i));
 }
 
 void RSound::setFadeOutSpeed(int fadeOutSpeed) {
@@ -394,16 +425,13 @@ void RSound::Channel_processTick(Channel *channel) {
 				if (note == 0 || delta == 0) {
 					Channel_turnOffActiveNotes(channel);
 				} else {
+					// In the original code this calculation can result in underflow
+					// of the duration, which is a byte value. This actually happens
+					// during playback of several tracks.
+					// Assignment to the noteDurationCounter byte field
+					// results in the same behavior as the original code,
+					// f.e. duration -1 becomes counter value 0xFF.
 					int duration = delta - channel->_noteDurationOffset;
-					if (duration < 0) {
-						// In the original code this calculation can result in underflow
-						// of the duration, which is a byte value. Not sure if this
-						// actually happens in any of the sound data.
-						warning("Calculated note duration is negative");
-						// Assignment to the noteDurationCounter byte field should
-						// result in the same behavior as the original code, i.e.
-						// duration -1 becomes counter value 0xFF.
-					}
 					channel->_noteDurationCounter = duration;
 
 					// If the note duration offset is 0 or positive, the notes played by
@@ -718,8 +746,8 @@ void RSound::Channel_processTick(Channel *channel) {
 }
 
 void RSound::processTickAllChannels() {
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		Channel_processTick(&_channels[i]);
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		Channel_processTick(getChannel(i));
 }
 
 void RSound::processTick() {
@@ -745,18 +773,20 @@ void RSound::processTick() {
 void RSound::resetChannelRange(int firstChannel, int lastChannel, bool includeChannel9) {
 	_ticksProcessingDisabled = true;
 
-	for (int i = firstChannel - 1; i < lastChannel; ++i) {
-		_channels[i]._deltaCounter = 0;
-		_channels[i]._pitchSlideStepSize = 0;
-		_channels[i]._volumeFadeStepSize = 0;
-		_channels[i]._panningSweepStepSize = 0;
+	for (int i = firstChannel; i <= lastChannel; ++i) {
+		Channel *chan = getChannel(i);
+		chan->_deltaCounter = 0;
+		chan->_pitchSlideStepSize = 0;
+		chan->_volumeFadeStepSize = 0;
+		chan->_panningSweepStepSize = 0;
 	}
 
 	if (lastChannel <= 8 && includeChannel9) {
-		_channels[8]._deltaCounter = 0;
-		_channels[8]._pitchSlideStepSize = 0;
-		_channels[8]._volumeFadeStepSize = 0;
-		_channels[8]._panningSweepStepSize = 0;
+		Channel *chan = getChannel(9);
+		chan->_deltaCounter = 0;
+		chan->_pitchSlideStepSize = 0;
+		chan->_volumeFadeStepSize = 0;
+		chan->_panningSweepStepSize = 0;
 	}
 
 	_ticksProcessingDisabled = false;
@@ -821,8 +851,8 @@ int RSound::command0() {
 
 int RSound::command1() {
 	setFadeOutSpeed(1);
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		_channels[i].setFadeOut(true);
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		getChannel(i)->setFadeOut(true);
 	return 0;
 }
 
@@ -842,9 +872,9 @@ int RSound::command2() {
 int RSound::command3() {
 	// Start fade-out to stop for channels 1-4 and 9.
 	setFadeOutSpeed(1);
-	for (int i = 0; i < 4; ++i)
-		_channels[i].setFadeOut(true);
-	_channels[8].setFadeOut(true);
+	for (int i = 1; i <= 4; ++i)
+		getChannel(i)->setFadeOut(true);
+	getChannel(9)->setFadeOut(true);
 	return 0;
 }
 
@@ -862,8 +892,8 @@ int RSound::command4() {
 int RSound::command5() {
 	// Start fade-out to stop for channels 5-8.
 	setFadeOutSpeed(1);
-	for (int i = 4; i < 8; ++i)
-		_channels[i].setFadeOut(true);
+	for (int i = 5; i <= 8; ++i)
+		getChannel(i)->setFadeOut(true);
 	return 0;
 }
 
@@ -876,8 +906,8 @@ int RSound::command6() {
 }
 
 int RSound::command7() {
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		unmuteChannel(_channels[i]._midiChannel, _channels[i]._volume);
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		unmuteChannel(i, getChannel(i)->_volume);
 	_mute = false;
 	_ticksProcessingDisabled = false;
 	return 0;
@@ -885,8 +915,8 @@ int RSound::command7() {
 
 int RSound::command8() {
 	int result = 0;
-	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i)
-		result |= _channels[i]._deltaCounter;
+	for (int i = 1; i <= RSOUND_CHANNEL_COUNT; ++i)
+		result |= getChannel(i)->_deltaCounter;
 
 	return result;
 }
diff --git a/engines/mads/nebular/sound/rsound.h b/engines/mads/nebular/sound/rsound.h
index cee08d24970..64a17a2a848 100644
--- a/engines/mads/nebular/sound/rsound.h
+++ b/engines/mads/nebular/sound/rsound.h
@@ -78,7 +78,6 @@ enum RSoundFadeCheckMode {
  */
 class Channel {
 public:
-	RSound *_owner = nullptr;
 	byte _midiChannel = 0;					// 1-9: the MIDI channel to which the data in this struct pertains
 
 	byte _deltaCounter = 0;					// number of ticks until the next event occurs; loaded from the delta byte of a note or chord event
@@ -128,12 +127,6 @@ public:
 	 * volume fade triggered by event 0xF8.
 	 */
 	void setFadeOut(bool fadeOut);
-
-	/**
-	 * Loads new sound data into the channel, starts playback by setting
-	 * _deltaCounter to 1 and resets pitch bend to center on the MT-32.
-	 */
-	void playData(byte *soundData);
 };
 
 /**
@@ -208,6 +201,12 @@ private:
 	void processChannelFadeOuts();
 	void Channel_processFadeOut(Channel *channel);
 
+	/**
+	 * Loads new sound data into the channel, starts playback by setting
+	 * _deltaCounter to 1 and resets pitch bend to center on the MT-32.
+	 */
+	void Channel_playData(Channel *channel, byte *soundData);
+
 	/**
 	 * Sends Note-Off (velocity 0) for all active notes on the
 	 * given channel's MIDI channel, then clears the active note table for it.
@@ -247,32 +246,70 @@ protected:
 	void resultCheck();
 
 	/**
-	 * Play the specified sound using any free channel from 5 to 8.
-	 * Returns the channel that was used (or nullptr if none was free),
-	 * since some commands poke the just-loaded channel's loop pointer
-	 * directly afterward.
+	 * Returns a pointer to the channel data corresponding to the
+	 * specified MIDI channel.
+	 */
+	Channel *getChannel(byte channel);
+
+	/*
+	 * MIDI channels are allocated to sound data either statically or
+	 * dynamically. Channels 1-4 and 9 are usually statically allocated,
+	 * where the command will specify the channel to use for loading the
+	 * sound data using playSoundStatic. Because channel 9 is the rhythm
+	 * channel and sound data must be specifically written for this
+	 * channel, it can only be allocated statically.
+	 * Channels 5-8 are usually dynamically allocated. The command will
+	 * use playSoundDynamic to look for a free channel and then load the
+	 * sound data into that channel.
+	 * Usually, commands will load music statically into channels 1-4 and 9,
+	 * and SFX dynamically into channels 5-8. However, there are many
+	 * exceptions.
 	 */
-	Channel *playSoundCh5To8(int offset);
 
 	/**
-	 * Play the specified sound using any free channel from 1 to 8.
+	 * Play the specified sound by statically allocating the specified
+	 * channel. This will unload any sound already playing on the
+	 * channel.
 	 */
-	Channel *playSoundCh1To8(int offset);
+	Channel *playSoundStatic(int offset, byte channel);
+	Channel *playSoundStatic(byte *soundData, byte channel);
 
 	/**
-	 * Allocates a MIDI channel, loads the specified sound data into it
-	 * and starts playback. Allocation will look for a free channel in
-	 * the range starting with the specified channel (default 5) and
-	 * ending with channel 8. If no suitable channel could be found,
-	 * nullptr is returned and the sound data is not played.
+	 * Play the specified sound by allocating one of the free dynamic
+	 * channels. Returns the channel that was used (or nullptr if none
+	 * was free), since some commands poke the just-loaded channel's
+	 * loop pointer directly afterward.
+	 */
+	Channel *playSoundDynamic(int offset);
+
+	/**
+	 * Play the specified sound by allocating any free melodic channel,
+	 * dynamic or static.
+	 */
+	Channel *playSoundAnyChannel(int offset);
+
+	/**
+	 * Dynamically allocates a melodic channel, loads the specified sound
+	 * data into it and starts playback. Allocation will look for a
+	 * free channel in the range starting with the specified channel
+	 * (default 5) and ending with channel 8. If no suitable channel
+	 * could be found, nullptr is returned and the sound data is not
+	 * played.
 	 */
 	Channel *allocateAndPlay(byte *pData, int startingChannel = 5);
 
 	/**
-	 * Checks to see whether the given block of data is already loaded into a channel.
+	 * Checks to see whether the given block of data is already loaded
+	 * into a channel and being played.
 	 */
 	bool isSoundPlaying(byte *pData);
 
+	/**
+	 * Checks to see whether the given block of data is already loaded
+	 * into the specified channel and being played.
+	 */
+	bool isSoundPlaying(byte channel, byte *pData);
+
 	uint16 generateRandomNumber();
 
 	// ---- Low-level MIDI send helpers -------------------------------
@@ -292,6 +329,12 @@ protected:
 	 */
 	void sendMidiChannelReset(int first, int last);
 
+	/**
+	 * Sets the volume for a channel. The value is set on the channel data
+	 * and it is sent out to the MT-32.
+	 */
+	void setChannelVolume(byte channel, byte volume);
+
 	/**
 	 * Sends a single SysEx message: bytes from pData up to (but not
 	 * including) a 0xFF terminator, via the MT32GM MIDI driver. Returns a
diff --git a/engines/mads/nebular/sound/rsound_nebular.cpp b/engines/mads/nebular/sound/rsound_nebular.cpp
index 024157a727b..eb974dad174 100644
--- a/engines/mads/nebular/sound/rsound_nebular.cpp
+++ b/engines/mads/nebular/sound/rsound_nebular.cpp
@@ -35,7 +35,7 @@ RSoundDemo::RSoundDemo(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const
 
 void RSoundDemo::startVoice(int channelIndex, int sequenceOffset) {
 	assert(channelIndex >= 0 && channelIndex < RSOUND_CHANNEL_COUNT);
-	_channels[channelIndex].playData(loadData(sequenceOffset));
+	playSoundStatic(sequenceOffset, channelIndex + 1);
 }
 
 int RSoundDemo::startVoiceInRange(int sequenceOffset, int firstChannel,
@@ -129,62 +129,56 @@ int RSound1::clampParam() {
 	return (_commandParam > 0x40) ? _commandParam - 0x40 : 0;
 }
 
-void RSound1::method1() {
+void RSound1::playCommand11_12_13SharedChannels() {
 	byte *pData = loadData(0x1166);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x13BC));
-		_channels[2].playData(loadData(0x155C));
-		_channels[3].playData(loadData(0x15D8));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x13BC, 2);
+		playSoundStatic(0x155C, 3);
+		playSoundStatic(0x15D8, 4);
 	}
 }
 
 int RSound1::command9() {
-	playSoundCh5To8(0xAD4);
+	playSoundDynamic(0xAD4);
 	return 0;
 }
 
 int RSound1::command10() {
 	byte *pData = loadData(0xCE4);
 	if (!isSoundPlaying(pData)) {
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0xD18));
-		_channels[2].playData(loadData(0xE9C));
-		_channels[3].playData(loadData(0xEE8));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0xD18, 2);
+		playSoundStatic(0xE9C, 3);
+		playSoundStatic(0xEE8, 4);
 	}
 	return 0;
 }
 
 int RSound1::command11() {
-	method1();
-	_channels[0]._volume = 0;
-	sendVolume(1, _channels[0]._volume);
-	_channels[1]._volume = 0;
-	sendVolume(2, _channels[1]._volume);
+	playCommand11_12_13SharedChannels();
+	setChannelVolume(1, 0);
+	setChannelVolume(2, 0);
 	return 0;
 }
 
 int RSound1::command12() {
-	method1();
-	_channels[0]._volume = 80;
-	sendVolume(1, _channels[0]._volume);
-	_channels[1]._volume = 0;
-	sendVolume(2, _channels[1]._volume);
+	playCommand11_12_13SharedChannels();
+	setChannelVolume(1, 80);
+	setChannelVolume(2, 0);
 	return 0;
 }
 
 int RSound1::command13() {
-	method1();
-	_channels[0]._volume = 80;
-	sendVolume(1, _channels[0]._volume);
-	_channels[1]._volume = 80;
-	sendVolume(2, _channels[1]._volume);
+	playCommand11_12_13SharedChannels();
+	setChannelVolume(1, 80);
+	setChannelVolume(2, 80);
 	return 0;
 }
 
 int RSound1::command14() {
-	playSoundCh5To8(0x16C2);
+	playSoundDynamic(0x16C2);
 	return 0;
 }
 
@@ -192,63 +186,63 @@ int RSound1::command15() {
 	byte *pData = loadData(0xF3A);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[4].playData(pData);
-		_channels[5].playData(loadData(0x102A));
-		_channels[6].playData(loadData(0x110E));
+		playSoundStatic(pData, 5);
+		playSoundStatic(0x102A, 6);
+		playSoundStatic(0x110E, 7);
 	}
 	return 0;
 }
 
 int RSound1::command16() {
-	playSoundCh5To8(0xADE);
+	playSoundDynamic(0xADE);
 	return 0;
 }
 
 int RSound1::command17() {
-	playSoundCh5To8(0xAE8);
+	playSoundDynamic(0xAE8);
 	return 0;
 }
 
 int RSound1::command18() {
-	playSoundCh5To8(0xAF2);
+	playSoundDynamic(0xAF2);
 	return 0;
 }
 
 int RSound1::command19() {
 	command1();
-	playSoundCh5To8(0xB04);
+	playSoundDynamic(0xB04);
 	return 0;
 }
 
 int RSound1::command20() {
-	playSoundCh5To8(0xB5E);
+	playSoundDynamic(0xB5E);
 	return 0;
 }
 
 int RSound1::command21() {
-	playSoundCh5To8(0xB4C);
+	playSoundDynamic(0xB4C);
 	return 0;
 }
 
 int RSound1::command22() {
-	playSoundCh5To8(0xB6E);
+	playSoundDynamic(0xB6E);
 	return 0;
 }
 
 int RSound1::command23() {
 	byte *pData = loadData(0xB7A);
 	pData[6] ^= 0x1F;
-	playSoundCh5To8(0xB7A);
+	playSoundDynamic(0xB7A);
 	return 0;
 }
 
 int RSound1::command24() {
-	playSoundCh5To8(0xB84);
+	playSoundDynamic(0xB84);
 	return 0;
 }
 
 int RSound1::command25() {
-	playSoundCh5To8(0xB8E);
+	playSoundDynamic(0xB8E);
 	return 0;
 }
 
@@ -258,7 +252,7 @@ int RSound1::command26() {
 	pData[8] = v1;
 	int v2 = clampParam() + 64;
 	pData[5] = v2;
-	_channels[7].playData(pData);
+	playSoundStatic(pData, 8);
 	return 0;
 }
 
@@ -268,12 +262,12 @@ int RSound1::command27() {
 	pData[8] = v1;
 	int v2 = clampParam() + 64;
 	pData[5] = v2;
-	_channels[7].playData(pData);
+	playSoundStatic(pData, 8);
 	return 0;
 }
 
 int RSound1::command28() {
-	playSoundCh5To8(0xB9E);
+	playSoundDynamic(0xB9E);
 	return 0;
 }
 
@@ -282,7 +276,7 @@ int RSound1::command29() {
 	int v = (clampParam() >> 1) + 32;
 	pData[0xB] = v;
 	if (!isSoundPlaying(pData))
-		playSoundCh5To8(0xC6C);
+		playSoundDynamic(0xC6C);
 	return 0;
 }
 
@@ -291,12 +285,12 @@ int RSound1::command30() {
 	int v = clampParam() + 63;
 	pData[0xB] = v;
 	if (!isSoundPlaying(pData))
-		playSoundCh1To8(0xC80);
+		playSoundAnyChannel(0xC80);
 	return 0;
 }
 
 int RSound1::command31() {
-	playSoundCh5To8(0xBBE);
+	playSoundDynamic(0xBBE);
 	return 0;
 }
 
@@ -306,13 +300,13 @@ int RSound1::command32() {
 	pData[0xB] = pData[0x17] = half + 68;
 	pData[0x11] = pData[0x1D] = half + 20;
 	if (!isSoundPlaying(pData))
-		playSoundCh1To8(0xC96);
+		playSoundAnyChannel(0xC96);
 	return 0;
 }
 
 int RSound1::command33() {
-	playSoundCh5To8(0xBD0);
-	playSoundCh5To8(0xBDA);
+	playSoundDynamic(0xBD0);
+	playSoundDynamic(0xBDA);
 	return 0;
 }
 
@@ -321,19 +315,19 @@ int RSound1::command34() {
 	int v = (generateRandomNumber() & 12) + 45;
 	pData[9] = v;
 	pData[0x10] = v + 36;
-	playSoundCh5To8(0xBE8);
+	playSoundDynamic(0xBE8);
 	return 0;
 }
 
 int RSound1::command35() {
-	playSoundCh5To8(0xBFC);
-	playSoundCh5To8(0xC0E);
-	playSoundCh5To8(0xC20);
+	playSoundDynamic(0xBFC);
+	playSoundDynamic(0xC0E);
+	playSoundDynamic(0xC20);
 	return 0;
 }
 
 int RSound1::command36() {
-	playSoundCh5To8(0xC3E);
+	playSoundDynamic(0xC3E);
 	return 0;
 }
 
@@ -342,35 +336,35 @@ int RSound1::command37() {
 	int r = generateRandomNumber() & 15;
 	pData[6] = r + 42;
 	pData[3] = 62 - (r << 1);
-	playSoundCh5To8(0xC4C);
+	playSoundDynamic(0xC4C);
 	return 0;
 }
 
 int RSound1::command38() {
-	playSoundCh1To8(0xC56);
-	playSoundCh5To8(0xC60);
+	playSoundAnyChannel(0xC56);
+	playSoundDynamic(0xC60);
 	return 0;
 }
 
 int RSound1::command39() {
 	byte *pData = loadData(0x1818);
 	if (!isSoundPlaying(pData)) {
-		_channels[4].playData(pData);
-		_channels[5].playData(loadData(0x1848));
-		_channels[6].playData(loadData(0x1874));
-		_channels[7].playData(loadData(0x18B4));
-		_channels[8].playData(loadData(0x18CE));
+		playSoundStatic(pData, 5);
+		playSoundStatic(0x1848, 6);
+		playSoundStatic(0x1874, 7);
+		playSoundStatic(0x18B4, 8);
+		playSoundStatic(0x18CE, 9);
 	}
 	return 0;
 }
 
 int RSound1::command40() {
-	playSoundCh5To8(0xC34);
+	playSoundDynamic(0xC34);
 	return 0;
 }
 
 int RSound1::command41() {
-	playSoundCh5To8(0xCD6);
+	playSoundDynamic(0xCD6);
 	return 0;
 }
 
@@ -386,7 +380,7 @@ byte RSoundDemo1::adjustedCommandParam() const {
 	return value > 0x40 ? value - 0x40 : 0;
 }
 
-void RSoundDemo1::startCommand111213() {
+void RSoundDemo1::playCommand11_12_13CommonChannels() {
 	if (isSequenceActive(0x1586))
 		return;
 
@@ -450,17 +444,17 @@ int RSoundDemo1::command(int commandId, int param) {
 		}
 		break;
 	case 11:
-		startCommand111213();
+		playCommand11_12_13CommonChannels();
 		setVoiceVolume(0, 0x00);
 		setVoiceVolume(1, 0x00);
 		break;
 	case 12:
-		startCommand111213();
+		playCommand11_12_13CommonChannels();
 		setVoiceVolume(0, 0x50);
 		setVoiceVolume(1, 0x00);
 		break;
 	case 13:
-		startCommand111213();
+		playCommand11_12_13CommonChannels();
 		setVoiceVolume(0, 0x50);
 		setVoiceVolume(1, 0x50);
 		break;
@@ -590,7 +584,7 @@ int RSoundDemo1::command(int commandId, int param) {
 
 /*-----------------------------------------------------------------------*/
 
-const uint16 RSound2::_table1[16] = {
+const uint16 RSound2::_command18RandomSfx[16] = {
 	0x3234, 0x3250, 0x326A, 0x3284, 0x329E, 0x32D6, 0x3304, 0x333C,
 	0x3352, 0x3378, 0x33B6, 0x33D0, 0x33EA, 0x3404, 0x341E, 0x343E
 };
@@ -623,12 +617,10 @@ int RSound2::command(int commandId, int param) {
 }
 
 int RSound2::command5() {
-	_pitchCycleCounter = 47;
-	_channels[3].setFadeOut(true);
-	_channels[4].setFadeOut(true);
-	_channels[5].setFadeOut(true);
-	_channels[6].setFadeOut(true);
-	_channels[7].setFadeOut(true);
+	_volumeCycleCounter = 47;
+	for (int i = 4; i <= 8; i++) {
+		getChannel(i)->setFadeOut(true);
+	}
 	return 0;
 }
 
@@ -636,9 +628,9 @@ int RSound2::command9() {
 	byte *pData = loadData(0x103C);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x11B2));
-		_channels[8].playData(loadData(0x127E));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x11B2, 2);
+		playSoundStatic(0x127E, 9);
 	}
 	return 0;
 }
@@ -647,8 +639,8 @@ int RSound2::command10() {
 	byte *pData = loadData(0x132E);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[2].playData(pData);
-		_channels[8].playData(loadData(0x1384));
+		playSoundStatic(pData, 3);
+		playSoundStatic(0x1384, 9);
 	}
 	return 0;
 }
@@ -657,29 +649,29 @@ int RSound2::command11() {
 	byte *pData = loadData(0x1548);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[2].playData(pData);
-		_channels[8].playData(loadData(0x1648));
+		playSoundStatic(pData, 3);
+		playSoundStatic(0x1648, 9);
 	}
 	return 0;
 }
 
 int RSound2::command12() {
 	byte *pData = loadData(0xE52);
-	_pitchCycleCounter += 16;
-	pData[3] = _pitchCycleCounter & 0x7F;
-	playSoundCh5To8(0xE52);
+	_volumeCycleCounter += 16;
+	pData[3] = _volumeCycleCounter & 0x7F;
+	playSoundDynamic(0xE52);
 	return 0;
 }
 
 int RSound2::command13() {
-	playSoundCh5To8(0xE5C);
-	playSoundCh5To8(0xE66);
+	playSoundDynamic(0xE5C);
+	playSoundDynamic(0xE66);
 	return 0;
 }
 
 int RSound2::command14() {
-	playSoundCh5To8(0xE70);
-	playSoundCh5To8(0xE8A);
+	playSoundDynamic(0xE70);
+	playSoundDynamic(0xE8A);
 	return 0;
 }
 
@@ -687,10 +679,10 @@ int RSound2::command15() {
 	byte *pData = loadData(0x1DFC);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundCh1To8(0x1DFC);
-		playSoundCh1To8(0x222E);
-		playSoundCh1To8(0x2648);
-		_channels[8].playData(loadData(0x26A2));
+		playSoundAnyChannel(0x1DFC);
+		playSoundAnyChannel(0x222E);
+		playSoundAnyChannel(0x2648);
+		playSoundStatic(0x26A2, 9);
 	}
 	return 0;
 }
@@ -699,12 +691,12 @@ int RSound2::command16() {
 	byte *pData = loadData(0x3456);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundCh1To8(0x3456);
-		playSoundCh1To8(0x3572);
-		playSoundCh1To8(0x367E);
-		playSoundCh1To8(0x37C6);
-		playSoundCh1To8(0x39AE);
-		playSoundCh1To8(0x3A3A);
+		playSoundAnyChannel(0x3456);
+		playSoundAnyChannel(0x3572);
+		playSoundAnyChannel(0x367E);
+		playSoundAnyChannel(0x37C6);
+		playSoundAnyChannel(0x39AE);
+		playSoundAnyChannel(0x3A3A);
 	}
 	return 0;
 }
@@ -712,20 +704,20 @@ int RSound2::command16() {
 int RSound2::command17() {
 	byte *pData = loadData(0x3AC0);
 	if (!isSoundPlaying(pData)) {
-		playSoundCh5To8(0x3AC0);
-		playSoundCh5To8(0x3C70);
-		playSoundCh5To8(0x3E16);
-		playSoundCh5To8(0x3FBE);
+		playSoundDynamic(0x3AC0);
+		playSoundDynamic(0x3C70);
+		playSoundDynamic(0x3E16);
+		playSoundDynamic(0x3FBE);
 	}
 	return 0;
 }
 
 int RSound2::command18() {
-	if (_channels[7]._deltaCounter)
+	if (getChannel(8)->_deltaCounter > 0)
 		return 0;
 
-	int idx = (generateRandomNumber() & 30) >> 1;
-	_channels[7].playData(loadData(_table1[idx]));
+	int idx = (generateRandomNumber() & 0x1E) >> 1;
+	playSoundStatic(_command18RandomSfx[idx], 8);
 	return 0;
 }
 
@@ -733,64 +725,64 @@ int RSound2::command19() {
 	byte *pData = loadData(0x2A64);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundCh1To8(0x2A64);
-		playSoundCh1To8(0x2BE2);
-		playSoundCh1To8(0x2DAC);
-		playSoundCh1To8(0x2ECE);
-		playSoundCh1To8(0x3026);
-		playSoundCh1To8(0x30C2);
+		playSoundAnyChannel(0x2A64);
+		playSoundAnyChannel(0x2BE2);
+		playSoundAnyChannel(0x2DAC);
+		playSoundAnyChannel(0x2ECE);
+		playSoundAnyChannel(0x3026);
+		playSoundAnyChannel(0x30C2);
 	}
 	return 0;
 }
 
 int RSound2::command20() {
-	playSoundCh5To8(0xF1E);
-	playSoundCh5To8(0xF1E);
-	playSoundCh5To8(0xF1E);
-	playSoundCh5To8(0xF1E);
+	playSoundDynamic(0xF1E);
+	playSoundDynamic(0xF1E);
+	playSoundDynamic(0xF1E);
+	playSoundDynamic(0xF1E);
 	return 0;
 }
 
 int RSound2::command21() {
-	playSoundCh5To8(0xF58);
+	playSoundDynamic(0xF58);
 	return 0;
 }
 
 int RSound2::command22() {
-	playSoundCh5To8(0xF44);
+	playSoundDynamic(0xF44);
 	return 0;
 }
 
 int RSound2::command23() {
-	playSoundCh5To8(0xEBA);
+	playSoundDynamic(0xEBA);
 	return 0;
 }
 
 int RSound2::command24() {
-	playSoundCh5To8(0xEB0);
+	playSoundDynamic(0xEB0);
 	return 0;
 }
 
 int RSound2::command25() {
-	playSoundCh5To8(0xEA6);
+	playSoundDynamic(0xEA6);
 	return 0;
 }
 
 int RSound2::command26() {
-	playSoundCh5To8(0xF4E);
+	playSoundDynamic(0xF4E);
 	return 0;
 }
 
 int RSound2::command27() {
-	Channel *ch = playSoundCh5To8(0xFAC);
-	if (ch)
+	Channel *ch = playSoundDynamic(0xFAC);
+	if (ch != nullptr)
 		ch->_innerLoopStart = loadData(0xFB8);
 
-	ch = playSoundCh5To8(0xFB2);
-	if (ch)
+	ch = playSoundDynamic(0xFB2);
+	if (ch != nullptr)
 		ch->_innerLoopStart = loadData(0xFB8);
 
-	playSoundCh5To8(0xFB8);
+	playSoundDynamic(0xFB8);
 	return 0;
 }
 
@@ -803,58 +795,58 @@ int RSound2::command28() {
 	pData[8] = v2;
 	int v3 = v2 + 0x0C;
 	pData[0xA] = v3;
-	playSoundCh5To8(0xEDA);
+	playSoundDynamic(0xEDA);
 	return 0;
 }
 
 int RSound2::command29() {
-	playSoundCh1To8(0xF80);
+	playSoundAnyChannel(0xF80);
 	return 0;
 }
 
 int RSound2::command30() {
-	playSoundCh5To8(0xF14);
+	playSoundDynamic(0xF14);
 	byte *pData = loadData(0xF0A);
 	pData[3] = 40;
-	playSoundCh5To8(0xF0A);
+	playSoundDynamic(0xF0A);
 	return 0;
 }
 
 int RSound2::command31() {
 	byte *pData = loadData(0xF0A);
 	pData[3] = 0x18;
-	playSoundCh5To8(0xF0A);
+	playSoundDynamic(0xF0A);
 	return 0;
 }
 
 int RSound2::command32() {
-	playSoundCh5To8(0xEC4);
+	playSoundDynamic(0xEC4);
 	return 0;
 }
 
 int RSound2::command33() {
-	playSoundCh5To8(0xED0);
+	playSoundDynamic(0xED0);
 	return 0;
 }
 
 int RSound2::command34() {
-	playSoundCh5To8(0xEE8);
+	playSoundDynamic(0xEE8);
 	return 0;
 }
 
 int RSound2::command35() {
-	playSoundCh5To8(0xEF8);
+	playSoundDynamic(0xEF8);
 	return 0;
 }
 
 int RSound2::command36() {
-	playSoundCh5To8(0xFF4);
-	playSoundCh5To8(0x1008);
+	playSoundDynamic(0xFF4);
+	playSoundDynamic(0x1008);
 	return 0;
 }
 
 int RSound2::command37() {
-	playSoundCh5To8(0xFCC);
+	playSoundDynamic(0xFCC);
 	return 0;
 }
 
@@ -862,39 +854,39 @@ int RSound2::command38() {
 	byte *pData = loadData(0x2B0E);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundCh1To8(0x2B0E);
-		playSoundCh1To8(0x2CD0);
-		playSoundCh1To8(0x2E46);
-		playSoundCh1To8(0x2F7C);
-		playSoundCh1To8(0x3074);
-		playSoundCh1To8(0x317E);
+		playSoundAnyChannel(0x2B0E);
+		playSoundAnyChannel(0x2CD0);
+		playSoundAnyChannel(0x2E46);
+		playSoundAnyChannel(0x2F7C);
+		playSoundAnyChannel(0x3074);
+		playSoundAnyChannel(0x317E);
 	}
 	return 0;
 }
 
 int RSound2::command39() {
-	playSoundCh5To8(0xFDA);
+	playSoundDynamic(0xFDA);
 	return 0;
 }
 
 int RSound2::command40() {
-	playSoundCh5To8(0xFE6);
+	playSoundDynamic(0xFE6);
 	return 0;
 }
 
 int RSound2::command41() {
-	playSoundCh1To8(0xF62);
+	playSoundAnyChannel(0xF62);
 	return 0;
 }
 
 int RSound2::command42() {
-	playSoundCh5To8(0xF92);
+	playSoundDynamic(0xF92);
 	return 0;
 }
 
 int RSound2::command43() {
-	playSoundCh5To8(0x1018);
-	playSoundCh5To8(0x102A);
+	playSoundDynamic(0x1018);
+	playSoundDynamic(0x102A);
 	return 0;
 }
 
@@ -907,13 +899,13 @@ const RSound3::CommandPtr RSound3::_commandList[61] = {
 	&RSound3::nullCommand, &RSound3::command13, &RSound3::command14, &RSound3::command15,
 	&RSound3::command16, &RSound3::command17, &RSound3::command18, &RSound3::command19,
 	&RSound3::command20, &RSound3::command21, &RSound3::command22, &RSound3::command23,
-	&RSound3::command24, &RSound3::command25, &RSound3::command26, &RSound3::command27x42,
+	&RSound3::command24, &RSound3::command25, &RSound3::command26, &RSound3::command27_42,
 	&RSound3::command28, &RSound3::command29, &RSound3::command30, &RSound3::command31,
 	&RSound3::command32, &RSound3::command33, &RSound3::command34, &RSound3::command35,
 	&RSound3::command36, &RSound3::command37, &RSound3::command38, &RSound3::command39,
-	&RSound3::command40, &RSound3::command41, &RSound3::command27x42, &RSound3::command43,
-	&RSound3::command44, &RSound3::command45, &RSound3::command46, &RSound3::command47x49,
-	&RSound3::command48, &RSound3::command47x49, &RSound3::command50, &RSound3::command51,
+	&RSound3::command40, &RSound3::command41, &RSound3::command27_42, &RSound3::command43,
+	&RSound3::command44, &RSound3::command45, &RSound3::command46, &RSound3::command47_49,
+	&RSound3::command48, &RSound3::command47_49, &RSound3::command50, &RSound3::command51,
 	&RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand, &RSound3::nullCommand,
 	&RSound3::nullCommand, &RSound3::command57, &RSound3::nullCommand, &RSound3::command59,
 	&RSound3::command60
@@ -932,35 +924,10 @@ int RSound3::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
-Channel *RSound3::method1(int offset, byte value) {
+Channel *RSound3::patchAndPlaySound(int offset, byte value) {
 	byte *pData = loadData(offset);
 	pData[5] = value;
-	return playSoundCh5To8(offset);
-}
-
-void RSound3::resetUpperChannelsTail() {
-	setFadeOutSpeed(1);
-	_channels[4].setFadeOut(true);
-	_channels[5].setFadeOut(true);
-	_channels[6].setFadeOut(true);
-	_channels[7].setFadeOut(true);
-}
-
-int RSound3::command1() {
-	command3();
-	resetUpperChannelsTail();
-	return 0;
-}
-
-int RSound3::command3() {
-	return RSound::command3();
-}
-
-int RSound3::command5() {
-	// The native driver performs an unused active-sequence probe before
-	// unconditionally entering the shared upper-channel reset tail.
-	resetUpperChannelsTail();
-	return 0;
+	return playSoundDynamic(offset);
 }
 
 int RSound3::command9() {
@@ -973,11 +940,11 @@ int RSound3::command10() {
 	byte *pData = loadData(0x14FE);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x1630));
-		_channels[2].playData(loadData(0x186E));
-		_channels[3].playData(loadData(0x1A68));
-		_channels[8].playData(loadData(0x1AA6));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x1630, 2);
+		playSoundStatic(0x186E, 3);
+		playSoundStatic(0x1A68, 4);
+		playSoundStatic(0x1AA6, 9);
 	}
 	return 0;
 }
@@ -992,64 +959,61 @@ void RSound3::setVariantByte(byte value) {
 int RSound3::command11() {
 	if (!isSoundPlaying(loadData(0x1AE6))) {
 		setVariantByte(0x64);
-		_channels[0].playData(loadData(0x1AE6));
-		_channels[1].playData(loadData(0x1E00));
-		_channels[2].playData(loadData(0x1E66));
-		_channels[3].playData(loadData(0x204A));
-		_channels[4].playData(loadData(0x229C));
-		_channels[5].playData(loadData(0x2748));
-		_channels[6].playData(loadData(0x2C56));
+		playSoundStatic(0x1AE6, 1);
+		playSoundStatic(0x1E00, 2);
+		playSoundStatic(0x1E66, 3);
+		playSoundStatic(0x204A, 4);
+		playSoundStatic(0x229C, 5);
+		playSoundStatic(0x2748, 6);
+		playSoundStatic(0x2C56, 7);
 	}
 	return 0;
 }
 
 int RSound3::command13() {
 	command1();
-	playSoundCh1To8(0x1364);
-	playSoundCh1To8(0x1364);
-	playSoundCh1To8(0x1364);
-	playSoundCh1To8(0x1364);
-	playSoundCh1To8(0x1364);
+	playSoundAnyChannel(0x1364);
+	playSoundAnyChannel(0x1364);
+	playSoundAnyChannel(0x1364);
+	playSoundAnyChannel(0x1364);
+	playSoundAnyChannel(0x1364);
 	return 0;
 }
 
 int RSound3::command14() {
-	_channels[0].playData(loadData(0x32DC));
-	_channels[1].playData(loadData(0x32FC));
-	_channels[2].playData(loadData(0x331C));
-	_channels[3].playData(loadData(0x333E));
-	_channels[4].playData(loadData(0x335C));
-	_channels[5].playData(loadData(0x339E));
-	_channels[6].playData(loadData(0x33DE));
-	_channels[7].playData(loadData(0x341E));
+	playSoundStatic(0x32DC, 1);
+	playSoundStatic(0x32FC, 2);
+	playSoundStatic(0x331C, 3);
+	playSoundStatic(0x333E, 4);
+	playSoundStatic(0x335C, 5);
+	playSoundStatic(0x339E, 6);
+	playSoundStatic(0x33DE, 7);
+	playSoundStatic(0x341E, 8);
 	return 0;
 }
 
 void RSound3::sendDualVolume(byte volume) {
-	_channels[0]._volume = volume;
-	sendVolume(1, volume);
-	_channels[0]._volume = volume;
-	sendVolume(2, volume);
+	setChannelVolume(1, volume);
+	// The original code sets volume on the channel data for channel 1 (again),
+	// but sends it to the MT-32 on channel 2. Most likely a bug.
+	setChannelVolume(2, volume);
 }
 
 int RSound3::command15() {
 	setVariantByte(0x60);
 	sendDualVolume(0x60);
 
-	if (_channels[3]._deltaCounter && _channels[3]._soundData == loadData(0x204A)) {
-		_channels[2]._fadeOutActive = true;
-		_channels[3]._fadeOutActive = true;
-		_channels[4]._fadeOutActive = true;
-		_channels[5]._fadeOutActive = true;
-		_channels[6]._fadeOutActive = true;
-		_channels[7]._fadeOutActive = true;
+	if (isSoundPlaying(4, loadData(0x204A))) {
+		for (int i = 3; i <= 8; i++) {
+			getChannel(i)->_fadeOutActive = true;
+		}
 		setFadeOutSpeed(1);
 		return 0;
 	}
 
 	command1();
-	_channels[0].playData(loadData(0x1AE6));
-	_channels[1].playData(loadData(0x1E00));
+	playSoundStatic(0x1AE6, 1);
+	playSoundStatic(0x1E00, 2);
 	return 0;
 }
 
@@ -1059,19 +1023,19 @@ int RSound3::command16() {
 	if (_command16AltFlag) {
 		byte *pData = loadData(0x345E);
 		if (!isSoundPlaying(pData)) {
-			_channels[0].playData(pData);
-			_channels[1].playData(loadData(0x364C));
-			_channels[2].playData(loadData(0x3806));
-			_channels[3].playData(loadData(0x399E));
+			playSoundStatic(pData, 1);
+			playSoundStatic(0x364C, 2);
+			playSoundStatic(0x3806, 3);
+			playSoundStatic(0x399E, 4);
 		}
 	} else {
 		byte *pData = loadData(0x3B26);
 		if (!isSoundPlaying(pData)) {
 			command1();
-			_channels[0].playData(pData);
-			_channels[1].playData(loadData(0x3BD8));
-			_channels[2].playData(loadData(0x3CF8));
-			_channels[3].playData(loadData(0x3E46));
+			playSoundStatic(pData, 1);
+			playSoundStatic(0x3BD8, 2);
+			playSoundStatic(0x3CF8, 3);
+			playSoundStatic(0x3E46, 4);
 		}
 	}
 	return 0;
@@ -1081,62 +1045,62 @@ int RSound3::command17() {
 	byte *pData = loadData(0x3F5C);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x4022));
-		_channels[2].playData(loadData(0x41F0));
-		_channels[3].playData(loadData(0x42F8));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x4022, 2);
+		playSoundStatic(0x41F0, 3);
+		playSoundStatic(0x42F8, 4);
 	}
 	return 0;
 }
 
 int RSound3::command18() {
 	command1();
-	_channels[0].playData(loadData(0x4492));
-	_channels[1].playData(loadData(0x45A4));
-	_channels[2].playData(loadData(0x46A2));
-	_channels[3].playData(loadData(0x47DC));
-	_channels[4].playData(loadData(0x49C0));
-	_channels[5].playData(loadData(0x4A4E));
+	playSoundStatic(0x4492, 1);
+	playSoundStatic(0x45A4, 2);
+	playSoundStatic(0x46A2, 3);
+	playSoundStatic(0x47DC, 4);
+	playSoundStatic(0x49C0, 5);
+	playSoundStatic(0x4A4E, 6);
 	return 0;
 }
 
 int RSound3::command19() {
 	if (!isSoundPlaying(loadData(0x1AE6)))
-		playSoundCh5To8(0x12B4);
+		playSoundDynamic(0x12B4);
 	return 0;
 }
 
 int RSound3::command20() {
 	if (!isSoundPlaying(loadData(0x1AE6)))
-		playSoundCh5To8(0x1246);
+		playSoundDynamic(0x1246);
 	return 0;
 }
 
 int RSound3::command21() {
 	if (!isSoundPlaying(loadData(0x1AE6))) {
-		playSoundCh5To8(0x1232);
-		playSoundCh5To8(0x123C);
+		playSoundDynamic(0x1232);
+		playSoundDynamic(0x123C);
 	}
 	return 0;
 }
 
 int RSound3::command22() {
-	playSoundCh5To8(0x126E);
+	playSoundDynamic(0x126E);
 	return 0;
 }
 
 int RSound3::command23() {
 	if (!isSoundPlaying(loadData(0x1AE6))) {
-		playSoundCh5To8(0x12D2);
-		playSoundCh5To8(0x12E0);
+		playSoundDynamic(0x12D2);
+		playSoundDynamic(0x12E0);
 	}
 	return 0;
 }
 
 int RSound3::command24() {
 	if (!isSoundPlaying(loadData(0x1AE6))) {
-		playSoundCh5To8(0x11E2);
-		playSoundCh5To8(0x120A);
+		playSoundDynamic(0x11E2);
+		playSoundDynamic(0x120A);
 	}
 	return 0;
 }
@@ -1144,162 +1108,162 @@ int RSound3::command24() {
 int RSound3::command25() {
 	// The dispatcher's preceding xor leaves ZF set, so the native jz
 	// selects 0x25 for both calls.
-	method1(0x11A6, 0x25);
-	method1(0x11C4, 0x25);
+	patchAndPlaySound(0x11A6, 0x25);
+	patchAndPlaySound(0x11C4, 0x25);
 	return 0;
 }
 
 int RSound3::command26() {
-	playSoundCh5To8(0x1252);
+	playSoundDynamic(0x1252);
 	return 0;
 }
 
-int RSound3::command27x42() {
-	playSoundCh5To8(0x14BE);
+int RSound3::command27_42() {
+	playSoundDynamic(0x14BE);
 	return 0;
 }
 
 int RSound3::command28() {
 	byte *pData = loadData(0x12EE);
-	pData[3] = 0x4D;
-	playSoundCh5To8(0x12EE);
+	pData[3] = 0x4D; // Medium volume
+	playSoundDynamic(0x12EE);
 	return 0;
 }
 
 int RSound3::command29() {
 	byte *pData = loadData(0x12EE);
-	pData[3] = 0x7F;
-	playSoundCh5To8(0x12EE);
+	pData[3] = 0x7F; // High volume
+	playSoundDynamic(0x12EE);
 	return 0;
 }
 
 int RSound3::command30() {
-	playSoundCh5To8(0x14B4);
-	playSoundCh5To8(0x14AA);
+	playSoundDynamic(0x14B4);
+	playSoundDynamic(0x14AA);
 	return 0;
 }
 
 int RSound3::command31() {
-	playSoundCh5To8(0x12F8);
-	playSoundCh5To8(0x130E);
+	playSoundDynamic(0x12F8);
+	playSoundDynamic(0x130E);
 	return 0;
 }
 
 int RSound3::command32() {
-	playSoundCh5To8(0x1472);
+	playSoundDynamic(0x1472);
 	return 0;
 }
 
 int RSound3::command33() {
-	playSoundCh5To8(0x147E);
+	playSoundDynamic(0x147E);
 	return 0;
 }
 
 int RSound3::command34() {
-	playSoundCh5To8(0x1488);
+	playSoundDynamic(0x1488);
 	return 0;
 }
 
 int RSound3::command35() {
-	playSoundCh5To8(0x1498);
+	playSoundDynamic(0x1498);
 	return 0;
 }
 
 int RSound3::command36() {
-	playSoundCh5To8(0x14DA);
-	playSoundCh5To8(0x14EE);
+	playSoundDynamic(0x14DA);
+	playSoundDynamic(0x14EE);
 	return 0;
 }
 
 int RSound3::command37() {
-	playSoundCh5To8(0x14CC);
+	playSoundDynamic(0x14CC);
 	return 0;
 }
 
 int RSound3::command38() {
-	playSoundCh5To8(0x133A);
+	playSoundDynamic(0x133A);
 	return 0;
 }
 
 int RSound3::command39() {
 	byte *pData = loadData(0x1346);
 	pData[3] = 77;
-	_command3940Toggle ^= 4;
-	pData[6] = _command3940Toggle + 0x28;
-	playSoundCh5To8(0x1346);
+	_command39_40NoteToggle ^= 4;
+	pData[6] = _command39_40NoteToggle + 0x28;
+	playSoundDynamic(0x1346);
 	return 0;
 }
 
 int RSound3::command40() {
 	byte *pData = loadData(0x1346);
 	pData[3] = 47;
-	_command3940Toggle ^= 4;
-	pData[6] = _command3940Toggle + 0x28;
-	playSoundCh5To8(0x1346);
+	_command39_40NoteToggle ^= 4;
+	pData[6] = _command39_40NoteToggle + 0x28;
+	playSoundDynamic(0x1346);
 	return 0;
 }
 
 int RSound3::command41() {
-	playSoundCh5To8(0x1184);
+	playSoundDynamic(0x1184);
 	return 0;
 }
 
 int RSound3::command43() {
-	playSoundCh5To8(0x1350);
-	playSoundCh5To8(0x135A);
+	playSoundDynamic(0x1350);
+	playSoundDynamic(0x135A);
 	return 0;
 }
 
 int RSound3::command44() {
-	playSoundCh5To8(0x12A0);
+	playSoundDynamic(0x12A0);
 	return 0;
 }
 
 int RSound3::command45() {
-	playSoundCh5To8(0x12AA);
+	playSoundDynamic(0x12AA);
 	return 0;
 }
 
 int RSound3::command46() {
-	playSoundCh5To8(0x13AA);
-	playSoundCh5To8(0x13C6);
+	playSoundDynamic(0x13AA);
+	playSoundDynamic(0x13C6);
 	return 0;
 }
 
-int RSound3::command47x49() {
-	playSoundCh5To8(0x13E6);
-	playSoundCh5To8(0x13FE);
+int RSound3::command47_49() {
+	playSoundDynamic(0x13E6);
+	playSoundDynamic(0x13FE);
 	return 0;
 }
 
 int RSound3::command48() {
-	playSoundCh5To8(0x141E);
+	playSoundDynamic(0x141E);
 	return 0;
 }
 
 int RSound3::command50() {
-	playSoundCh5To8(0x1436);
-	playSoundCh5To8(0x144C);
+	playSoundDynamic(0x1436);
+	playSoundDynamic(0x144C);
 	return 0;
 }
 
 int RSound3::command51() {
-	playSoundCh5To8(0x125C);
+	playSoundDynamic(0x125C);
 	return 0;
 }
 
 int RSound3::command57() {
-	playSoundCh5To8(0x1466);
+	playSoundDynamic(0x1466);
 	return 0;
 }
 
 int RSound3::command59() {
-	playSoundCh5To8(0x1324);
+	playSoundDynamic(0x1324);
 	return 0;
 }
 
 int RSound3::command60() {
-	playSoundCh5To8(0x132E);
+	playSoundDynamic(0x132E);
 	return 0;
 }
 
@@ -1337,28 +1301,40 @@ int RSound4::command(int commandId, int param) {
 }
 
 void RSound4::tickCallback() {
-	if (!_callbackPeriod)
-		return;
-	if (--_callbackCounter)
+	if (_callbackPeriod == 0 || --_callbackCounter > 0)
 		return;
 
 	_callbackCounter = _callbackPeriod;
-	if (_callbackFnPtr)
+	if (_callbackFnPtr != nullptr)
 		(this->*_callbackFnPtr)();
 }
 
+void RSound4::stopChannel(byte channel) {
+	getChannel(channel)->_pSrc = loadData(0x1188);
+}
+
 int RSound4::command9() {
 	command1();
 	setFadeOutSpeed((byte)_commandParam);
 	return 0;
 }
 
-byte RSound4::paramToVariant() {
-	return (byte)((_commandParam >> 1) + 36);
+void RSound4::playCommand10_58CommonChannels() {
+	playSoundStatic(0x1274, 1);
+	playSoundStatic(0x13A6, 2);
+	playSoundStatic(0x15E4, 3);
+}
+
+int RSound4::command10() {
+	command1();
+	playSoundStatic(0x17DE, 4);
+	playSoundStatic(0x181C, 9);
+	playCommand10_58CommonChannels();
+	return 0;
 }
 
 void RSound4::setCommand12Variant() {
-	byte value = paramToVariant();
+	byte value = (byte)((_commandParam >> 1) + 36);
 	loadData(0x1966)[1] = value;
 	loadData(0x1DC8)[1] = value;
 	loadData(0x1FBA)[1] = value;
@@ -1367,98 +1343,84 @@ void RSound4::setCommand12Variant() {
 }
 
 int RSound4::command12() {
-	if (_channels[4]._soundData == loadData(0x1966) && _channels[4]._deltaCounter) {
+	if (isSoundPlaying(5, loadData(0x1966))) {
 		setCommand12Variant();
 		return 0;
 	}
 
 	setCommand12Variant();
 	command1();
-	_channels[4].playData(loadData(0x1966));
-	_channels[5].playData(loadData(0x1DC8));
-	_channels[6].playData(loadData(0x1FBA));
-	_channels[7].playData(loadData(0x211E));
-	_channels[8].playData(loadData(0x24A8));
-	return 0;
-}
-
-void RSound4::loadIntroChannels() {
-	_channels[0].playData(loadData(0x1274));
-	_channels[1].playData(loadData(0x13A6));
-	_channels[2].playData(loadData(0x15E4));
-}
-
-int RSound4::command10() {
-	command1();
-	_channels[3].playData(loadData(0x17DE));
-	_channels[8].playData(loadData(0x181C));
-	loadIntroChannels();
+	playSoundStatic(0x1966, 5);
+	playSoundStatic(0x1DC8, 6);
+	playSoundStatic(0x1FBA, 7);
+	playSoundStatic(0x211E, 8);
+	playSoundStatic(0x24A8, 9);
 	return 0;
 }
 
 int RSound4::command19() {
-	playSoundCh5To8(0x1196);
+	playSoundDynamic(0x1196);
 	return 0;
 }
 
 int RSound4::command20() {
-	playSoundCh5To8(0x118A);
+	playSoundDynamic(0x118A);
 	return 0;
 }
 
 int RSound4::command21() {
-	playSoundCh5To8(0x1260);
-	playSoundCh5To8(0x126A);
+	playSoundDynamic(0x1260);
+	playSoundDynamic(0x126A);
 	return 0;
 }
 
 int RSound4::command27() {
-	playSoundCh5To8(0x1220);
+	playSoundDynamic(0x1220);
 	return 0;
 }
 
 int RSound4::command30() {
-	playSoundCh5To8(0x1216);
-	playSoundCh5To8(0x120C);
+	playSoundDynamic(0x1216);
+	playSoundDynamic(0x120C);
 	return 0;
 }
 
 int RSound4::command32() {
-	playSoundCh5To8(0x11D4);
+	playSoundDynamic(0x11D4);
 	return 0;
 }
 
 int RSound4::command33() {
-	playSoundCh5To8(0x11E0);
+	playSoundDynamic(0x11E0);
 	return 0;
 }
 
 int RSound4::command34() {
-	playSoundCh5To8(0x11EA);
+	playSoundDynamic(0x11EA);
 	return 0;
 }
 
 int RSound4::command35() {
-	playSoundCh5To8(0x11FA);
+	playSoundDynamic(0x11FA);
 	return 0;
 }
 
 int RSound4::command36() {
-	playSoundCh5To8(0x123C);
-	playSoundCh5To8(0x1250);
+	playSoundDynamic(0x123C);
+	playSoundDynamic(0x1250);
 	return 0;
 }
 
 int RSound4::command37() {
-	playSoundCh5To8(0x122E);
+	playSoundDynamic(0x122E);
 	return 0;
 }
 
 int RSound4::command52() {
-	_channels[0]._pSrc = loadData(0x1188);
-	_channels[1]._pSrc = loadData(0x1188);
-	_channels[2]._pSrc = loadData(0x1188);
-	_channels[4].playData(loadData(0x2A0C));
+	stopChannel(1);
+	stopChannel(2);
+	stopChannel(3);
+	playSoundStatic(0x2A0C, 5);
 	return 0;
 }
 
@@ -1466,55 +1428,58 @@ int RSound4::command53() {
 	command1();
 	_callbackCounter = 56;
 	_callbackPeriod = 56;
-	playSoundCh1To8(0x1888);
-	playSoundCh1To8(0x18DE);
+	playSoundAnyChannel(0x1888);
+	playSoundAnyChannel(0x18DE);
 	return 0;
 }
 
-void RSound4::loadCommand54() {
+void RSound4::command54Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundCh1To8(0x18B2);
-	playSoundCh1To8(0x1904);
+	playSoundAnyChannel(0x18B2);
+	playSoundAnyChannel(0x1904);
 }
 
 int RSound4::command54() {
-	_callbackFnPtr = &RSound4::loadCommand54;
+	// Add an instrument to command 53's music at the start of the next bar.
+	_callbackFnPtr = &RSound4::command54Callback;
 	return 0;
 }
 
-void RSound4::loadCommand55() {
+void RSound4::command55Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundCh1To8(0x191E);
+	playSoundAnyChannel(0x191E);
 }
 
 int RSound4::command55() {
-	_callbackFnPtr = &RSound4::loadCommand55;
+	// Add an instrument to command 53's music at the start of the next bar.
+	_callbackFnPtr = &RSound4::command55Callback;
 	return 0;
 }
 
-void RSound4::loadCommand56() {
+void RSound4::command56Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundCh1To8(0x185C);
+	playSoundAnyChannel(0x185C);
 }
 
 int RSound4::command56() {
-	_callbackFnPtr = &RSound4::loadCommand56;
+	// Add an instrument to command 53's music at the start of the next bar.
+	_callbackFnPtr = &RSound4::command56Callback;
 	return 0;
 }
 
 int RSound4::command57() {
-	playSoundCh5To8(0x11C8);
+	playSoundDynamic(0x11C8);
 	return 0;
 }
 
 int RSound4::command58() {
-	_channels[4]._pSrc = loadData(0x1188);
-	loadIntroChannels();
+	stopChannel(5);
+	playCommand10_58CommonChannels();
 	return 0;
 }
 
 int RSound4::command59() {
-	playSoundCh5To8(0x11BE);
+	playSoundDynamic(0x11BE);
 	return 0;
 }
 
@@ -1523,11 +1488,11 @@ int RSound4::command59() {
 const RSound5::CommandPtr RSound5::_commandList[42] = {
 	&RSound5::command0, &RSound5::command1, &RSound5::command2, &RSound5::command3,
 	&RSound5::command4, &RSound5::command5, &RSound5::command6, &RSound5::command7,
-	&RSound5::command8, &RSound5::command9, &RSound5::command10, &RSound5::command11x24,
-	&RSound5::command12x25, &RSound5::command13, &RSound5::command14, &RSound5::command15,
+	&RSound5::command8, &RSound5::command9, &RSound5::command10, &RSound5::command11_24,
+	&RSound5::command12_25, &RSound5::command13, &RSound5::command14, &RSound5::command15,
 	&RSound5::command16, &RSound5::command17, &RSound5::command18, &RSound5::command19,
 	&RSound5::command20, &RSound5::command21, &RSound5::command22, &RSound5::command23,
-	&RSound5::command11x24, &RSound5::command12x25, &RSound5::command26, &RSound5::command27,
+	&RSound5::command11_24, &RSound5::command12_25, &RSound5::command26, &RSound5::command27,
 	&RSound5::command28, &RSound5::command29, &RSound5::command30, &RSound5::command31,
 	&RSound5::command32, &RSound5::command33, &RSound5::command34, &RSound5::command35,
 	&RSound5::command36, &RSound5::command37, &RSound5::command38, &RSound5::command39,
@@ -1547,190 +1512,195 @@ int RSound5::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
+void RSound5::stopChannel(byte channel) {
+	getChannel(channel)->_pSrc = loadData(0x1182);
+}
+
 int RSound5::command9() {
-	playSoundCh5To8(0x11C4);
+	playSoundDynamic(0x11C4);
 	return 0;
 }
 
 int RSound5::command10() {
-	playSoundCh5To8(0x1238);
+	playSoundDynamic(0x1238);
 	return 0;
 }
 
-int RSound5::command11x24() {
-	playSoundCh5To8(0x1196);
+int RSound5::command11_24() {
+	playSoundDynamic(0x1196);
 	return 0;
 }
 
-int RSound5::command12x25() {
-	playSoundCh5To8(0x1242);
+int RSound5::command12_25() {
+	playSoundDynamic(0x1242);
 	return 0;
 }
 
 int RSound5::command13() {
-	playSoundCh5To8(0x125E);
-	playSoundCh5To8(0x1268);
+	playSoundDynamic(0x125E);
+	playSoundDynamic(0x1268);
 	return 0;
 }
 
 int RSound5::command14() {
-	_channels[7].playData(loadData(0x1272));
+	playSoundStatic(0x1272, 8);
 	return 0;
 }
 
 int RSound5::command15() {
-	if (_channels[7]._soundData == loadData(0x1272)) {
+	Channel *chan = getChannel(8);
+	if (chan->_soundData == loadData(0x1272)) {
 		byte *pData = loadData(0x1288);
-		_channels[7]._innerLoopStart = pData;
-		_channels[7]._outerLoopStart = pData;
-		_channels[7]._deltaCounter = 1;
+		chan->_innerLoopStart = pData;
+		chan->_outerLoopStart = pData;
+		chan->_deltaCounter = 1;
 	}
 	return 0;
 }
 
 int RSound5::command16() {
-	playSoundCh5To8(0x129A);
-	playSoundCh5To8(0x129A);
-	playSoundCh5To8(0x129A);
-	playSoundCh5To8(0x129A);
+	playSoundDynamic(0x129A);
+	playSoundDynamic(0x129A);
+	playSoundDynamic(0x129A);
+	playSoundDynamic(0x129A);
 	return 0;
 }
 
 int RSound5::command17() {
-	playSoundCh5To8(0x11B4);
+	playSoundDynamic(0x11B4);
 	return 0;
 }
 
 int RSound5::command18() {
-	playSoundCh5To8(0x12E4);
-	playSoundCh5To8(0x12F6);
-	playSoundCh5To8(0x1308);
-	playSoundCh5To8(0x131A);
+	playSoundDynamic(0x12E4);
+	playSoundDynamic(0x12F6);
+	playSoundDynamic(0x1308);
+	playSoundDynamic(0x131A);
 	return 0;
 }
 
 int RSound5::command19() {
-	playSoundCh5To8(0x132C);
+	playSoundDynamic(0x132C);
 	return 0;
 }
 
 int RSound5::command20() {
-	playSoundCh5To8(0x1368);
+	playSoundDynamic(0x1368);
 	return 0;
 }
 
 int RSound5::command21() {
-	playSoundCh5To8(0x1388);
+	playSoundDynamic(0x1388);
 	return 0;
 }
 
 int RSound5::command22() {
-	playSoundCh5To8(0x139A);
+	playSoundDynamic(0x139A);
 	return 0;
 }
 
 int RSound5::command23() {
-	playSoundCh5To8(0x13AA);
-	playSoundCh5To8(0x13AA);
-	playSoundCh5To8(0x13AA);
-	playSoundCh5To8(0x13AA);
+	playSoundDynamic(0x13AA);
+	playSoundDynamic(0x13AA);
+	playSoundDynamic(0x13AA);
+	playSoundDynamic(0x13AA);
 	return 0;
 }
 
 int RSound5::command26() {
-	playSoundCh5To8(0x13D6);
+	playSoundDynamic(0x13D6);
 	return 0;
 }
 
 int RSound5::command27() {
-	playSoundCh5To8(0x13F0);
+	playSoundDynamic(0x13F0);
 	return 0;
 }
 
 int RSound5::command28() {
-	playSoundCh5To8(0x121C);
+	playSoundDynamic(0x121C);
 	return 0;
 }
 
-void RSound5::loadTailChannels() {
-	_channels[3].playData(loadData(0x1688));
-	_channels[8].playData(loadData(0x1882));
+void RSound5::playCommand29_38CommonChannels() {
+	playSoundStatic(0x1688, 4);
+	playSoundStatic(0x1882, 9);
 }
 
 int RSound5::command29() {
 	byte *pData = loadData(0x1488);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x1534));
-		_channels[2].playData(loadData(0x15EA));
-		loadTailChannels();
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x1534, 2);
+		playSoundStatic(0x15EA, 3);
+		playCommand29_38CommonChannels();
 	}
 	return 0;
 }
 
 int RSound5::command30() {
-	playSoundCh5To8(0x1212);
-	playSoundCh5To8(0x1208);
+	playSoundDynamic(0x1212);
+	playSoundDynamic(0x1208);
 	return 0;
 }
 
 int RSound5::command31() {
-	playSoundCh5To8(0x140A);
+	playSoundDynamic(0x140A);
 	return 0;
 }
 
 int RSound5::command32() {
-	playSoundCh5To8(0x11D0);
+	playSoundDynamic(0x11D0);
 	return 0;
 }
 
 int RSound5::command33() {
-	playSoundCh5To8(0x11DC);
+	playSoundDynamic(0x11DC);
 	return 0;
 }
 
 int RSound5::command34() {
-	playSoundCh5To8(0x11E6);
+	playSoundDynamic(0x11E6);
 	return 0;
 }
 
 int RSound5::command35() {
-	playSoundCh5To8(0x11F6);
+	playSoundDynamic(0x11F6);
 	return 0;
 }
 
 int RSound5::command36() {
-	playSoundCh5To8(0x1464);
-	playSoundCh5To8(0x1478);
+	playSoundDynamic(0x1464);
+	playSoundDynamic(0x1478);
 	return 0;
 }
 
 int RSound5::command37() {
-	playSoundCh5To8(0x122A);
+	playSoundDynamic(0x122A);
 	return 0;
 }
 
 int RSound5::command38() {
-	_channels[4]._pSrc = loadData(0x1182);
-	loadTailChannels();
+	stopChannel(5);
+	playCommand29_38CommonChannels();
 	return 0;
 }
 
 int RSound5::command39() {
-	playSoundCh5To8(0x141E);
-	playSoundCh5To8(0x1428);
+	playSoundDynamic(0x141E);
+	playSoundDynamic(0x1428);
 	return 0;
 }
 
 int RSound5::command40() {
-	playSoundCh5To8(0x1432);
+	playSoundDynamic(0x1432);
 	return 0;
 }
 
 int RSound5::command41() {
-	_channels[8]._pSrc = loadData(0x1182);
-	_channels[3].playData(loadData(0x1BB6));
+	stopChannel(9);
+	playSoundStatic(0x1BB6, 4);
 	return 0;
 }
 
@@ -1740,11 +1710,11 @@ const RSound6::CommandPtr RSound6::_commandList[30] = {
 	&RSound6::command0, &RSound6::command1, &RSound6::command2, &RSound6::command3,
 	&RSound6::command4, &RSound6::command5, &RSound6::command6, &RSound6::command7,
 	&RSound6::command8, &RSound6::command9, &RSound6::command10, &RSound6::command11,
-	&RSound6::command12, &RSound6::command13x14, &RSound6::command13x14, &RSound6::command15,
+	&RSound6::command12, &RSound6::command13_14, &RSound6::command13_14, &RSound6::command15,
 	&RSound6::command16, &RSound6::command17, &RSound6::command18, &RSound6::command19,
 	&RSound6::command20, &RSound6::command21, &RSound6::command22, &RSound6::command23,
 	&RSound6::command24, &RSound6::command25, &RSound6::nullCommand, &RSound6::nullCommand,
-	&RSound6::nullCommand, &RSound6::command28
+	&RSound6::nullCommand, &RSound6::command29
 };
 
 RSound6::RSound6(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) : 
@@ -1761,148 +1731,150 @@ int RSound6::command(int commandId, int param) {
 }
 
 void RSound6::tickCallback() {
-	if (!_callbackPeriod)
-		return;
-	if (--_callbackCounter)
+	if (_callbackPeriod == 0 || --_callbackCounter > 0)
 		return;
 
 	_callbackCounter = _callbackPeriod;
-	if (_callbackFnPtr)
+	if (_callbackFnPtr != nullptr)
 		(this->*_callbackFnPtr)();
 }
 
-void RSound6::reloadCommand24() {
-	_callbackFnPtr = nullptr;
-	command1();
-	_callbackCounter = 84;
-	_callbackPeriod = 84;
-	_channels[0].playData(loadData(0x1A38));
-	_channels[1].playData(loadData(0x1BBE));
-	_channels[8].playData(loadData(0x1B90));
-}
-
-void RSound6::reloadCommand28() {
-	_callbackFnPtr = nullptr;
-	command1();
-	_callbackCounter = 84;
-	_callbackPeriod = 84;
-	_channels[0].playData(loadData(0x130A));
-	_channels[1].playData(loadData(0x13B6));
-	_channels[2].playData(loadData(0x146C));
-	_channels[3].playData(loadData(0x150A));
-	_channels[8].playData(loadData(0x1704));
-}
-
 int RSound6::command9() {
-	playSoundCh5To8(0x111C);
+	playSoundDynamic(0x111C);
 	return 0;
 }
 
 int RSound6::command10() {
-	playSoundCh5To8(0x1190);
+	playSoundDynamic(0x1190);
 	return 0;
 }
 
 int RSound6::command11() {
-	playSoundCh5To8(0x11A4);
-	playSoundCh5To8(0x11A4);
-	playSoundCh5To8(0x11A4);
-	playSoundCh5To8(0x11A4);
+	playSoundDynamic(0x11A4);
+	playSoundDynamic(0x11A4);
+	playSoundDynamic(0x11A4);
+	playSoundDynamic(0x11A4);
 	return 0;
 }
 
 int RSound6::command12() {
-	playSoundCh5To8(0x11C8);
+	playSoundDynamic(0x11C8);
 	return 0;
 }
 
-int RSound6::command13x14() {
-	playSoundCh5To8(0x11E8);
+int RSound6::command13_14() {
+	playSoundDynamic(0x11E8);
 	return 0;
 }
 
 int RSound6::command15() {
-	playSoundCh5To8(0x11F2);
-	playSoundCh5To8(0x11F2);
-	playSoundCh5To8(0x11F2);
-	playSoundCh5To8(0x11F2);
+	playSoundDynamic(0x11F2);
+	playSoundDynamic(0x11F2);
+	playSoundDynamic(0x11F2);
+	playSoundDynamic(0x11F2);
 	return 0;
 }
 
 int RSound6::command16() {
-	playSoundCh5To8(0x121E);
+	playSoundDynamic(0x121E);
 	return 0;
 }
 
 int RSound6::command17() {
-	playSoundCh5To8(0x1228);
-	playSoundCh5To8(0x1228);
-	playSoundCh5To8(0x1228);
-	playSoundCh5To8(0x1228);
+	playSoundDynamic(0x1228);
+	playSoundDynamic(0x1228);
+	playSoundDynamic(0x1228);
+	playSoundDynamic(0x1228);
 	return 0;
 }
 
 int RSound6::command18() {
-	playSoundCh5To8(0x1254);
+	playSoundDynamic(0x1254);
 	return 0;
 }
 
 int RSound6::command19() {
-	playSoundCh5To8(0x1266);
+	playSoundDynamic(0x1266);
 	return 0;
 }
 
 int RSound6::command20() {
-	playSoundCh5To8(0x1278);
+	playSoundDynamic(0x1278);
 	return 0;
 }
 
 int RSound6::command21() {
-	_channels[4].playData(loadData(0x1282));
-	playSoundCh5To8(0x1282);
-	playSoundCh5To8(0x1282);
-	playSoundCh5To8(0x12AA);
+	playSoundStatic(0x1282, 5);
+	playSoundDynamic(0x1282);
+	playSoundDynamic(0x1282);
+	playSoundDynamic(0x12AA);
 	return 0;
 }
 
 int RSound6::command22() {
-	_channels[4].playData(loadData(0x12CE));
-	_channels[5].playData(loadData(0x12CE));
-	_channels[6].playData(loadData(0x12CE));
-	_channels[7].playData(loadData(0x12CE));
+	playSoundStatic(0x12CE, 5);
+	playSoundStatic(0x12CE, 6);
+	playSoundStatic(0x12CE, 7);
+	playSoundStatic(0x12CE, 8);
 	return 0;
 }
 
 int RSound6::command23() {
-	playSoundCh5To8(0x1174);
+	playSoundDynamic(0x1174);
 	return 0;
 }
 
+void RSound6::command24Callback() {
+	_callbackFnPtr = nullptr;
+	command1();
+	_callbackCounter = 84;
+	_callbackPeriod = 84;
+	playSoundStatic(0x1A38, 1);
+	playSoundStatic(0x1BBE, 2);
+	playSoundStatic(0x1B90, 9);
+}
+
 int RSound6::command24() {
-	if (_channels[0]._deltaCounter && _channels[0]._soundData == loadData(0x130A)) {
-		_callbackFnPtr = &RSound6::reloadCommand24;
+	if (isSoundPlaying(1, loadData(0x130A))) {
+		// The music of command 29 is playing. Cut into command 24's music
+		// at the next bar, which is when the callback is invoked.
+		_callbackFnPtr = &RSound6::command24Callback;
 		return 0;
 	}
 
-	reloadCommand24();
+	command24Callback();
 	return 0;
 }
 
 int RSound6::command25() {
-	_channels[4].playData(loadData(0x12FE));
+	playSoundStatic(0x12FE, 5);
 	return 0;
 }
 
-int RSound6::command28() {
+void RSound6::command29Callback() {
+	_callbackFnPtr = nullptr;
+	command1();
+	_callbackCounter = 84;
+	_callbackPeriod = 84;
+	playSoundStatic(0x130A, 1);
+	playSoundStatic(0x13B6, 2);
+	playSoundStatic(0x146C, 3);
+	playSoundStatic(0x150A, 4);
+	playSoundStatic(0x1704, 9);
+}
+
+int RSound6::command29() {
 	if (isSoundPlaying(loadData(0x130A)))
 		return 0;
 
-	if (_channels[0]._deltaCounter && _channels[0]._soundData == loadData(0x1A38)) {
-		_callbackFnPtr = &RSound6::reloadCommand28;
+	if (isSoundPlaying(1, loadData(0x1A38))) {
+		// The music of command 24 is playing. Cut into command 29's music
+		// at the next bar, which is when the callback is invoked.
+		_callbackFnPtr = &RSound6::command29Callback;
 		return 0;
 	}
 
-	reloadCommand28();
+	command29Callback();
 	return 0;
 }
 
@@ -1936,125 +1908,126 @@ int RSound7::command(int commandId, int param) {
 
 int RSound7::command9() {
 	command1();
-	_channels[0].playData(loadData(0x1C0E));
-	_channels[1].playData(loadData(0x1C88));
-	_channels[2].playData(loadData(0x1CD4));
-	_channels[3].playData(loadData(0x1D4E));
+	playSoundStatic(0x1C0E, 1);
+	playSoundStatic(0x1C88, 2);
+	playSoundStatic(0x1CD4, 3);
+	playSoundStatic(0x1D4E, 4);
 	return 0;
 }
 
 int RSound7::command15() {
-	playSoundCh5To8(0x125C);
+	playSoundDynamic(0x125C);
 	return 0;
 }
 
 int RSound7::command16() {
-	playSoundCh5To8(0x12DE);
+	playSoundDynamic(0x12DE);
 	return 0;
 }
 
 int RSound7::command17() {
-	playSoundCh5To8(0x12C2);
+	playSoundDynamic(0x12C2);
 	return 0;
 }
 
 int RSound7::command18() {
-	_channels[7].playData(loadData(0x12FC));
+	playSoundStatic(0x12FC, 8);
 	return 0;
 }
 
 int RSound7::command19() {
-	if (_channels[7]._soundData == loadData(0x12FC)) {
+	Channel *chan = getChannel(8);
+	if (chan->_soundData == loadData(0x12FC)) {
 		byte *pData = loadData(0x1312);
-		_channels[7]._innerLoopStart = pData;
-		_channels[7]._outerLoopStart = pData;
-		_channels[7]._deltaCounter = 1;
+		chan->_innerLoopStart = pData;
+		chan->_outerLoopStart = pData;
+		chan->_deltaCounter = 1;
 	}
 	return 0;
 }
 
 int RSound7::command20() {
-	playSoundCh5To8(0x1324);
-	playSoundCh5To8(0x1324);
+	playSoundDynamic(0x1324);
+	playSoundDynamic(0x1324);
 	return 0;
 }
 
 int RSound7::command21() {
-	playSoundCh5To8(0x1336);
+	playSoundDynamic(0x1336);
 	return 0;
 }
 
 int RSound7::command22() {
-	playSoundCh5To8(0x1340);
+	playSoundDynamic(0x1340);
 	return 0;
 }
 
 int RSound7::command23() {
-	playSoundCh5To8(0x12B4);
+	playSoundDynamic(0x12B4);
 	return 0;
 }
 
 int RSound7::command24() {
-	_channels[0].playData(loadData(0x137C));
-	_channels[1].playData(loadData(0x1406));
-	_channels[2].playData(loadData(0x1492));
-	_channels[3].playData(loadData(0x1516));
-	_channels[4].playData(loadData(0x1588));
+	playSoundStatic(0x137C, 1);
+	playSoundStatic(0x1406, 2);
+	playSoundStatic(0x1492, 3);
+	playSoundStatic(0x1516, 4);
+	playSoundStatic(0x1588, 5);
 	return 0;
 }
 
 int RSound7::command25() {
 	command1();
-	_channels[0].playData(loadData(0x1612));
-	_channels[1].playData(loadData(0x16C8));
-	_channels[2].playData(loadData(0x177E));
-	_channels[3].playData(loadData(0x1838));
+	playSoundStatic(0x1612, 1);
+	playSoundStatic(0x16C8, 2);
+	playSoundStatic(0x177E, 3);
+	playSoundStatic(0x1838, 4);
 	return 0;
 }
 
 int RSound7::command27() {
-	_channels[0].playData(loadData(0x1932));
-	_channels[1].playData(loadData(0x1986));
-	_channels[2].playData(loadData(0x19EC));
-	_channels[3].playData(loadData(0x1A66));
-	_channels[4].playData(loadData(0x1B3C));
+	playSoundStatic(0x1932, 1);
+	playSoundStatic(0x1986, 2);
+	playSoundStatic(0x19EC, 3);
+	playSoundStatic(0x1A66, 4);
+	playSoundStatic(0x1B3C, 5);
 	return 0;
 }
 
 int RSound7::command30() {
-	playSoundCh5To8(0x12AA);
-	playSoundCh5To8(0x12A0);
+	playSoundDynamic(0x12AA);
+	playSoundDynamic(0x12A0);
 	return 0;
 }
 
 int RSound7::command32() {
-	playSoundCh5To8(0x1268);
+	playSoundDynamic(0x1268);
 	return 0;
 }
 
 int RSound7::command33() {
-	playSoundCh5To8(0x1274);
+	playSoundDynamic(0x1274);
 	return 0;
 }
 
 int RSound7::command34() {
-	playSoundCh5To8(0x127E);
+	playSoundDynamic(0x127E);
 	return 0;
 }
 
 int RSound7::command35() {
-	playSoundCh5To8(0x128E);
+	playSoundDynamic(0x128E);
 	return 0;
 }
 
 int RSound7::command36() {
-	playSoundCh5To8(0x1358);
-	playSoundCh5To8(0x136C);
+	playSoundDynamic(0x1358);
+	playSoundDynamic(0x136C);
 	return 0;
 }
 
 int RSound7::command37() {
-	playSoundCh5To8(0x134A);
+	playSoundDynamic(0x134A);
 	return 0;
 }
 
@@ -2087,118 +2060,118 @@ int RSound8::command(int commandId, int param) {
 }
 
 int RSound8::command9() {
-	playSoundCh5To8(0x10BA);
+	playSoundDynamic(0x10BA);
 	return 0;
 }
 
 int RSound8::command10() {
-	_channels[0].playData(loadData(0x115A));
-	_channels[1].playData(loadData(0x115A));
-	_channels[2].playData(loadData(0x115A));
-	_channels[3].playData(loadData(0x1150));
+	playSoundStatic(0x115A, 1);
+	playSoundStatic(0x115A, 2);
+	playSoundStatic(0x115A, 3);
+	playSoundStatic(0x1150, 4);
 	return 0;
 }
 
 int RSound8::command11() {
-	playSoundCh5To8(0x1194);
+	playSoundDynamic(0x1194);
 	return 0;
 }
 
 int RSound8::command12() {
-	playSoundCh5To8(0x11B2);
+	playSoundDynamic(0x11B2);
 	return 0;
 }
 
 int RSound8::command13() {
-	playSoundCh5To8(0x11D0);
-	playSoundCh5To8(0x11D0);
-	playSoundCh5To8(0x11D0);
-	playSoundCh5To8(0x11D0);
+	playSoundDynamic(0x11D0);
+	playSoundDynamic(0x11D0);
+	playSoundDynamic(0x11D0);
+	playSoundDynamic(0x11D0);
 	return 0;
 }
 
-void RSound8::setCommand1415Variant(byte v1, byte v2) {
+void RSound8::playCommand14_15Variant(byte v1, byte v2) {
 	byte *pData = loadData(0x1204);
 	pData[3] = v1;
 	pData[6] = v2;
 	pData[9] = v2;
-	playSoundCh5To8(0x1204);
-	playSoundCh5To8(0x1204);
-	playSoundCh5To8(0x1204);
-	playSoundCh5To8(0x1204);
+	playSoundDynamic(0x1204);
+	playSoundDynamic(0x1204);
+	playSoundDynamic(0x1204);
+	playSoundDynamic(0x1204);
 }
 
 int RSound8::command14() {
-	setCommand1415Variant(40, 1);
+	playCommand14_15Variant(40, 1);
 	return 0;
 }
 
 int RSound8::command15() {
-	setCommand1415Variant(100, 255);
+	playCommand14_15Variant(100, 255);
 	return 0;
 }
 
 int RSound8::command16() {
-	playSoundCh5To8(0x112E);
-	playSoundCh5To8(0x112E);
+	playSoundDynamic(0x112E);
+	playSoundDynamic(0x112E);
 	return 0;
 }
 
 int RSound8::command17() {
-	playSoundCh5To8(0x1234);
+	playSoundDynamic(0x1234);
 	return 0;
 }
 
 int RSound8::command18() {
-	playSoundCh5To8(0x1244);
+	playSoundDynamic(0x1244);
 	return 0;
 }
 
 int RSound8::command19() {
-	playSoundCh5To8(0x1254);
+	playSoundDynamic(0x1254);
 	return 0;
 }
 
 int RSound8::command20() {
-	playSoundCh5To8(0x125E);
+	playSoundDynamic(0x125E);
 	return 0;
 }
 
 int RSound8::command21() {
-	playSoundCh5To8(0x126E);
+	playSoundDynamic(0x126E);
 	return 0;
 }
 
 int RSound8::command22() {
-	playSoundCh5To8(0x1278);
+	playSoundDynamic(0x1278);
 	return 0;
 }
 
 int RSound8::command23() {
-	_channels[0].playData(loadData(0x128E));
-	_channels[1].playData(loadData(0x128E));
-	_channels[2].playData(loadData(0x128E));
-	_channels[3].playData(loadData(0x128E));
+	playSoundStatic(0x128E, 1);
+	playSoundStatic(0x128E, 2);
+	playSoundStatic(0x128E, 3);
+	playSoundStatic(0x128E, 4);
 	return 0;
 }
 
 int RSound8::command24() {
-	playSoundCh5To8(0x12B4);
+	playSoundDynamic(0x12B4);
 	return 0;
 }
 
 int RSound8::command25() {
-	playSoundCh5To8(0x12CA);
+	playSoundDynamic(0x12CA);
 	return 0;
 }
 
 int RSound8::command26() {
-	playSoundCh5To8(0x12DC);
+	playSoundDynamic(0x12DC);
 	return 0;
 }
 
 int RSound8::command27() {
-	playSoundCh5To8(0x1112);
+	playSoundDynamic(0x1112);
 	return 0;
 }
 
@@ -2206,9 +2179,9 @@ int RSound8::command28() {
 	byte *pData = loadData(0x130A);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[0].playData(pData);
-		_channels[1].playData(loadData(0x1480));
-		_channels[8].playData(loadData(0x154C));
+		playSoundStatic(pData, 1);
+		playSoundStatic(0x1480, 2);
+		playSoundStatic(0x154C, 9);
 	}
 	return 0;
 }
@@ -2217,51 +2190,51 @@ int RSound8::command29() {
 	byte *pData = loadData(0x15FC);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		_channels[2].playData(pData);
-		_channels[8].playData(loadData(0x1652));
+		playSoundStatic(pData, 3);
+		playSoundStatic(0x1652, 9);
 	}
 	return 0;
 }
 
 int RSound8::command30() {
-	playSoundCh5To8(0x1108);
-	playSoundCh5To8(0x10FE);
+	playSoundDynamic(0x1108);
+	playSoundDynamic(0x10FE);
 	return 0;
 }
 
 int RSound8::command31() {
-	playSoundCh5To8(0x1140);
+	playSoundDynamic(0x1140);
 	return 0;
 }
 
 int RSound8::command32() {
-	playSoundCh5To8(0x10C6);
+	playSoundDynamic(0x10C6);
 	return 0;
 }
 
 int RSound8::command33() {
-	playSoundCh5To8(0x10D2);
+	playSoundDynamic(0x10D2);
 	return 0;
 }
 
 int RSound8::command34() {
-	playSoundCh5To8(0x10DC);
+	playSoundDynamic(0x10DC);
 	return 0;
 }
 
 int RSound8::command35() {
-	playSoundCh5To8(0x10EC);
+	playSoundDynamic(0x10EC);
 	return 0;
 }
 
 int RSound8::command36() {
-	playSoundCh5To8(0x12E6);
-	playSoundCh5To8(0x12FA);
+	playSoundDynamic(0x12E6);
+	playSoundDynamic(0x12FA);
 	return 0;
 }
 
 int RSound8::command37() {
-	playSoundCh5To8(0x1120);
+	playSoundDynamic(0x1120);
 	return 0;
 }
 
@@ -2300,13 +2273,11 @@ int RSound9::command(int commandId, int param) {
 }
 
 void RSound9::tickCallback() {
-	if (!_callbackPeriod)
-		return;
-	if (--_callbackCounter)
+	if (_callbackPeriod == 0 || --_callbackCounter > 0)
 		return;
 
 	_callbackCounter = _callbackPeriod;
-	if (_callbackFnPtr)
+	if (_callbackFnPtr != nullptr)
 		(this->*_callbackFnPtr)();
 }
 
@@ -2320,66 +2291,66 @@ int RSound9::command0() {
 int RSound9::command9() {
 	_callbackCounter = 1848;
 	_callbackPeriod = 84;
-	_channels[0].playData(loadData(0x16E4));
-	_channels[1].playData(loadData(0x1E9E));
-	_channels[2].playData(loadData(0x2F9C));
-	_channels[3].playData(loadData(0x2644));
-	_channels[4].playData(loadData(0x1FF4));
-	_channels[5].playData(loadData(0x2382));
-	_channels[6].playData(loadData(0x1AAE));
+	playSoundStatic(0x16E4, 1);
+	playSoundStatic(0x1E9E, 2);
+	playSoundStatic(0x2F9C, 3);
+	playSoundStatic(0x2644, 4);
+	playSoundStatic(0x1FF4, 5);
+	playSoundStatic(0x2382, 6);
+	playSoundStatic(0x1AAE, 7);
 	return 0;
 }
 
 int RSound9::command10() {
-	_channels[0].playData(loadData(0x31E2));
-	_channels[1].playData(loadData(0x31FA));
-	_channels[2].playData(loadData(0x3212));
-	_channels[3].playData(loadData(0x322C));
+	playSoundStatic(0x31E2, 1);
+	playSoundStatic(0x31FA, 2);
+	playSoundStatic(0x3212, 3);
+	playSoundStatic(0x322C, 4);
 	return 0;
 }
 
 int RSound9::command11() {
-	_channels[7].playData(loadData(0x33E2));
+	playSoundStatic(0x33E2, 8);
 	return 0;
 }
 
 int RSound9::command12() {
-	_channels[7].playData(loadData(0x342E));
+	playSoundStatic(0x342E, 8);
 	return 0;
 }
 
 int RSound9::command13() {
-	_channels[7].playData(loadData(0x343A));
+	playSoundStatic(0x343A, 8);
 	return 0;
 }
 
 int RSound9::command14() {
-	_channels[7].playData(loadData(0x3442));
+	playSoundStatic(0x3442, 8);
 	return 0;
 }
 
 int RSound9::command15() {
-	_channels[7].playData(loadData(0x3462));
+	playSoundStatic(0x3462, 8);
 	return 0;
 }
 
 int RSound9::command16() {
-	_channels[7].playData(loadData(0x347A));
+	playSoundStatic(0x347A, 8);
 	return 0;
 }
 
 int RSound9::command17() {
-	_channels[7].playData(loadData(0x3470));
+	playSoundStatic(0x3470, 8);
 	return 0;
 }
 
 int RSound9::command18() {
-	playSoundCh5To8(0x3248);
+	playSoundDynamic(0x3248);
 	return 0;
 }
 
 int RSound9::command19() {
-	playSoundCh5To8(0x3262);
+	playSoundDynamic(0x3262);
 	return 0;
 }
 
@@ -2387,7 +2358,7 @@ int RSound9::command20() {
 	int v = (generateRandomNumber() & 24) + 77;
 	byte *pData = loadData(0x3284);
 	pData[6] = v & 0x7F;
-	playSoundCh5To8(0x3284);
+	playSoundDynamic(0x3284);
 	return 0;
 }
 
@@ -2395,7 +2366,7 @@ int RSound9::command21() {
 	byte *pData = loadData(0x3298);
 	pData[9] = 70;
 	if (!isSoundPlaying(pData))
-		playSoundCh5To8(0x3298);
+		playSoundDynamic(0x3298);
 	return 0;
 }
 
@@ -2403,42 +2374,42 @@ int RSound9::command22() {
 	byte *pData = loadData(0x3298);
 	pData[9] = 45;
 	if (!isSoundPlaying(pData))
-		playSoundCh5To8(0x3298);
+		playSoundDynamic(0x3298);
 	return 0;
 }
 
 int RSound9::command23() {
-	Channel *chan = playSoundCh5To8(0x32B0);
-	if (chan)
+	Channel *chan = playSoundDynamic(0x32B0);
+	if (chan != nullptr)
 		chan->_innerLoopStart = loadData(0x32CE);
 
-	chan = playSoundCh5To8(0x32B6);
-	if (chan)
+	chan = playSoundDynamic(0x32B6);
+	if (chan != nullptr)
 		chan->_innerLoopStart = loadData(0x32CE);
 
-	chan = playSoundCh5To8(0x32C8);
-	if (chan)
+	chan = playSoundDynamic(0x32C8);
+	if (chan != nullptr)
 		chan->_innerLoopStart = loadData(0x32CE);
 	return 0;
 }
 
 int RSound9::command24() {
-	playSoundCh5To8(0x32E0);
+	playSoundDynamic(0x32E0);
 	return 0;
 }
 
 int RSound9::command25() {
-	playSoundCh5To8(0x32F6);
+	playSoundDynamic(0x32F6);
 	return 0;
 }
 
 int RSound9::command26() {
-	playSoundCh5To8(0x331A);
+	playSoundDynamic(0x331A);
 	return 0;
 }
 
 int RSound9::command27() {
-	playSoundCh5To8(0x3332);
+	playSoundDynamic(0x3332);
 	return 0;
 }
 
@@ -2446,7 +2417,7 @@ int RSound9::command28() {
 	int v = (generateRandomNumber() & 28) + 15;
 	byte *pData = loadData(0x334A);
 	pData[6] = v & 0x7F;
-	_channels[7].playData(pData);
+	playSoundStatic(pData, 8);
 	return 0;
 }
 
@@ -2454,29 +2425,29 @@ int RSound9::command29() {
 	int v = (generateRandomNumber() & 12) + 33;
 	byte *pData = loadData(0x335E);
 	pData[6] = v & 0x7F;
-	playSoundCh5To8(0x335E);
+	playSoundDynamic(0x335E);
 	return 0;
 }
 
 int RSound9::command30() {
-	playSoundCh5To8(0x3386);
+	playSoundDynamic(0x3386);
 	return 0;
 }
 
 int RSound9::command31() {
-	playSoundCh5To8(0x3396);
-	playSoundCh5To8(0x33A4);
-	playSoundCh5To8(0x33B2);
+	playSoundDynamic(0x3396);
+	playSoundDynamic(0x33A4);
+	playSoundDynamic(0x33B2);
 	return 0;
 }
 
 int RSound9::command32() {
-	playSoundCh5To8(0x33C0);
+	playSoundDynamic(0x33C0);
 	return 0;
 }
 
 int RSound9::command33() {
-	playSoundCh5To8(0x33CA);
+	playSoundDynamic(0x33CA);
 	return 0;
 }
 
@@ -2489,28 +2460,28 @@ int RSound9::command34() {
 	*loadData(0x7DCD) = 2;
 	*loadData(0x8791) = 2;
 
-	_channels[0].playData(loadData(0x4D2A));
-	_channels[1].playData(loadData(0x51AA));
-	_channels[2].playData(loadData(0x5634));
-	_channels[3].playData(loadData(0x6844));
-	_channels[4].playData(loadData(0x7DD0));
+	playSoundStatic(0x4D2A, 1);
+	playSoundStatic(0x51AA, 2);
+	playSoundStatic(0x5634, 3);
+	playSoundStatic(0x6844, 4);
+	playSoundStatic(0x7DD0, 5);
 	return 0;
 }
 
 int RSound9::command35() {
-	playSoundCh5To8(0x344C);
+	playSoundDynamic(0x344C);
 	return 0;
 }
 
 int RSound9::command36() {
-	playSoundCh5To8(0x334A);
+	playSoundDynamic(0x334A);
 
-	Channel *chan = playSoundCh5To8(0x32C2);
-	if (chan)
+	Channel *chan = playSoundDynamic(0x32C2);
+	if (chan != nullptr)
 		chan->_innerLoopStart = loadData(0x3378);
 
-	chan = playSoundCh5To8(0x32BC);
-	if (chan)
+	chan = playSoundDynamic(0x32BC);
+	if (chan != nullptr)
 		chan->_innerLoopStart = loadData(0x3368);
 	return 0;
 }
@@ -2519,169 +2490,169 @@ int RSound9::command37() {
 	int v = (generateRandomNumber() & 2) + 72;
 	byte *pData = loadData(0x349C);
 	pData[6] = v & 0x7F;
-	playSoundCh5To8(0x349C);
+	playSoundDynamic(0x349C);
 	return 0;
 }
 
 int RSound9::command38() {
-	_callbackFnPtr = &RSound9::loadCommand38;
+	_callbackFnPtr = &RSound9::command38Callback;
 	return 0;
 }
 
-void RSound9::loadCommand38() {
+void RSound9::command38Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x1878));
-	_channels[1].playData(loadData(0x1F64));
-	_channels[2].playData(loadData(0x308E));
-	_channels[3].playData(loadData(0x2A10));
-	_channels[4].playData(loadData(0x21C8));
-	_channels[5].playData(loadData(0x2558));
-	_channels[6].playData(loadData(0x1E9A));
+	playSoundStatic(0x1878, 1);
+	playSoundStatic(0x1F64, 2);
+	playSoundStatic(0x308E, 3);
+	playSoundStatic(0x2A10, 4);
+	playSoundStatic(0x21C8, 5);
+	playSoundStatic(0x2558, 6);
+	playSoundStatic(0x1E9A, 7);
 }
 
 int RSound9::command39() {
-	_callbackFnPtr = &RSound9::loadCommand39;
+	_callbackFnPtr = &RSound9::command39Callback;
 	return 0;
 }
 
-void RSound9::loadCommand39() {
+void RSound9::command39Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x1A24));
-	_channels[1].playData(loadData(0x1FD0));
-	_channels[2].playData(loadData(0x318A));
-	_channels[3].playData(loadData(0x2E4A));
-	_channels[4].playData(loadData(0x2380));
-	_channels[5].playData(loadData(0x2642));
-	_channels[6].playData(loadData(0x1E9C));
+	playSoundStatic(0x1A24, 1);
+	playSoundStatic(0x1FD0, 2);
+	playSoundStatic(0x318A, 3);
+	playSoundStatic(0x2E4A, 4);
+	playSoundStatic(0x2380, 5);
+	playSoundStatic(0x2642, 6);
+	playSoundStatic(0x1E9C, 7);
 }
 
 int RSound9::command40() {
-	_callbackFnPtr = &RSound9::loadCommand40;
+	_callbackFnPtr = &RSound9::command40Callback;
 	return 0;
 }
 
-void RSound9::loadCommand40() {
+void RSound9::command40Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x4F00));
-	_channels[1].playData(loadData(0x534A));
-	_channels[2].playData(loadData(0x5CDE));
-	_channels[3].playData(loadData(0x6F8E));
-	_channels[4].playData(loadData(0x8110));
+	playSoundStatic(0x4F00, 1);
+	playSoundStatic(0x534A, 2);
+	playSoundStatic(0x5CDE, 3);
+	playSoundStatic(0x6F8E, 4);
+	playSoundStatic(0x8110, 5);
 }
 
 int RSound9::command41() {
-	_callbackFnPtr = &RSound9::loadCommand41;
+	_callbackFnPtr = &RSound9::command41Callback;
 	return 0;
 }
 
-void RSound9::loadCommand41() {
+void RSound9::command41Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x4F26));
-	_channels[1].playData(loadData(0x53BC));
-	_channels[2].playData(loadData(0x5DFE));
-	_channels[3].playData(loadData(0x747E));
-	_channels[4].playData(loadData(0x8340));
+	playSoundStatic(0x4F26, 1);
+	playSoundStatic(0x53BC, 2);
+	playSoundStatic(0x5DFE, 3);
+	playSoundStatic(0x747E, 4);
+	playSoundStatic(0x8340, 5);
 }
 
 int RSound9::command42() {
-	_callbackFnPtr = &RSound9::loadCommand42;
+	_callbackFnPtr = &RSound9::command42Callback;
 	return 0;
 }
 
-void RSound9::loadCommand42() {
+void RSound9::command42Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x4F30));
-	_channels[1].playData(loadData(0x555C));
-	_channels[2].playData(loadData(0x6582));
-	_channels[3].playData(loadData(0x7A2E));
-	_channels[4].playData(loadData(0x8480));
+	playSoundStatic(0x4F30, 1);
+	playSoundStatic(0x555C, 2);
+	playSoundStatic(0x6582, 3);
+	playSoundStatic(0x7A2E, 4);
+	playSoundStatic(0x8480, 5);
 }
 
 int RSound9::command43() {
 	_callbackCounter = 80;
 	_callbackPeriod = 80;
-	_channels[0].playData(loadData(0x34BE));
-	_channels[1].playData(loadData(0x3A46));
-	_channels[2].playData(loadData(0x3F52));
-	_channels[3].playData(loadData(0x439A));
+	playSoundStatic(0x34BE, 1);
+	playSoundStatic(0x3A46, 2);
+	playSoundStatic(0x3F52, 3);
+	playSoundStatic(0x439A, 4);
 	return 0;
 }
 
 int RSound9::command44_46() {
-	_callbackFnPtr = &RSound9::loadCommand44_46;
+	_callbackFnPtr = &RSound9::command44_46Callback;
 	return 0;
 }
 
-void RSound9::loadCommand44_46() {
+void RSound9::command44_46Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x3518));
-	_channels[1].playData(loadData(0x3AA2));
-	_channels[2].playData(loadData(0x403A));
-	_channels[3].playData(loadData(0x4486));
+	playSoundStatic(0x3518, 1);
+	playSoundStatic(0x3AA2, 2);
+	playSoundStatic(0x403A, 3);
+	playSoundStatic(0x4486, 4);
 }
 
 int RSound9::command45() {
-	_callbackFnPtr = &RSound9::loadCommand45;
+	_callbackFnPtr = &RSound9::command45Callback;
 	return 0;
 }
 
-void RSound9::loadCommand45() {
+void RSound9::command45Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x37AC));
-	_channels[1].playData(loadData(0x3CF8));
-	_channels[2].playData(loadData(0x41E6));
-	_channels[3].playData(loadData(0x4630));
+	playSoundStatic(0x37AC, 1);
+	playSoundStatic(0x3CF8, 2);
+	playSoundStatic(0x41E6, 3);
+	playSoundStatic(0x4630, 4);
 }
 
 int RSound9::command47() {
-	_callbackFnPtr = &RSound9::loadCommand47;
+	_callbackFnPtr = &RSound9::command47Callback;
 	return 0;
 }
 
-void RSound9::loadCommand47() {
+void RSound9::command47Callback() {
 	_callbackFnPtr = nullptr;
-	_channels[0].playData(loadData(0x47D6));
-	_channels[1].playData(loadData(0x48FA));
-	_channels[2].playData(loadData(0x4A20));
-	_channels[3].playData(loadData(0x4BAE));
+	playSoundStatic(0x47D6, 1);
+	playSoundStatic(0x48FA, 2);
+	playSoundStatic(0x4A20, 3);
+	playSoundStatic(0x4BAE, 4);
 }
 
 int RSound9::command48() {
 	byte *pData = loadData(0x34B0);
 	pData[6] ^= 0x1F;
 	pData[0xA] ^= 0x1F;
-	playSoundCh5To8(0x34B0);
+	playSoundDynamic(0x34B0);
 	return 0;
 }
 
 int RSound9::command49() {
-	_channels[0].playData(loadData(0x12CA));
-	_channels[1].playData(loadData(0x132A));
-	_channels[2].playData(loadData(0x1384));
-	_channels[3].playData(loadData(0x1666));
-	_channels[4].playData(loadData(0x1682));
-	_channels[5].playData(loadData(0x16A0));
-	_channels[6].playData(loadData(0x16BE));
+	playSoundStatic(0x12CA, 1);
+	playSoundStatic(0x132A, 2);
+	playSoundStatic(0x1384, 3);
+	playSoundStatic(0x1666, 4);
+	playSoundStatic(0x1682, 5);
+	playSoundStatic(0x16A0, 6);
+	playSoundStatic(0x16BE, 7);
 	return 0;
 }
 
 int RSound9::command50() {
-	_callbackFnPtr = &RSound9::loadCommand50;
+	_callbackFnPtr = &RSound9::command50Callback;
 	return 0;
 }
 
-void RSound9::loadCommand50() {
+void RSound9::command50Callback() {
 	_callbackFnPtr = nullptr;
 
 	*loadData(0x6841) = 0;
 	*loadData(0x7DCD) = 0;
 	*loadData(0x8791) = 0;
 
-	_channels[0].playData(loadData(0x50B8));
-	_channels[1].playData(loadData(0x7DCE));
-	_channels[2].playData(loadData(0x676A));
-	_channels[3].playData(loadData(0x7D08));
-	_channels[4].playData(loadData(0x85C4));
+	playSoundStatic(0x50B8, 1);
+	playSoundStatic(0x7DCE, 2);
+	playSoundStatic(0x676A, 3);
+	playSoundStatic(0x7D08, 4);
+	playSoundStatic(0x85C4, 5);
 }
 
 int RSound9::command51() {
@@ -2693,11 +2664,11 @@ int RSound9::command51() {
 	*loadData(0x7DCD) = 2;
 	*loadData(0x8791) = 2;
 
-	_channels[0].playData(loadData(0x4D3C));
-	_channels[1].playData(loadData(0x51EE));
-	_channels[2].playData(loadData(0x5A62));
-	_channels[3].playData(loadData(0x6986));
-	_channels[4].playData(loadData(0x7E5C));
+	playSoundStatic(0x4D3C, 1);
+	playSoundStatic(0x51EE, 2);
+	playSoundStatic(0x5A62, 3);
+	playSoundStatic(0x6986, 4);
+	playSoundStatic(0x7E5C, 5);
 	return 0;
 }
 
diff --git a/engines/mads/nebular/sound/rsound_nebular.h b/engines/mads/nebular/sound/rsound_nebular.h
index c6ea4b15f20..63ffa6ea8ee 100644
--- a/engines/mads/nebular/sound/rsound_nebular.h
+++ b/engines/mads/nebular/sound/rsound_nebular.h
@@ -61,7 +61,7 @@ private:
 	 * Shared loader for command11/12/13 - matches method1 in the
 	 * disassembly (isSoundPlaying-gated command1() + 4-channel load).
 	 */
-	void method1();
+	void playCommand11_12_13SharedChannels();
 
 	/**
 	 * Clamp helper: param > 0x40 ? param - 0x40 : 0. Matches the same
@@ -115,7 +115,7 @@ private:
 	bool _command23Toggle;
 
 	byte adjustedCommandParam() const;
-	void startCommand111213();
+	void playCommand11_12_13CommonChannels();
 	int executeDemoCommonCommand(int commandId);
 
 public:
@@ -130,15 +130,13 @@ private:
 
 	/**
 	 * Random-pick table used by command18 (TABLE1 in the disassembly).
-	 * Only entries 0-14 are ever reachable (command18 masks the random
-	 * index with decimal 30 = 0x1E), but all 16 confirmed clean entries
-	 * are kept for completeness. A further two rows of data follow
-	 * TABLE1 in seg001 but were never listed as reachable by any command
-	 * in this batch, and their values look like they may be text/other
-	 * data rather than further table entries - excluded until a command
-	 * turns up that actually reaches them.
+	 * A further two rows of data follow TABLE1 in seg001 but were never
+	 * listed as reachable by any command in this batch, and their values
+	 * look like they may be text/other data rather than further table
+	 * entries - excluded until a command turns up that actually reaches
+	 * them.
 	 */
-	static const uint16 _table1[16];
+	static const uint16 _command18RandomSfx[16];
 
 	/**
 	 * Persistent counter (initial value
@@ -146,7 +144,7 @@ private:
 	 * command12 runs; the low 7 bits are written into the sound data's
 	 * pitch/note byte before playback. command5 resets it back to 47.
 	 */
-	byte _pitchCycleCounter = 0x2F;
+	byte _volumeCycleCounter = 0x2F;
 
 	int command5();
 
@@ -208,15 +206,17 @@ private:
 	 * Toggle used by command39/40 (initially 0). Shared between both commands: flips bit 2 (^= 4) on
 	 * every call to either one, and the post-toggle value + 0x28 is
 	 * written into the same sound data's byte 6, regardless of which of
-	 * the two commands triggered the toggle.
+	 * the two commands triggered the toggle. This alternates the two
+	 * command's played note value between 0x28 and 0x2C.
 	 */
-	byte _command3940Toggle = 0;
+	byte _command39_40NoteToggle = 0;
 
 	/**
 	 * Shared helper: pData[5] = value, then plays pData. Command 25
-	 * calls it for both native sequence offsets.
+	 * calls it for both native sequence offsets. "method1" in
+	 * disassembly.
 	 */
-	Channel *method1(int offset, byte value);
+	Channel *patchAndPlaySound(int offset, byte value);
 
 	/**
 	 * Matches the disassembly's OTHER "method1" (a same-named but
@@ -240,18 +240,6 @@ private:
 	 */
 	void sendDualVolume(byte volume);
 
-	/**
-	 * Shared tail used by both command1
-	 * (falls through into it after calling command3()) and command5
-	 * (jumps straight into it after its isSoundPlaying gate). Enables
-	 * channels 5-8 (1-based; indices 4-7) - notably never reaches
-	 * channel 9.
-	 */
-	void resetUpperChannelsTail();
-
-	int command1();
-	int command3();
-	int command5();
 	int command9();
 	int command10();
 	int command11();
@@ -269,7 +257,7 @@ private:
 	int command24();
 	int command25();
 	int command26();
-	int command27x42();
+	int command27_42();
 	int command28();
 	int command29();
 	int command30();
@@ -288,7 +276,7 @@ private:
 	int command44();
 	int command45();
 	int command46();
-	int command47x49();
+	int command47_49();
 	int command48();
 	int command50();
 	int command51();
@@ -319,29 +307,29 @@ private:
 	typedef int (RSound4:: *CommandPtr)();
 	static const CommandPtr _commandList[60];
 
-	/**
-	 * Called only from command12's shared
-	 * tail; computes (param >> 1) + 36.
-	 */
-	byte paramToVariant();
-
 	/**
 	 * Writes the same variant byte into
 	 * offset 1 of the five sound blocks command12 (re)loads.
+	 * Value is computed from command param.
 	 */
 	void setCommand12Variant();
 
 	/**
-	 * Shared tail of both command10 and
-	 * command58, loading channels 1-3 (1-based; indices 0-2).
+	 * Shared tail of both command10 and command58, loading channels 1-3.
 	 */
-	void loadIntroChannels();
+	void playCommand10_58CommonChannels();
 
 	void tickCallback() override;
 
-	void loadCommand54();
-	void loadCommand55();
-	void loadCommand56();
+	void command54Callback();
+	void command55Callback();
+	void command56Callback();
+
+	/**
+	 * Loads a null stream (containing event 00 00) into the specified
+	 * channel, which stops playback on that channel.
+	 */
+	void stopChannel(byte channel);
 
 	int command9();
 	int command10();
@@ -377,15 +365,20 @@ private:
 	static const CommandPtr _commandList[42];
 
 	/**
-	 * Shared tail of command29 and
-	 * command38, loading channels 4 and 9 (1-based; indices 3 and 8).
+	 * Shared tail of command29 and command38, loading channels 4 and 9.
 	 */
-	void loadTailChannels();
+	void playCommand29_38CommonChannels();
+
+	/**
+	 * Loads a null stream (containing event 00 00) into the specified
+	 * channel, which stops playback on that channel.
+	 */
+	void stopChannel(byte channel);
 
 	int command9();
 	int command10();
-	int command11x24();
-	int command12x25();
+	int command11_24();
+	int command12_25();
 	int command13();
 	int command14();
 	int command15();
@@ -440,20 +433,20 @@ private:
 	void tickCallback() override;
 
 	/**
-	 * command24/command28's
+	 * command24/command29's
 	 * own full-reload bodies. When channel 1 is currently playing the
 	 * OTHER command's theme, that command doesn't interrupt it
 	 * immediately - it just points _callbackFnPtr at this same reload
 	 * logic so the switch happens on the next callback tick instead.
 	 */
-	void reloadCommand24();
-	void reloadCommand28();
+	void command24Callback();
+	void command29Callback();
 
 	int command9();
 	int command10();
 	int command11();
 	int command12();
-	int command13x14();
+	int command13_14();
 	int command15();
 	int command16();
 	int command17();
@@ -465,7 +458,7 @@ private:
 	int command23();
 	int command24();
 	int command25();
-	int command28();
+	int command29();
 public:
 	RSound6(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
 
@@ -512,7 +505,7 @@ private:
 	 * Shared tail of command14/command15 -
 	 * mutates three bytes of the shared sound data then plays it 4 times.
 	 */
-	void setCommand1415Variant(byte v1, byte v2);
+	void playCommand14_15Variant(byte v1, byte v2);
 
 	int command9();
 	int command10();
@@ -613,16 +606,16 @@ private:
 	int command50();
 	int command51();
 
-	// Deferred loader bodies, scheduled via _callbackFnPtr by the commands above
-	void loadCommand38();
-	void loadCommand39();
-	void loadCommand40();
-	void loadCommand41();
-	void loadCommand42();
-	void loadCommand44_46();
-	void loadCommand45();
-	void loadCommand47();
-	void loadCommand50();
+	// Deferred callbacks, scheduled via _callbackFnPtr by the commands above
+	void command38Callback();
+	void command39Callback();
+	void command40Callback();
+	void command41Callback();
+	void command42Callback();
+	void command44_46Callback();
+	void command45Callback();
+	void command47Callback();
+	void command50Callback();
 public:
 	RSound9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
 


Commit: 75aee5d537a52045e9aec5470de66cd5185c06a4
    https://github.com/scummvm/scummvm/commit/75aee5d537a52045e9aec5470de66cd5185c06a4
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
MADS: Refactor Rex demo MT-32 code

Changed paths:
    engines/mads/nebular/sound/rsound.cpp
    engines/mads/nebular/sound/rsound.h
    engines/mads/nebular/sound/rsound_nebular.cpp
    engines/mads/nebular/sound/rsound_nebular.h


diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index 3d01b5c5651..661992c348e 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -77,6 +77,17 @@ RSound::RSound(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common:
 	_fadeOutSpeed = (_fadeOutCheckMode == kRSoundFadeCheckAlternating ? 2 : 0);
 	_fadeOutCounter = _fadeOutSpeed;
 
+	_dynamicStartChannel = 5;
+	_dynamicIncludeChannel9 = false;
+	// The full game RSOUND drivers command 2 and 3 include channel 9 when
+	// initializing channel data and fading channels to stop, but command 2
+	// does not include channel 9 when initializing MIDI channels. Instead,
+	// command 4 includes channel 9 when initializing MIDI channels, but
+	// command 4 and 5 do not include channel 9 when initializing channel
+	// data and fading channels to stop. This is probably a bug, which is
+	// not replicated in this reimplementation.
+	_staticIncludeChannel9 = true;
+
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
 		_channels[i]._midiChannel = i + 1;
 	}
@@ -178,7 +189,7 @@ Channel *RSound::playSoundStatic(int offset, byte channel) {
 }
 
 Channel *RSound::playSoundDynamic(int offset) {
-	return allocateAndPlay(loadData(offset));
+	return allocateAndPlay(loadData(offset), _dynamicStartChannel);
 }
 
 Channel *RSound::playSoundAnyChannel(int offset) {
@@ -214,6 +225,10 @@ Channel *RSound::allocateAndPlay(byte *pData, int startingChannel) {
 	return foundChannel;
 }
 
+bool RSound::isSoundPlaying(int offset) {
+	return isSoundPlaying(loadData(offset));
+}
+
 bool RSound::isSoundPlaying(byte *pData) {
 	// Deliberately excludes channel 9, matching the disassembly -
 	// same as allocateAndPlay()'s scan never reaching channel 9 either.
@@ -825,13 +840,19 @@ void RSound::resetAllChannels() {
  * controllers, volume=100, pan=center) of MIDI channels [first, last]
  * (both inclusive, 1-based). Shared tail used by command0/command2/command4.
  */
-void RSound::sendMidiChannelReset(int first, int last) {
+void RSound::sendMidiChannelReset(int first, int last, bool includeChannel9) {
 	for (int ch = first; ch <= last; ++ch) {
 		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | ch, MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
 		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | ch, MidiDriver::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
 		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | ch, MidiDriver::MIDI_CONTROLLER_VOLUME, 100);
 		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | ch, MidiDriver::MIDI_CONTROLLER_PANNING, 0x40);
 	}
+	if (last <= 8 && includeChannel9) {
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | 9, MidiDriver::MIDI_CONTROLLER_ALL_NOTES_OFF, 0);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | 9, MidiDriver::MIDI_CONTROLLER_RESET_ALL_CONTROLLERS, 0);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | 9, MidiDriver::MIDI_CONTROLLER_VOLUME, 100);
+		_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | 9, MidiDriver::MIDI_CONTROLLER_PANNING, 0x40);
+	}
 }
 
 int RSound::command0() {
@@ -857,42 +878,39 @@ int RSound::command1() {
 }
 
 int RSound::command2() {
-	// Channels 1-4 and 9 (also reinitializes the held-notes
+	// Initialize the static channels (also reinitializes the active notes
 	// table) plus the MIDI channel reset for those same channels.
-	resetChannelRange(1, 4, true);
+	resetChannelRange(1, _dynamicStartChannel - 1, _staticIncludeChannel9);
 	clearActiveNotes();
 	setFadeOutSpeed(0);
-	// The original code does not reset MIDI channel 9, which is
-	// probably an oversight.
-	sendMidiChannelReset(1, 4);
-	sendMidiChannelReset(9, 9);
+	sendMidiChannelReset(1, _dynamicStartChannel - 1, _staticIncludeChannel9);
+	sendSysEx(_sysExOffset);
 	return 0;
 }
 
 int RSound::command3() {
-	// Start fade-out to stop for channels 1-4 and 9.
+	// Start fade-out to stop for the static channels.
 	setFadeOutSpeed(1);
-	for (int i = 1; i <= 4; ++i)
+	for (int i = 1; i < _dynamicStartChannel; ++i)
 		getChannel(i)->setFadeOut(true);
-	getChannel(9)->setFadeOut(true);
+	if (_staticIncludeChannel9)
+		getChannel(9)->setFadeOut(true);
 	return 0;
 }
 
 int RSound::command4() {
-	// Channels 5-8 (does NOT touch the held-notes
+	// Initialize the dynamic Channels (does NOT touch the active notes
 	// table) plus the MIDI channel reset for those same channels.
-	resetChannelRange(5, 8);
+	resetChannelRange(_dynamicStartChannel, 8, _dynamicIncludeChannel9);
 	setFadeOutSpeed(0);
-	// The original code also resets MIDI channel 9, which is
-	// probably incorrect.
-	sendMidiChannelReset(5, 8);
+	sendMidiChannelReset(_dynamicStartChannel, 8, _dynamicIncludeChannel9);
 	return 0;
 }
 
 int RSound::command5() {
-	// Start fade-out to stop for channels 5-8.
+	// Start fade-out to stop for the dynamic channels.
 	setFadeOutSpeed(1);
-	for (int i = 5; i <= 8; ++i)
+	for (int i = _dynamicStartChannel; i <= (_dynamicIncludeChannel9 ? 9 : 8); ++i)
 		getChannel(i)->setFadeOut(true);
 	return 0;
 }
diff --git a/engines/mads/nebular/sound/rsound.h b/engines/mads/nebular/sound/rsound.h
index 64a17a2a848..998a3385272 100644
--- a/engines/mads/nebular/sound/rsound.h
+++ b/engines/mads/nebular/sound/rsound.h
@@ -216,6 +216,18 @@ private:
 protected:
 	int _commandParam;
 
+	// Specifies the lowest channel in the channel range that is used for
+	// dynamic channel allocation. F.e. if this is 5, channels 1-4 are
+	// initialized and stopped by the static commands 2 and 3, and channels
+	// 5-8 are initialized and stopped by the dyanmic commands 4 and 5. Also,
+	// channels 5-8 are used by playSoundDynamic.
+	// Channel 9 can be included with the static or dynamic channels by setting
+	// the corresponding boolean field. This will only affect commands 2-5;
+	// channel 9 is never used by playSoundDynamic.
+	byte _dynamicStartChannel;
+	bool _staticIncludeChannel9;
+	bool _dynamicIncludeChannel9;
+
 	void setFadeOutSpeed(int fadeOutSpeed);
 
 	/**
@@ -296,12 +308,13 @@ protected:
 	 * could be found, nullptr is returned and the sound data is not
 	 * played.
 	 */
-	Channel *allocateAndPlay(byte *pData, int startingChannel = 5);
+	Channel *allocateAndPlay(byte *pData, int startingChannel);
 
 	/**
 	 * Checks to see whether the given block of data is already loaded
 	 * into a channel and being played.
 	 */
+	bool isSoundPlaying(int offset);
 	bool isSoundPlaying(byte *pData);
 
 	/**
@@ -327,7 +340,7 @@ protected:
 	 * controllers, volume=100, pan=center) of MIDI channels [first, last]
 	 * (inclusive, 1-based). Shared tail used by command0/command2/command4.
 	 */
-	void sendMidiChannelReset(int first, int last);
+	void sendMidiChannelReset(int first, int last, bool includeChannel9 = false);
 
 	/**
 	 * Sets the volume for a channel. The value is set on the channel data
diff --git a/engines/mads/nebular/sound/rsound_nebular.cpp b/engines/mads/nebular/sound/rsound_nebular.cpp
index eb974dad174..7a8071f03a8 100644
--- a/engines/mads/nebular/sound/rsound_nebular.cpp
+++ b/engines/mads/nebular/sound/rsound_nebular.cpp
@@ -25,79 +25,6 @@ namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
-RSoundDemo::RSoundDemo(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common::Path &filename,
-		int dataOffset, int dataSize, int sysExOffset,
-		int firstEffectChannel) :
-		RSound(mixer, midiDriver, filename, dataOffset, dataSize, sysExOffset,
-				kRSoundFadeCheckAlternating),
-		_firstEffectChannel(firstEffectChannel) {
-}
-
-void RSoundDemo::startVoice(int channelIndex, int sequenceOffset) {
-	assert(channelIndex >= 0 && channelIndex < RSOUND_CHANNEL_COUNT);
-	playSoundStatic(sequenceOffset, channelIndex + 1);
-}
-
-int RSoundDemo::startVoiceInRange(int sequenceOffset, int firstChannel,
-		int lastChannel) {
-	assert(firstChannel >= 0 && firstChannel <= lastChannel && lastChannel < 8);
-
-	for (int channel = firstChannel; channel <= lastChannel; ++channel) {
-		if (!_channels[channel]._deltaCounter) {
-			startVoice(channel, sequenceOffset);
-			return channel;
-		}
-	}
-
-	for (int channel = lastChannel; channel >= firstChannel; --channel) {
-		if (_channels[channel]._fadeOutActive) {
-			startVoice(channel, sequenceOffset);
-			return channel;
-		}
-	}
-
-	return -1;
-}
-
-int RSoundDemo::startAnyVoice(int sequenceOffset) {
-	// Both demo overlays exclude rhythm channel 9 from their melodic pools.
-	return startVoiceInRange(sequenceOffset, 0, 7);
-}
-
-int RSoundDemo::startEffectVoice(int sequenceOffset) {
-	return startVoiceInRange(sequenceOffset, _firstEffectChannel, 7);
-}
-
-void RSoundDemo::requestStopRange(int firstChannel, int channelCount) {
-	assert(firstChannel >= 0 && channelCount >= 0 &&
-			firstChannel + channelCount <= RSOUND_CHANNEL_COUNT);
-	for (int channel = firstChannel;
-			channel < firstChannel + channelCount; ++channel)
-		_channels[channel].setFadeOut(true);
-}
-
-void RSoundDemo::requestStopAll() {
-	requestStopRange(0, RSOUND_CHANNEL_COUNT);
-}
-
-void RSoundDemo::stopAndResetRange(int firstChannel, int channelCount) {
-	assert(firstChannel >= 0 && channelCount > 0 &&
-			firstChannel + channelCount <= RSOUND_CHANNEL_COUNT);
-	resetChannelRange(firstChannel + 1, firstChannel + channelCount);
-	clearActiveNotesRange(firstChannel + 1, firstChannel + channelCount);
-	sendMidiChannelReset(firstChannel + 1, firstChannel + channelCount);
-}
-
-void RSoundDemo::setVoiceVolume(int channelIndex, byte volume) {
-	assert(channelIndex >= 0 && channelIndex < RSOUND_CHANNEL_COUNT);
-	_channels[channelIndex]._volume = volume;
-	sendVolume(channelIndex + 1, volume);
-}
-
-bool RSoundDemo::isSequenceActive(int sequenceOffset) {
-	return isSoundPlaying(loadData(sequenceOffset));
-}
-
 const RSound1::CommandPtr RSound1::_commandList[42] = {
 	&RSound1::command0, &RSound1::command1, &RSound1::command2, &RSound1::command3,
 	&RSound1::command4, &RSound1::command5, &RSound1::command6, &RSound1::command7,
@@ -370,9 +297,35 @@ int RSound1::command41() {
 
 /*-----------------------------------------------------------------------*/
 
+const RSoundDemo1::CommandPtr RSoundDemo1::_commandList[41] = {
+	&RSoundDemo1::command0, &RSoundDemo1::command1, &RSoundDemo1::command2, &RSoundDemo1::command3,
+	&RSoundDemo1::command4, &RSoundDemo1::command5, &RSoundDemo1::command6, &RSoundDemo1::command7,
+	&RSoundDemo1::command8, &RSoundDemo1::command9, &RSoundDemo1::command10, &RSoundDemo1::command11,
+	&RSoundDemo1::command12, &RSoundDemo1::command13, &RSoundDemo1::command14, &RSoundDemo1::command15,
+	&RSoundDemo1::command16, &RSoundDemo1::command17, &RSoundDemo1::command18, &RSoundDemo1::command19,
+	&RSoundDemo1::command20, &RSoundDemo1::command21, &RSoundDemo1::command22, &RSoundDemo1::command23,
+	&RSoundDemo1::command24, &RSoundDemo1::command25, &RSoundDemo1::command26, &RSoundDemo1::command27,
+	&RSoundDemo1::command28, &RSoundDemo1::command29, &RSoundDemo1::command30, &RSoundDemo1::command31,
+	&RSoundDemo1::command32, &RSoundDemo1::command33, &RSoundDemo1::command34, &RSoundDemo1::command35,
+	&RSoundDemo1::command36, &RSoundDemo1::command37, &RSoundDemo1::command38, &RSoundDemo1::command39,
+	&RSoundDemo1::command40
+};
+
 RSoundDemo1::RSoundDemo1(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
-		RSoundDemo(mixer, midiDriver, "rsound.001", 0x12E0, 0x1D28, 0x67, 0),
+		RSound(mixer, midiDriver, "rsound.001", 0x12E0, 0x1D28, 0x67, kRSoundFadeCheckAlternating),
 		_command23Toggle(false) {
+	// The demo RSOUND.001 driver command 2 includes channel 5 when 
+	// initializing channel data, but command 2 and 3 do not include channel 5
+	// when initializing MIDI channels and fading channels to stop. Instead,
+	// command 4 includes channel 5 when initializing channel data, but
+	// command 4 and 5 do not include channel 5 when initializing MIDI
+	// channels and fading channels to stop. This is probably a bug, which is
+	// not replicated in this reimplementation.
+	_dynamicStartChannel = 5;
+	// The demo RSOUND.001 driver processes channel 9 with the dynamic
+	// commands 4 and 5, instead of the static commands 2 and 3.
+	_dynamicIncludeChannel9 = true;
+	_staticIncludeChannel9 = false;
 }
 
 byte RSoundDemo1::adjustedCommandParam() const {
@@ -381,44 +334,14 @@ byte RSoundDemo1::adjustedCommandParam() const {
 }
 
 void RSoundDemo1::playCommand11_12_13CommonChannels() {
-	if (isSequenceActive(0x1586))
+	if (isSoundPlaying(0x1586))
 		return;
 
-	requestStopAll();
-	startVoice(0, 0x1586);
-	startVoice(1, 0x17DC);
-	startVoice(2, 0x197C);
-	startVoice(3, 0x19F8);
-}
-
-int RSoundDemo1::executeDemoCommonCommand(int commandId) {
-	switch (commandId) {
-	case 0:
-		return RSound::command0();
-	case 1:
-		requestStopAll();
-		return 0;
-	case 2:
-		stopAndResetRange(0, 4);
-		return 0;
-	case 3:
-		requestStopRange(0, 4);
-		return 0;
-	case 4:
-		stopAndResetRange(4, 5);
-		return 0;
-	case 5:
-		requestStopRange(4, 5);
-		return 0;
-	case 6:
-		return RSound::command6();
-	case 7:
-		return RSound::command7();
-	case 8:
-		return RSound::command8();
-	default:
-		return 0;
-	}
+	command1();
+	playSoundStatic(0x1586, 1);
+	playSoundStatic(0x17DC, 2);
+	playSoundStatic(0x197C, 3);
+	playSoundStatic(0x19F8, 4);
 }
 
 int RSoundDemo1::command(int commandId, int param) {
@@ -427,158 +350,214 @@ int RSoundDemo1::command(int commandId, int param) {
 
 	_commandParam = param;
 	_ticksSinceLastCommand = 0;
-	if (commandId <= 8)
-		return executeDemoCommonCommand(commandId);
-
-	switch (commandId) {
-	case 9:
-		startAnyVoice(0x0F34);
-		break;
-	case 10:
-		if (!isSequenceActive(0x1104)) {
-			requestStopAll();
-			startVoice(4, 0x1104);
-			startVoice(5, 0x1138);
-			startVoice(6, 0x12BC);
-			startVoice(7, 0x1308);
-		}
-		break;
-	case 11:
-		playCommand11_12_13CommonChannels();
-		setVoiceVolume(0, 0x00);
-		setVoiceVolume(1, 0x00);
-		break;
-	case 12:
-		playCommand11_12_13CommonChannels();
-		setVoiceVolume(0, 0x50);
-		setVoiceVolume(1, 0x00);
-		break;
-	case 13:
-		playCommand11_12_13CommonChannels();
-		setVoiceVolume(0, 0x50);
-		setVoiceVolume(1, 0x50);
-		break;
-	case 14:
-		startAnyVoice(0x1AE2);
-		break;
-	case 15:
-		if (!isSequenceActive(0x135A)) {
-			requestStopAll();
-			startVoice(4, 0x135A);
-			startVoice(5, 0x144A);
-			startVoice(6, 0x152E);
-		}
-		break;
-	case 16:
-		startAnyVoice(0x0F3E);
-		break;
-	case 17:
-		startAnyVoice(0x0F48);
-		break;
-	case 18:
-		startAnyVoice(0x0F52);
-		break;
-	case 19:
-		requestStopAll();
-		startAnyVoice(0x0F64);
-		break;
-	case 20:
-		startAnyVoice(0x0FBE);
-		break;
-	case 21:
-		startAnyVoice(0x0FAC);
-		break;
-	case 22: {
-		byte *data = sequenceData(0x0FCE);
-		data[6] = (generateRandomNumber() & 0x07) + 0x73;
-		startAnyVoice(0x0FCE);
-		break;
-	}
-	case 23:
-		_command23Toggle = !_command23Toggle;
-		startAnyVoice(_command23Toggle ? 0x0FD8 : 0x0FE0);
-		break;
-	case 24:
-		startAnyVoice(0x0FE8);
-		break;
-	case 25:
-		startAnyVoice(0x0FF2);
-		break;
-	case 26:
-	case 27: {
-		const int sequenceOffset = commandId == 26 ? 0x10F8 : 0x10EC;
-		byte *data = sequenceData(sequenceOffset);
-		data[8] = (generateRandomNumber() & 0x18) + 0x2D;
-		data[5] = adjustedCommandParam() + 0x40;
-		startVoice(7, sequenceOffset);
-		break;
-	}
-	case 28:
-		startAnyVoice(0x1002);
-		break;
-	case 29: {
-		byte *data = sequenceData(0x109A);
-		data[11] = (adjustedCommandParam() >> 1) + 0x20;
-		if (!isSequenceActive(0x109A))
-			startAnyVoice(0x109A);
-		break;
-	}
-	case 30: {
-		byte *data = sequenceData(0x10AE);
-		data[11] = adjustedCommandParam() + 0x3F;
-		if (!isSequenceActive(0x10AE))
-			startAnyVoice(0x10AE);
-		break;
-	}
-	case 31:
-		startAnyVoice(0x1022);
-		break;
-	case 32: {
-		const byte value = adjustedCommandParam() >> 1;
-		byte *data = sequenceData(0x10C4);
-		data[11] = data[23] = value + 0x44;
-		data[17] = data[29] = value + 0x14;
-		if (!isSequenceActive(0x10C4))
-			startAnyVoice(0x10C4);
-		break;
+	return (this->*_commandList[commandId])();
+}
+
+int RSoundDemo1::command9() {
+	playSoundAnyChannel(0x0F34);
+	return 0;
+}
+
+int RSoundDemo1::command10() {
+	if (!isSoundPlaying(0x1104)) {
+		command1();
+		playSoundStatic(0x1104, 5);
+		playSoundStatic(0x1138, 6);
+		playSoundStatic(0x12BC, 7);
+		playSoundStatic(0x1308, 8);
 	}
-	case 33:
-		startAnyVoice(0x1034);
-		startAnyVoice(0x103E);
-		break;
-	case 34: {
-		byte *data = sequenceData(0x104C);
-		data[9] = (generateRandomNumber() & 0x0C) + 0x2D;
-		data[16] = data[9] + 0x24;
-		startAnyVoice(0x104C);
-		break;
+	return 0;
+}
+
+int RSoundDemo1::command11() {
+	playCommand11_12_13CommonChannels();
+	setChannelVolume(1, 0x00);
+	setChannelVolume(2, 0x00);
+	return 0;
+}
+
+int RSoundDemo1::command12() {
+	playCommand11_12_13CommonChannels();
+	setChannelVolume(1, 0x50);
+	setChannelVolume(2, 0x00);
+	return 0;
+}
+
+int RSoundDemo1::command13() {
+	playCommand11_12_13CommonChannels();
+	setChannelVolume(1, 0x50);
+	setChannelVolume(2, 0x50);
+	return 0;
+}
+
+int RSoundDemo1::command14() {
+	playSoundAnyChannel(0x1AE2);
+	return 0;
+}
+
+int RSoundDemo1::command15() {
+	if (!isSoundPlaying(0x135A)) {
+		command1();
+		playSoundStatic(0x135A, 5);
+		playSoundStatic(0x144A, 6);
+		playSoundStatic(0x152E, 7);
 	}
-	case 35:
-		startAnyVoice(0x1060);
-		break;
-	case 36:
-		startAnyVoice(0x1078);
-		break;
-	case 37:
-		startAnyVoice(0x1086);
-		break;
-	case 38:
-		startAnyVoice(0x1090);
-		break;
-	case 39:
-		if (!isSequenceActive(0x1C38)) {
-			startVoice(4, 0x1C38);
-			startVoice(5, 0x1C68);
-			startVoice(6, 0x1C94);
-			startVoice(7, 0x1CD4);
-			startVoice(8, 0x1CEE);
-		}
-		break;
-	case 40:
-		startAnyVoice(0x106E);
-		break;
+	return 0;
+}
+
+int RSoundDemo1::command16() {
+	playSoundAnyChannel(0x0F3E);
+	return 0;
+}
+
+int RSoundDemo1::command17() {
+	playSoundAnyChannel(0x0F48);
+	return 0;
+}
+
+int RSoundDemo1::command18() {
+	playSoundAnyChannel(0x0F52);
+	return 0;
+}
+
+int RSoundDemo1::command19() {
+	command1();
+	playSoundAnyChannel(0x0F64);
+	return 0;
+}
+
+int RSoundDemo1::command20() {
+	playSoundAnyChannel(0x0FBE);
+	return 0;
+}
+
+int RSoundDemo1::command21() {
+	playSoundAnyChannel(0x0FAC);
+	return 0;
+}
+
+int RSoundDemo1::command22() {
+	byte *data = loadData(0x0FCE);
+	data[6] = (generateRandomNumber() & 0x07) + 0x73;
+	playSoundAnyChannel(0x0FCE);
+	return 0;
+}
+
+int RSoundDemo1::command23() {
+	_command23Toggle = !_command23Toggle;
+	playSoundAnyChannel(_command23Toggle ? 0x0FD8 : 0x0FE0);
+	return 0;
+}
+
+int RSoundDemo1::command24() {
+	playSoundAnyChannel(0x0FE8);
+	return 0;
+}
+
+int RSoundDemo1::command25() {
+	playSoundAnyChannel(0x0FF2);
+	return 0;
+}
+
+int RSoundDemo1::patchAndPlaySound(int offset) {
+	byte *data = loadData(offset);
+	data[8] = (generateRandomNumber() & 0x18) + 0x2D;
+	data[5] = adjustedCommandParam() + 0x40;
+	playSoundStatic(data, 8);
+	return 0;
+}
+
+int RSoundDemo1::command26() {
+	return patchAndPlaySound(0x10F8);
+}
+
+int RSoundDemo1::command27() {
+	return patchAndPlaySound(0x10EC);
+}
+
+int RSoundDemo1::command28() {
+	playSoundAnyChannel(0x1002);
+	return 0;
+}
+
+int RSoundDemo1::command29() {
+	byte *data = loadData(0x109A);
+	data[11] = (adjustedCommandParam() >> 1) + 0x20;
+	if (!isSoundPlaying(0x109A))
+		playSoundAnyChannel(0x109A);
+	return 0;
+}
+
+int RSoundDemo1::command30() {
+	byte *data = loadData(0x10AE);
+	data[11] = adjustedCommandParam() + 0x3F;
+	if (!isSoundPlaying(0x10AE))
+		playSoundAnyChannel(0x10AE);
+	return 0;
+}
+
+int RSoundDemo1::command31() {
+	playSoundAnyChannel(0x1022);
+	return 0;
+}
+
+int RSoundDemo1::command32() {
+	const byte value = adjustedCommandParam() >> 1;
+	byte *data = loadData(0x10C4);
+	data[11] = data[23] = value + 0x44;
+	data[17] = data[29] = value + 0x14;
+	if (!isSoundPlaying(0x10C4))
+		playSoundAnyChannel(0x10C4);
+	return 0;
+}
+
+int RSoundDemo1::command33() {
+	playSoundAnyChannel(0x1034);
+	playSoundAnyChannel(0x103E);
+	return 0;
+}
+
+int RSoundDemo1::command34() {
+	byte *data = loadData(0x104C);
+	data[9] = (generateRandomNumber() & 0x0C) + 0x2D;
+	data[16] = data[9] + 0x24;
+	playSoundAnyChannel(0x104C);
+	return 0;
+}
+
+int RSoundDemo1::command35() {
+	playSoundAnyChannel(0x1060);
+	return 0;
+}
+
+int RSoundDemo1::command36() {
+	playSoundAnyChannel(0x1078);
+	return 0;
+}
+
+int RSoundDemo1::command37() {
+	playSoundAnyChannel(0x1086);
+	return 0;
+}
+
+int RSoundDemo1::command38() {
+	playSoundAnyChannel(0x1090);
+	return 0;
+}
+
+int RSoundDemo1::command39() {
+	if (!isSoundPlaying(0x1C38)) {
+		playSoundStatic(0x1C38, 5);
+		playSoundStatic(0x1C68, 6);
+		playSoundStatic(0x1C94, 7);
+		playSoundStatic(0x1CD4, 8);
+		playSoundStatic(0x1CEE, 9);
 	}
+	return 0;
+}
 
+int RSoundDemo1::command40() {
+	playSoundAnyChannel(0x106E);
 	return 0;
 }
 
@@ -2674,40 +2653,27 @@ int RSound9::command51() {
 
 /*-----------------------------------------------------------------------*/
 
-RSoundDemo9::RSoundDemo9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
-		RSoundDemo(mixer, midiDriver, "rsound.009", 0x11D0, 0x3664, 0x69, 5) {
-}
+const RSoundDemo9::CommandPtr RSoundDemo9::_commandList[40] = {
+	&RSoundDemo9::command0, &RSoundDemo9::command1, &RSoundDemo9::command2, &RSoundDemo9::command3,
+	&RSoundDemo9::command4, &RSoundDemo9::command5, &RSoundDemo9::command6, &RSoundDemo9::command7,
+	&RSoundDemo9::command8, &RSoundDemo9::nullCommand, &RSoundDemo9::nullCommand, &RSoundDemo9::command11,
+	&RSoundDemo9::command12, &RSoundDemo9::command13, &RSoundDemo9::command14, &RSoundDemo9::command15,
+	&RSoundDemo9::command16, &RSoundDemo9::command17, &RSoundDemo9::command18, &RSoundDemo9::command19,
+	&RSoundDemo9::command20, &RSoundDemo9::command21, &RSoundDemo9::command22, &RSoundDemo9::command23,
+	&RSoundDemo9::command24, &RSoundDemo9::command25, &RSoundDemo9::command26, &RSoundDemo9::command27,
+	&RSoundDemo9::command28, &RSoundDemo9::command29, &RSoundDemo9::command30, &RSoundDemo9::command31,
+	&RSoundDemo9::command32, &RSoundDemo9::command33, &RSoundDemo9::command34_39, &RSoundDemo9::command35,
+	&RSoundDemo9::command36, &RSoundDemo9::command37, &RSoundDemo9::command38, &RSoundDemo9::command34_39
+};
 
-int RSoundDemo9::executeDemoCommonCommand(int commandId) {
-	switch (commandId) {
-	case 0:
-		return RSound::command0();
-	case 1:
-		requestStopRange(0, 9);
-		return 0;
-	case 2:
-		stopAndResetRange(0, 5);
-		// The opening overlay repeats its first embedded DT1 record here.
-		sendSysEx(0x69);
-		return 0;
-	case 3:
-		requestStopRange(0, 5);
-		return 0;
-	case 4:
-		stopAndResetRange(5, 4);
-		return 0;
-	case 5:
-		requestStopRange(5, 4);
-		return 0;
-	case 6:
-		return RSound::command6();
-	case 7:
-		return RSound::command7();
-	case 8:
-		return RSound::command8();
-	default:
-		return 0;
-	}
+RSoundDemo9::RSoundDemo9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
+		RSound(mixer, midiDriver, "rsound.009", 0x11D0, 0x3664, 0x69, kRSoundFadeCheckAlternating) {
+	// The demo RSOUND.009 driver uses channel range 1-5 for the static init
+	// and stop commands 2 and 3, and 6-9 for the dynamic init and stop
+	// commands 4 and 5.
+	_dynamicStartChannel = 6;
+	_dynamicIncludeChannel9 = true;
+	_staticIncludeChannel9 = false;
 }
 
 int RSoundDemo9::command(int commandId, int param) {
@@ -2716,138 +2682,182 @@ int RSoundDemo9::command(int commandId, int param) {
 
 	_commandParam = param;
 	_ticksSinceLastCommand = 0;
-	if (commandId <= 8)
-		return executeDemoCommonCommand(commandId);
-
-	switch (commandId) {
-	case 9:
-	case 10:
-		break;
-	case 11:
-		startVoice(7, 0x1454);
-		break;
-	case 12:
-		startVoice(7, 0x14A0);
-		break;
-	case 13:
-		startVoice(7, 0x14AC);
-		break;
-	case 14:
-		startVoice(7, 0x14B4);
-		break;
-	case 15:
-		startVoice(7, 0x14D4);
-		break;
-	case 16:
-		startVoice(7, 0x14EC);
-		break;
-	case 17:
-		startVoice(7, 0x14E2);
-		break;
-	case 18:
-		startEffectVoice(0x12BA);
-		break;
-	case 19:
-		startEffectVoice(0x12D4);
-		break;
-	case 20: {
-		byte *data = sequenceData(0x12F6);
-		data[6] = (byte)(((generateRandomNumber() & 0x38) + 0x4D) & 0x7F);
-		startEffectVoice(0x12F6);
-		break;
-	}
-	case 21:
-	case 22: {
-		byte *data = sequenceData(0x130A);
-		data[9] = commandId == 21 ? 0x46 : 0x2D;
-		if (!isSequenceActive(0x130A))
-			startEffectVoice(0x130A);
-		break;
-	}
-	case 23: {
-		static const int sequences[] = { 0x1322, 0x1328, 0x133A };
-		for (uint index = 0; index < 3; ++index) {
-			const int channel = startEffectVoice(sequences[index]);
-			if (channel >= 0)
-				voice(channel)._innerLoopStart = loadData(0x1340);
-		}
-		break;
-	}
-	case 24:
-		startEffectVoice(0x1352);
-		break;
-	case 25:
-		startEffectVoice(0x1368);
-		break;
-	case 26:
-		startEffectVoice(0x138C);
-		break;
-	case 27:
-		startEffectVoice(0x13A4);
-		break;
-	case 28: {
-		byte *data = sequenceData(0x13BC);
-		data[6] = (byte)(((generateRandomNumber() & 0x1C) + 0x0F) & 0x7F);
-		startEffectVoice(0x13BC);
-		break;
-	}
-	case 29: {
-		byte *data = sequenceData(0x13D0);
-		data[6] = (byte)(((generateRandomNumber() & 0x0C) + 0x21) & 0x7F);
-		startEffectVoice(0x13D0);
-		break;
-	}
-	case 30:
-		startEffectVoice(0x13F8);
-		break;
-	case 31:
-		startEffectVoice(0x1408);
-		startEffectVoice(0x1416);
-		startEffectVoice(0x1424);
-		break;
-	case 32:
-		startEffectVoice(0x1432);
-		break;
-	case 33:
-		startEffectVoice(0x143C);
-		break;
-	case 34:
-	case 39:
-		startVoice(0, 0x1522);
-		startVoice(1, 0x1700);
-		startVoice(2, 0x1892);
-		startVoice(3, 0x21F2);
-		startVoice(4, 0x2E4A);
-		break;
-	case 35:
-		startEffectVoice(0x14BE);
-		break;
-	case 36: {
-		startEffectVoice(0x13BC);
-
-		int channel = startEffectVoice(0x1334);
-		if (channel >= 0)
-			voice(channel)._innerLoopStart = loadData(0x13EA);
-
-		channel = startEffectVoice(0x132E);
-		if (channel >= 0)
-			voice(channel)._innerLoopStart = loadData(0x13DA);
-		break;
-	}
-	case 37: {
-		byte *data = sequenceData(0x150E);
-		data[6] = (byte)(((generateRandomNumber() & 0x02) + 0x48) & 0x7F);
-		startEffectVoice(0x150E);
-		break;
-	}
-	case 38:
-		startVoice(0, 0x35C4);
-		startVoice(1, 0x35CE);
-		startVoice(2, 0x3612);
-		startVoice(3, 0x3656);
-		break;
+	return (this->*_commandList[commandId])();
+}
+
+int RSoundDemo9::command11() {
+	playSoundStatic(0x1454, 8);
+	return 0;
+}
+
+int RSoundDemo9::command12() {
+	playSoundStatic(0x14A0, 8);
+	return 0;
+}
+
+int RSoundDemo9::command13() {
+	playSoundStatic(0x14AC, 8);
+	return 0;
+}
+
+int RSoundDemo9::command14() {
+	playSoundStatic(0x14B4, 8);
+	return 0;
+}
+
+int RSoundDemo9::command15() {
+	playSoundStatic(0x14D4, 8);
+	return 0;
+}
+
+int RSoundDemo9::command16() {
+	playSoundStatic(0x14EC, 8);
+	return 0;
+}
+
+int RSoundDemo9::command17() {
+	playSoundStatic(0x14E2, 8);
+	return 0;
+}
+
+int RSoundDemo9::command18() {
+	playSoundDynamic(0x12BA);
+	return 0;
+}
+
+int RSoundDemo9::command19() {
+	playSoundDynamic(0x12D4);
+	return 0;
+}
+
+int RSoundDemo9::command20() {
+	byte *data = loadData(0x12F6);
+	data[6] = (byte)(((generateRandomNumber() & 0x38) + 0x4D) & 0x7F);
+	playSoundDynamic(0x12F6);
+	return 0;
+}
+
+int RSoundDemo9::patchAndPlaySound(byte param) {
+	byte *data = loadData(0x130A);
+	data[9] = param;
+	if (!isSoundPlaying(0x130A))
+		playSoundDynamic(0x130A);
+	return 0;
+}
+
+int RSoundDemo9::command21() {
+	return patchAndPlaySound(0x46);
+}
+
+int RSoundDemo9::command22() {
+	return patchAndPlaySound(0x2D);
+}
+
+int RSoundDemo9::command23() {
+	static const int sequences[] = { 0x1322, 0x1328, 0x133A };
+	for (uint index = 0; index < 3; ++index) {
+		Channel *channel = playSoundDynamic(sequences[index]);
+		if (channel != nullptr)
+			channel->_innerLoopStart = loadData(0x1340);
 	}
+	return 0;
+}
+
+int RSoundDemo9::command24() {
+	playSoundDynamic(0x1352);
+	return 0;
+}
+
+int RSoundDemo9::command25() {
+	playSoundDynamic(0x1368);
+	return 0;
+}
+
+int RSoundDemo9::command26() {
+	playSoundDynamic(0x138C);
+	return 0;
+}
+
+int RSoundDemo9::command27() {
+	playSoundDynamic(0x13A4);
+	return 0;
+}
+
+int RSoundDemo9::command28() {
+	byte *data = loadData(0x13BC);
+	data[6] = (byte)(((generateRandomNumber() & 0x1C) + 0x0F) & 0x7F);
+	playSoundDynamic(0x13BC);
+	return 0;
+}
+
+int RSoundDemo9::command29() {
+	byte *data = loadData(0x13D0);
+	data[6] = (byte)(((generateRandomNumber() & 0x0C) + 0x21) & 0x7F);
+	playSoundDynamic(0x13D0);
+	return 0;
+}
+
+int RSoundDemo9::command30() {
+	playSoundDynamic(0x13F8);
+	return 0;
+}
+
+int RSoundDemo9::command31() {
+	playSoundDynamic(0x1408);
+	playSoundDynamic(0x1416);
+	playSoundDynamic(0x1424);
+	return 0;
+}
+
+int RSoundDemo9::command32() {
+	playSoundDynamic(0x1432);
+	return 0;
+}
+
+int RSoundDemo9::command33() {
+	playSoundDynamic(0x143C);
+	return 0;
+}
+
+int RSoundDemo9::command34_39() {
+	playSoundStatic(0x1522, 1);
+	playSoundStatic(0x1700, 2);
+	playSoundStatic(0x1892, 3);
+	playSoundStatic(0x21F2, 4);
+	playSoundStatic(0x2E4A, 5);
+	return 0;
+}
+
+int RSoundDemo9::command35() {
+	playSoundDynamic(0x14BE);
+	return 0;
+}
+
+int RSoundDemo9::command36() {
+	playSoundDynamic(0x13BC);
+
+	Channel *channel = playSoundDynamic(0x1334);
+	if (channel != nullptr)
+		channel->_innerLoopStart = loadData(0x13EA);
+
+	channel = playSoundDynamic(0x132E);
+	if (channel != nullptr)
+		channel->_innerLoopStart = loadData(0x13DA);
+	return 0;
+}
+
+int RSoundDemo9::command37() {
+	byte *data = loadData(0x150E);
+	data[6] = (byte)(((generateRandomNumber() & 0x02) + 0x48) & 0x7F);
+	playSoundDynamic(0x150E);
+	return 0;
+}
 
+int RSoundDemo9::command38() {
+	playSoundStatic(0x35C4, 1);
+	playSoundStatic(0x35CE, 2);
+	playSoundStatic(0x3612, 3);
+	playSoundStatic(0x3656, 4);
 	return 0;
 }
 
diff --git a/engines/mads/nebular/sound/rsound_nebular.h b/engines/mads/nebular/sound/rsound_nebular.h
index 63ffa6ea8ee..2e5f2c40a94 100644
--- a/engines/mads/nebular/sound/rsound_nebular.h
+++ b/engines/mads/nebular/sound/rsound_nebular.h
@@ -28,30 +28,6 @@ namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
-/** Shared mechanics of the two distinct Rex demo Roland overlays. */
-class RSoundDemo : public RSound {
-private:
-	int _firstEffectChannel;
-
-protected:
-	RSoundDemo(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common::Path &filename,
-			int dataOffset, int dataSize, int sysExOffset,
-			int firstEffectChannel);
-
-	void startVoice(int channelIndex, int sequenceOffset);
-	int startVoiceInRange(int sequenceOffset, int firstChannel,
-			int lastChannel);
-	int startAnyVoice(int sequenceOffset);
-	int startEffectVoice(int sequenceOffset);
-	void requestStopRange(int firstChannel, int channelCount);
-	void requestStopAll();
-	void stopAndResetRange(int firstChannel, int channelCount);
-	void setVoiceVolume(int channelIndex, byte volume);
-	bool isSequenceActive(int sequenceOffset);
-	byte *sequenceData(int sequenceOffset) { return loadData(sequenceOffset); }
-	Channel &voice(int channelIndex) { return _channels[channelIndex]; }
-};
-
 class RSound1 : public RSound {
 private:
 	typedef int (RSound1:: *CommandPtr)();
@@ -110,16 +86,53 @@ public:
 };
 
 /** Demo RSOUND.001: `RLND AGAdemo 6-11-92`; 41 commands. */
-class RSoundDemo1 : public RSoundDemo {
+class RSoundDemo1 : public RSound {
 private:
+	typedef int (RSoundDemo1::*CommandPtr)();
+	static const CommandPtr _commandList[41];
+
 	bool _command23Toggle;
 
 	byte adjustedCommandParam() const;
 	void playCommand11_12_13CommonChannels();
-	int executeDemoCommonCommand(int commandId);
+	int patchAndPlaySound(int offset);
+
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
+	int command39();
+	int command40();
 
 public:
-	explicit RSoundDemo1(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
+	RSoundDemo1(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
+
 	int command(int commandId, int param) override;
 };
 
@@ -623,12 +636,47 @@ public:
 };
 
 /** Demo RSOUND.009: `RLND AGAdemo 6-25-92`; 40 commands. */
-class RSoundDemo9 : public RSoundDemo {
+class RSoundDemo9 : public RSound {
 private:
-	int executeDemoCommonCommand(int commandId);
+	typedef int (RSoundDemo9::*CommandPtr)();
+	static const CommandPtr _commandList[40];
+
+	int patchAndPlaySound(byte param);
+
+	int command9();
+	int command10();
+	int command11();
+	int command12();
+	int command13();
+	int command14();
+	int command15();
+	int command16();
+	int command17();
+	int command18();
+	int command19();
+	int command20();
+	int command21();
+	int command22();
+	int command23();
+	int command24();
+	int command25();
+	int command26();
+	int command27();
+	int command28();
+	int command29();
+	int command30();
+	int command31();
+	int command32();
+	int command33();
+	int command34_39();
+	int command35();
+	int command36();
+	int command37();
+	int command38();
 
 public:
-	explicit RSoundDemo9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
+	RSoundDemo9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver);
+
 	int command(int commandId, int param) override;
 };
 


Commit: 1acb7ec2a1f91192369106e8d915ca037d904063
    https://github.com/scummvm/scummvm/commit/1acb7ec2a1f91192369106e8d915ca037d904063
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
MADS: Rex MT-32 music / SFX volume control

Changed paths:
    engines/mads/nebular/sound/rsound.cpp
    engines/mads/nebular/sound/rsound.h
    engines/mads/nebular/sound/rsound_nebular.cpp
    engines/mads/nebular/sound/rsound_nebular.h


diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index 661992c348e..204e74128c0 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -29,7 +29,7 @@ namespace MADS {
 namespace RexNebular {
 namespace Sound {
 
-void Channel::loadData(byte *soundData) {
+void Channel::loadData(byte *soundData, byte source) {
 	_pitchSlideStepSize = 0;
 	_panningSweepStepSize = 0;
 	_innerLoopCounter = 0;
@@ -44,6 +44,8 @@ void Channel::loadData(byte *soundData) {
 	_innerLoopStart = soundData;
 	_outerLoopStart = soundData;
 	_soundData = soundData;
+
+	_source = source;
 }
 
 void Channel::setFadeOut(bool fadeOut) {
@@ -77,15 +79,17 @@ RSound::RSound(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver, const Common:
 	_fadeOutSpeed = (_fadeOutCheckMode == kRSoundFadeCheckAlternating ? 2 : 0);
 	_fadeOutCounter = _fadeOutSpeed;
 
+	// The default settings for these fields match those used by RSOUND.003-008
+	// of the full version of Rex: static channels 1-4 and 9 and dynamic
+	// channels 5-8.
+	// There is a (probable) bug in the implementation of commands 2 and 4 of
+	// RSOUND.003-008. Command 2 includes channel 9 when stopping channel
+	// playback, but does not include channel 9 when initializing MIDI
+	// channels. Conversely, command 4 does not include channel 9 when stopping
+	// playback, but does include channel 9 when initializing MIDI channels.
+	// This inconsistency is not replicated in this reimplementation.
 	_dynamicStartChannel = 5;
 	_dynamicIncludeChannel9 = false;
-	// The full game RSOUND drivers command 2 and 3 include channel 9 when
-	// initializing channel data and fading channels to stop, but command 2
-	// does not include channel 9 when initializing MIDI channels. Instead,
-	// command 4 includes channel 9 when initializing MIDI channels, but
-	// command 4 and 5 do not include channel 9 when initializing channel
-	// data and fading channels to stop. This is probably a bug, which is
-	// not replicated in this reimplementation.
 	_staticIncludeChannel9 = true;
 
 	for (int i = 0; i < RSOUND_CHANNEL_COUNT; ++i) {
@@ -175,28 +179,28 @@ Channel *RSound::getChannel(byte channel) {
 	return &_channels[channel - 1];
 }
 
-Channel *RSound::playSoundStatic(byte *soundData, byte channel) {
+Channel *RSound::playSoundStatic(byte *soundData, byte channel, byte source) {
 	assert(channel >= 1 && channel <= RSOUND_CHANNEL_COUNT);
 
 	Channel *chan = getChannel(channel);
-	Channel_playData(chan, soundData);
+	Channel_playData(chan, soundData, source);
 
 	return chan;
 }
 
-Channel *RSound::playSoundStatic(int offset, byte channel) {
-	return playSoundStatic(loadData(offset), channel);
+Channel *RSound::playSoundStatic(int offset, byte channel, byte source) {
+	return playSoundStatic(loadData(offset), channel, source);
 }
 
-Channel *RSound::playSoundDynamic(int offset) {
-	return allocateAndPlay(loadData(offset), _dynamicStartChannel);
+Channel *RSound::playSoundDynamic(int offset, byte source) {
+	return allocateAndPlay(loadData(offset), _dynamicStartChannel, source);
 }
 
-Channel *RSound::playSoundAnyChannel(int offset) {
-	return allocateAndPlay(loadData(offset), 1);
+Channel *RSound::playSoundAnyChannel(int offset, byte source) {
+	return allocateAndPlay(loadData(offset), 1, source);
 }
 
-Channel *RSound::allocateAndPlay(byte *pData, int startingChannel) {
+Channel *RSound::allocateAndPlay(byte *pData, int startingChannel, byte source) {
 	int endChannel = RSOUND_CHANNEL_COUNT - 1;
 
 	Channel *foundChannel = nullptr;
@@ -220,7 +224,7 @@ Channel *RSound::allocateAndPlay(byte *pData, int startingChannel) {
 	}
 
 	if (foundChannel != nullptr)
-		Channel_playData(foundChannel, pData);
+		Channel_playData(foundChannel, pData, source);
 
 	return foundChannel;
 }
@@ -257,25 +261,26 @@ int RSound::getTicksSinceLastCommand() {
 /*-----------------------------------------------------------------------*/
 
 void RSound::sendNoteOn(byte midiChannel, byte note, byte velocity) {
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel, note, velocity);
+	_midiDriver->send(getChannel(midiChannel)->_source, MidiDriver::MIDI_COMMAND_NOTE_ON | midiChannel, note, velocity);
 }
 
 void RSound::sendProgramChange(byte midiChannel, byte program) {
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | midiChannel, program, 0);
+	_midiDriver->send(getChannel(midiChannel)->_source, MidiDriver::MIDI_COMMAND_PROGRAM_CHANGE | midiChannel, program, 0);
 }
 
 void RSound::sendVolume(byte midiChannel, byte volume) {
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
+	_midiDriver->send(getChannel(midiChannel)->_source,
+					  MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel,
 					  MidiDriver::MIDI_CONTROLLER_VOLUME, volume);
 }
 
 void RSound::sendPitchBend(byte midiChannel, byte value) {
 	// LSB always 0 - only coarse (MSB) control is used
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_PITCH_BEND | midiChannel, 0, value);
+	_midiDriver->send(getChannel(midiChannel)->_source, MidiDriver::MIDI_COMMAND_PITCH_BEND | midiChannel, 0, value);
 }
 
 void RSound::sendPanning(byte midiChannel, byte value) {
-	_midiDriver->send(MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel, MidiDriver::MIDI_CONTROLLER_PANNING, value);
+	_midiDriver->send(getChannel(midiChannel)->_source, MidiDriver::MIDI_COMMAND_CONTROL_CHANGE | midiChannel, MidiDriver::MIDI_CONTROLLER_PANNING, value);
 }
 
 void RSound::muteChannel(byte midiChannel) {
@@ -348,11 +353,11 @@ void RSound::stopAllNotes() {
 
 /*-----------------------------------------------------------------------*/
 
-void RSound::Channel_playData(Channel *channel, byte *soundData) {
+void RSound::Channel_playData(Channel *channel, byte *soundData, byte source) {
 	bool ticksProcessingDisabled = _ticksProcessingDisabled;
 	_ticksProcessingDisabled = true;
 
-	channel->loadData(soundData);
+	channel->loadData(soundData, source);
 	channel->_deltaCounter = 1;
 	sendPitchBend(channel->_midiChannel, 0x40);
 
@@ -621,7 +626,7 @@ void RSound::Channel_processTick(Channel *channel) {
 				// Reload data to completely reset channel state, but keep
 				// fade-out to stop state.
 				bool fadeOutActive = channel->_fadeOutActive;
-				channel->loadData(channel->_soundDataStart);
+				channel->loadData(channel->_soundDataStart, channel->_source);
 				channel->_fadeOutActive = fadeOutActive;
 				break;
 			}
@@ -780,12 +785,12 @@ void RSound::processTick() {
 /*-----------------------------------------------------------------------*/
 
 /**
- * Zeroes _deltaCounter and the three fade-step fields for channels in
- * [first, last].
+ * Stops playback and effects processing by zeroing _deltaCounter and the
+ * three step size fields for channels in [first, last].
  * Deliberately does NOT touch the loop pointers, volume, program, pan etc,
  * matching the original.
  */
-void RSound::resetChannelRange(int firstChannel, int lastChannel, bool includeChannel9) {
+void RSound::stopChannelRange(int firstChannel, int lastChannel, bool includeChannel9) {
 	_ticksProcessingDisabled = true;
 
 	for (int i = firstChannel; i <= lastChannel; ++i) {
@@ -831,7 +836,7 @@ byte *RSound::loadData(int offset) {
  * command0.
  */
 void RSound::resetAllChannels() {
-	resetChannelRange(1, RSOUND_CHANNEL_COUNT);
+	stopChannelRange(1, RSOUND_CHANNEL_COUNT);
 	clearActiveNotes();
 }
 
@@ -878,9 +883,9 @@ int RSound::command1() {
 }
 
 int RSound::command2() {
-	// Initialize the static channels (also reinitializes the active notes
-	// table) plus the MIDI channel reset for those same channels.
-	resetChannelRange(1, _dynamicStartChannel - 1, _staticIncludeChannel9);
+	// Stops playback on the static channels (also reinitializes the active
+	// notes table) plus the MIDI channel reset for those same channels.
+	stopChannelRange(1, _dynamicStartChannel - 1, _staticIncludeChannel9);
 	clearActiveNotes();
 	setFadeOutSpeed(0);
 	sendMidiChannelReset(1, _dynamicStartChannel - 1, _staticIncludeChannel9);
@@ -899,9 +904,9 @@ int RSound::command3() {
 }
 
 int RSound::command4() {
-	// Initialize the dynamic Channels (does NOT touch the active notes
+	// Stops playback on the dynamic Channels (does NOT touch the active notes
 	// table) plus the MIDI channel reset for those same channels.
-	resetChannelRange(_dynamicStartChannel, 8, _dynamicIncludeChannel9);
+	stopChannelRange(_dynamicStartChannel, 8, _dynamicIncludeChannel9);
 	setFadeOutSpeed(0);
 	sendMidiChannelReset(_dynamicStartChannel, 8, _dynamicIncludeChannel9);
 	return 0;
diff --git a/engines/mads/nebular/sound/rsound.h b/engines/mads/nebular/sound/rsound.h
index 998a3385272..201072e0b07 100644
--- a/engines/mads/nebular/sound/rsound.h
+++ b/engines/mads/nebular/sound/rsound.h
@@ -70,7 +70,7 @@ enum RSoundFadeCheckMode {
  *   0x09 _volumeFadeCounter	0x16 _pSrc
  *   0x0A _pitchSlideCounter
  *   0x0B _panningSweepCounter
- * _owner and _midiChannel below are NOT part of the original struct (it
+ * _source and _midiChannel below are NOT part of the original struct (it
  * has no equivalent fields) - they're C++-side conveniences so Channel
  * methods and callers don't need the MIDI channel number (array index+1)
  * threaded through separately. Any future raw "[bx+N]" disassembly offset
@@ -79,6 +79,7 @@ enum RSoundFadeCheckMode {
 class Channel {
 public:
 	byte _midiChannel = 0;					// 1-9: the MIDI channel to which the data in this struct pertains
+	byte _source = 0;						// The source used for the multisource driver (0: music, 1: SFX)
 
 	byte _deltaCounter = 0;					// number of ticks until the next event occurs; loaded from the delta byte of a note or chord event
 											// 0: channel is not active
@@ -119,7 +120,7 @@ public:
 	 * touch volume, program, velocity, key-on state or delta/duration -
 	 * matches the original disassembly.
 	 */
-	void loadData(byte *soundData);
+	void loadData(byte *soundData, byte source);
 
 	/**
 	 * Marks the channel as fading out (fading toward silence) and stopping
@@ -205,7 +206,7 @@ private:
 	 * Loads new sound data into the channel, starts playback by setting
 	 * _deltaCounter to 1 and resets pitch bend to center on the MT-32.
 	 */
-	void Channel_playData(Channel *channel, byte *soundData);
+	void Channel_playData(Channel *channel, byte *soundData, byte source);
 
 	/**
 	 * Sends Note-Off (velocity 0) for all active notes on the
@@ -224,6 +225,8 @@ protected:
 	// Channel 9 can be included with the static or dynamic channels by setting
 	// the corresponding boolean field. This will only affect commands 2-5;
 	// channel 9 is never used by playSoundDynamic.
+	// There are a number of variations of these settings in the RSOUND.00x
+	// drivers used by Rex.
 	byte _dynamicStartChannel;
 	bool _staticIncludeChannel9;
 	bool _dynamicIncludeChannel9;
@@ -231,10 +234,11 @@ protected:
 	void setFadeOutSpeed(int fadeOutSpeed);
 
 	/**
-	 * Clear the active and fade state for MIDI channels in [first, last].
-	 * Specify includeChannel9 to also reset MIDI channel 9.
+	 * Stop playback by clearing the delta and effects counters for MIDI
+	 * channels in [first, last]. Specify includeChannel9 to also reset MIDI
+	 * channel 9.
 	 */
-	void resetChannelRange(int firstChannel, int lastChannel, bool includeChannel9 = false);
+	void stopChannelRange(int firstChannel, int lastChannel, bool includeChannel9 = false);
 
 	/** Reset the per-MIDI-channel active note tracking table to empty. */
 	void clearActiveNotes();
@@ -283,8 +287,8 @@ protected:
 	 * channel. This will unload any sound already playing on the
 	 * channel.
 	 */
-	Channel *playSoundStatic(int offset, byte channel);
-	Channel *playSoundStatic(byte *soundData, byte channel);
+	Channel *playSoundStatic(int offset, byte channel, byte source = 0);
+	Channel *playSoundStatic(byte *soundData, byte channel, byte source = 0);
 
 	/**
 	 * Play the specified sound by allocating one of the free dynamic
@@ -292,13 +296,13 @@ protected:
 	 * was free), since some commands poke the just-loaded channel's
 	 * loop pointer directly afterward.
 	 */
-	Channel *playSoundDynamic(int offset);
+	Channel *playSoundDynamic(int offset, byte source = 1);
 
 	/**
 	 * Play the specified sound by allocating any free melodic channel,
 	 * dynamic or static.
 	 */
-	Channel *playSoundAnyChannel(int offset);
+	Channel *playSoundAnyChannel(int offset, byte source = 1);
 
 	/**
 	 * Dynamically allocates a melodic channel, loads the specified sound
@@ -308,7 +312,7 @@ protected:
 	 * could be found, nullptr is returned and the sound data is not
 	 * played.
 	 */
-	Channel *allocateAndPlay(byte *pData, int startingChannel);
+	Channel *allocateAndPlay(byte *pData, int startingChannel, byte source);
 
 	/**
 	 * Checks to see whether the given block of data is already loaded
@@ -382,7 +386,7 @@ protected:
 	int command2();
 	int command3();
 	int command4();
-	int command5();
+	virtual int command5();
 	int command6();
 	int command7();
 	int command8();
diff --git a/engines/mads/nebular/sound/rsound_nebular.cpp b/engines/mads/nebular/sound/rsound_nebular.cpp
index 7a8071f03a8..64e1f2c843c 100644
--- a/engines/mads/nebular/sound/rsound_nebular.cpp
+++ b/engines/mads/nebular/sound/rsound_nebular.cpp
@@ -40,11 +40,19 @@ const RSound1::CommandPtr RSound1::_commandList[42] = {
 };
 
 RSound1::RSound1(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) : 
-	RSound(mixer, midiDriver, "rsound.001", 0x1350, 0x1A90, 0x67, kRSoundFadeCheckAlternating) {
+		RSound(mixer, midiDriver, "rsound.001", 0x1350, 0x1A90, 0x67, kRSoundFadeCheckAlternating) {
+	// Rex full game RSOUND.001 includes channel 9 with the dynamic channel
+	// commands 4 and 5.
+	// The original code actually uses channels 4-8 instead of 5-8 for
+	// playSoundDynamic, which is inconsistent with command 2-5. This
+	// inconsistency has not been replicated in this reimplementation for now.
+	_dynamicStartChannel = 5;
+	_dynamicIncludeChannel9 = true;
+	_staticIncludeChannel9 = false;
 }
 
 int RSound1::command(int commandId, int param) {
-	if (commandId > 41)
+	if (commandId < 0 || commandId > 41)
 		return 0;
 
 	_commandParam = param;
@@ -105,7 +113,7 @@ int RSound1::command13() {
 }
 
 int RSound1::command14() {
-	playSoundDynamic(0x16C2);
+	playSoundDynamic(0x16C2, 0);
 	return 0;
 }
 
@@ -179,7 +187,7 @@ int RSound1::command26() {
 	pData[8] = v1;
 	int v2 = clampParam() + 64;
 	pData[5] = v2;
-	playSoundStatic(pData, 8);
+	playSoundStatic(pData, 8, 1);
 	return 0;
 }
 
@@ -189,7 +197,7 @@ int RSound1::command27() {
 	pData[8] = v1;
 	int v2 = clampParam() + 64;
 	pData[5] = v2;
-	playSoundStatic(pData, 8);
+	playSoundStatic(pData, 8, 1);
 	return 0;
 }
 
@@ -314,16 +322,17 @@ const RSoundDemo1::CommandPtr RSoundDemo1::_commandList[41] = {
 RSoundDemo1::RSoundDemo1(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 		RSound(mixer, midiDriver, "rsound.001", 0x12E0, 0x1D28, 0x67, kRSoundFadeCheckAlternating),
 		_command23Toggle(false) {
-	// The demo RSOUND.001 driver command 2 includes channel 5 when 
-	// initializing channel data, but command 2 and 3 do not include channel 5
-	// when initializing MIDI channels and fading channels to stop. Instead,
-	// command 4 includes channel 5 when initializing channel data, but
-	// command 4 and 5 do not include channel 5 when initializing MIDI
-	// channels and fading channels to stop. This is probably a bug, which is
-	// not replicated in this reimplementation.
+	// Like the full game RSOUND.001, the demo RSOUND.001 driver includes
+	// channel 9 with the dynamic commands 4 and 5.
+	// Command 2 includes channel 5 when stopping playback, but command 2 and 3
+	// do not include channel 5 when initializing MIDI channels and fading
+	// channels to stop. Instead, command 4 includes channel 5 when stopping
+	// playback, but command 4 and 5 do not include channel 5 when initializing
+	// MIDI channels and fading channels to stop.
+	// This inconsistency is not replicated in this reimplementation.
+	// Interestingly, the demo RSOUND.001 does not include the playSoundDynamic
+	// function. All dynamically allocated sound data uses playSoundAnyChannel.
 	_dynamicStartChannel = 5;
-	// The demo RSOUND.001 driver processes channel 9 with the dynamic
-	// commands 4 and 5, instead of the static commands 2 and 3.
 	_dynamicIncludeChannel9 = true;
 	_staticIncludeChannel9 = false;
 }
@@ -391,7 +400,7 @@ int RSoundDemo1::command13() {
 }
 
 int RSoundDemo1::command14() {
-	playSoundAnyChannel(0x1AE2);
+	playSoundAnyChannel(0x1AE2, 0);
 	return 0;
 }
 
@@ -463,7 +472,7 @@ int RSoundDemo1::patchAndPlaySound(int offset) {
 	byte *data = loadData(offset);
 	data[8] = (generateRandomNumber() & 0x18) + 0x2D;
 	data[5] = adjustedCommandParam() + 0x40;
-	playSoundStatic(data, 8);
+	playSoundStatic(data, 8, 1);
 	return 0;
 }
 
@@ -584,10 +593,22 @@ const RSound2::CommandPtr RSound2::_commandList[44] = {
 
 RSound2::RSound2(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) : 
 	RSound(mixer, midiDriver, "rsound.002", 0x1390, 0x42F0, 0x87, kRSoundFadeCheckAlternating) {
+	// RSOUND.002 is a bit of a mess when it comes to dynamic and static
+	// channels.
+	// playSoundDynamic uses channels 4-8. Commands 3 and 5 are consistent with
+	// this, as they fade out channels 1-3 and 9, and 4-8, respectively. These
+	// ranges have been reimplemented in this driver code.
+	// However, the original code commands 2 and 4 are not consistent with this
+	// and not consistent with itself. Playback is stopped on channels 1-5 and
+	// 6-9, while MIDI initialization is done on channels 1-4 and 5-9. These
+	// inconsistencies are not replicated in this reimplementation for now.
+	_dynamicStartChannel = 4;
+	_dynamicIncludeChannel9 = false;
+	_staticIncludeChannel9 = true;
 }
 
 int RSound2::command(int commandId, int param) {
-	if (commandId > 43)
+	if (commandId < 0 || commandId > 43)
 		return 0;
 
 	_commandParam = param;
@@ -595,6 +616,12 @@ int RSound2::command(int commandId, int param) {
 	return (this->*_commandList[commandId])();
 }
 
+int RSound2::command0() {
+	_volumeCycleCounter = 47;
+
+	return RSound::command0();
+}
+
 int RSound2::command5() {
 	_volumeCycleCounter = 47;
 	for (int i = 4; i <= 8; i++) {
@@ -658,9 +685,9 @@ int RSound2::command15() {
 	byte *pData = loadData(0x1DFC);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundAnyChannel(0x1DFC);
-		playSoundAnyChannel(0x222E);
-		playSoundAnyChannel(0x2648);
+		playSoundAnyChannel(0x1DFC, 0);
+		playSoundAnyChannel(0x222E, 0);
+		playSoundAnyChannel(0x2648, 0);
 		playSoundStatic(0x26A2, 9);
 	}
 	return 0;
@@ -670,12 +697,12 @@ int RSound2::command16() {
 	byte *pData = loadData(0x3456);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundAnyChannel(0x3456);
-		playSoundAnyChannel(0x3572);
-		playSoundAnyChannel(0x367E);
-		playSoundAnyChannel(0x37C6);
-		playSoundAnyChannel(0x39AE);
-		playSoundAnyChannel(0x3A3A);
+		playSoundAnyChannel(0x3456, 0);
+		playSoundAnyChannel(0x3572, 0);
+		playSoundAnyChannel(0x367E, 0);
+		playSoundAnyChannel(0x37C6, 0);
+		playSoundAnyChannel(0x39AE, 0);
+		playSoundAnyChannel(0x3A3A, 0);
 	}
 	return 0;
 }
@@ -683,10 +710,10 @@ int RSound2::command16() {
 int RSound2::command17() {
 	byte *pData = loadData(0x3AC0);
 	if (!isSoundPlaying(pData)) {
-		playSoundDynamic(0x3AC0);
-		playSoundDynamic(0x3C70);
-		playSoundDynamic(0x3E16);
-		playSoundDynamic(0x3FBE);
+		playSoundDynamic(0x3AC0, 0);
+		playSoundDynamic(0x3C70, 0);
+		playSoundDynamic(0x3E16, 0);
+		playSoundDynamic(0x3FBE, 0);
 	}
 	return 0;
 }
@@ -704,12 +731,12 @@ int RSound2::command19() {
 	byte *pData = loadData(0x2A64);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundAnyChannel(0x2A64);
-		playSoundAnyChannel(0x2BE2);
-		playSoundAnyChannel(0x2DAC);
-		playSoundAnyChannel(0x2ECE);
-		playSoundAnyChannel(0x3026);
-		playSoundAnyChannel(0x30C2);
+		playSoundAnyChannel(0x2A64, 0);
+		playSoundAnyChannel(0x2BE2, 0);
+		playSoundAnyChannel(0x2DAC, 0);
+		playSoundAnyChannel(0x2ECE, 0);
+		playSoundAnyChannel(0x3026, 0);
+		playSoundAnyChannel(0x30C2, 0);
 	}
 	return 0;
 }
@@ -819,8 +846,8 @@ int RSound2::command35() {
 }
 
 int RSound2::command36() {
-	playSoundDynamic(0xFF4);
-	playSoundDynamic(0x1008);
+	playSoundDynamic(0xFF4, 0);
+	playSoundDynamic(0x1008, 0);
 	return 0;
 }
 
@@ -833,12 +860,12 @@ int RSound2::command38() {
 	byte *pData = loadData(0x2B0E);
 	if (!isSoundPlaying(pData)) {
 		command1();
-		playSoundAnyChannel(0x2B0E);
-		playSoundAnyChannel(0x2CD0);
-		playSoundAnyChannel(0x2E46);
-		playSoundAnyChannel(0x2F7C);
-		playSoundAnyChannel(0x3074);
-		playSoundAnyChannel(0x317E);
+		playSoundAnyChannel(0x2B0E, 0);
+		playSoundAnyChannel(0x2CD0, 0);
+		playSoundAnyChannel(0x2E46, 0);
+		playSoundAnyChannel(0x2F7C, 0);
+		playSoundAnyChannel(0x3074, 0);
+		playSoundAnyChannel(0x317E, 0);
 	}
 	return 0;
 }
@@ -895,7 +922,7 @@ RSound3::RSound3(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound3::command(int commandId, int param) {
-	if (commandId > 60)
+	if (commandId < 0 || commandId > 60)
 		return 0;
 
 	_commandParam = param;
@@ -909,6 +936,12 @@ Channel *RSound3::patchAndPlaySound(int offset, byte value) {
 	return playSoundDynamic(offset);
 }
 
+int RSound3::command5() {
+	if (!isSoundPlaying(0x1AE6))
+		return RSound::command5();
+	return 0;
+}
+
 int RSound3::command9() {
 	command1();
 	setFadeOutSpeed((byte)_commandParam);
@@ -1149,8 +1182,8 @@ int RSound3::command35() {
 }
 
 int RSound3::command36() {
-	playSoundDynamic(0x14DA);
-	playSoundDynamic(0x14EE);
+	playSoundDynamic(0x14DA, 0);
+	playSoundDynamic(0x14EE, 0);
 	return 0;
 }
 
@@ -1271,7 +1304,7 @@ RSound4::RSound4(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound4::command(int commandId, int param) {
-	if (commandId > 59)
+	if (commandId < 0 || commandId > 59)
 		return 0;
 
 	_commandParam = param;
@@ -1292,6 +1325,14 @@ void RSound4::stopChannel(byte channel) {
 	getChannel(channel)->_pSrc = loadData(0x1188);
 }
 
+int RSound4::command0() {
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+
+	return RSound::command0();
+}
+
 int RSound4::command9() {
 	command1();
 	setFadeOutSpeed((byte)_commandParam);
@@ -1385,8 +1426,8 @@ int RSound4::command35() {
 }
 
 int RSound4::command36() {
-	playSoundDynamic(0x123C);
-	playSoundDynamic(0x1250);
+	playSoundDynamic(0x123C, 0);
+	playSoundDynamic(0x1250, 0);
 	return 0;
 }
 
@@ -1407,15 +1448,15 @@ int RSound4::command53() {
 	command1();
 	_callbackCounter = 56;
 	_callbackPeriod = 56;
-	playSoundAnyChannel(0x1888);
-	playSoundAnyChannel(0x18DE);
+	playSoundAnyChannel(0x1888, 0);
+	playSoundAnyChannel(0x18DE, 0);
 	return 0;
 }
 
 void RSound4::command54Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundAnyChannel(0x18B2);
-	playSoundAnyChannel(0x1904);
+	playSoundAnyChannel(0x18B2, 0);
+	playSoundAnyChannel(0x1904, 0);
 }
 
 int RSound4::command54() {
@@ -1426,7 +1467,7 @@ int RSound4::command54() {
 
 void RSound4::command55Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundAnyChannel(0x191E);
+	playSoundAnyChannel(0x191E, 0);
 }
 
 int RSound4::command55() {
@@ -1437,7 +1478,7 @@ int RSound4::command55() {
 
 void RSound4::command56Callback() {
 	_callbackFnPtr = nullptr;
-	playSoundAnyChannel(0x185C);
+	playSoundAnyChannel(0x185C, 0);
 }
 
 int RSound4::command56() {
@@ -1483,7 +1524,7 @@ RSound5::RSound5(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound5::command(int commandId, int param) {
-	if (commandId > 41)
+	if (commandId < 0 || commandId > 41)
 		return 0;
 
 	_commandParam = param;
@@ -1522,7 +1563,7 @@ int RSound5::command13() {
 }
 
 int RSound5::command14() {
-	playSoundStatic(0x1272, 8);
+	playSoundStatic(0x1272, 8, 1);
 	return 0;
 }
 
@@ -1650,8 +1691,8 @@ int RSound5::command35() {
 }
 
 int RSound5::command36() {
-	playSoundDynamic(0x1464);
-	playSoundDynamic(0x1478);
+	playSoundDynamic(0x1464, 0);
+	playSoundDynamic(0x1478, 0);
 	return 0;
 }
 
@@ -1701,7 +1742,7 @@ RSound6::RSound6(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound6::command(int commandId, int param) {
-	if (commandId > 29)
+	if (commandId < 0 || commandId > 29)
 		return 0;
 
 	_commandParam = param;
@@ -1718,6 +1759,14 @@ void RSound6::tickCallback() {
 		(this->*_callbackFnPtr)();
 }
 
+int RSound6::command0() {
+	_callbackCounter = 0;
+	_callbackPeriod = 0;
+	_callbackFnPtr = nullptr;
+
+	return RSound::command0();
+}
+
 int RSound6::command9() {
 	playSoundDynamic(0x111C);
 	return 0;
@@ -1783,7 +1832,7 @@ int RSound6::command20() {
 }
 
 int RSound6::command21() {
-	playSoundStatic(0x1282, 5);
+	playSoundStatic(0x1282, 5, 1);
 	playSoundDynamic(0x1282);
 	playSoundDynamic(0x1282);
 	playSoundDynamic(0x12AA);
@@ -1791,10 +1840,10 @@ int RSound6::command21() {
 }
 
 int RSound6::command22() {
-	playSoundStatic(0x12CE, 5);
-	playSoundStatic(0x12CE, 6);
-	playSoundStatic(0x12CE, 7);
-	playSoundStatic(0x12CE, 8);
+	playSoundStatic(0x12CE, 5, 1);
+	playSoundStatic(0x12CE, 6, 1);
+	playSoundStatic(0x12CE, 7, 1);
+	playSoundStatic(0x12CE, 8, 1);
 	return 0;
 }
 
@@ -1826,7 +1875,7 @@ int RSound6::command24() {
 }
 
 int RSound6::command25() {
-	playSoundStatic(0x12FE, 5);
+	playSoundStatic(0x12FE, 5, 1);
 	return 0;
 }
 
@@ -1877,7 +1926,7 @@ RSound7::RSound7(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound7::command(int commandId, int param) {
-	if (commandId > 37)
+	if (commandId < 0 || commandId > 37)
 		return 0;
 
 	_commandParam = param;
@@ -1910,7 +1959,7 @@ int RSound7::command17() {
 }
 
 int RSound7::command18() {
-	playSoundStatic(0x12FC, 8);
+	playSoundStatic(0x12FC, 8, 1);
 	return 0;
 }
 
@@ -2000,8 +2049,8 @@ int RSound7::command35() {
 }
 
 int RSound7::command36() {
-	playSoundDynamic(0x1358);
-	playSoundDynamic(0x136C);
+	playSoundDynamic(0x1358, 0);
+	playSoundDynamic(0x136C, 0);
 	return 0;
 }
 
@@ -2030,7 +2079,7 @@ RSound8::RSound8(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 }
 
 int RSound8::command(int commandId, int param) {
-	if (commandId > 37)
+	if (commandId < 0 || commandId > 37)
 		return 0;
 
 	_commandParam = param;
@@ -2044,10 +2093,10 @@ int RSound8::command9() {
 }
 
 int RSound8::command10() {
-	playSoundStatic(0x115A, 1);
-	playSoundStatic(0x115A, 2);
-	playSoundStatic(0x115A, 3);
-	playSoundStatic(0x1150, 4);
+	playSoundStatic(0x115A, 1, 1);
+	playSoundStatic(0x115A, 2, 1);
+	playSoundStatic(0x115A, 3, 1);
+	playSoundStatic(0x1150, 4, 1);
 	return 0;
 }
 
@@ -2127,10 +2176,10 @@ int RSound8::command22() {
 }
 
 int RSound8::command23() {
-	playSoundStatic(0x128E, 1);
-	playSoundStatic(0x128E, 2);
-	playSoundStatic(0x128E, 3);
-	playSoundStatic(0x128E, 4);
+	playSoundStatic(0x128E, 1, 1);
+	playSoundStatic(0x128E, 2, 1);
+	playSoundStatic(0x128E, 3, 1);
+	playSoundStatic(0x128E, 4, 1);
 	return 0;
 }
 
@@ -2207,8 +2256,8 @@ int RSound8::command35() {
 }
 
 int RSound8::command36() {
-	playSoundDynamic(0x12E6);
-	playSoundDynamic(0x12FA);
+	playSoundDynamic(0x12E6, 0);
+	playSoundDynamic(0x12FA, 0);
 	return 0;
 }
 
@@ -2237,13 +2286,19 @@ const RSound9::CommandPtr RSound9::_commandList[52] = {
 
 RSound9::RSound9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) : 
 	RSound(mixer, midiDriver, "rsound.009", 0x1520, 0x8920, 0x6F, kRSoundFadeCheckAlternating) {
+	// The full game RSOUND.009 driver uses static channels 1-5 and dynamic
+	// channels 6-9.
+	_dynamicStartChannel = 6;
+	_dynamicIncludeChannel9 = true;
+	_staticIncludeChannel9 = false;
+
 	_callbackCounter = 0;
 	_callbackPeriod = 0;
 	_callbackFnPtr = nullptr;
 }
 
 int RSound9::command(int commandId, int param) {
-	if (commandId > 51)
+	if (commandId < 0 || commandId > 51)
 		return 0;
 
 	_commandParam = param;
@@ -2264,6 +2319,7 @@ int RSound9::command0() {
 	_callbackCounter = 0;
 	_callbackPeriod = 0;
 	_callbackFnPtr = nullptr;
+
 	return RSound::command0();
 }
 
@@ -2289,37 +2345,37 @@ int RSound9::command10() {
 }
 
 int RSound9::command11() {
-	playSoundStatic(0x33E2, 8);
+	playSoundStatic(0x33E2, 8, 1);
 	return 0;
 }
 
 int RSound9::command12() {
-	playSoundStatic(0x342E, 8);
+	playSoundStatic(0x342E, 8, 1);
 	return 0;
 }
 
 int RSound9::command13() {
-	playSoundStatic(0x343A, 8);
+	playSoundStatic(0x343A, 8, 1);
 	return 0;
 }
 
 int RSound9::command14() {
-	playSoundStatic(0x3442, 8);
+	playSoundStatic(0x3442, 8, 1);
 	return 0;
 }
 
 int RSound9::command15() {
-	playSoundStatic(0x3462, 8);
+	playSoundStatic(0x3462, 8, 1);
 	return 0;
 }
 
 int RSound9::command16() {
-	playSoundStatic(0x347A, 8);
+	playSoundStatic(0x347A, 8, 1);
 	return 0;
 }
 
 int RSound9::command17() {
-	playSoundStatic(0x3470, 8);
+	playSoundStatic(0x3470, 8, 1);
 	return 0;
 }
 
@@ -2396,7 +2452,7 @@ int RSound9::command28() {
 	int v = (generateRandomNumber() & 28) + 15;
 	byte *pData = loadData(0x334A);
 	pData[6] = v & 0x7F;
-	playSoundStatic(pData, 8);
+	playSoundStatic(pData, 8, 1);
 	return 0;
 }
 
@@ -2668,9 +2724,8 @@ const RSoundDemo9::CommandPtr RSoundDemo9::_commandList[40] = {
 
 RSoundDemo9::RSoundDemo9(Audio::Mixer *mixer, MidiDriver_MT32GM *midiDriver) :
 		RSound(mixer, midiDriver, "rsound.009", 0x11D0, 0x3664, 0x69, kRSoundFadeCheckAlternating) {
-	// The demo RSOUND.009 driver uses channel range 1-5 for the static init
-	// and stop commands 2 and 3, and 6-9 for the dynamic init and stop
-	// commands 4 and 5.
+	// Like the full game, the demo RSOUND.009 driver uses static channels 1-5
+	// and dynamic channels 6-9.
 	_dynamicStartChannel = 6;
 	_dynamicIncludeChannel9 = true;
 	_staticIncludeChannel9 = false;
@@ -2686,37 +2741,37 @@ int RSoundDemo9::command(int commandId, int param) {
 }
 
 int RSoundDemo9::command11() {
-	playSoundStatic(0x1454, 8);
+	playSoundStatic(0x1454, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command12() {
-	playSoundStatic(0x14A0, 8);
+	playSoundStatic(0x14A0, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command13() {
-	playSoundStatic(0x14AC, 8);
+	playSoundStatic(0x14AC, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command14() {
-	playSoundStatic(0x14B4, 8);
+	playSoundStatic(0x14B4, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command15() {
-	playSoundStatic(0x14D4, 8);
+	playSoundStatic(0x14D4, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command16() {
-	playSoundStatic(0x14EC, 8);
+	playSoundStatic(0x14EC, 8, 1);
 	return 0;
 }
 
 int RSoundDemo9::command17() {
-	playSoundStatic(0x14E2, 8);
+	playSoundStatic(0x14E2, 8, 1);
 	return 0;
 }
 
diff --git a/engines/mads/nebular/sound/rsound_nebular.h b/engines/mads/nebular/sound/rsound_nebular.h
index 2e5f2c40a94..e4c21a06f6f 100644
--- a/engines/mads/nebular/sound/rsound_nebular.h
+++ b/engines/mads/nebular/sound/rsound_nebular.h
@@ -159,7 +159,8 @@ private:
 	 */
 	byte _volumeCycleCounter = 0x2F;
 
-	int command5();
+	int command0() override;
+	int command5() override;
 
 	int command9();
 	int command10();
@@ -253,6 +254,8 @@ private:
 	 */
 	void sendDualVolume(byte volume);
 
+	int command5() override;
+
 	int command9();
 	int command10();
 	int command11();
@@ -344,6 +347,8 @@ private:
 	 */
 	void stopChannel(byte channel);
 
+	int command0() override;
+
 	int command9();
 	int command10();
 	int command12();
@@ -455,6 +460,8 @@ private:
 	void command24Callback();
 	void command29Callback();
 
+	int command0() override;
+
 	int command9();
 	int command10();
 	int command11();


Commit: 03e90922c932bc5ed41eb0e95a9baf6b1e7c8bfa
    https://github.com/scummvm/scummvm/commit/03e90922c932bc5ed41eb0e95a9baf6b1e7c8bfa
Author: Coen Rampen (crampen at gmail.com)
Date: 2026-10-09T18:19:49+02:00

Commit Message:
MADS: Fix Rex MT-32 volume fade-out bug

Changed paths:
    engines/mads/nebular/sound/rsound.cpp


diff --git a/engines/mads/nebular/sound/rsound.cpp b/engines/mads/nebular/sound/rsound.cpp
index 204e74128c0..c2761f1d2be 100644
--- a/engines/mads/nebular/sound/rsound.cpp
+++ b/engines/mads/nebular/sound/rsound.cpp
@@ -31,6 +31,7 @@ namespace Sound {
 
 void Channel::loadData(byte *soundData, byte source) {
 	_pitchSlideStepSize = 0;
+	_volumeFadeStepSize = 0;
 	_panningSweepStepSize = 0;
 	_innerLoopCounter = 0;
 	_outerLoopCounter = 0;
@@ -713,7 +714,7 @@ void RSound::Channel_processTick(Channel *channel) {
 	// Process effects
 
 	// Volume fade
-	if (channel->_volumeFadeStepSize > 0 && --channel->_volumeFadeCounter == 0) {
+	if (channel->_volumeFadeStepSize != 0 && --channel->_volumeFadeCounter == 0) {
 		channel->_volumeFadeCounter = channel->_volumeFadeSpeed;
 		int newVolume = channel->_volume + channel->_volumeFadeStepSize;
 		if (newVolume < 0 || newVolume > 0x7F) {




More information about the Scummvm-git-logs mailing list