[Scummvm-git-logs] scummvm master -> 1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f

neuromancer noreply at scummvm.org
Tue Mar 3 07:23:04 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:
0146ecd5af FREESCAPE: restored rise/fall keys
14102847c9 FREESCAPE: fixed celestial bodies when using shaders
c3cbb7025e FREESCAPE: better wb-based music for dark and eclipse
c229bdfea8 FREESCAPE: added sound playback for driller (c64)
3233011b5c FREESCAPE: first pass on the music/sound for dark side (c64)
1dc0f0b189 FREESCAPE: allow to switch between music/sound in c64


Commit: 0146ecd5afaf8b06b343e146a3b854e55f1d67c3
    https://github.com/scummvm/scummvm/commit/0146ecd5afaf8b06b343e146a3b854e55f1d67c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: restored rise/fall keys

Changed paths:
    engines/freescape/freescape.cpp
    engines/freescape/movement.cpp


diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 610d4f98cdb..ef0e4654650 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -616,12 +616,6 @@ void FreescapeEngine::processInput() {
 			case kActionMoveRight:
 				_strafeRight = true;
 				break;
-			case kActionRiseOrFlyUp:
-				_moveUp = true;
-				break;
-			case kActionLowerOrFlyDown:
-				_moveDown = true;
-				break;
 			case kActionShoot:
 				shoot();
 				break;
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 39374d6cb36..56b47946853 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -312,6 +312,7 @@ bool FreescapeEngine::rise() {
 	debugC(1, kFreescapeDebugMove, "playerHeightNumber: %d", _playerHeightNumber);
 	int previousAreaID = _currentArea->getAreaID();
 	if (_flyMode) {
+		_moveUp = true;
 		Math::Vector3d destination = _position;
 		destination.y() = destination.y() + _playerSteps[_playerStepIndex];
 		resolveCollisions(destination);
@@ -345,6 +346,7 @@ bool FreescapeEngine::rise() {
 void FreescapeEngine::lower() {
 	debugC(1, kFreescapeDebugMove, "playerHeightNumber: %d", _playerHeightNumber);
 	if (_flyMode) {
+		_moveDown = true;
 		Math::Vector3d destination = _position;
 		destination.y() = destination.y() - _playerSteps[_playerStepIndex];
 		resolveCollisions(destination);


Commit: 14102847c9ffc9464a5744b15fc6e37cc5deeee9
    https://github.com/scummvm/scummvm/commit/14102847c9ffc9464a5744b15fc6e37cc5deeee9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: fixed celestial bodies when using shaders

Changed paths:
    engines/freescape/gfx_opengl_shaders.cpp


diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index b689f982f95..16976d074ec 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -490,23 +490,30 @@ void OpenGLShaderRenderer::drawCelestialBody(const Math::Vector3d position, floa
 		verts.push_back(z);
 	}
 
-	// === Apply billboard effect to MVP matrix ===
-	// Replicate the legacy code's matrix modification
-	Math::Matrix4 billboardMVP = _mvpMatrix;
-
-	// Zero out rotation for rows 1, 3 (skip row 2), set diagonal to 1.0
-	// This matches: for (int i = 1; i < 4; i++) for (int j = 0; j < 4; j++)
+	// === Apply billboard effect to modelview matrix only ===
+	// Matrix4 operator()(i,j) uses (column, row) convention, matching
+	// OpenGL column-major m[col*4+row]. Zero columns 1 and 3 (skip 2),
+	// set diagonal to 1.0 — same as legacy glGetFloatv billboard.
+	Math::Matrix4 billboardMV = _modelViewMatrix;
 	for (int i = 1; i < 4; i++) {
 		for (int j = 0; j < 4; j++) {
 			if (i == 2)
 				continue;
 			if (i == j)
-				billboardMVP(i, j) = 2.5f;
+				billboardMV(i, j) = 1.0f;
 			else
-				billboardMVP(i, j) = 0.0f;
+				billboardMV(i, j) = 0.0f;
 		}
 	}
 
+	// Recombine with projection (same pattern as positionCamera)
+	Math::Matrix4 proj = _projectionMatrix;
+	Math::Matrix4 model = billboardMV;
+	proj.transpose();
+	model.transpose();
+	Math::Matrix4 billboardMVP = proj * model;
+	billboardMVP.transpose();
+
 	// === Bind VBO ===
 	glBindBuffer(GL_ARRAY_BUFFER, _triangleVBO);
 	glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_DYNAMIC_DRAW);


Commit: c3cbb7025ecc16d7e7b1b4a089df7fc9e573c361
    https://github.com/scummvm/scummvm/commit/c3cbb7025ecc16d7e7b1b4a089df7fc9e573c361
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: better wb-based music for dark and eclipse

Changed paths:
    engines/freescape/games/eclipse/atari.music.cpp
    engines/freescape/wb.cpp
    engines/freescape/wb.h


diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index b3518d87c43..e4760ce4138 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -38,6 +38,7 @@
 #include "audio/softsynth/ay8912.h"
 
 #include "freescape/freescape.h"
+#include "freescape/wb.h"
 
 #include "common/endian.h"
 #include "common/debug.h"
@@ -121,16 +122,27 @@ private:
 		byte decayTarget;  // Target volume to hold
 		byte attackRate;   // Increment per tick
 		byte releaseRate;  // Decrement per tick
-		byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
+		byte envelopeFlags; // Instrument byte 4
+		byte envelopeToggle; // Bit7 envelope direction toggle
+		bool envelopeDone; // Mirrors original per-note envelope completion flag
+		bool useHardwareEnvelope;
 
 		// Effects
-		byte effectMode;   // 0=none, 1=vibrato, 2=arpeggio
+		byte effectMode;   // 0=none, 1=pattern FX ($7D), 2=instrument FX ($7C)
 		bool portaUp;
 		bool portaDown;
 		int16 portaStep;
 		int16 portaTarget;
 		byte arpeggioMask;
 		byte arpeggioPos;
+		byte arpeggioTable[16];
+		byte arpeggioTableLen;
+		byte effect7BBaseNote;
+		byte effect7BParam;
+		bool effect7BActive;
+		int16 effect7BPeriod;
+		byte delay;
+		byte delayCounter;
 
 		// Vibrato
 		byte vibratoSpeed;  // Phase increment per tick
@@ -139,8 +151,19 @@ private:
 		int8 vibratoDir;    // +1 or -1
 
 		// Noise
-		bool noiseEnabled;  // Instrument flags bit 1: noise mode
+		bool noiseEnabled;  // Instrument flags bit 0/1: noise mode
+		bool toneEnabled;   // If false, channel uses noise-only mode
+		bool skipTranspose; // Noise-only mode bypasses order-list transpose
 		bool freqSweep;     // Instrument flags bit 2: frequency sweep
+		byte noisePeriod;   // YM noise period source from instrument byte 5
+		byte noiseCounter;  // Instrument flags high nibble countdown ($54)
+
+		// Instrument byte-5 period modulation ($04A2..$05C8 path)
+		byte modParam;      // Raw instrument byte 5
+		byte modSpan;       // High nibble
+		byte modPos;        // Running position (mirrors +$30)
+		int8 modDir;        // -1/1 (mirrors sign of +$2D)
+		int16 modStep;      // Derived note-step delta (mirrors $C9A)
 
 		// Period
 		int16 basePeriod;
@@ -155,10 +178,9 @@ private:
 	byte _tickSpeed;
 	byte _tickCounter;
 	int _tickSampleCount;
-
-	// Arpeggio working table
-	byte _arpeggioTable[16];
-	int _arpeggioTableLen;
+	bool _hwEnvelopeDirty;
+	uint16 _hwEnvelopePeriod;
+	byte _hwEnvelopeShape;
 
 	// --- Methods ---
 	void loadTables();
@@ -169,7 +191,7 @@ private:
 	void triggerNote(int ch);
 	void processEffects(int ch);
 	void processEnvelope(int ch);
-	void buildArpeggioTable(byte mask);
+	void buildArpeggioTable(ChannelState &c, byte mask);
 	void tickUpdate();
 	void writeYMRegisters();
 
@@ -207,8 +229,8 @@ EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSi
 	: AY8912Stream(rate, 2000000), // YM2149 at 2 MHz on Atari ST
 	  _data(data), _dataSize(dataSize),
 	  _musicActive(false), _tickSpeed(6), _tickCounter(0),
-	  _tickSampleCount(0),
-	  _arpeggioTableLen(0), _numPatterns(0) {
+	  _tickSampleCount(0), _hwEnvelopeDirty(false), _hwEnvelopePeriod(0), _hwEnvelopeShape(0),
+	  _numPatterns(0) {
 
 	memset(_periods, 0, sizeof(_periods));
 	memset(_instruments, 0, sizeof(_instruments));
@@ -216,7 +238,6 @@ EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSi
 	memset(_patternPtrs, 0, sizeof(_patternPtrs));
 	memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
 	memset(_channels, 0, sizeof(_channels));
-	memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
 
 	// Reset all YM registers
 	for (int r = 0; r < 14; r++)
@@ -305,7 +326,9 @@ void EclipseAtariMusicStream::startSong(int songNum) {
 	int songIdx = songNum - 1;
 	_tickSpeed = 6;
 	_tickCounter = 0;
-	_arpeggioTableLen = 0;
+	_hwEnvelopeDirty = false;
+	_hwEnvelopePeriod = 0;
+	_hwEnvelopeShape = 0;
 
 	// Silence all YM channels
 	for (int r = 0; r < 14; r++)
@@ -334,9 +357,11 @@ void EclipseAtariMusicStream::initChannel(int ch) {
 	memset(&c, 0, sizeof(ChannelState));
 	c.duration = 1;
 	c.durationCounter = 0;
-	c.envelopePhase = 3; // Start in release (silent)
 	c.attackLevel = 0x36; // Default from instrument 1
 	c.decayTarget = 0x36;
+	c.toneEnabled = true;
+	c.envelopeDone = true;
+	c.useHardwareEnvelope = false;
 }
 
 // ---------------------------------------------------------------------------
@@ -360,7 +385,7 @@ void EclipseAtariMusicStream::readOrderList(int ch) {
 		}
 
 		if (cmd > 0xC0) {
-			c.transpose = (int8)((cmd + 0x20) & 0xFF);
+			c.transpose = WBCommon::decodeOrderTranspose(cmd);
 			continue;
 		}
 
@@ -407,19 +432,22 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 		}
 
 		if (cmd == 0xFC) {
-			// Song change command — read parameter, restart
-			byte newSong = readDataByte(c.patternOffset + c.patternPos);
+			// Song jump command: mirror TEMUSIC mailbox semantics.
+			byte command = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
-			if (newSong >= 1 && newSong <= 2) {
-				startSong(newSong);
+			if (command == 0) {
+				_musicActive = false;
+				for (int r = 0; r < 14; r++)
+					setReg(r, 0);
+				setReg(7, 0x3F);
+			} else if (command >= 1 && command <= 2) {
+				startSong(command);
 			}
 			return;
 		}
 
 		if (cmd >= 0xF0) {
-			_tickSpeed = cmd & 0x0F;
-			if (_tickSpeed == 0)
-				_tickSpeed = 1;
+			_tickSpeed = WBCommon::decodeTickSpeed(cmd);
 			continue;
 		}
 
@@ -433,36 +461,52 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 				c.decayTarget = inst.targetVol;
 				c.attackRate = inst.attackRate;
 				c.releaseRate = inst.releaseRate;
-
-				// Instrument effect type: high nibble = vibrato speed,
-				// low nibble = vibrato depth (in period units)
-				if (inst.effectType != 0) {
-					c.effectMode = 1; // vibrato
-					c.vibratoSpeed = (inst.effectType >> 4) & 0x0F;
-					c.vibratoDepth = inst.effectType & 0x0F;
-					c.vibratoPos = 0;
-					c.vibratoDir = 1;
-					c.portaUp = false;
-					c.portaDown = false;
-				} else if (inst.arpeggioData != 0) {
-					c.effectMode = 2; // arpeggio
+				c.envelopeFlags = inst.envFlags;
+
+				// Instrument byte 5 is used as a PSG/noise control parameter.
+				c.modParam = inst.effectType;
+				c.modSpan = (inst.effectType >> 4) & 0x0F;
+				c.modPos = 0;
+				c.modDir = 1;
+				c.noisePeriod = inst.effectType & 0x3F;
+				c.toneEnabled = true;
+				c.noiseEnabled = false;
+				c.skipTranspose = false;
+				c.freqSweep = false;
+				c.noiseCounter = inst.flags >> 4;
+
+				// Instrument byte 6 preloads the channel interval table and forces mode $7C.
+				if (inst.arpeggioData != 0) {
+					c.effectMode = 2;
 					c.arpeggioMask = inst.arpeggioData;
-					buildArpeggioTable(inst.arpeggioData);
-				} else {
-					c.effectMode = 0;
+					c.arpeggioPos = 0;
+					buildArpeggioTable(c, inst.arpeggioData);
 				}
 
-				// Noise flags from instrument byte 7
-				c.noiseEnabled = (inst.flags & 0x02) != 0;
-				c.freqSweep = (inst.flags & 0x04) != 0;
+				// Instrument byte 7 flags:
+				// bit0/1 noise modes, bit2 frequency sweep, bit3 retrigger.
+				if (inst.flags & 0x01) {
+					// Bit 0: tone + noise with fixed noise seed period.
+					c.noiseEnabled = true;
+					c.noisePeriod = 0x20;
+				} else if (inst.flags & 0x02) {
+					// Bit 1: note-relative noise-only mode.
+					c.noiseEnabled = true;
+					c.toneEnabled = false;
+					c.skipTranspose = true;
+					if (c.noisePeriod == 0)
+						c.noisePeriod = 1;
+				} else if (inst.flags & 0x04) {
+					// Bit 2: tone + noise mode with frequency sweep.
+					c.noiseEnabled = true;
+					c.freqSweep = true;
+				}
 			}
 			continue;
 		}
 
 		if (cmd >= 0x80) {
-			c.duration = cmd & 0x3F;
-			if (c.duration == 0)
-				c.duration = 1;
+			c.duration = WBCommon::decodeDuration(cmd);
 			continue;
 		}
 
@@ -470,40 +514,81 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 			c.portaUp = true;
 			c.portaDown = false;
 			c.effectMode = 1;
-			continue;
+			// Original parser consumes the next byte and uses it as the immediate note.
+			if (c.patternOffset + c.patternPos >= _dataSize)
+				return;
+			c.prevNote = c.note;
+			c.note = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.durationCounter = c.duration;
+			triggerNote(ch);
+			return;
 		}
 
 		if (cmd == 0x7E) {
 			c.portaDown = true;
 			c.portaUp = false;
 			c.effectMode = 1;
-			continue;
+			// Original parser consumes the next byte and uses it as the immediate note.
+			if (c.patternOffset + c.patternPos >= _dataSize)
+				return;
+			c.prevNote = c.note;
+			c.note = readDataByte(c.patternOffset + c.patternPos);
+			c.patternPos++;
+			c.durationCounter = c.duration;
+			triggerNote(ch);
+			return;
 		}
 
 		if (cmd == 0x7D) {
-			// Arpeggio / effect mode 1 — param is bitmask into interval table
+			// Pattern effect 1: parameterized interval table (channel-local)
 			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
-			c.effectMode = 2; // arpeggio
+			c.effectMode = 1;
 			c.arpeggioMask = param;
 			c.arpeggioPos = 0;
-			buildArpeggioTable(param);
+			buildArpeggioTable(c, param);
 			continue;
 		}
 
 		if (cmd == 0x7C) {
-			// Effect mode 2 — alternate arpeggio/vibrato
-			byte param = readDataByte(c.patternOffset + c.patternPos);
-			c.patternPos++;
-			c.effectMode = 2; // arpeggio
-			c.arpeggioMask = param;
+			// Pattern effect 2: switch mode only (no parameter byte consumed).
+			c.effectMode = 2;
+			continue;
+		}
+
+		if (cmd == 0x7B) {
+			// TEMUSIC delayed slide command:
+			//   byte1 = base note (+transpose), byte2 low nibble = delay window.
+			c.arpeggioTableLen = 0;
 			c.arpeggioPos = 0;
-			buildArpeggioTable(param);
+			c.effectMode = 1;
+			c.effect7BActive = false;
+
+			if (c.patternOffset + c.patternPos < _dataSize) {
+				byte base = readDataByte(c.patternOffset + c.patternPos);
+				c.patternPos++;
+				base = (byte)(base + c.transpose);
+				c.effect7BBaseNote = base;
+
+				int baseNote = c.effect7BBaseNote;
+				if (baseNote < 1)
+					baseNote = 1;
+				if (baseNote >= kTENumPeriods)
+					baseNote = kTENumPeriods - 1;
+				c.effect7BPeriod = getPeriod(baseNote);
+			}
+			if (c.patternOffset + c.patternPos < _dataSize) {
+				c.effect7BParam = readDataByte(c.patternOffset + c.patternPos);
+				c.patternPos++;
+			}
+			c.effect7BActive = true;
 			continue;
 		}
 
 		if (cmd == 0x7A) {
-			// Delay command — skip parameter byte (same as wb.cpp)
+			// Delay command — consumed and copied to per-note delay counter.
+			c.delay = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
 			continue;
 		}
@@ -526,37 +611,86 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
 void EclipseAtariMusicStream::triggerNote(int ch) {
 	ChannelState &c = _channels[ch];
 
-	if (c.note == 0) {
-		// Rest — silence channel
-		c.outputPeriod = 0;
-		c.volume = 0;
-		c.envelopePhase = 3;
-		return;
-	}
-
 	// Apply transpose and clamp
-	int note = c.note + c.transpose;
-	if (note < 1) note = 1;
-	if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+	int note = c.note;
+	if (!c.skipTranspose)
+		note += c.transpose;
+	bool isRest = (note == 0);
+	if (!isRest) {
+		if (note < 1)
+			note = 1;
+		if (note >= kTENumPeriods)
+			note = kTENumPeriods - 1;
+	}
 
-	c.basePeriod = getPeriod(note);
+	c.basePeriod = isRest ? 0 : getPeriod(note);
 	c.outputPeriod = c.basePeriod;
+	c.useHardwareEnvelope = false;
 
-	if (c.basePeriod == 0) {
+	if (!isRest && c.basePeriod == 0) {
 		warning("TE-Atari: ch%d note %d has period 0", ch, note);
 		return;
 	}
 
 	// Reset envelope
-	c.envelopePhase = 0;
+	c.envelopeToggle = 0;
+	c.envelopeDone = false;
 	c.volume = c.attackLevel;
+	c.delayCounter = c.delay;
+	c.arpeggioPos = 0;
+	c.modStep = 0;
+	const InstrumentDesc &inst = _instruments[c.instrumentIdx];
+	c.noiseCounter = inst.flags >> 4;
+
+	// Reapply byte-7 mixer mode on every note trigger.
+	c.toneEnabled = true;
+	c.noiseEnabled = false;
+	c.skipTranspose = false;
+	c.freqSweep = false;
+	if (inst.flags & 0x01) {
+		c.noiseEnabled = true;
+		c.noisePeriod = 0x20;
+	} else if (inst.flags & 0x02) {
+		c.noiseEnabled = true;
+		c.toneEnabled = false;
+		c.skipTranspose = true;
+		if (c.noisePeriod == 0)
+			c.noisePeriod = 1;
+	} else if (inst.flags & 0x04) {
+		c.noiseEnabled = true;
+		c.freqSweep = true;
+	}
+
+	if (!isRest && c.modParam != 0 && note + 1 < kTENumPeriods) {
+		int16 periodDelta = ABS((int16)getPeriod(note) - (int16)getPeriod(note + 1));
+		byte depth = c.modParam & 0x0F;
+		while (depth > 0) {
+			periodDelta = (periodDelta >> 1) | 1;
+			depth--;
+		}
+		c.modStep = periodDelta;
+	}
+
+	// TEMUSIC instrument byte 4 bit7 selects YM hardware envelope mode.
+	if (!isRest && (c.envelopeFlags & 0x80)) {
+		c.useHardwareEnvelope = true;
+		c.envelopeDone = true;
+
+		// Envelope style comes from low nibble; period follows note pitch scale.
+		_hwEnvelopeShape = c.envelopeFlags & 0x0F;
+		uint16 envPeriod = (c.basePeriod > 0) ? (uint16)MAX(1, c.basePeriod >> 4) : 1;
+		_hwEnvelopePeriod = envPeriod;
+		_hwEnvelopeDirty = true;
+	}
 
 	debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d NOTE note=%d(+%d) period=%d inst=%d vol=%d",
 		ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.volume);
 
 	// Set up portamento if active
-	if (c.portaUp || c.portaDown) {
-		int prevNote = c.prevNote + c.transpose;
+	if (!isRest && (c.portaUp || c.portaDown)) {
+		int prevNote = c.prevNote;
+		if (!c.skipTranspose)
+			prevNote += c.transpose;
 		if (prevNote < 1) prevNote = 1;
 		if (prevNote >= kTENumPeriods) prevNote = kTENumPeriods - 1;
 
@@ -579,6 +713,28 @@ void EclipseAtariMusicStream::triggerNote(int ch) {
 void EclipseAtariMusicStream::processEffects(int ch) {
 	ChannelState &c = _channels[ch];
 
+	// Noise gate: in TEMUSIC, instrument high nibble is a countdown that
+	// can auto-disable channel noise after N ticks.
+	if (c.noiseEnabled) {
+		if (c.noiseCounter > 0) {
+			c.noiseCounter--;
+			if (c.noiseCounter == 0)
+				c.noiseEnabled = false;
+		}
+	}
+
+	int16 period = c.basePeriod;
+
+	// Instrument flag bit2 enables the original per-tick sweep path (+$64, 12-bit wrap),
+	// and advances the shared noise period source.
+	if (c.freqSweep) {
+		c.basePeriod = (c.basePeriod + 0x64) & 0x0FFF;
+		if (c.basePeriod == 0)
+			c.basePeriod = 1;
+		c.noisePeriod = (c.noisePeriod + 1) & 0x1F;
+		period = c.basePeriod;
+	}
+
 	// Portamento takes priority (active during porta regardless of effectMode)
 	if (c.portaUp) {
 		c.basePeriod -= c.portaStep;
@@ -586,52 +742,83 @@ void EclipseAtariMusicStream::processEffects(int ch) {
 			c.basePeriod = c.portaTarget;
 			c.portaUp = false;
 		}
-		c.outputPeriod = c.basePeriod;
-		return;
-	}
-
-	if (c.portaDown) {
+		period = c.basePeriod;
+	} else if (c.portaDown) {
 		c.basePeriod += c.portaStep;
 		if (c.basePeriod >= c.portaTarget) {
 			c.basePeriod = c.portaTarget;
 			c.portaDown = false;
 		}
-		c.outputPeriod = c.basePeriod;
-		return;
-	}
-
-	if (c.effectMode == 1 && c.vibratoDepth > 0) {
-		// Vibrato: triangle wave pitch oscillation around basePeriod
-		c.vibratoPos += c.vibratoDir;
-		if (c.vibratoPos >= (int8)c.vibratoDepth) {
-			c.vibratoPos = c.vibratoDepth;
-			c.vibratoDir = -1;
-		} else if (c.vibratoPos <= -(int8)c.vibratoDepth) {
-			c.vibratoPos = -(int8)c.vibratoDepth;
-			c.vibratoDir = 1;
-		}
-		// Scale vibrato offset by speed (higher speed = faster oscillation)
-		int16 offset = c.vibratoPos * c.vibratoSpeed;
-		c.outputPeriod = c.basePeriod + offset;
-		if (c.outputPeriod < 1) c.outputPeriod = 1;
-		return;
-	}
-
-	if (c.effectMode == 2 && _arpeggioTableLen > 0) {
-		// Arpeggio: cycle through note offsets from interval table
-		int note = c.note + c.transpose;
-		int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+		period = c.basePeriod;
+	} else if (c.effectMode != 0 && c.arpeggioTableLen > 0) {
+		// Channel-local interval cycling (used by $7D/$7C paths).
+		int note = c.note;
+		if (!c.skipTranspose)
+			note += c.transpose;
+		int offset = c.arpeggioTable[c.arpeggioPos % c.arpeggioTableLen];
 		note += offset;
 		if (note < 1) note = 1;
 		if (note >= kTENumPeriods) note = kTENumPeriods - 1;
-		c.outputPeriod = getPeriod(note);
+		period = getPeriod(note);
 		c.arpeggioPos++;
-		if (c.arpeggioPos >= _arpeggioTableLen)
+		if (c.arpeggioPos >= c.arpeggioTableLen)
 			c.arpeggioPos = 0;
-		return;
+	} else if (c.effect7BActive) {
+		// $7B path: delayed slide from a base note period toward current note period.
+		byte window = c.effect7BParam & 0x0F;
+		if ((int)c.durationCounter + (int)window <= (int)c.duration) {
+			int16 target = c.basePeriod;
+			int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+			int16 delta = ABS(target - c.effect7BPeriod) / steps;
+			if (delta == 0)
+				delta = 1;
+
+			if (c.effect7BPeriod < target) {
+				c.effect7BPeriod += delta;
+				if (c.effect7BPeriod > target)
+					c.effect7BPeriod = target;
+			} else if (c.effect7BPeriod > target) {
+				c.effect7BPeriod -= delta;
+				if (c.effect7BPeriod < target)
+					c.effect7BPeriod = target;
+			}
+			period = c.effect7BPeriod;
+		}
 	}
 
-	c.outputPeriod = c.basePeriod;
+	// TEMUSIC enters byte-5 modulation only while mode is clear.
+	if (c.effectMode == 0 && c.modParam != 0 && c.modStep > 0 && c.modSpan > 0) {
+		// $7A delay applies to this modulation path, not to all effects.
+		if (c.delayCounter > 0) {
+			c.delayCounter--;
+		} else {
+			if (c.modDir < 0) {
+				if (c.modPos > 0) {
+					c.modPos--;
+				} else {
+					c.modDir = 1;
+					if (c.modPos < c.modSpan)
+						c.modPos++;
+				}
+			} else {
+				c.modPos++;
+				if (c.modPos > c.modSpan) {
+					c.modPos = c.modSpan;
+					c.modDir = -1;
+					if (c.modPos > 0)
+						c.modPos--;
+				}
+			}
+
+			int center = c.modSpan >> 1;
+			int offset = (center - c.modPos) * c.modStep;
+			period += offset;
+			if (period < 1)
+				period = 1;
+		}
+	}
+
+	c.outputPeriod = period;
 }
 
 // ---------------------------------------------------------------------------
@@ -641,41 +828,71 @@ void EclipseAtariMusicStream::processEffects(int ch) {
 
 void EclipseAtariMusicStream::processEnvelope(int ch) {
 	ChannelState &c = _channels[ch];
+	// Noise-only instruments may validly run with zero tone period.
+	if (c.outputPeriod == 0 && !c.noiseEnabled)
+		return;
+	if (c.useHardwareEnvelope)
+		return;
 
-	switch (c.envelopePhase) {
-	case 0: // Attack — volume set to attackLevel in triggerNote
-		if (c.attackRate == 0) {
-			c.volume = c.decayTarget;
-			c.envelopePhase = 2;
-		} else {
-			c.envelopePhase = 1;
-		}
-		break;
+	byte env = c.envelopeFlags;
 
-	case 1: // Decay — fade toward target
-		if (c.volume < c.decayTarget) {
-			c.volume++;
-		} else if (c.volume > c.decayTarget) {
-			c.volume--;
+	// Instrument env byte bit7: oscillating level between attackLevel and target.
+	if (env & 0x80) {
+		byte step = env & 0x0F;
+		if (c.envelopeToggle == 0) {
+			if (c.volume == c.decayTarget) {
+				c.envelopeToggle = 1;
+				if (c.volume == c.attackLevel) {
+					c.envelopeToggle = 0;
+				} else {
+					c.volume = (byte)(c.volume + step);
+				}
+			} else {
+				c.volume = (byte)(c.volume - step);
+				c.envelopeToggle = 0;
+			}
 		} else {
-			c.envelopePhase = 2;
+			if (c.volume == c.attackLevel) {
+				c.envelopeToggle = 0;
+			} else {
+				c.volume = (byte)(c.volume + step);
+			}
 		}
-		break;
+	} else {
+		c.envelopeToggle = 0;
+		// Original routine skips non-bit7 envelope work only when duration counter is exactly zero.
+		if (c.durationCounter == 0)
+			return;
+
+		if (!c.envelopeDone) {
+			if (env == 0x00) {
+				// Special case: immediate jump to target, mark envelope done.
+				c.envelopeDone = true;
+				c.volume = c.decayTarget;
+				if (c.volume > 63)
+					c.volume = 63;
+				return;
+			}
 
-	case 2: // Sustain — hold
-		break;
+			if (env != 0xFF) {
+				byte div = env & 0x7F;
+				byte triggerPoint = (div != 0) ? (c.duration / div) : 0;
 
-	case 3: // Release
-		if (c.releaseRate > 0) {
-			if (c.volume > c.releaseRate) {
-				c.volume -= c.releaseRate;
-			} else {
-				c.volume = 0;
+				if (c.durationCounter == triggerPoint) {
+					c.envelopeDone = true;
+				} else if (c.volume != c.decayTarget) {
+					c.volume = (byte)(c.volume + c.attackRate);
+				}
 			}
-		} else {
-			c.volume = 0;
 		}
-		break;
+
+		if (c.envelopeDone) {
+			byte next = (byte)(c.volume - c.releaseRate);
+			if ((int8)next < 0)
+				c.volume = 0;
+			else
+				c.volume = next;
+		}
 	}
 
 	if (c.volume > 63)
@@ -686,18 +903,9 @@ void EclipseAtariMusicStream::processEnvelope(int ch) {
 // Arpeggio table builder
 // ---------------------------------------------------------------------------
 
-void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
-	_arpeggioTableLen = 0;
-	_arpeggioTable[_arpeggioTableLen++] = 0; // Base note
-
-	for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
-		if (mask & (1 << i)) {
-			_arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
-		}
-	}
-
-	if (_arpeggioTableLen <= 1)
-		_arpeggioTableLen = 0;
+void EclipseAtariMusicStream::buildArpeggioTable(ChannelState &c, byte mask) {
+	c.arpeggioTableLen = WBCommon::buildArpeggioTable(_arpeggioIntervals, mask, c.arpeggioTable, 16, false);
+	c.arpeggioPos = 0;
 }
 
 // ---------------------------------------------------------------------------
@@ -706,37 +914,56 @@ void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
 
 void EclipseAtariMusicStream::writeYMRegisters() {
 	byte mixer = 0x3F; // Start with all disabled (bits 0-2=tone, bits 3-5=noise)
+	if (_hwEnvelopeDirty) {
+		setReg(11, _hwEnvelopePeriod & 0xFF);
+		setReg(12, (_hwEnvelopePeriod >> 8) & 0xFF);
+		setReg(13, _hwEnvelopeShape & 0x0F);
+		_hwEnvelopeDirty = false;
+	}
 
-	for (int ch = 0; ch < kTENumChannels; ch++) {
+	// TEMUSIC channel loop runs 2 -> 0; keep that order so global noise register ownership matches.
+	for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
 		ChannelState &c = _channels[ch];
 
-		if (!c.active || c.outputPeriod == 0 || c.volume == 0) {
+		bool hasTone = c.toneEnabled && (c.outputPeriod > 0);
+		bool hasNoise = c.noiseEnabled;
+		bool usesHwEnvelope = c.useHardwareEnvelope;
+		if (!c.active || (!usesHwEnvelope && c.volume == 0) || (!hasTone && !hasNoise)) {
 			// Channel silent
 			setReg(8 + ch, 0); // Volume = 0
 			continue;
 		}
 
-		// Enable tone for this channel (bits 0-2)
-		mixer &= ~(1 << ch);
+		// Enable tone for this channel (bits 0-2), unless in noise-only mode.
+		if (hasTone)
+			mixer &= ~(1 << ch);
 
 		// Enable noise for this channel if instrument has noise flag (bits 3-5)
-		if (c.noiseEnabled) {
+		if (hasNoise) {
 			mixer &= ~(1 << (ch + 3));
-			// Set noise period from note (lower period = higher pitched noise)
-			byte noisePeriod = (c.outputPeriod >> 4) & 0x1F;
-			if (noisePeriod == 0) noisePeriod = 1;
+			byte noisePeriod = c.noisePeriod;
+			if (noisePeriod == 0 && c.outputPeriod > 0)
+				noisePeriod = (c.outputPeriod >> 4) & 0x1F;
+			if (noisePeriod == 0)
+				noisePeriod = 1;
 			setReg(6, noisePeriod);
 		}
 
-		// Set tone period (2 registers per channel)
-		uint16 period = (uint16)c.outputPeriod;
-		setReg(ch * 2, period & 0xFF);       // Fine tune
-		setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+		// Set tone period (2 registers per channel) when tone path is active.
+		if (hasTone) {
+			uint16 period = (uint16)c.outputPeriod;
+			setReg(ch * 2, period & 0xFF);       // Fine tune
+			setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+		}
 
 		// Set volume (internal 0-63 → YM 0-15)
-		byte ymVol = c.volume >> 2;
-		if (ymVol > 15) ymVol = 15;
-		setReg(8 + ch, ymVol);
+		if (usesHwEnvelope) {
+			setReg(8 + ch, 0x10); // Enable YM hardware envelope on this channel
+		} else {
+			byte ymVol = c.volume >> 2;
+			if (ymVol > 15) ymVol = 15;
+			setReg(8 + ch, ymVol);
+		}
 	}
 
 	setReg(7, mixer);
@@ -750,35 +977,41 @@ void EclipseAtariMusicStream::tickUpdate() {
 	if (!_musicActive)
 		return;
 
-	// Tick speed gating: the 68000 code uses subq.b #1; bpl; reset which
-	// gives a period of (tickSpeed + 1) ticks before the sequencer advances.
-	// Counter counts: tickSpeed → tickSpeed-1 → ... → 0 → -1(wrap&reset)
-	_tickCounter++;
-	bool sequencerTick = false;
-	if (_tickCounter > _tickSpeed) {
-		_tickCounter = 0;
-		sequencerTick = true;
-	}
+	// Sequencer step occurs when tick counter is zero, then the counter advances.
+	// This matches the original TEMUSIC update loop and wb.cpp behavior.
+	bool sequencerTick = (_tickCounter == 0);
 
 	if (sequencerTick) {
-		for (int ch = 0; ch < kTENumChannels; ch++) {
+		for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
 			if (!_channels[ch].active)
 				continue;
 
-			if (_channels[ch].durationCounter > 0) {
-				_channels[ch].durationCounter--;
-			}
+			_channels[ch].durationCounter--;
 
-			if (_channels[ch].durationCounter == 0) {
-				if (_channels[ch].envelopePhase < 3)
-					_channels[ch].envelopePhase = 3;
+			if (_channels[ch].durationCounter < 0) {
+				// Original step boundary reset: clear transient note/effect flags
+				// before parsing the next command stream, except mode 2 persistence.
+				ChannelState &c = _channels[ch];
+				if (c.effectMode != 2) {
+					c.effectMode = 0;
+					c.arpeggioMask = 0;
+					c.arpeggioPos = 0;
+					c.arpeggioTableLen = 0;
+				}
+				c.effect7BActive = false;
+				c.portaUp = false;
+				c.portaDown = false;
+				c.noiseEnabled = false;
+				c.freqSweep = false;
+				c.envelopeDone = false;
+				c.useHardwareEnvelope = false;
 				readPatternCommands(ch);
 			}
 		}
 	}
 
 	// Every tick: process effects and envelope
-	for (int ch = 0; ch < kTENumChannels; ch++) {
+	for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
 		if (!_channels[ch].active)
 			continue;
 
@@ -786,6 +1019,10 @@ void EclipseAtariMusicStream::tickUpdate() {
 		processEnvelope(ch);
 	}
 
+	_tickCounter++;
+	if (_tickCounter >= _tickSpeed)
+		_tickCounter = 0;
+
 	writeYMRegisters();
 }
 
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
index e64e90cc922..dfa2ce8d429 100644
--- a/engines/freescape/wb.cpp
+++ b/engines/freescape/wb.cpp
@@ -54,6 +54,39 @@
 
 namespace Freescape {
 
+namespace WBCommon {
+
+int8 decodeOrderTranspose(byte cmd) {
+	return (int8)((cmd + 0x20) & 0xFF);
+}
+
+byte decodeTickSpeed(byte cmd) {
+	byte speed = cmd & 0x0F;
+	return speed == 0 ? 1 : speed;
+}
+
+byte decodeDuration(byte cmd) {
+	byte duration = cmd & 0x3F;
+	return duration == 0 ? 1 : duration;
+}
+
+byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte maxLen, bool includeBase) {
+	byte len = 0;
+	if (includeBase && maxLen > 0)
+		outTable[len++] = 0;
+
+	for (int i = 0; i < 8 && len < maxLen; i++) {
+		if (mask & (1 << i))
+			outTable[len++] = intervals[i];
+	}
+
+	if (includeBase && len <= 1)
+		return 0;
+	return len;
+}
+
+} // End of namespace WBCommon
+
 // TEXT-relative offsets for data tables within HDSMUSIC.AM
 // All addresses verified against disassembly of the 68000 code.
 static const uint32 kPeriodTableOffset    = 0x0AAE; // 48 x uint16 BE (note 0=silence, 1-47=C-1..B-3)
@@ -154,6 +187,13 @@ private:
 		int16 portaTarget;
 		byte arpeggioMask;
 		byte arpeggioPos;
+		byte arpeggioTable[16];
+		byte arpeggioTableLen;
+		byte effect7BBaseNote;
+		byte effect7BRate;
+		byte effect7BCounter;
+		bool effect7BUseBase;
+		bool effect7BActive;
 		byte vibratoPos;
 
 		// Period
@@ -163,6 +203,7 @@ private:
 		// Delay
 		byte delay;
 		byte delayCounter;
+		bool pendingNoteOn;
 
 		bool active;
 	};
@@ -173,10 +214,7 @@ private:
 	bool _musicActive;
 	byte _tickSpeed;    // Ticks between sequencer steps
 	byte _tickCounter;  // Current tick count (counts up to _tickSpeed)
-
-	// Arpeggio working table (built by buildArpeggioTable)
-	byte _arpeggioTable[16];
-	int _arpeggioTableLen;
+	int _pendingSongCommand; // -1=no pending, else mailbox command from $DD
 
 	// --- Methods ---
 
@@ -188,7 +226,7 @@ private:
 	void triggerNote(int ch);
 	void processEffects(int ch);
 	void processEnvelope(int ch);
-	void buildArpeggioTable(byte mask);
+	void buildArpeggioTable(int ch, byte mask);
 	uint16 getPeriod(int note) const;
 
 	byte readDataByte(uint32 offset) const {
@@ -219,7 +257,7 @@ WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
 	: Paula(stereo, rate, rate / 50), // 50 Hz PAL VBI interrupt rate
 	  _data(data), _dataSize(dataSize),
 	  _musicActive(false), _tickSpeed(6), _tickCounter(0),
-	  _arpeggioTableLen(0), _numPatterns(0), _firstSampleOffset(0) {
+	  _pendingSongCommand(-1), _numPatterns(0), _firstSampleOffset(0) {
 
 	memset(_periods, 0, sizeof(_periods));
 	memset(_instruments, 0, sizeof(_instruments));
@@ -229,7 +267,6 @@ WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
 	memset(_patternPtrs, 0, sizeof(_patternPtrs));
 	memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
 	memset(_channels, 0, sizeof(_channels));
-	memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
 
 	// Standard Amiga panning: channels 0,3 left — channels 1,2 right
 	setChannelPanning(0, PANNING_LEFT);
@@ -366,7 +403,7 @@ void WallyBebenStream::startSong(int songNum) {
 	int songIdx = songNum - 1;
 	_tickSpeed = 6;
 	_tickCounter = 0;
-	_arpeggioTableLen = 0;
+	_pendingSongCommand = -1;
 
 	// Silence all Paula channels
 	for (int ch = 0; ch < NUM_VOICES; ch++) {
@@ -374,7 +411,7 @@ void WallyBebenStream::startSong(int songNum) {
 	}
 
 	// Initialize each channel from the song table
-	for (int ch = 0; ch < 4; ch++) {
+	for (int ch = 3; ch >= 0; ch--) {
 		initChannel(ch);
 		_channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
 		_channels[ch].orderListPos = 0;
@@ -388,7 +425,7 @@ void WallyBebenStream::startSong(int songNum) {
 	startPaula();
 
 	debug(3, "WB: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
-	for (int ch = 0; ch < 4; ch++) {
+	for (int ch = 3; ch >= 0; ch--) {
 		debug(3, "WB: ch%d orderList=$%X pattern=$%X",
 			ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
 	}
@@ -405,7 +442,7 @@ void WallyBebenStream::initChannel(int ch) {
 	// $C0 envelope command still produce sound (Env 0 has all zeros = silence)
 	c.attackLevel = 64;
 	c.decayTarget = 64;
-	c.sustainLevel = 0; // Instant transition (no fade)
+	c.sustainLevel = 64;
 	c.releaseRate = 0;
 }
 
@@ -434,7 +471,7 @@ void WallyBebenStream::readOrderList(int ch) {
 		if (cmd > 0xC0) {
 			// Transpose command: transpose = (cmd + 0x20) & 0xFF
 			// Stored as signed offset
-			c.transpose = (int8)((cmd + 0x20) & 0xFF);
+			c.transpose = WBCommon::decodeOrderTranspose(cmd);
 			continue;
 		}
 
@@ -462,7 +499,7 @@ void WallyBebenStream::readOrderList(int ch) {
 // Pattern format: $FF=end-pattern, $FE=end-song, $F0-$FD=speed,
 //   $E0-$EF=instrument, $C0-$DF=envelope, $80-$BF=duration,
 //   $7F/$7E=portamento, $7D/$7C=vibrato, $7B=arpeggio,
-//   $7A=delay, $00-$79=note (0=rest, 1-47=C-1..B-3)
+//   $7A=delay, $00-$5F=note (0=rest, 1-47=C-1..B-3)
 // ---------------------------------------------------------------------------
 
 void WallyBebenStream::readPatternCommands(int ch) {
@@ -488,16 +525,16 @@ void WallyBebenStream::readPatternCommands(int ch) {
 		}
 
 		if (cmd == 0xDD) {
-			// Song change — read parameter byte, ignore for now
+			// Song change command: hand off through mailbox-equivalent path.
+			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
-			continue;
+			_pendingSongCommand = param;
+			return;
 		}
 
 		if (cmd >= 0xF0) {
 			// Set speed: low nibble
-			_tickSpeed = cmd & 0x0F;
-			if (_tickSpeed == 0)
-				_tickSpeed = 1;
+			_tickSpeed = WBCommon::decodeTickSpeed(cmd);
 			continue;
 		}
 
@@ -533,16 +570,14 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			if (env.arpeggioMask != 0) {
 				c.effectMode = 2;
 				c.arpeggioMask = env.arpeggioMask;
-				buildArpeggioTable(env.arpeggioMask);
+				buildArpeggioTable(ch, env.arpeggioMask);
 			}
 			continue;
 		}
 
 		if (cmd >= 0x80) {
 			// Set duration: low 6 bits (0-63)
-			c.duration = cmd & 0x3F;
-			if (c.duration == 0)
-				c.duration = 1;
+			c.duration = WBCommon::decodeDuration(cmd);
 			continue;
 		}
 
@@ -551,6 +586,7 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			c.portaUp = true;
 			c.portaDown = false;
 			c.effectMode = 1;
+			c.effect7BActive = false;
 			continue;
 		}
 
@@ -559,6 +595,7 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			c.portaDown = true;
 			c.portaUp = false;
 			c.effectMode = 1;
+			c.effect7BActive = false;
 			continue;
 		}
 
@@ -567,8 +604,9 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
 			c.effectMode = 1;
+			c.effect7BActive = false;
 			c.arpeggioMask = param;
-			buildArpeggioTable(param);
+			buildArpeggioTable(ch, param);
 			continue;
 		}
 
@@ -577,13 +615,15 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			byte param = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
 			c.effectMode = 2;
+			c.effect7BActive = false;
 			c.arpeggioMask = param;
-			buildArpeggioTable(param);
+			buildArpeggioTable(ch, param);
 			continue;
 		}
 
 		if (cmd == 0x7B) {
-			// Arpeggio
+			// Dedicated arpeggio/slide mode:
+			//  xx + transpose -> base note, yy -> rate.
 			byte base = readDataByte(c.patternOffset + c.patternPos);
 			c.patternPos++;
 			byte rate = readDataByte(c.patternOffset + c.patternPos);
@@ -591,10 +631,18 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			c.effectMode = 1;
 			c.arpeggioMask = 0;
 			c.arpeggioPos = 0;
-			// Store arpeggio base note with transpose applied
-			_arpeggioTable[0] = base;
-			_arpeggioTable[1] = rate;
-			_arpeggioTableLen = 2;
+			c.arpeggioTableLen = 0;
+			c.effect7BActive = true;
+			c.effect7BCounter = 0;
+			c.effect7BUseBase = true;
+
+			int baseNote = base + c.transpose;
+			if (baseNote < 1)
+				baseNote = 1;
+			if (baseNote > 47)
+				baseNote = 47;
+			c.effect7BBaseNote = (byte)baseNote;
+			c.effect7BRate = rate;
 			continue;
 		}
 
@@ -606,11 +654,24 @@ void WallyBebenStream::readPatternCommands(int ch) {
 			continue;
 		}
 
-		// Note value (0x00-0x79)
+		if (cmd > 0x5F) {
+			// Original command parser accepts notes in range 0x00-0x5F.
+			warning("WallyBeben: ch%d unknown pattern command $%02X", ch, cmd);
+			continue;
+		}
+
+		// Note value (0x00-0x5F)
 		c.prevNote = c.note;
 		c.note = cmd;
-		c.durationCounter = c.duration;
-		triggerNote(ch);
+		if (c.note != 0 && c.delay > 0) {
+			// Delay note-on by c.delay frames.
+			c.delayCounter = c.delay;
+			c.pendingNoteOn = true;
+		} else {
+			c.durationCounter = c.duration;
+			c.pendingNoteOn = false;
+			triggerNote(ch);
+		}
 		return; // One note per sequencer step
 	}
 
@@ -624,12 +685,14 @@ void WallyBebenStream::readPatternCommands(int ch) {
 
 void WallyBebenStream::triggerNote(int ch) {
 	ChannelState &c = _channels[ch];
+	c.pendingNoteOn = false;
 
 	if (c.note == 0) {
 		// Rest — silence channel
 		c.outputPeriod = 0;
 		c.volume = 0;
 		c.envelopePhase = 3;
+		c.effect7BActive = false;
 		setChannelVolume(ch, 0);
 		return;
 	}
@@ -643,6 +706,11 @@ void WallyBebenStream::triggerNote(int ch) {
 	c.basePeriod = getPeriod(note);
 	c.outputPeriod = c.basePeriod;
 
+	if (c.effect7BActive) {
+		c.effect7BCounter = 0;
+		c.effect7BUseBase = true;
+	}
+
 	if (c.basePeriod == 0) {
 		warning("WallyBeben: ch%d note %d (raw %d + transpose %d) has period 0",
 			ch, note, c.note, c.transpose);
@@ -681,11 +749,12 @@ void WallyBebenStream::triggerNote(int ch) {
 
 	// setChannelData calls enableChannel internally — do NOT call enableChannel again!
 	if (inst.loopFlag && loopLen > 2 && loopOff + loopLen <= totalLen) {
-		// Looped sample: initial play of full sample, then loop region
+		uint32 initialLen = loopOff + loopLen;
+		// Looped sample: initial play through loop end, then loop region
 		setChannelData(ch,
 			sampleData,                // initial data pointer
 			sampleData + loopOff,      // loop start pointer
-			totalLen,                  // initial length in bytes
+			initialLen,                // initial length in bytes
 			loopLen);                  // loop length in bytes
 	} else {
 		// One-shot: play once, then 1-byte silence loop
@@ -731,6 +800,9 @@ void WallyBebenStream::triggerNote(int ch) {
 void WallyBebenStream::processEffects(int ch) {
 	ChannelState &c = _channels[ch];
 
+	if (c.pendingNoteOn)
+		return;
+
 	if (c.effectMode == 0) {
 		c.outputPeriod = c.basePeriod;
 		return;
@@ -761,16 +833,37 @@ void WallyBebenStream::processEffects(int ch) {
 		return;
 	}
 
+	// Dedicated $7B effect: cycle between base note and current note.
+	if (c.effect7BActive) {
+		int rate = c.effect7BRate;
+		if (rate <= 0)
+			rate = 1;
+
+		c.effect7BCounter++;
+		if (c.effect7BCounter >= rate) {
+			c.effect7BCounter = 0;
+			c.effect7BUseBase = !c.effect7BUseBase;
+		}
+
+		int note = c.effect7BUseBase ? c.effect7BBaseNote : (c.note + c.transpose);
+		if (note < 1)
+			note = 1;
+		if (note > 47)
+			note = 47;
+		c.outputPeriod = getPeriod(note);
+		return;
+	}
+
 	// Arpeggio effect (effectMode 1 or 2)
-	if (_arpeggioTableLen > 0) {
+	if (c.arpeggioTableLen > 0) {
 		int note = c.note + c.transpose;
-		int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+		int offset = c.arpeggioTable[c.arpeggioPos % c.arpeggioTableLen];
 		note += offset;
 		if (note < 1) note = 1;
 		if (note > 47) note = 47;
 		c.outputPeriod = getPeriod(note);
 		c.arpeggioPos++;
-		if (c.arpeggioPos >= _arpeggioTableLen)
+		if (c.arpeggioPos >= c.arpeggioTableLen)
 			c.arpeggioPos = 0;
 	} else {
 		c.outputPeriod = c.basePeriod;
@@ -784,7 +877,7 @@ void WallyBebenStream::processEffects(int ch) {
 // Envelope table bytes (per entry, 8 bytes at TEXT+$D0A):
 //   byte 0 (attackLevel):  initial volume on note-on
 //   byte 1 (decayTarget):  target volume to fade toward and hold
-//   byte 2 (sustainLevel): fade rate (0 = instant jump to target, >0 = per-tick)
+//   byte 2 (sustainLevel): sustain volume while note is active
 //   byte 3 (releaseRate):  volume decrease per tick on note-off
 //   byte 4 (modDepth):     modulation depth
 //   byte 5 (vibratoWave):  vibrato waveform selector
@@ -796,28 +889,22 @@ void WallyBebenStream::processEnvelope(int ch) {
 	ChannelState &c = _channels[ch];
 
 	switch (c.envelopePhase) {
-	case 0: // Attack — volume already set to attackLevel in triggerNote
-		if (c.sustainLevel == 0) {
-			// Instant transition to hold level
-			c.volume = c.decayTarget;
-			c.envelopePhase = 2; // Skip fade, go straight to hold
-		} else {
-			// Begin gradual fade toward target
-			c.envelopePhase = 1;
-		}
+	case 0: // Attack — start from attack level, then enter decay.
+		c.volume = MIN(c.attackLevel, (byte)64);
+		c.envelopePhase = 1;
 		break;
 
-	case 1: // Fade — move volume toward hold level (decayTarget)
-		if (c.volume < c.decayTarget) {
-			c.volume++;
-		} else if (c.volume > c.decayTarget) {
+	case 1: // Decay — decrease toward decay target.
+		if (c.volume > c.decayTarget) {
 			c.volume--;
 		} else {
-			c.envelopePhase = 2; // Reached target
+			c.volume = c.sustainLevel;
+			c.envelopePhase = 2;
 		}
 		break;
 
-	case 2: // Hold — maintain volume until note-off
+	case 2: // Sustain — hold sustain level until note-off.
+		c.volume = c.sustainLevel;
 		break;
 
 	case 3: // Release — decrease volume on note-off
@@ -842,20 +929,10 @@ void WallyBebenStream::processEnvelope(int ch) {
 // Arpeggio table builder — unpacks a bitmask into interval offsets
 // ---------------------------------------------------------------------------
 
-void WallyBebenStream::buildArpeggioTable(byte mask) {
-	_arpeggioTableLen = 0;
-
-	// Always include 0 (base note) as first entry
-	_arpeggioTable[_arpeggioTableLen++] = 0;
-
-	for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
-		if (mask & (1 << i)) {
-			_arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
-		}
-	}
-
-	if (_arpeggioTableLen <= 1)
-		_arpeggioTableLen = 0; // No arpeggio if only base note
+void WallyBebenStream::buildArpeggioTable(int ch, byte mask) {
+	ChannelState &c = _channels[ch];
+	c.arpeggioTableLen = WBCommon::buildArpeggioTable(_arpeggioIntervals, mask, c.arpeggioTable, 16, true);
+	c.arpeggioPos = 0;
 }
 
 // ---------------------------------------------------------------------------
@@ -879,9 +956,11 @@ void WallyBebenStream::interrupt() {
 
 	// Sequencer step: when tick counter reaches 0
 	if (_tickCounter == 0) {
-		for (int ch = 0; ch < 4; ch++) {
+		for (int ch = 3; ch >= 0; ch--) {
 			if (!_channels[ch].active)
 				continue;
+			if (_channels[ch].pendingNoteOn)
+				continue;
 
 			if (_channels[ch].durationCounter > 0) {
 				_channels[ch].durationCounter--;
@@ -898,8 +977,41 @@ void WallyBebenStream::interrupt() {
 		}
 	}
 
+	// Apply pending song command ($DD xx) after parser step.
+	if (_pendingSongCommand != -1) {
+		byte command = (byte)_pendingSongCommand;
+		_pendingSongCommand = -1;
+
+		if (command == 0) {
+			_musicActive = false;
+			for (int ch = 0; ch < NUM_VOICES; ch++)
+				clearVoice(ch);
+			return;
+		}
+		if (command >= 1 && command <= 2) {
+			startSong(command);
+			return;
+		}
+	}
+
+	// Delay-before-note handling ($7A): countdown in frames, then trigger note-on.
+	for (int ch = 3; ch >= 0; ch--) {
+		ChannelState &c = _channels[ch];
+		if (!c.active || !c.pendingNoteOn)
+			continue;
+
+		if (c.delayCounter > 0)
+			c.delayCounter--;
+
+		if (c.delayCounter == 0) {
+			c.pendingNoteOn = false;
+			c.durationCounter = c.duration;
+			triggerNote(ch);
+		}
+	}
+
 	// Every frame: process effects and envelope, update Paula
-	for (int ch = 0; ch < 4; ch++) {
+	for (int ch = 3; ch >= 0; ch--) {
 		if (!_channels[ch].active)
 			continue;
 
diff --git a/engines/freescape/wb.h b/engines/freescape/wb.h
index 1dcc22be052..f39ee5c6be6 100644
--- a/engines/freescape/wb.h
+++ b/engines/freescape/wb.h
@@ -27,6 +27,38 @@
 
 namespace Freescape {
 
+namespace WBCommon {
+
+/**
+ * Decode order-list transpose command ($C1-$FE).
+ * Formula used by Wally Beben engines: (cmd + $20) & $FF.
+ */
+int8 decodeOrderTranspose(byte cmd);
+
+/**
+ * Decode speed command ($F0-$FD): low nibble, with 0 coerced to 1.
+ */
+byte decodeTickSpeed(byte cmd);
+
+/**
+ * Decode duration command ($80-$BF): low 6 bits, with 0 coerced to 1.
+ */
+byte decodeDuration(byte cmd);
+
+/**
+ * Expand an arpeggio bitmask using the 8-byte interval lookup table.
+ *
+ * @param intervals   Source interval table (8 entries)
+ * @param mask        Bitmask selecting entries from intervals
+ * @param outTable    Output sequence buffer
+ * @param maxLen      Capacity of output buffer
+ * @param includeBase Whether to prepend 0 (base note)
+ * @return Number of valid entries in outTable
+ */
+byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte maxLen, bool includeBase);
+
+} // End of namespace WBCommon
+
 /**
  * Create a music stream for the Wally Beben custom music engine
  * used in the Amiga version of Dark Side.


Commit: c229bdfea8296e6d2de1abf5dd195cc28d9b123c
    https://github.com/scummvm/scummvm/commit/c229bdfea8296e6d2de1abf5dd195cc28d9b123c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: added sound playback for driller (c64)

Changed paths:
  A engines/freescape/games/driller/c64.sfx.cpp
  A engines/freescape/games/driller/c64.sfx.h
    engines/freescape/freescape.h
    engines/freescape/games/driller/c64.cpp
    engines/freescape/games/driller/driller.cpp
    engines/freescape/games/driller/driller.h
    engines/freescape/module.mk
    engines/freescape/sound/common.cpp


diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 0cd8bdeb912..4089b64173d 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -492,6 +492,7 @@ public:
 	void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
 
 	void playSoundCPC(int index, Audio::SoundHandle &handle);
+	virtual void playSoundC64(int index);
 	virtual void playSoundFx(int index, bool sync);
 	virtual void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number);
 	Common::HashMap<uint16, soundFx *> _soundsFx;
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index cca4fd71a0a..a12e2733b4c 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -158,7 +158,35 @@ void DrillerEngine::loadAssetsC64FullGame() {
 	} else
 		error("Unknown C64 release");
 
-	_playerSid = new DrillerSIDPlayer();
+	// TODO: Re-enable music player once SFX coexistence is resolved.
+	// The SID emulator enforces a singleton, so only one instance can exist.
+	// _playerSid = new DrillerSIDPlayer();
+	_playerC64Sfx = new DrillerC64SFXPlayer();
+
+	// C64 SFX index mapping
+	// Based on analysis of the C64 binary SFX routines
+	_soundIndexShoot = 2;           // SFX #2 - Dual-voice noise sweep (explosion/drilling)
+	_soundIndexCollide = 3;         // SFX #3 - Noise pitch slide (collision)
+	_soundIndexStepUp = 5;          // SFX #5 - Pulse slide up
+	_soundIndexStepDown = 4;        // SFX #4 - Pulse slide down
+	_soundIndexFall = 11;           // SFX #11 - Triangle slide down fast (falling)
+	_soundIndexStart = 8;           // SFX #8 - Triangle slide up (teleporter/energy)
+	_soundIndexMenu = 6;            // SFX #6 - Triangle blip
+	_soundIndexAreaChange = 8;      // SFX #8 - Triangle slide up
+	_soundIndexHit = 7;             // SFX #7 - Dual noise burst
+	_soundIndexNoShield = 9;        // SFX #9 - Dual slide noise (damage)
+	_soundIndexNoEnergy = 9;        // SFX #9 - Dual slide noise (damage)
+	_soundIndexFallen = 18;         // SFX #18 - Major explosion
+	_soundIndexTimeout = 10;        // SFX #10 - Programmed noise bursts (alarm)
+	_soundIndexForceEndGame = 18;   // SFX #18 - Major explosion
+	_soundIndexCrushed = 18;        // SFX #18 - Major explosion
+	_soundIndexMissionComplete = 14; // SFX #14 - 3-step chord
+}
+
+void DrillerEngine::playSoundC64(int index) {
+	debugC(1, kFreescapeDebugMedia, "Playing C64 SFX %d", index);
+	if (_playerC64Sfx)
+		_playerC64Sfx->playSfx(index);
 }
 
 
diff --git a/engines/freescape/games/driller/c64.sfx.cpp b/engines/freescape/games/driller/c64.sfx.cpp
new file mode 100644
index 00000000000..2a6cf712a0c
--- /dev/null
+++ b/engines/freescape/games/driller/c64.sfx.cpp
@@ -0,0 +1,687 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/games/driller/c64.sfx.h"
+#include "freescape/freescape.h"
+
+#include "common/debug.h"
+#include "common/textconsole.h"
+
+namespace Freescape {
+
+DrillerC64SFXPlayer::DrillerC64SFXPlayer()
+	: _sid(nullptr),
+	  _v1Counter(0xFF), _v1FreqLo(0), _v1FreqHi(0),
+	  _v1DeltaLo(0), _v1DeltaHi(0), _v1TickCtr(0), _v1TickReload(0),
+	  _v3Counter(0xFF), _v3FreqLo(0), _v3FreqHi(0),
+	  _v3DeltaLo(0), _v3DeltaHi(0),
+	  _noiseTimer(0), _noiseCounter(0), _noiseReload(0), _noiseDec(0),
+	  _sfxPhase(0), _sfxPhaseTimer(0), _sfxActiveIndex(0) {
+	initSID();
+}
+
+DrillerC64SFXPlayer::~DrillerC64SFXPlayer() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+}
+
+void DrillerC64SFXPlayer::initSID() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+
+	_sid = SID::Config::create(SID::Config::kSidPAL);
+	if (!_sid || !_sid->init())
+		error("Failed to initialise SID emulator for SFX");
+
+	for (int i = 0; i < 0x18; i++)
+		sidWrite(i, 0);
+	sidWrite(kSIDVolume, 0x0F);
+
+	_sid->start(new Common::Functor0Mem<void, DrillerC64SFXPlayer>(this, &DrillerC64SFXPlayer::onTimer), 50);
+}
+
+void DrillerC64SFXPlayer::sidWrite(int reg, uint8 data) {
+	if (_sid) {
+		debugC(4, kFreescapeDebugMedia, "SFX SID Write: Reg $%02X = $%02X", reg, data);
+		_sid->writeReg(reg, data);
+	}
+}
+
+void DrillerC64SFXPlayer::onTimer() {
+	sfxTick();
+}
+
+bool DrillerC64SFXPlayer::isSfxActive() const {
+	return (_v1Counter != 0xFF) || (_v3Counter != 0xFF) || (_noiseTimer != 0) || (_sfxPhase != 0);
+}
+
+void DrillerC64SFXPlayer::stopAllSfx() {
+	_v1Counter = 0xFF;
+	_v3Counter = 0xFF;
+	_noiseTimer = 0;
+	_sfxPhase = 0;
+	silenceAllVoices();
+}
+
+void DrillerC64SFXPlayer::silenceAllVoices() {
+	for (int i = 0; i <= 0x17; i++)
+		sidWrite(i, 0);
+	sidWrite(kSIDVolume, 0x0F);
+}
+
+// --- Noise burst subroutine ($C818) ---
+void DrillerC64SFXPlayer::noiseBurst(uint8 param) {
+	sidWrite(kSIDV1FreqLo, 0);
+	sidWrite(kSIDV1FreqHi, param);
+	sidWrite(kSIDV1AD, 0x0A);
+	sidWrite(kSIDV1SR, 0x09);
+	sidWrite(kSIDV1Ctrl, 0x81); // Noise + gate
+}
+
+// --- Tick handler ($C96F) - called every frame (50Hz) ---
+
+void DrillerC64SFXPlayer::sfxTick() {
+	tickNoiseBurst();
+	tickVoice1Slide();
+	tickVoice3Slide();
+	tickPhase();
+}
+
+void DrillerC64SFXPlayer::tickNoiseBurst() {
+	if (_noiseTimer == 0)
+		return;
+
+	_noiseCounter--;
+	if (_noiseCounter == 0) {
+		noiseBurst(_noiseTimer);
+		_noiseCounter = _noiseReload;
+		_noiseReload--;
+		_noiseDec--;
+		if (_noiseDec == 0) {
+			_noiseTimer = 0;
+		}
+	}
+}
+
+// Tick handler for Voice 1 pitch slide.
+// Matches the original 6502 at $C97F:
+//   - Check counter==0 BEFORE decrementing (expired last frame → mark 0xFF)
+//   - On counter expiry: do NOT silence - ADSR release handles decay
+//   - Write freq to SID THEN decrement counter
+void DrillerC64SFXPlayer::tickVoice1Slide() {
+	if (_v1Counter == 0xFF)
+		return;
+
+	// Counter expired last frame - silence V1.
+	// Matches the $C950 pattern: write 0 to ctrl (no waveform, no gate).
+	// The game is silent between SFX, so the voice must be stopped.
+	if (_v1Counter == 0) {
+		_v1Counter = 0xFF;
+		sidWrite(kSIDV1Ctrl, 0);
+		return;
+	}
+
+	// Tick prescaler: skip frames until prescaler expires
+	if (_v1TickCtr != 0) {
+		_v1TickCtr--;
+		return;
+	}
+	_v1TickCtr = _v1TickReload;
+
+	// 16-bit frequency addition (CLC; ADC)
+	uint16 freq = (_v1FreqHi << 8) | _v1FreqLo;
+	uint16 delta = (_v1DeltaHi << 8) | _v1DeltaLo;
+	freq += delta;
+	_v1FreqLo = freq & 0xFF;
+	_v1FreqHi = (freq >> 8) & 0xFF;
+
+	// Write to SID FIRST
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// THEN decrement counter
+	_v1Counter--;
+}
+
+// Tick handler for Voice 3 pitch slide (no tick prescaler).
+void DrillerC64SFXPlayer::tickVoice3Slide() {
+	if (_v3Counter == 0xFF)
+		return;
+
+	if (_v3Counter == 0) {
+		_v3Counter = 0xFF;
+		return;
+	}
+
+	uint16 freq = (_v3FreqHi << 8) | _v3FreqLo;
+	uint16 delta = (_v3DeltaHi << 8) | _v3DeltaLo;
+	freq += delta;
+	_v3FreqLo = freq & 0xFF;
+	_v3FreqHi = (freq >> 8) & 0xFF;
+
+	sidWrite(kSIDV3FreqLo, _v3FreqLo);
+	sidWrite(kSIDV3FreqHi, _v3FreqHi);
+
+	_v3Counter--;
+}
+
+// Phase state machine for multi-step SFX (#6, #14, #15).
+// Handles gate-off timing and chord/phase transitions that the
+// original implements via busy-wait loops.
+void DrillerC64SFXPlayer::tickPhase() {
+	if (_sfxPhase == 0)
+		return;
+
+	if (_sfxPhaseTimer > 0) {
+		_sfxPhaseTimer--;
+		if (_sfxPhaseTimer > 0)
+			return;
+	}
+
+	// Timer expired - handle phase transition
+	switch (_sfxActiveIndex) {
+	case 6:
+		// SFX #6: gate off after 1 frame
+		if (_sfxPhase == 1) {
+			sidWrite(kSIDV1Ctrl, 0x10); // Triangle, gate off
+			_sfxPhase = 0;
+		}
+		break;
+
+	case 14:
+		// SFX #14: 3-step chord using inline data from $C7AA
+		// V1 freqHi: $22, $22, $39
+		// V2 freqHi: $26, $30, $9A
+		// Timing:    $0C, $08, $1E
+		switch (_sfxPhase) {
+		case 1: // Step 1 done → Step 2
+			sidWrite(kSIDV1FreqHi, 0x22);
+			sidWrite(kSIDV2FreqHi, 0x30);
+			_sfxPhase = 2;
+			_sfxPhaseTimer = 8; // $08 frames
+			break;
+		case 2: // Step 2 done → Step 3
+			sidWrite(kSIDV1FreqHi, 0x39);
+			sidWrite(kSIDV2FreqHi, 0x9A);
+			_sfxPhase = 3;
+			_sfxPhaseTimer = 30; // $1E frames
+			break;
+		case 3: // Step 3 done → gate off
+			sidWrite(kSIDV1Ctrl, 0x46); // Pulse+sync, gate off
+			sidWrite(kSIDV2Ctrl, 0x46);
+			_sfxPhase = 0;
+			break;
+		default:
+			_sfxPhase = 0;
+			break;
+		}
+		break;
+
+	case 15:
+		// SFX #15: two-phase pulse effect
+		switch (_sfxPhase) {
+		case 1: // Phase 1 slide done → gate off, short wait
+			sidWrite(kSIDV1Ctrl, 0x40); // Pulse, gate off
+			_sfxPhase = 2;
+			_sfxPhaseTimer = 3; // Approximate busy-wait delay
+			break;
+		case 2: // Wait done → Phase 2: new slide params
+			_v1FreqLo = 0x00;
+			_v1FreqHi = 0x08;
+			_v1DeltaLo = 0x00;
+			_v1DeltaHi = 0x00;
+			_v1Counter = 6;
+			_v1TickCtr = 1;
+			_v1TickReload = 8;
+			sidWrite(kSIDV1FreqLo, 0x00);
+			sidWrite(kSIDV1FreqHi, 0x08);
+			sidWrite(kSIDV1Ctrl, 0x41); // Pulse + gate
+			_sfxPhase = 0; // V1 slide handler takes over
+			break;
+		default:
+			_sfxPhase = 0;
+			break;
+		}
+		break;
+
+	default:
+		_sfxPhase = 0;
+		break;
+	}
+}
+
+// --- SFX dispatch ---
+
+void DrillerC64SFXPlayer::playSfx(int sfxIndex) {
+	debugC(1, kFreescapeDebugMedia, "DrillerC64SFX: Playing SFX %d", sfxIndex);
+
+	// Stop any ongoing SFX state before starting new one
+	_v1Counter = 0xFF;
+	_v3Counter = 0xFF;
+	_noiseTimer = 0;
+	_sfxPhase = 0;
+
+	// Reset all SID registers to a clean state.
+	// Music and SFX are mutually exclusive on C64; during gameplay only
+	// SFX play and the game is silent between them. The SID starts from
+	// a zeroed state (music stopped at game start), and many SFX routines
+	// don't write all registers (e.g. SFX #2 never writes V1 SR). Without
+	// this reset, stale values from a previous SFX (e.g. SR=$F8 from #8)
+	// would cause subsequent SFX to sustain indefinitely.
+	for (int i = 0; i < 0x18; i++)
+		sidWrite(i, 0);
+
+	switch (sfxIndex) {
+	case 2:
+		sfx2();
+		break;
+	case 3:
+		sfx3();
+		break;
+	case 4:
+		sfx4();
+		break;
+	case 5:
+		sfx5();
+		break;
+	case 6:
+		sfx6();
+		break;
+	case 7:
+		sfx7();
+		break;
+	case 8:
+		sfx8();
+		break;
+	case 9:
+		sfx9();
+		break;
+	case 10:
+		sfx10();
+		break;
+	case 11:
+		sfx11();
+		break;
+	case 14:
+		sfx14();
+		break;
+	case 15:
+		sfx15();
+		break;
+	case 16:
+		sfx16();
+		break;
+	case 17:
+		sfx17();
+		break;
+	case 18:
+		sfx18();
+		break;
+	default:
+		debugC(1, kFreescapeDebugMedia, "DrillerC64SFX: Unknown SFX index %d", sfxIndex);
+		break;
+	}
+}
+
+// --- Individual SFX routines ---
+// All verified against C64 binary (driller.bin, PRG load $0400)
+
+// SFX #2 ($C55A) - Dual-Voice Noise Sweep Down
+void DrillerC64SFXPlayer::sfx2() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	// V1: waveform $15 (triangle+ring+gate), AD=$0B
+	sidWrite(kSIDV1AD, 0x0B);
+	sidWrite(kSIDV1Ctrl, 0x15);
+	// V1 slide: start $4800, delta $FF00, 48 frames, no prescaler
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x48;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0xFF;
+	_v1Counter = 48;
+	_v1TickCtr = 0;
+	_v1TickReload = 0;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// V2: waveform $15, AD=$0B, SR=$09, freq=$0600
+	sidWrite(kSIDV2FreqLo, 0x00);
+	sidWrite(kSIDV2FreqHi, 0x06);
+	sidWrite(kSIDV2AD, 0x0B);
+	sidWrite(kSIDV2SR, 0x09);
+	sidWrite(kSIDV2Ctrl, 0x15);
+
+	// V3: freq=$0C00, no delta, 48 frames (ring modulation source)
+	_v3FreqLo = 0x00;
+	_v3FreqHi = 0x0C;
+	_v3DeltaLo = 0x00;
+	_v3DeltaHi = 0x00;
+	_v3Counter = 48;
+	sidWrite(kSIDV3FreqLo, _v3FreqLo);
+	sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #3 ($C5A6) - Noise Pitch Slide
+void DrillerC64SFXPlayer::sfx3() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	// V1: waveform $15, AD=$0F, no prescaler
+	sidWrite(kSIDV1AD, 0x0F);
+	sidWrite(kSIDV1Ctrl, 0x15);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0xAF;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0xF7;
+	_v1Counter = 12;
+	_v1TickCtr = 0;
+	_v1TickReload = 0;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// V3: freq=$2000, no delta, 12 frames
+	_v3FreqLo = 0x00;
+	_v3FreqHi = 0x20;
+	_v3DeltaLo = 0x00;
+	_v3DeltaHi = 0x00;
+	_v3Counter = 12;
+	sidWrite(kSIDV3FreqLo, _v3FreqLo);
+	sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #4 ($C5E3) - Pulse Slide Down
+void DrillerC64SFXPlayer::sfx4() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1PwLo, 0xB4);
+	sidWrite(kSIDV1PwHi, 0x0E);
+	sidWrite(kSIDV1AD, 0x2F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1Ctrl, 0x41);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x14;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0xFD;
+	_v1Counter = 2;
+	_v1TickCtr = 1;       // Prescaler: tick every 7 frames
+	_v1TickReload = 6;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #5 ($C62A) - Pulse Slide Up
+void DrillerC64SFXPlayer::sfx5() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1PwLo, 0xB4);
+	sidWrite(kSIDV1PwHi, 0x0E);
+	sidWrite(kSIDV1AD, 0x2F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1Ctrl, 0x41);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x10;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0x03;
+	_v1Counter = 2;
+	_v1TickCtr = 1;       // Prescaler: tick every 7 frames
+	_v1TickReload = 6;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #6 ($C681) - Triangle Blip
+// Original: gate on, ~250-cycle busy loop (~0.25ms), gate off.
+// We use the phase system to gate off after 1 frame.
+void DrillerC64SFXPlayer::sfx6() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1AD, 0x1F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1FreqLo, 0x00);
+	sidWrite(kSIDV1FreqHi, 0x77);
+	sidWrite(kSIDV1Ctrl, 0x11); // Triangle + gate on
+
+	_sfxActiveIndex = 6;
+	_sfxPhase = 1;
+	_sfxPhaseTimer = 1;
+}
+
+// SFX #7 ($C6B0) - Dual Noise Burst (one-shot)
+void DrillerC64SFXPlayer::sfx7() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1FreqLo, 0x00);
+	sidWrite(kSIDV1FreqHi, 0x01);
+	sidWrite(kSIDV1AD, 0x0A);
+	sidWrite(kSIDV1SR, 0x09);
+	sidWrite(kSIDV1Ctrl, 0x81);
+
+	sidWrite(kSIDV2FreqLo, 0x00);
+	sidWrite(kSIDV2FreqHi, 0x06);
+	sidWrite(kSIDV2AD, 0x0A);
+	sidWrite(kSIDV2SR, 0x09);
+	sidWrite(kSIDV2Ctrl, 0x81);
+}
+
+// SFX #8 ($C6DB) - Triangle Slide Up
+void DrillerC64SFXPlayer::sfx8() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1AD, 0x1F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1Ctrl, 0x11);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x1E;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0x03;
+	_v1Counter = 35;
+	_v1TickCtr = 0;
+	_v1TickReload = 0;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #9 ($C70F) - Dual Slide (V1+V3)
+void DrillerC64SFXPlayer::sfx9() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1AD, 0x0F);
+	sidWrite(kSIDV1Ctrl, 0x15);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x60;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0xF7;
+	_v1Counter = 25;
+	_v1TickCtr = 0;
+	_v1TickReload = 0;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// V3: freq=$5000, delta=$0000 (no slide), 25 frames
+	_v3FreqLo = 0x00;
+	_v3FreqHi = 0x50;
+	_v3DeltaLo = 0x00;
+	_v3DeltaHi = 0x00;  // Fixed: was 0x07
+	_v3Counter = 25;
+	sidWrite(kSIDV3FreqLo, _v3FreqLo);
+	sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #10 ($C74C) - Programmed Noise Bursts
+void DrillerC64SFXPlayer::sfx10() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	_noiseTimer = 13;
+	_noiseReload = 6;
+	_noiseCounter = 6;   // Fixed: was 1 (first burst after 6 frames, not immediately)
+	_noiseDec = 5;
+}
+
+// SFX #11 ($C766) - Triangle Slide Down Fast
+void DrillerC64SFXPlayer::sfx11() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1AD, 0x1F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1Ctrl, 0x11);
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x46;
+	_v1DeltaLo = 0x00;
+	_v1DeltaHi = 0xFE;
+	_v1Counter = 30;
+	_v1TickCtr = 0;
+	_v1TickReload = 0;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #14 ($C7B3) - 3-Step Chord (V1+V2)
+// Inline data at $C7AA: V1 freqHi {$22,$22,$39}, V2 freqHi {$26,$30,$9A},
+// timing {$0C,$08,$1E}. Original blocks game loop via busy-wait.
+void DrillerC64SFXPlayer::sfx14() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	sidWrite(kSIDV1PwLo, 0xB4);
+	sidWrite(kSIDV1PwHi, 0x0E);
+	sidWrite(kSIDV1AD, 0x09);
+	sidWrite(kSIDV1SR, 0x00);
+
+	sidWrite(kSIDV2PwLo, 0xB4);
+	sidWrite(kSIDV2PwHi, 0x0E);
+	sidWrite(kSIDV2AD, 0x09);
+	sidWrite(kSIDV2SR, 0x00);
+
+	// Step 1: V1=$2200, V2=$2600
+	sidWrite(kSIDV1FreqLo, 0x00);
+	sidWrite(kSIDV1FreqHi, 0x22);
+	sidWrite(kSIDV2FreqLo, 0x00);
+	sidWrite(kSIDV2FreqHi, 0x26);
+	sidWrite(kSIDV1Ctrl, 0x47); // Pulse + sync + gate
+	sidWrite(kSIDV2Ctrl, 0x47);
+
+	_sfxActiveIndex = 14;
+	_sfxPhase = 1;
+	_sfxPhaseTimer = 12; // $0C frames
+}
+
+// SFX #15 ($C810) - Two-Phase Pulse Effect
+void DrillerC64SFXPlayer::sfx15() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	// Phase 1: pulse slide
+	sidWrite(kSIDV1PwLo, 0xB4);
+	sidWrite(kSIDV1PwHi, 0x32);
+	sidWrite(kSIDV1AD, 0x2F);
+	sidWrite(kSIDV1SR, 0xF8);
+	sidWrite(kSIDV1Ctrl, 0x41);
+
+	_v1FreqLo = 0x00;
+	_v1FreqHi = 0x0C;
+	_v1DeltaLo = 0x03;   // Fixed: was 0x00 (delta is $0003, not $0300)
+	_v1DeltaHi = 0x00;   // Fixed: was 0x03
+	_v1Counter = 11;
+	_v1TickCtr = 1;       // Fixed: was 0
+	_v1TickReload = 1;    // Fixed: was 0 (slide every 2 frames)
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// Phase system: 11 iterations × 2 frames/iteration = 22 frames
+	_sfxActiveIndex = 15;
+	_sfxPhase = 1;
+	_sfxPhaseTimer = 23;  // Wait for slide to finish + 1 frame margin
+}
+
+// SFX #16 ($C896) - Filtered Effect (one-shot, no slide)
+void DrillerC64SFXPlayer::sfx16() {
+	// V1: waveform $15, freq=$080A, AD=$0A, SR=$09
+	sidWrite(kSIDV1FreqLo, 0x0A);
+	sidWrite(kSIDV1FreqHi, 0x08);
+	sidWrite(kSIDV1AD, 0x0A);
+	sidWrite(kSIDV1SR, 0x09);
+	sidWrite(kSIDV1Ctrl, 0x15);
+
+	// V3: freq=$010A (ring modulation source)
+	sidWrite(kSIDV3FreqLo, 0x0A);
+	sidWrite(kSIDV3FreqHi, 0x01);
+
+	// Filter: cutoff=$780, filter V1, LP enable
+	sidWrite(kSIDFilterLo, 0x00);
+	sidWrite(kSIDFilterHi, 0x78);
+	sidWrite(kSIDFilterCtrl, 0x01);
+	sidWrite(kSIDVolume, 0x1F);   // Vol=$0F + LP filter enable bit
+}
+
+// SFX #17 ($C8C8) - Freq Echo (one-shot, no slide)
+void DrillerC64SFXPlayer::sfx17() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	// V1: waveform $15, freq=$001D, AD=$DD
+	sidWrite(kSIDV1FreqLo, 0x1D);
+	sidWrite(kSIDV1FreqHi, 0x00);
+	sidWrite(kSIDV1AD, 0xDD);
+	sidWrite(kSIDV1Ctrl, 0x15);
+
+	// V3: original reads V1.FreqHi ($D401) → V3.FreqLo, V3.FreqHi=$02
+	// Since V1.FreqHi is $00, V3 freq = $0200
+	sidWrite(kSIDV3FreqLo, 0x00);
+	sidWrite(kSIDV3FreqHi, 0x02);
+}
+
+// SFX #18 ($C8EF) - Dual Noise with Modulation (major explosion)
+void DrillerC64SFXPlayer::sfx18() {
+	sidWrite(kSIDVolume, 0x0F);
+
+	// V1: waveform $15, AD=$0B
+	sidWrite(kSIDV1AD, 0x0B);
+	sidWrite(kSIDV1Ctrl, 0x15);
+
+	// V2: waveform $15, AD=$0B, SR=$09, freq=$0600
+	sidWrite(kSIDV2FreqLo, 0x00);
+	sidWrite(kSIDV2FreqHi, 0x06);
+	sidWrite(kSIDV2AD, 0x0B);
+	sidWrite(kSIDV2SR, 0x09);
+	sidWrite(kSIDV2Ctrl, 0x15);
+
+	// V1 slide: start $C8C8, delta $00F0, 10 frames, prescaler=6
+	_v1FreqLo = 0xC8;
+	_v1FreqHi = 0xC8;
+	_v1DeltaLo = 0xF0;   // Fixed: was 0x00 (delta is $00F0, not $F000)
+	_v1DeltaHi = 0x00;   // Fixed: was 0xF0
+	_v1Counter = 10;
+	_v1TickCtr = 1;       // Fixed: was 0
+	_v1TickReload = 6;
+	sidWrite(kSIDV1FreqLo, _v1FreqLo);
+	sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+	// V3: freq=$07C8 (ring modulation source), no delta, 10 frames
+	_v3FreqLo = 0xC8;
+	_v3FreqHi = 0x07;    // Fixed: was 0x00
+	_v3DeltaLo = 0x00;
+	_v3DeltaHi = 0x00;
+	_v3Counter = 10;
+	sidWrite(kSIDV3FreqLo, _v3FreqLo);
+	sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/driller/c64.sfx.h b/engines/freescape/games/driller/c64.sfx.h
new file mode 100644
index 00000000000..325cb4b3a7f
--- /dev/null
+++ b/engines/freescape/games/driller/c64.sfx.h
@@ -0,0 +1,139 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DRILLER_C64_SFX_H
+#define FREESCAPE_DRILLER_C64_SFX_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// SID register offsets
+enum SIDRegs {
+	kSIDV1FreqLo  = 0x00,
+	kSIDV1FreqHi  = 0x01,
+	kSIDV1PwLo    = 0x02,
+	kSIDV1PwHi    = 0x03,
+	kSIDV1Ctrl    = 0x04,
+	kSIDV1AD      = 0x05,
+	kSIDV1SR      = 0x06,
+
+	kSIDV2FreqLo  = 0x07,
+	kSIDV2FreqHi  = 0x08,
+	kSIDV2PwLo    = 0x09,
+	kSIDV2PwHi    = 0x0A,
+	kSIDV2Ctrl    = 0x0B,
+	kSIDV2AD      = 0x0C,
+	kSIDV2SR      = 0x0D,
+
+	kSIDV3FreqLo  = 0x0E,
+	kSIDV3FreqHi  = 0x0F,
+	kSIDV3PwLo    = 0x10,
+	kSIDV3PwHi    = 0x11,
+	kSIDV3Ctrl    = 0x12,
+	kSIDV3AD      = 0x13,
+	kSIDV3SR      = 0x14,
+
+	kSIDFilterLo  = 0x15,
+	kSIDFilterHi  = 0x16,
+	kSIDFilterCtrl = 0x17,
+	kSIDVolume    = 0x18
+};
+
+class DrillerC64SFXPlayer {
+public:
+	DrillerC64SFXPlayer();
+	~DrillerC64SFXPlayer();
+
+	void playSfx(int sfxIndex);
+	void sfxTick();           // Called every frame (50Hz) from onTimer
+	void stopAllSfx();
+
+	bool isSfxActive() const;
+
+private:
+	SID::SID *_sid;
+
+	void sidWrite(int reg, uint8 data);
+	void initSID();
+
+	// Voice 1 pitch slide state ($CC5B-$CC61)
+	uint8 _v1Counter;      // 0xFF=inactive, 0=expired (marked 0xFF next tick)
+	uint8 _v1FreqLo;
+	uint8 _v1FreqHi;
+	uint8 _v1DeltaLo;
+	uint8 _v1DeltaHi;
+	uint8 _v1TickCtr;
+	uint8 _v1TickReload;
+
+	// Voice 3 pitch slide state ($CC62-$CC66)
+	uint8 _v3Counter;
+	uint8 _v3FreqLo;
+	uint8 _v3FreqHi;
+	uint8 _v3DeltaLo;
+	uint8 _v3DeltaHi;
+
+	// Noise burst timer ($CC67-$CC6A)
+	uint8 _noiseTimer;
+	uint8 _noiseCounter;
+	uint8 _noiseReload;
+	uint8 _noiseDec;
+
+	// Phase state machine for multi-step SFX (#6, #14, #15)
+	uint8 _sfxPhase;        // 0=inactive
+	uint8 _sfxPhaseTimer;   // frames until next phase transition
+	uint8 _sfxActiveIndex;  // which SFX owns the phase state
+
+	// Tick handlers
+	void tickVoice1Slide();
+	void tickVoice3Slide();
+	void tickNoiseBurst();
+	void tickPhase();
+
+	// Helper to silence all voices
+	void silenceAllVoices();
+
+	// Noise burst subroutine ($C818)
+	void noiseBurst(uint8 param);
+
+	// Individual SFX routines
+	void sfx2();   // Dual-voice noise sweep down
+	void sfx3();   // Noise pitch slide
+	void sfx4();   // Pulse slide down
+	void sfx5();   // Pulse slide up
+	void sfx6();   // Triangle blip
+	void sfx7();   // Dual noise burst
+	void sfx8();   // Triangle slide up
+	void sfx9();   // Dual slide (V1+V3) noise
+	void sfx10();  // Programmed noise bursts
+	void sfx11();  // Triangle slide down (fast)
+	void sfx14();  // 3-step chord (V1+V2)
+	void sfx15();  // Two-phase pulse effect
+	void sfx16();  // Filtered effect (V1+V3)
+	void sfx17();  // Freq echo (V1->V3)
+	void sfx18();  // Dual noise with modulation
+
+	void onTimer();
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DRILLER_C64_SFX_H
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 6730a9afc3a..3a1fa4de6f8 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -97,10 +97,12 @@ DrillerEngine::DrillerEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 	_borderExtra = nullptr;
 	_borderExtraTexture = nullptr;
 	_playerSid = nullptr;
+	_playerC64Sfx = nullptr;
 }
 
 DrillerEngine::~DrillerEngine() {
 	delete _playerSid;
+	delete _playerC64Sfx;
 	delete _drillBase;
 
 	if (_borderExtra) {
@@ -262,9 +264,11 @@ void DrillerEngine::gotoArea(uint16 areaID, int entranceID) {
 	_gameStateVars[0x1f] = 0;
 
 	if (areaID == _startArea && entranceID == _startEntrance) {
-		if (isC64())
-			_playerSid->startMusic();
-		else {
+		if (isC64()) {
+			// TODO: Re-enable music once SFX coexistence is resolved
+			// _playerSid->startMusic();
+			playSound(_soundIndexStart, true, _soundFxHandle);
+		} else {
 			playSound(_soundIndexStart, true, _soundFxHandle);
 			// Start playing music, if any, in any supported format
 			playMusic("Matt Gray - The Best Of Reformation - 07 Driller Theme");
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index e57f131fddc..167ba9d2bcb 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -23,6 +23,7 @@
 #include "audio/mixer.h"
 
 #include "engines/freescape/games/driller/c64.music.h"
+#include "engines/freescape/games/driller/c64.sfx.h"
 
 namespace Freescape {
 
@@ -45,6 +46,9 @@ public:
 	bool _useAutomaticDrilling;
 
 	DrillerSIDPlayer *_playerSid;
+	DrillerC64SFXPlayer *_playerC64Sfx;
+
+	void playSoundC64(int index) override;
 
 	// Only used for Amiga and Atari ST
 	Font _fontSmall;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 413a85bfdf8..321ad4e2140 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -25,6 +25,7 @@ MODULE_OBJS := \
 	games/driller/atari.o \
 	games/driller/c64.o \
 	games/driller/c64.music.o \
+	games/driller/c64.sfx.o \
 	games/driller/cpc.o \
 	games/driller/dos.o \
 	games/driller/driller.o \
diff --git a/engines/freescape/sound/common.cpp b/engines/freescape/sound/common.cpp
index 8c59906ca41..5d69e261363 100644
--- a/engines/freescape/sound/common.cpp
+++ b/engines/freescape/sound/common.cpp
@@ -40,6 +40,11 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
 	_syncSound = sync;
 
 	debugC(1, kFreescapeDebugMedia, "Playing sound %d with sync: %d", index, sync);
+	if (isC64()) {
+		playSoundC64(index);
+		return;
+	}
+
 	if (isAmiga() || isAtariST()) {
 		playSoundFx(index, sync);
 		return;
@@ -67,6 +72,10 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
 	playWav(filename);
 	_syncSound = sync;
 }
+void FreescapeEngine::playSoundC64(int index) {
+	debugC(1, kFreescapeDebugMedia, "C64 sound %d not implemented for this engine", index);
+}
+
 void FreescapeEngine::playWav(const Common::Path &filename) {
 
 	Common::SeekableReadStream *s = _dataBundle->createReadStreamForMember(filename);


Commit: 3233011b5c52e8c87658f7eb4443c861cdd045e6
    https://github.com/scummvm/scummvm/commit/3233011b5c52e8c87658f7eb4443c861cdd045e6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: first pass on the music/sound for dark side (c64)

Changed paths:
  A engines/freescape/games/dark/c64.music.cpp
  A engines/freescape/games/dark/c64.music.h
  A engines/freescape/games/dark/c64.sfx.cpp
  A engines/freescape/games/dark/c64.sfx.h
    engines/freescape/games/dark/c64.cpp
    engines/freescape/games/dark/dark.cpp
    engines/freescape/games/dark/dark.h
    engines/freescape/module.mk


diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index 50fc9e4db9f..bd5f973747a 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -162,8 +162,18 @@ void DarkEngine::loadAssetsC64FullGame() {
 	colorFile2.open("darkside.c64.title.colors2");
 
 	_title = loadAndConvertDoodleImage(&file, &colorFile1, &colorFile2, (byte *)&kC64Palette);
+
+	// TODO: SFX and music both need a SID instance, but only one can be active at a time.
+	// Disable SFX for now so the music player can work.
+	//_playerC64Sfx = new DarkSideC64SFXPlayer();
+	_playerC64Music = new DarkSideC64MusicPlayer();
 }
 
+void DarkEngine::playSoundC64(int index) {
+	debugC(1, kFreescapeDebugMedia, "Playing Dark Side C64 SFX %d", index);
+	if (_playerC64Sfx)
+		_playerC64Sfx->playSfx(index);
+}
 
 void DarkEngine::drawC64UI(Graphics::Surface *surface) {
 	uint8 r, g, b;
diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
new file mode 100644
index 00000000000..9310b2d80a2
--- /dev/null
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -0,0 +1,752 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/debug.h"
+#include "common/endian.h"
+#include "common/textconsole.h"
+#include "freescape/games/dark/c64.music.h"
+
+namespace Freescape {
+
+// ============================================================
+// Data tables extracted from darkside.prg (load address $0400)
+// ============================================================
+
+// Frequency table: hi bytes at $0F38, lo bytes at $0F97 (95 entries each)
+// Index 0 = rest (freq 0), indices 1-94 = notes spanning 8 octaves
+static const uint8 kFreqHi[96] = {
+	0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
+	0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
+	0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+	0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19, 0x1B, 0x1C, 0x1E, 0x20, 0x22, 0x24, 0x26, 0x28,
+	0x2B, 0x2D, 0x30, 0x33, 0x36, 0x39, 0x3D, 0x40, 0x44, 0x48, 0x4C, 0x51, 0x56, 0x5B, 0x60, 0x66,
+	0x6C, 0x73, 0x7A, 0x81, 0x89, 0x91, 0x99, 0xA3, 0xAC, 0xB7, 0xC1, 0xCD, 0xD9, 0xE6, 0xF4,
+	0x00 // safety padding
+};
+
+static const uint8 kFreqLo[96] = {
+	0x00, 0x23, 0x34, 0x46, 0x5A, 0x6E, 0x84, 0x9B, 0xB3, 0xCD, 0xE9, 0x06, 0x25, 0x45, 0x68, 0x8C,
+	0xB3, 0xDC, 0x08, 0x36, 0x67, 0x9B, 0xD2, 0x0C, 0x49, 0x8B, 0xD0, 0x19, 0x67, 0xB9, 0x10, 0x6C,
+	0xCE, 0x35, 0xA3, 0x17, 0x93, 0x15, 0x9F, 0x3C, 0xCD, 0x72, 0x20, 0xD8, 0x9C, 0x6B, 0x46, 0x2F,
+	0x25, 0x2A, 0x3F, 0x64, 0x9A, 0xE3, 0x3F, 0xB1, 0x38, 0xD6, 0x8D, 0x5E, 0x4B, 0x55, 0x7E, 0xC8,
+	0x34, 0xC6, 0x7F, 0x61, 0x6F, 0xAC, 0x7E, 0xBC, 0x95, 0xA9, 0xFC, 0xA1, 0x69, 0x8C, 0xFE, 0xC2,
+	0xDF, 0x58, 0x34, 0x78, 0x2B, 0x53, 0xF7, 0x1F, 0xD2, 0x19, 0xFC, 0x85, 0xBD, 0xB0, 0x67,
+	0x00 // safety padding
+};
+
+// Instrument table at $1010 (18 instruments x 8 bytes)
+// Bytes: ctrl, AD, SR, envCtl, vibrato, pwMod, autoFx, flags
+// Instruments 16-17 are stored between $1090-$109F (past the nominal 16-entry table)
+static const uint8 kInstruments[18 * 8] = {
+	0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: Pulse (silence)
+	0x41, 0x0C, 0xDC, 0x02, 0x00, 0x33, 0x00, 0x08, // 1: Pulse+G
+	0x41, 0xDC, 0xDD, 0x00, 0x00, 0x15, 0x00, 0x80, // 2: Pulse+G
+	0x41, 0x1A, 0x04, 0x00, 0x00, 0x05, 0x00, 0x04, // 3: Pulse+G
+	0x11, 0x32, 0x3A, 0x00, 0x00, 0x11, 0x00, 0x80, // 4: Triangle+G
+	0x41, 0x0B, 0xAC, 0x20, 0x00, 0x01, 0x00, 0x04, // 5: Pulse+G
+	0x41, 0x0B, 0x6C, 0x40, 0x33, 0x10, 0x00, 0x00, // 6: Pulse+G
+	0x45, 0x0A, 0x8B, 0x88, 0x46, 0x13, 0x00, 0x80, // 7: Pulse+Ring+G
+	0x41, 0x16, 0x00, 0x60, 0x00, 0x17, 0x00, 0x80, // 8: Pulse+G
+	0x21, 0x08, 0x17, 0x00, 0x00, 0x00, 0x80, 0x00, // 9: Sawtooth+G
+	0x15, 0xCC, 0xDC, 0x40, 0x00, 0x11, 0x00, 0x80, // 10: Tri+Ring+G
+	0x81, 0x42, 0x38, 0x10, 0x00, 0x01, 0x03, 0x02, // 11: Noise+G
+	0x41, 0x8B, 0xAF, 0x80, 0x00, 0x00, 0x00, 0x04, // 12: Pulse+G
+	0x41, 0x1B, 0x4A, 0x14, 0x53, 0x02, 0x00, 0x00, // 13: Pulse+G
+	0x81, 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, // 14: Noise+G
+	0x41, 0x0B, 0x6C, 0x62, 0x62, 0x42, 0x00, 0x00, // 15: Pulse+G
+	0x41, 0x0C, 0x00, 0x88, 0x11, 0x00, 0x00, 0x01, // 16: Pulse+G (percussion, envelope seq)
+	0x81, 0x0B, 0x20, 0x66, 0x00, 0x00, 0x00, 0x01, // 17: Noise+G (percussion, envelope seq)
+};
+
+// Envelope volume tables ($154B and $156B, 16 entries each)
+static const uint8 kEnvVolume[2][16] = {
+	{ 0xFA, 0x01, 0xFF, 0x20, 0x0A, 0x12, 0x04, 0x16, 0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x00 },
+	{ 0x10, 0x0A, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00 },
+};
+
+// Envelope waveform control tables ($155B and $157B, 16 entries each)
+static const uint8 kEnvControl[2][16] = {
+	{ 0x81, 0x41, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
+	{ 0x81, 0x41, 0x81, 0x80, 0x40, 0x40, 0x40, 0x40, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
+};
+
+// Arpeggio semitone intervals ($1591)
+static const uint8 kArpIntervals[8] = { 3, 4, 5, 7, 8, 9, 10, 12 };
+
+// SID register base offset per channel
+static const int kSIDOffset[3] = { 0, 7, 14 };
+
+// ---- Pattern data (29 patterns from $122D-$1542) ----
+
+static const uint8 kPattern00[] = { 0xC0, 0xBF, 0x00, 0xFF };
+static const uint8 kPattern01[] = { 0xF5, 0xC2, 0xBF, 0x10, 0x10, 0xFF };
+static const uint8 kPattern02[] = { 0xC1, 0x8F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF };
+static const uint8 kPattern03[] = { 0xC4, 0x80, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0xFF };
+static const uint8 kPattern04[] = { 0xC6, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xFF };
+static const uint8 kPattern05[] = { 0x81, 0x34, 0x36, 0x34, 0x2F, 0x32, 0x30, 0x2B, 0x91, 0x2F, 0xFF };
+static const uint8 kPattern06[] = { 0x83, 0x34, 0x37, 0x34, 0x8F, 0x3C, 0x83, 0x3B, 0x8F, 0x3A, 0x8F, 0x36, 0xFF };
+static const uint8 kPattern07[] = { 0xC1, 0x81, 0x10, 0x10, 0x12, 0x10, 0x13, 0x10, 0x15, 0x13, 0xFF };
+static const uint8 kPattern08[] = { 0xC5, 0x9F, 0x7D, 0x89, 0x40, 0x7D, 0x91, 0x40, 0x7D, 0xA1, 0x40, 0x7D, 0x91, 0x40, 0xFF };
+static const uint8 kPattern09[] = { 0xC0, 0x9F, 0x00, 0xFF };
+static const uint8 kPattern10[] = { 0xCA, 0x9F, 0x7E, 0x0A, 0x7F, 0x4C, 0x83, 0x1C, 0x23, 0x28, 0x2F, 0x1C, 0x23, 0x28, 0x2F, 0x8F, 0x7F, 0x40, 0x7E, 0x10, 0x9F, 0x7B, 0x40, 0x3B, 0x21, 0x8F, 0x34, 0x40, 0x9F, 0x7B, 0x0D, 0x3B, 0x4C, 0x7F, 0x3C, 0xFF };
+static const uint8 kPattern11[] = { 0xC7, 0xFF };
+static const uint8 kPattern12[] = { 0xC2, 0xBF, 0x10, 0xFF };
+static const uint8 kPattern13[] = { 0xC8, 0x80, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xFF };
+static const uint8 kPattern14[] = { 0x80, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0xFF };
+static const uint8 kPattern15[] = { 0xCB, 0x81, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0xFF };
+static const uint8 kPattern16[] = { 0xC1, 0x87, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x83, 0x1C, 0x81, 0x10, 0x0E, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0A, 0x87, 0x09, 0x15, 0x09, 0x15, 0x09, 0x15, 0x09, 0x83, 0x15, 0x81, 0x09, 0x0A, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0E, 0xFF };
+static const uint8 kPattern17[] = { 0xCC, 0xBF, 0x7D, 0x89, 0x40, 0x7C, 0xA2, 0x3E, 0x3C, 0x7D, 0x91, 0x3F, 0xFF };
+static const uint8 kPattern18[] = { 0x83, 0x1C, 0xFF };
+static const uint8 kPattern19[] = { 0xC0, 0x93, 0x00, 0xC1, 0x81, 0x23, 0x83, 0x21, 0x81, 0x1F, 0x83, 0x1E, 0xFF };
+static const uint8 kPattern20[] = { 0xC1, 0x8B, 0x1C, 0x83, 0x23, 0x97, 0x23, 0x83, 0x21, 0x23, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x1E, 0x83, 0x23, 0x9B, 0x23, 0x81, 0x21, 0x23, 0x83, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x21, 0x83, 0x23, 0x93, 0x21, 0x83, 0x21, 0x23, 0x24, 0x26, 0x24, 0x23, 0x21, 0x9B, 0x1E, 0x83, 0x23, 0x93, 0x23, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xD1, 0x30, 0x30, 0xD0, 0x7A, 0x18, 0x12, 0xFF };
+static const uint8 kPattern21[] = { 0xC8, 0xFF };
+static const uint8 kPattern22[] = { 0xC4, 0xFF };
+static const uint8 kPattern23[] = { 0xCF, 0xA3, 0x7B, 0x37, 0x02, 0x36, 0x83, 0x2F, 0x34, 0x36, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x34, 0x8B, 0x7B, 0x36, 0x02, 0x34, 0x83, 0x2F, 0x93, 0x2F, 0x83, 0x36, 0x36, 0x37, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x7B, 0x34, 0x04, 0x37, 0x8B, 0x34, 0x81, 0x36, 0x37, 0x93, 0x34, 0x83, 0x34, 0x36, 0x37, 0x39, 0x37, 0x36, 0x34, 0xA3, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x85, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x83, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xFF };
+static const uint8 kPattern24[] = { 0xC1, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0xFF };
+static const uint8 kPattern25[] = { 0xCF, 0x99, 0x7B, 0x34, 0x04, 0x32, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x7B, 0x34, 0x04, 0x32, 0x83, 0x7B, 0x37, 0x02, 0x36, 0x81, 0x37, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x32, 0x83, 0x32, 0x36, 0x7B, 0x36, 0x02, 0x37, 0x9B, 0x36, 0x83, 0x7B, 0x30, 0x03, 0x32, 0x30, 0x32, 0x81, 0x30, 0x85, 0x7B, 0x30, 0x02, 0x32, 0x93, 0x30, 0x83, 0x30, 0x32, 0x34, 0x8B, 0x7B, 0x36, 0x04, 0x37, 0x83, 0x34, 0x97, 0x7B, 0x36, 0x04, 0x34, 0x81, 0x36, 0x37, 0x36, 0x34, 0x99, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x12, 0x83, 0x12, 0xFF };
+static const uint8 kPattern26[] = { 0xCC, 0xBF, 0x7D, 0x8A, 0x40, 0x7D, 0x91, 0x3F, 0x7D, 0x94, 0x3D, 0x7D, 0x91, 0x3F, 0xFF };
+static const uint8 kPattern27[] = { 0xCF, 0x83, 0x2F, 0x34, 0x91, 0x34, 0x85, 0x7B, 0x34, 0x12, 0x36, 0x2F, 0x81, 0x34, 0x34, 0x36, 0x8D, 0x34, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x93, 0x33, 0x83, 0x33, 0x81, 0x33, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x99, 0x33, 0x81, 0x2F, 0x83, 0x7B, 0x2D, 0x12, 0x2F, 0x8B, 0x2D, 0x83, 0x34, 0x34, 0x8B, 0x7B, 0x37, 0x22, 0x36, 0x93, 0x37, 0x87, 0x7B, 0x34, 0x04, 0x36, 0x83, 0x34, 0xA1, 0x7B, 0x36, 0x12, 0x34, 0xD1, 0x81, 0x1C, 0x8D, 0x1C, 0x81, 0x1C, 0x83, 0x1C, 0x1C, 0x81, 0x1C, 0x1C, 0xFF };
+static const uint8 kPattern28[] = { 0xCF, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xD0, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0xCF, 0x80, 0x32, 0x34, 0x9D, 0x36, 0xD0, 0x81, 0x12, 0x83, 0x12, 0x12, 0x81, 0x0E, 0x83, 0x12, 0x1C, 0x85, 0x12, 0x81, 0x0E, 0x83, 0x12, 0xCF, 0x80, 0x30, 0x32, 0x9D, 0x34, 0xD1, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x1C, 0xCF, 0x87, 0x30, 0x80, 0x33, 0x34, 0x9D, 0x36, 0xC0, 0x9B, 0x00, 0xD1, 0x81, 0x18, 0x18, 0xFF };
+
+static const uint8 *kPatterns[29] = {
+	kPattern00, kPattern01, kPattern02, kPattern03, kPattern04,
+	kPattern05, kPattern06, kPattern07, kPattern08, kPattern09,
+	kPattern10, kPattern11, kPattern12, kPattern13, kPattern14,
+	kPattern15, kPattern16, kPattern17, kPattern18, kPattern19,
+	kPattern20, kPattern21, kPattern22, kPattern23, kPattern24,
+	kPattern25, kPattern26, kPattern27, kPattern28,
+};
+
+// ---- Order lists (song 0, 3 channels) ----
+
+static const uint8 kOrderList0[] = {
+	0xC2, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+	0x02, 0x02, 0x02, 0x02, 0x01, 0x10, 0x10, 0x10, 0x10, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9,
+	0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9, 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18,
+	0xC9, 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9, 0x18, 0xC2, 0x18, 0x18, 0x18, 0x18,
+	0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x10, 0x10, 0x01,
+	0x01, 0xFF
+};
+
+static const uint8 kOrderList1[] = {
+	0xCE, 0x09, 0x01, 0x0C, 0x09, 0x01, 0x01, 0x01, 0x09, 0x01, 0x01, 0x09, 0x00, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xC2,
+	0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x08, 0x08, 0x02, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1A, 0x1A, 0x08, 0x08, 0xB6,
+	0x08, 0x08, 0xC2, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+	0x03, 0x03, 0x03, 0x03, 0xCE, 0x0C, 0xC9, 0x0C, 0xC7, 0x0C, 0xC9, 0x0C, 0xC2, 0x16, 0x0E,
+	0x0E, 0xBD, 0x0E, 0x0E, 0xBB, 0x0E, 0x0E, 0xBD, 0x0E, 0x0E, 0xCE, 0x09, 0x01, 0x0C, 0x09,
+	0xFF
+};
+
+static const uint8 kOrderList2[] = {
+	0xC2, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x09, 0x04, 0x05, 0x09, 0x04, 0x06, 0x09, 0x08, 0x08,
+	0x08, 0x08, 0x0B, 0x05, 0x06, 0x09, 0xAA, 0x04, 0xC2, 0x09, 0xB6, 0x04, 0xC2, 0x09, 0x0D,
+	0x09, 0xC4, 0x0D, 0xC2, 0x09, 0xC5, 0x0D, 0xC2, 0x09, 0xC7, 0x0D, 0xC2, 0x09, 0xCE, 0x0D,
+	0x12, 0xC0, 0x0F, 0xD0, 0x0D, 0x12, 0xC0, 0x0F, 0xD1, 0x0D, 0x12, 0xC0, 0x0F, 0xD3, 0x0D,
+	0x12, 0xC0, 0x0F, 0xB6, 0x15, 0x0E, 0x0E, 0x15, 0x0E, 0x0E, 0xC2, 0x15, 0x0E, 0x0E, 0x16,
+	0x0E, 0x0E, 0x09, 0xCE, 0x0C, 0xC2, 0x13, 0x14, 0x14, 0x17, 0xCE, 0x17, 0xC2, 0x19, 0xCE,
+	0x19, 0xC2, 0x1B, 0xB6, 0x1B, 0xCE, 0x01, 0xC2, 0x01, 0xCE, 0x03, 0x03, 0x03, 0x03, 0xC2,
+	0x03, 0x03, 0x03, 0x03, 0x0B, 0x05, 0x06, 0x09, 0xAA, 0x04, 0xC2, 0x09, 0xB6, 0x04, 0xC2,
+	0x09, 0x0D, 0x12, 0x0F, 0xC4, 0x0D, 0x12, 0xC2, 0x0F, 0xC5, 0x0D, 0x12, 0xC2, 0x0F, 0xC7,
+	0x0D, 0x12, 0xC2, 0x0F, 0x15, 0x0E, 0x0E, 0xBD, 0x0E, 0x0E, 0xBB, 0x0E, 0x0E, 0xBD, 0x0E,
+	0x0E, 0xC2, 0x1C, 0x02, 0x02, 0xFF
+};
+
+static const uint8 *kOrderLists[3] = { kOrderList0, kOrderList1, kOrderList2 };
+
+// ============================================================
+// Implementation
+// ============================================================
+
+void DarkSideC64MusicPlayer::ChannelState::reset() {
+	orderData = nullptr;
+	orderPos = 0;
+	patData = nullptr;
+	patOffset = 0;
+	instIdx = 0;
+	noteActive = 0;
+	curNote = 0;
+	transpose = 0;
+	freqLo = 0;
+	freqHi = 0;
+	pwLo = 0;
+	pwHi = 0;
+	durReload = 1;
+	durCounter = 1;
+	effectMode = 0;
+	effectParam = 0;
+	arpPattern = 0;
+	arpParam2 = 0;
+	arpSeqPos = 0;
+	arpSeqLen = 0;
+	memset(arpSeqData, 0, sizeof(arpSeqData));
+	portaDelta = 0;
+	portaTarget = 0;
+	vibPhase = 0;
+	vibCounter = 0;
+	pwDirection = 0;
+	delayCounter = 0;
+	envCounter = 0;
+	envTable = 0;
+	envSeqActive = false;
+	sustainMode = false;
+	waveform = 0;
+}
+
+DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
+	_musicActive = false;
+	_speedDiv = 1;
+	_speedCounter = 1;
+	for (int i = 0; i < 3; i++)
+		_ch[i].reset();
+	initSID();
+}
+
+DarkSideC64MusicPlayer::~DarkSideC64MusicPlayer() {
+	if (_sid)
+		_sid->stop();
+	delete _sid;
+}
+
+void DarkSideC64MusicPlayer::initSID() {
+	_sid = SID::Config::create(SID::Config::kSidPAL);
+	if (!_sid || !_sid->init()) {
+		warning("DarkSideC64MusicPlayer: Failed to create SID emulator");
+		return;
+	}
+	_sid->start(new Common::Functor0Mem<void, DarkSideC64MusicPlayer>(this, &DarkSideC64MusicPlayer::onTimer), 50);
+}
+
+void DarkSideC64MusicPlayer::sidWrite(int reg, uint8 data) {
+	if (_sid)
+		_sid->writeReg(reg, data);
+}
+
+void DarkSideC64MusicPlayer::silenceAll() {
+	for (int i = 0; i <= 0x18; i++)
+		sidWrite(i, 0);
+}
+
+bool DarkSideC64MusicPlayer::isPlaying() const {
+	return _musicActive;
+}
+
+void DarkSideC64MusicPlayer::startMusic() {
+	setupSong();
+}
+
+void DarkSideC64MusicPlayer::stopMusic() {
+	_musicActive = false;
+	silenceAll();
+}
+
+void DarkSideC64MusicPlayer::setupSong() {
+	silenceAll();
+
+	// Set filter: LP+HP on, full volume ($5F)
+	sidWrite(0x18, 0x5F);
+	sidWrite(0x17, 0x00);
+
+	_speedDiv = 1;
+	_speedCounter = 1;
+
+	for (int ch = 0; ch < 3; ch++) {
+		_ch[ch].reset();
+		_ch[ch].orderData = kOrderLists[ch];
+		loadNextPattern(ch);
+	}
+
+	_musicActive = true;
+}
+
+void DarkSideC64MusicPlayer::loadNextPattern(int ch) {
+	int safety = 200;
+	while (safety-- > 0) {
+		uint8 byte = _ch[ch].orderData[_ch[ch].orderPos];
+
+		if (byte == 0xFF) {
+			// Loop to start
+			_ch[ch].orderPos = 0;
+			continue;
+		}
+
+		_ch[ch].orderPos++;
+
+		if (byte >= 0x80) {
+			// Transpose: (byte + $40) & $FF
+			_ch[ch].transpose = (byte + 0x40) & 0xFF;
+			continue;
+		}
+
+		// Pattern number
+		if (byte < 29) {
+			_ch[ch].patData = kPatterns[byte];
+			_ch[ch].patOffset = 0;
+		}
+		break;
+	}
+}
+
+uint8 DarkSideC64MusicPlayer::readPatByte(int ch) {
+	uint8 b = _ch[ch].patData[_ch[ch].patOffset];
+	_ch[ch].patOffset++;
+	return b;
+}
+
+// ---- Main timer callback (50 Hz) ----
+
+void DarkSideC64MusicPlayer::onTimer() {
+	if (!_musicActive)
+		return;
+
+	// Increment envelope counters for all channels
+	for (int ch = 0; ch < 3; ch++) {
+		if (_ch[ch].envCounter < 15)
+			_ch[ch].envCounter++;
+	}
+
+	// Speed counter
+	_speedCounter--;
+	bool newBeat = (_speedCounter == 0);
+	if (newBeat)
+		_speedCounter = _speedDiv;
+
+	// Process channels (2 down to 0, matching original)
+	for (int ch = 2; ch >= 0; ch--)
+		processChannel(ch, newBeat);
+}
+
+void DarkSideC64MusicPlayer::processChannel(int ch, bool newBeat) {
+	int off = kSIDOffset[ch];
+
+	// Handle delay countdown
+	if (_ch[ch].delayCounter > 0) {
+		_ch[ch].delayCounter--;
+		if (_ch[ch].delayCounter == 0 && _ch[ch].noteActive) {
+			// Delay expired: gate on now
+			sidWrite(off + 4, _ch[ch].waveform | 0x01);
+		}
+	}
+
+	if (newBeat) {
+		if (_ch[ch].durCounter > 0)
+			_ch[ch].durCounter--;
+
+		if (_ch[ch].durCounter == 0) {
+			parseCommands(ch);
+		}
+	}
+
+	// Apply continuous effects
+	applyContinuousEffects(ch);
+
+	// Gate-off at half duration (if not in sustain mode and not in envelope seq mode)
+	if (!_ch[ch].sustainMode && !_ch[ch].envSeqActive && _ch[ch].noteActive) {
+		if (_ch[ch].durReload > 1 && _ch[ch].durCounter == _ch[ch].durReload / 2) {
+			sidWrite(off + 4, _ch[ch].waveform & 0xFE);
+		}
+	}
+}
+
+// ---- Command parser ----
+
+void DarkSideC64MusicPlayer::parseCommands(int ch) {
+	int safety = 200;
+	while (safety-- > 0) {
+		uint8 cmd = readPatByte(ch);
+
+		if (cmd == 0xFF) {
+			// End of pattern: load next from order list
+			loadNextPattern(ch);
+			continue;
+		}
+
+		if (cmd == 0xFE) {
+			// End of song: stop
+			stopMusic();
+			return;
+		}
+
+		if (cmd == 0xFD) {
+			// Filter control: read 1 parameter byte
+			uint8 filterVal = readPatByte(ch);
+			sidWrite(0x18, filterVal);
+			sidWrite(0x17, (filterVal >> 4) | 0x07);
+			continue;
+		}
+
+		if (cmd >= 0xF0) {
+			// Speed: low nybble
+			_speedDiv = cmd & 0x0F;
+			if (_speedDiv == 0)
+				_speedDiv = 1;
+			continue;
+		}
+
+		if (cmd >= 0xC0) {
+			// Instrument: (cmd & 0x1F) * 8
+			uint8 instNum = cmd & 0x1F;
+			if (instNum < 18)
+				_ch[ch].instIdx = instNum * 8;
+			continue;
+		}
+
+		if (cmd >= 0x80) {
+			// Duration: cmd & 0x3F
+			_ch[ch].durReload = cmd & 0x3F;
+			if (_ch[ch].durReload == 0)
+				_ch[ch].durReload = 1;
+			continue;
+		}
+
+		if (cmd == 0x7F) {
+			// Portamento up
+			_ch[ch].effectMode = 0xDE;
+			continue;
+		}
+
+		if (cmd == 0x7E) {
+			// Portamento down
+			_ch[ch].effectMode = 0xFE;
+			continue;
+		}
+
+		if (cmd == 0x7D) {
+			// Vibrato mode 1
+			_ch[ch].effectMode = 1;
+			_ch[ch].effectParam = readPatByte(ch);
+			_ch[ch].arpPattern = 0;
+			_ch[ch].vibPhase = 0;
+			_ch[ch].vibCounter = 0;
+			continue;
+		}
+
+		if (cmd == 0x7C) {
+			// Vibrato mode 2
+			_ch[ch].effectMode = 2;
+			_ch[ch].effectParam = readPatByte(ch);
+			_ch[ch].vibPhase = 0;
+			_ch[ch].vibCounter = 0;
+			continue;
+		}
+
+		if (cmd == 0x7B) {
+			// Arpeggio: read 2 bytes
+			uint8 arpX = readPatByte(ch);
+			uint8 arpY = readPatByte(ch);
+			_ch[ch].effectMode = 1;
+			_ch[ch].effectParam = 0;
+			_ch[ch].arpPattern = (arpX + _ch[ch].transpose) & 0xFF;
+			_ch[ch].arpParam2 = arpY;
+			_ch[ch].arpSeqPos = 0;
+			unpackArpeggio(ch);
+			continue;
+		}
+
+		if (cmd == 0x7A) {
+			// Delay: read 1 byte (delay value)
+			_ch[ch].delayCounter = readPatByte(ch);
+			continue;
+		}
+
+		// Note (0x00-0x5F)
+		applyNote(ch, cmd);
+		break;
+	}
+}
+
+// ---- Note handling ----
+
+void DarkSideC64MusicPlayer::applyNote(int ch, uint8 note) {
+	int off = kSIDOffset[ch];
+	uint8 instBase = _ch[ch].instIdx;
+
+	if (note == 0) {
+		// Rest: gate off
+		_ch[ch].noteActive = 0;
+		sidWrite(off + 4, _ch[ch].waveform & 0xFE);
+		_ch[ch].durCounter = _ch[ch].durReload;
+		return;
+	}
+
+	// Compute actual note index with transpose
+	uint8 actualNote = (note + _ch[ch].transpose) & 0xFF;
+	if (actualNote > 94)
+		actualNote = 94;
+
+	// Previous frequency (for portamento)
+	uint16 prevFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+
+	// Look up new frequency
+	_ch[ch].freqLo = kFreqLo[actualNote];
+	_ch[ch].freqHi = kFreqHi[actualNote];
+	_ch[ch].curNote = actualNote;
+	_ch[ch].noteActive = 1;
+
+	// Load instrument parameters
+	uint8 ctrl = kInstruments[instBase + 0];
+	uint8 ad = kInstruments[instBase + 1];
+	uint8 sr = kInstruments[instBase + 2];
+	uint8 vibrato = kInstruments[instBase + 4];
+	uint8 autoFx = kInstruments[instBase + 6];
+	uint8 flags = kInstruments[instBase + 7];
+
+	_ch[ch].waveform = ctrl;
+	_ch[ch].sustainMode = (flags & 0x08) != 0;
+	_ch[ch].envSeqActive = (flags & 0x01) != 0;
+	_ch[ch].envTable = (vibrato >> 4) & 0x01;
+
+	// Write ADSR
+	sidWrite(off + 5, ad);
+	sidWrite(off + 6, sr);
+
+	// Write PW
+	sidWrite(off + 2, _ch[ch].pwLo);
+	sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
+
+	// Write frequency
+	sidWrite(off + 0, _ch[ch].freqLo);
+	sidWrite(off + 1, _ch[ch].freqHi);
+
+	// Setup portamento if active
+	if (_ch[ch].effectMode == 0xDE || _ch[ch].effectMode == 0xFE) {
+		uint16 newFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+		_ch[ch].portaTarget = newFreq;
+		// Slide from previous frequency
+		_ch[ch].freqLo = prevFreq & 0xFF;
+		_ch[ch].freqHi = (prevFreq >> 8) & 0xFF;
+		if (_ch[ch].durReload > 0) {
+			_ch[ch].portaDelta = (int16)(newFreq - prevFreq) / (int16)_ch[ch].durReload;
+		}
+		sidWrite(off + 0, _ch[ch].freqLo);
+		sidWrite(off + 1, _ch[ch].freqHi);
+	}
+
+	// Setup auto-effect from instrument
+	if (autoFx != 0 && _ch[ch].effectMode == 0) {
+		_ch[ch].effectMode = 1;
+		_ch[ch].effectParam = autoFx;
+	}
+
+	// Gate on (unless delay is active)
+	if (_ch[ch].delayCounter > 0) {
+		// Delay: don't gate on yet, gate off first
+		sidWrite(off + 4, ctrl & 0xFE);
+	} else if (_ch[ch].envSeqActive) {
+		// Envelope sequencer controls the gate
+		_ch[ch].envCounter = 0;
+		sidWrite(off + 4, kEnvControl[_ch[ch].envTable][0]);
+	} else {
+		sidWrite(off + 4, ctrl | 0x01);
+	}
+
+	// Reset envelope counter
+	_ch[ch].envCounter = 0;
+
+	// Set duration
+	_ch[ch].durCounter = _ch[ch].durReload;
+}
+
+// ---- Continuous effects ----
+
+void DarkSideC64MusicPlayer::applyContinuousEffects(int ch) {
+	// PW modulation
+	applyPWModulation(ch);
+
+	// Frequency effects (mutually exclusive based on effectMode)
+	switch (_ch[ch].effectMode) {
+	case 1:
+		if (_ch[ch].arpPattern != 0)
+			applyArpeggio(ch);
+		else
+			applyVibrato(ch);
+		break;
+	case 2:
+		applyVibrato(ch);
+		break;
+	case 0xDE:
+	case 0xFE:
+		applyPortamento(ch);
+		break;
+	default:
+		break;
+	}
+
+	// Envelope sequencer
+	if (_ch[ch].envSeqActive)
+		applyEnvelope(ch);
+
+	// Write frequency to SID
+	int off = kSIDOffset[ch];
+	sidWrite(off + 0, _ch[ch].freqLo);
+	sidWrite(off + 1, _ch[ch].freqHi);
+
+	// Write PW to SID
+	sidWrite(off + 2, _ch[ch].pwLo);
+	sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
+}
+
+void DarkSideC64MusicPlayer::applyVibrato(int ch) {
+	if (!_ch[ch].noteActive)
+		return;
+
+	uint8 vibParam = _ch[ch].effectParam;
+	if (vibParam == 0)
+		return;
+
+	uint8 depth = vibParam >> 4;
+	uint8 speed = vibParam & 0x0F;
+	if (depth == 0 || speed == 0)
+		return;
+
+	uint8 note = _ch[ch].curNote;
+	if (note == 0 || note >= 94)
+		return;
+
+	// Compute frequency delta between this note and the next semitone
+	uint16 curFreq = ((uint16)kFreqHi[note] << 8) | kFreqLo[note];
+	uint16 nextFreq = ((uint16)kFreqHi[note + 1] << 8) | kFreqLo[note + 1];
+	int16 semitoneDelta = (int16)(nextFreq - curFreq);
+
+	// Scale by depth
+	int16 vibDelta = semitoneDelta * depth / 16;
+
+	// Oscillate: counter counts up to speed, then phase flips
+	_ch[ch].vibCounter++;
+	if (_ch[ch].vibCounter >= speed) {
+		_ch[ch].vibCounter = 0;
+		_ch[ch].vibPhase ^= 1;
+	}
+
+	// Apply offset
+	int16 offset = _ch[ch].vibPhase ? vibDelta : -vibDelta;
+	int16 baseFreq = (int16)curFreq;
+	uint16 newFreq = (uint16)(baseFreq + offset);
+
+	_ch[ch].freqLo = newFreq & 0xFF;
+	_ch[ch].freqHi = (newFreq >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyArpeggio(int ch) {
+	if (!_ch[ch].noteActive || _ch[ch].arpSeqLen == 0)
+		return;
+
+	// Cycle through unpacked arpeggio intervals
+	uint8 interval = _ch[ch].arpSeqData[_ch[ch].arpSeqPos];
+	_ch[ch].arpSeqPos++;
+	if (_ch[ch].arpSeqPos >= _ch[ch].arpSeqLen)
+		_ch[ch].arpSeqPos = 0;
+
+	// Compute arpeggiated note
+	uint8 arpNote = _ch[ch].curNote + interval;
+	if (arpNote > 94)
+		arpNote = 94;
+
+	_ch[ch].freqLo = kFreqLo[arpNote];
+	_ch[ch].freqHi = kFreqHi[arpNote];
+}
+
+void DarkSideC64MusicPlayer::unpackArpeggio(int ch) {
+	uint8 bits = _ch[ch].arpPattern;
+
+	// First entry: base note (interval 0)
+	_ch[ch].arpSeqData[0] = 0;
+	uint8 len = 1;
+
+	for (int i = 0; i < 8 && len < 19; i++) {
+		if (bits & (1 << i)) {
+			_ch[ch].arpSeqData[len] = kArpIntervals[i];
+			len++;
+		}
+	}
+
+	_ch[ch].arpSeqLen = len;
+	_ch[ch].arpSeqPos = 0;
+}
+
+void DarkSideC64MusicPlayer::applyPortamento(int ch) {
+	if (!_ch[ch].noteActive)
+		return;
+
+	uint16 curFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+	int32 newFreq = (int32)curFreq + _ch[ch].portaDelta;
+
+	// Clamp and check if target reached
+	if (_ch[ch].portaDelta > 0 && (uint16)newFreq >= _ch[ch].portaTarget) {
+		newFreq = _ch[ch].portaTarget;
+	} else if (_ch[ch].portaDelta < 0 && (uint16)newFreq <= _ch[ch].portaTarget) {
+		newFreq = _ch[ch].portaTarget;
+	}
+
+	_ch[ch].freqLo = (uint16)newFreq & 0xFF;
+	_ch[ch].freqHi = ((uint16)newFreq >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyPWModulation(int ch) {
+	uint8 instBase = _ch[ch].instIdx;
+	uint8 pwMod = kInstruments[instBase + 5];
+	if (pwMod == 0)
+		return;
+
+	uint8 flags = kInstruments[instBase + 7];
+
+	uint16 pw = ((uint16)_ch[ch].pwHi << 8) | _ch[ch].pwLo;
+
+	if (flags & 0x04) {
+		// Direct add (one-directional sweep)
+		pw += pwMod;
+	} else {
+		// Triangle sweep between pwHi=$08 and pwHi=$0F
+		if (_ch[ch].pwDirection == 0) {
+			pw += pwMod;
+			if ((_ch[ch].pwHi + (pw >> 8)) >= 0x0F || (pw >> 8) >= 0x0F) {
+				pw = ((uint16)_ch[ch].pwHi << 8) | _ch[ch].pwLo;
+				pw += pwMod;
+				if ((pw >> 8) >= 0x0F)
+					_ch[ch].pwDirection = 1;
+			}
+		} else {
+			pw -= pwMod;
+			if ((pw >> 8) <= 0x08)
+				_ch[ch].pwDirection = 0;
+		}
+	}
+
+	_ch[ch].pwLo = pw & 0xFF;
+	_ch[ch].pwHi = (pw >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyEnvelope(int ch) {
+	if (_ch[ch].envCounter >= 15)
+		return;
+
+	int off = kSIDOffset[ch];
+	uint8 tbl = _ch[ch].envTable;
+
+	// Write waveform control from envelope table
+	sidWrite(off + 4, kEnvControl[tbl][_ch[ch].envCounter]);
+
+	// Write AD register from envelope volume table
+	sidWrite(off + 5, kEnvVolume[tbl][_ch[ch].envCounter]);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/dark/c64.music.h b/engines/freescape/games/dark/c64.music.h
new file mode 100644
index 00000000000..78d2d2f7185
--- /dev/null
+++ b/engines/freescape/games/dark/c64.music.h
@@ -0,0 +1,127 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DARK_C64_MUSIC_H
+#define FREESCAPE_DARK_C64_MUSIC_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// 3-channel SID music player for Dark Side C64.
+// Implements the Wally Beben byte-stream sequencer from darkside.prg ($0901).
+class DarkSideC64MusicPlayer {
+public:
+	DarkSideC64MusicPlayer();
+	~DarkSideC64MusicPlayer();
+
+	void startMusic();
+	void stopMusic();
+	bool isPlaying() const;
+
+private:
+	SID::SID *_sid;
+
+	void sidWrite(int reg, uint8 data);
+	void initSID();
+	void onTimer();
+
+	// Song setup
+	void setupSong();
+	void silenceAll();
+	void loadNextPattern(int ch);
+	void unpackArpeggio(int ch);
+
+	// Per-tick processing
+	void processChannel(int ch, bool newBeat);
+	void parseCommands(int ch);
+	void applyNote(int ch, uint8 note);
+	void applyContinuousEffects(int ch);
+	void applyVibrato(int ch);
+	void applyArpeggio(int ch);
+	void applyPortamento(int ch);
+	void applyPWModulation(int ch);
+	void applyEnvelope(int ch);
+
+	uint8 readPatByte(int ch);
+
+	// Global state
+	bool _musicActive;
+	uint8 _speedDiv;       // $15A1: speed divider
+	uint8 _speedCounter;   // $15A2: frames until next beat
+
+	// Per-channel state
+	struct ChannelState {
+		const uint8 *orderData;
+		uint8 orderPos;        // $15A3
+
+		const uint8 *patData;
+		int patOffset;
+
+		uint8 instIdx;         // $15F6: instrument# * 8
+		uint8 noteActive;      // $1599
+		uint8 curNote;         // $159C: note index (with transpose)
+		uint8 transpose;       // $15C4
+
+		uint8 freqLo;          // $15A6
+		uint8 freqHi;          // $15A9
+		uint8 pwLo;            // $15AC
+		uint8 pwHi;            // $15AF
+
+		uint8 durReload;       // $15B8
+		uint8 durCounter;      // $15BB
+
+		uint8 effectMode;      // $15BE: 0=none, 1=arp/vib1, 2=vib2, 0xDE=portaUp, 0xFE=portaDn
+		uint8 effectParam;     // $15C1
+
+		uint8 arpPattern;      // $15C7
+		uint8 arpParam2;       // $15CA
+		uint8 arpSeqPos;       // $15D9
+		uint8 arpSeqData[20];  // $160C
+		uint8 arpSeqLen;
+
+		int16 portaDelta;      // $15CD/$15CE: per-tick freq delta
+		uint16 portaTarget;    // target frequency for portamento
+
+		uint8 vibPhase;        // $15DE
+		uint8 vibCounter;      // $15E1
+
+		uint8 pwDirection;     // $15D6: 0=up, 1=down
+
+		uint8 delayCounter;    // $15E7
+
+		uint8 envCounter;      // $15FC
+		uint8 envTable;        // envelope table set (0 or 1)
+		bool envSeqActive;     // flags bit 0
+
+		bool sustainMode;      // flags bit 3
+
+		uint8 waveform;        // current waveform ctrl byte (from instrument)
+
+		void reset();
+	};
+
+	ChannelState _ch[3];
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DARK_C64_MUSIC_H
diff --git a/engines/freescape/games/dark/c64.sfx.cpp b/engines/freescape/games/dark/c64.sfx.cpp
new file mode 100644
index 00000000000..ee796b47d54
--- /dev/null
+++ b/engines/freescape/games/dark/c64.sfx.cpp
@@ -0,0 +1,473 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/games/dark/c64.sfx.h"
+#include "freescape/freescape.h"
+
+#include "common/debug.h"
+#include "common/textconsole.h"
+
+namespace Freescape {
+
+// 25 SFX entries extracted from dark2.prg at $C802 (address $C802-$CBEA).
+// Each entry is 40 bytes in the original 6502 format.
+// See SOUND_ANALYSIS.md for full documentation.
+static const DarkSideSFXData kDarkSideSFXData[25] = {
+	// SFX #1: Shoot (Noise, high→silence)
+	{2, 1, 0,
+	 {0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {21, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0xEE, 0x02, 0x80, 0x13, 0xF6},
+
+	// SFX #2: Hit (Noise, quick high→mid x3)
+	{2, 3, 0,
+	 {0x00, 0x44, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {4, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0xEA, 0x01, 0x80, 0x23, 0xDA},
+
+	// SFX #3: Step down (Pulse, high→silence)
+	{2, 1, 0,
+	 {0x00, 0x34, 0x00, 0x00, 0x00, 0x08, 0x00, 0x2C, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {50, 6, 2, 5, 0, 0, 0, 0, 0}, 1, 0,
+	 0x72, 0x06, 0x40, 0x35, 0xF4},
+
+	// SFX #4: Step up (Saw, silence→mid ascending)
+	{2, 1, 0,
+	 {0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {46, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0xA2, 0x03, 0x20, 0x34, 0xF4},
+
+	// SFX #5: Area change (Noise, flat→high sweep)
+	{3, 1, 0,
+	 {0x00, 0x34, 0x00, 0x34, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {14, 9, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0x64, 0x05, 0x80, 0x52, 0x6A},
+
+	// SFX #6: Menu / area select (Triangle, hi→lo→lo→hi bounce)
+	{3, 1, 0,
+	 {0x00, 0x74, 0x00, 0x24, 0x00, 0x24, 0x00, 0x78, 0x00, 0x24,
+	  0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {2, 2, 8, 6, 1, 0, 0, 0, 0}, 2, 0,
+	 0xEA, 0x06, 0x10, 0x19, 0xF9},
+
+	// SFX #7: Noise burst sequence
+	{3, 1, 0,
+	 {0x00, 0x1C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x7C, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {4, 7, 2, 21, 0, 0, 0, 0, 0}, 1, 0,
+	 0x38, 0x0F, 0x80, 0x38, 0xFC},
+
+	// SFX #8: Triangle slide (repeat x5)
+	{2, 5, 0,
+	 {0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {6, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 0,
+	 0xD0, 0x02, 0x10, 0x19, 0xF7},
+
+	// SFX #9: Game start (Triangle, rising 4-note arpeggio)
+	{4, 1, 0,
+	 {0x00, 0x90, 0x00, 0xC0, 0x00, 0x4C, 0x00, 0x4C, 0x00, 0x54,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {7, 10, 6, 8, 0, 0, 0, 0, 0}, 1, 0,
+	 0xC8, 0x00, 0x10, 0x32, 0xF4},
+
+	// SFX #10: Noise burst (repeat x2)
+	{3, 2, 0,
+	 {0x00, 0x68, 0x00, 0x70, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {2, 1, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0x06, 0x04, 0x80, 0x15, 0xF9},
+
+	// SFX #11: Tri+Ring modulation
+	{2, 1, 0,
+	 {0x00, 0x00, 0x00, 0x2C, 0x00, 0x18, 0x00, 0x24, 0x00, 0x90,
+	  0x00, 0x30, 0x00, 0x90, 0x00, 0xB0, 0x00, 0xC0, 0x00, 0x14}, 0,
+	 {4, 8, 14, 19, 18, 4, 8, 3, 0x80}, 1, 0xB0,
+	 0x00, 0xCC, 0x14, 0x25, 0xF7},
+
+	// SFX #12: Long noise sequence (6 notes)
+	{6, 1, 0,
+	 {0x00, 0x4C, 0x00, 0x28, 0x00, 0x34, 0x00, 0x14, 0x00, 0x30,
+	  0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {5, 9, 4, 13, 14, 0, 0, 0, 0}, 1, 0,
+	 0x26, 0x0D, 0x80, 0x16, 0xFA},
+
+	// SFX #13: Noise, 4-note descending
+	{4, 1, 0,
+	 {0x00, 0x7C, 0x00, 0x7C, 0x00, 0x50, 0x00, 0x44, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {2, 4, 4, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0x90, 0x01, 0x80, 0x33, 0xFA},
+
+	// SFX #14: Fall (Pulse, long high→silence slide)
+	{2, 1, 0,
+	 {0x00, 0xFC, 0x00, 0x00, 0x00, 0x90, 0x00, 0x6C, 0x00, 0x6C,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {43, 5, 4, 7, 0, 0, 0, 0, 0}, 2, 0,
+	 0x30, 0x07, 0x40, 0x33, 0x79},
+
+	// SFX #15: ECD destroy pre-noise (7-step rising sweep)
+	{7, 1, 0,
+	 {0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x30, 0x00, 0x50,
+	  0x00, 0xA0, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {9, 12, 11, 5, 4, 2, 0, 0, 0}, 1, 0,
+	 0x80, 0x02, 0x80, 0x35, 0xF9},
+
+	// SFX #16: Tri+Ring (identical to #11)
+	{2, 1, 0,
+	 {0x00, 0x00, 0x00, 0x2C, 0x00, 0x18, 0x00, 0x24, 0x00, 0x90,
+	  0x00, 0x30, 0x00, 0x90, 0x00, 0xB0, 0x00, 0xC0, 0x00, 0x14}, 0,
+	 {4, 8, 14, 19, 18, 4, 8, 3, 0x80}, 1, 0xB0,
+	 0x00, 0xCC, 0x14, 0x25, 0xF7},
+
+	// SFX #17: Triangle slide down
+	{2, 1, 0,
+	 {0x00, 0xB0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {5, 4, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0x90, 0x02, 0x10, 0x36, 0xF6},
+
+	// SFX #18: Sawtooth oscillation (repeat x3)
+	{4, 3, 0,
+	 {0x00, 0xAC, 0x00, 0x84, 0x00, 0xAC, 0x00, 0x84, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {4, 2, 5, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0xAA, 0x05, 0x20, 0x16, 0xF8},
+
+	// SFX #19: Restore ECD (Noise, quick burst)
+	{3, 1, 0,
+	 {0x00, 0x18, 0x00, 0x24, 0x00, 0x14, 0x00, 0x00, 0x00, 0x34,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {2, 3, 2, 9, 0, 0, 0, 0, 0}, 1, 0,
+	 0x62, 0x0D, 0x80, 0x16, 0xFA},
+
+	// SFX #20: No shield / depleted (Triangle, 4-note warning x2)
+	{4, 2, 0,
+	 {0x00, 0x5C, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x60, 0x00, 0x8C,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {4, 4, 4, 5, 0, 0, 0, 0, 0}, 1, 0,
+	 0x88, 0x09, 0x10, 0x14, 0xF6},
+
+	// SFX #21: Sawtooth multi-note (speed=3)
+	{4, 1, 0,
+	 {0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x00, 0xBC, 0x00, 0x8C,
+	  0x00, 0xC0, 0x00, 0x34, 0x00, 0x74, 0x00, 0x48, 0x00, 0x88}, 0,
+	 {2, 6, 15, 4, 18, 4, 4, 5, 2}, 3, 3,
+	 0xEA, 0x0B, 0x20, 0x32, 0xF8},
+
+	// SFX #22: Noise ascending→silence
+	{3, 1, 0,
+	 {0x00, 0xB8, 0x00, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+	  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {8, 7, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0xEA, 0x06, 0x80, 0x32, 0xF7},
+
+	// SFX #23: Pulse pattern (repeat x5)
+	{5, 5, 0,
+	 {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x24, 0x00, 0x00,
+	  0x00, 0x40, 0x00, 0x40, 0x00, 0x30, 0x00, 0x28, 0x00, 0x00}, 0,
+	 {7, 9, 8, 9, 13, 0, 17, 11, 0}, 1, 0,
+	 0x84, 0x08, 0x40, 0x15, 0xF1},
+
+	// SFX #24: Sawtooth sweep (repeat x5)
+	{2, 5, 0,
+	 {0x00, 0xE0, 0x00, 0x44, 0x00, 0x68, 0x00, 0x40, 0x00, 0x40,
+	  0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+	 {6, 0, 5, 0, 0, 0, 0, 0, 0}, 1, 0,
+	 0x12, 0x01, 0x20, 0x39, 0xF8},
+
+	// SFX #25: Noise multi-bounce (speed=2)
+	{4, 1, 0,
+	 {0x00, 0x7C, 0x00, 0x00, 0x00, 0x28, 0x00, 0x28, 0x00, 0x58,
+	  0x00, 0x68, 0x00, 0x6C, 0x00, 0x64, 0x00, 0x54, 0x00, 0x3C}, 0,
+	 {6, 5, 7, 7, 4, 5, 14, 8, 3}, 2, 0,
+	 0x6E, 0x05, 0x80, 0x74, 0xF4},
+};
+
+DarkSideC64SFXPlayer::DarkSideC64SFXPlayer()
+	: _sid(nullptr),
+	  _state(0),
+	  _numNotes(0), _repeatCount(0), _waveform(0), _speed(1),
+	  _repeatLeft(0), _notesLeft(0), _freqIndex(0), _durIndex(0),
+	  _durCounter(0), _speedCounter(0),
+	  _curFreqLo(0), _curFreqHi(0) {
+	memset(_startFreqs, 0, sizeof(_startFreqs));
+	memset(_deltas, 0, sizeof(_deltas));
+	memset(_durCopies, 0, sizeof(_durCopies));
+	initSID();
+}
+
+DarkSideC64SFXPlayer::~DarkSideC64SFXPlayer() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+}
+
+void DarkSideC64SFXPlayer::initSID() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+
+	_sid = SID::Config::create(SID::Config::kSidPAL);
+	if (!_sid || !_sid->init())
+		error("Failed to initialise SID emulator for Dark Side SFX");
+
+	for (int i = 0; i < 0x19; i++)
+		sidWrite(i, 0);
+	sidWrite(kDarkSIDVolume, 0x0F);
+
+	_sid->start(new Common::Functor0Mem<void, DarkSideC64SFXPlayer>(this, &DarkSideC64SFXPlayer::onTimer), 50);
+}
+
+void DarkSideC64SFXPlayer::sidWrite(int reg, uint8 data) {
+	if (_sid) {
+		debugC(4, kFreescapeDebugMedia, "Dark SFX SID Write: Reg $%02X = $%02X", reg, data);
+		_sid->writeReg(reg, data);
+	}
+}
+
+void DarkSideC64SFXPlayer::onTimer() {
+	sfxTick();
+}
+
+bool DarkSideC64SFXPlayer::isSfxActive() const {
+	return _state != 0;
+}
+
+void DarkSideC64SFXPlayer::stopAllSfx() {
+	_state = 0;
+	silenceAll();
+}
+
+void DarkSideC64SFXPlayer::silenceV1() {
+	// $CE5C: zero V1 SID registers ($D400-$D406), clear state
+	_state = 0;
+	for (int i = kDarkSIDV1FreqLo; i <= kDarkSIDV1SR; i++)
+		sidWrite(i, 0);
+}
+
+void DarkSideC64SFXPlayer::silenceAll() {
+	// $CE57: zero all SID registers ($D400-$D413), clear state
+	_state = 0;
+	for (int i = 0; i <= 0x13; i++)
+		sidWrite(i, 0);
+	sidWrite(kDarkSIDVolume, 0x0F);
+}
+
+// Signed 32÷16 division matching the original $2122 routine.
+// Computes (dividend / divisor) as a signed 16-bit result.
+static int16 signedDivide(int16 dividend, uint8 divisor) {
+	if (divisor == 0)
+		return 0;
+	return dividend / (int16)divisor;
+}
+
+void DarkSideC64SFXPlayer::setupSfx(int index) {
+	const DarkSideSFXData &sfx = kDarkSideSFXData[index];
+
+	debugC(1, kFreescapeDebugMedia, "Dark Side C64 SFX: setup #%d (notes=%d repeat=%d wf=$%02X)",
+		   index + 1, sfx.numNotes, sfx.repeatCount, sfx.waveform);
+
+	// Call silence V1 first ($CE5C)
+	silenceV1();
+
+	// Gate off
+	sidWrite(kDarkSIDV1Ctrl, 0);
+
+	// Load SID registers from descriptor
+	sidWrite(kDarkSIDV1PwLo, sfx.pwLo);
+	sidWrite(kDarkSIDV1PwHi, sfx.pwHi);
+	sidWrite(kDarkSIDV1AD, sfx.attackDecay);
+	sidWrite(kDarkSIDV1SR, sfx.sustainRelease);
+
+	// Store working copies
+	_numNotes = sfx.numNotes;
+	_repeatCount = sfx.repeatCount;
+	_waveform = sfx.waveform;
+	_speed = sfx.speed;
+
+	// Compute per-note start frequencies, deltas, and duration copies.
+	// Matches the $CCFC loop: for each note transition, compute
+	// delta = (freq[i+1] - freq[i]) / duration[i]
+	int numTransitions = sfx.numNotes;
+	for (int i = 0; i < numTransitions && i < 9; i++) {
+		// Frequency waypoints are stored as (lo, hi) pairs at offsets 0,2,4...
+		uint16 freqStart = sfx.freqWaypoints[i * 2] | (sfx.freqWaypoints[i * 2 + 1] << 8);
+		uint16 freqEnd = sfx.freqWaypoints[(i + 1) * 2] | (sfx.freqWaypoints[(i + 1) * 2 + 1] << 8);
+
+		_startFreqs[i] = (int16)freqStart;
+
+		int16 diff = (int16)(freqEnd - freqStart);
+		_deltas[i] = signedDivide(diff, sfx.durations[i]);
+
+		_durCopies[i] = sfx.durations[i];
+
+		debugC(2, kFreescapeDebugMedia, "  Note %d: freq $%04X→$%04X dur=%d delta=%d",
+			   i, freqStart, freqEnd, sfx.durations[i], _deltas[i]);
+	}
+
+	// Set state to start phase
+	_state = 1;
+}
+
+void DarkSideC64SFXPlayer::playSfx(int sfxIndex) {
+	// Guard: SFX indices are 1-based, table is 0-based
+	if (sfxIndex < 1 || sfxIndex > 25) {
+		debugC(1, kFreescapeDebugMedia, "Dark Side C64 SFX: invalid index %d", sfxIndex);
+		return;
+	}
+
+	// Guard: if SFX is already active, drop the request ($CC90-$CC93)
+	if (_state != 0) {
+		debugC(2, kFreescapeDebugMedia, "Dark Side C64 SFX: busy, dropping #%d", sfxIndex);
+		return;
+	}
+
+	setupSfx(sfxIndex - 1);
+}
+
+void DarkSideC64SFXPlayer::sfxTick() {
+	if (_state == 0)
+		return;
+
+	if (_state == 1) {
+		tickStart();
+		return;
+	}
+
+	if (_state == 2) {
+		tickSlide();
+		return;
+	}
+
+	// State $FF equivalent: cleanup → inactive
+	_state = 0;
+}
+
+// State 1: Start phase ($CD8B-$CDD1)
+// Load repeat count, reset indices, load first note, gate on, transition to slide.
+void DarkSideC64SFXPlayer::tickStart() {
+	_repeatLeft = _repeatCount;
+
+	// Reset note counters and indices
+	_notesLeft = _numNotes - 1;
+	_freqIndex = 0;
+	_durIndex = 0;
+
+	// Load speed counter
+	_speedCounter = _speed;
+
+	// Load first note frequency
+	uint16 freq = (uint16)_startFreqs[0];
+	_curFreqLo = freq & 0xFF;
+	_curFreqHi = (freq >> 8) & 0xFF;
+	sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+	sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+	// Load first duration
+	_durCounter = _durCopies[0];
+
+	// Gate on: waveform | 0x01
+	sidWrite(kDarkSIDV1Ctrl, _waveform | 0x01);
+
+	// Transition to slide state
+	_state = 2;
+}
+
+// State 2: Slide phase ($CDD2-$CE4E)
+// Decrement speed counter, update frequency with delta, advance notes.
+void DarkSideC64SFXPlayer::tickSlide() {
+	// Speed prescaler
+	_speedCounter--;
+	if (_speedCounter != 0)
+		return;
+
+	// Reload speed counter
+	_speedCounter = _speed;
+
+	// Update frequency: add delta for current note
+	int noteIdx = _freqIndex / 2;
+	uint16 freq = (_curFreqHi << 8) | _curFreqLo;
+	freq = (uint16)((int16)freq + _deltas[noteIdx]);
+	_curFreqLo = freq & 0xFF;
+	_curFreqHi = (freq >> 8) & 0xFF;
+
+	sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+	sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+	// Decrement duration
+	_durCounter--;
+	if (_durCounter != 0)
+		return;
+
+	// Duration expired: advance to next note
+	_freqIndex += 2;
+	_durIndex++;
+	_notesLeft--;
+
+	if (_notesLeft != 0) {
+		// Load next note parameters
+		noteIdx = _freqIndex / 2;
+		_durCounter = _durCopies[_durIndex];
+		_speedCounter = _speed;
+
+		// Load start frequency for next note
+		freq = (uint16)_startFreqs[noteIdx];
+		_curFreqLo = freq & 0xFF;
+		_curFreqHi = (freq >> 8) & 0xFF;
+		sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+		sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+		return;
+	}
+
+	// All notes done: check repeat
+	_repeatLeft--;
+	if (_repeatLeft != 0) {
+		// Restart sequence ($CD91): reload counters, keep same SFX data
+		_notesLeft = _numNotes - 1;
+		_freqIndex = 0;
+		_durIndex = 0;
+		_speedCounter = _speed;
+
+		// Load first note frequency
+		freq = (uint16)_startFreqs[0];
+		_curFreqLo = freq & 0xFF;
+		_curFreqHi = (freq >> 8) & 0xFF;
+		sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+		sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+		_durCounter = _durCopies[0];
+		return;
+	}
+
+	// Sequence finished: gate off
+	_state = 0;
+	sidWrite(kDarkSIDV1Ctrl, _waveform & 0xFE);  // Clear gate bit
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/dark/c64.sfx.h b/engines/freescape/games/dark/c64.sfx.h
new file mode 100644
index 00000000000..297347931d4
--- /dev/null
+++ b/engines/freescape/games/dark/c64.sfx.h
@@ -0,0 +1,131 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DARK_C64_SFX_H
+#define FREESCAPE_DARK_C64_SFX_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// SID register offsets (shared with Driller SFX player)
+enum DarkSIDRegs {
+	kDarkSIDV1FreqLo  = 0x00,
+	kDarkSIDV1FreqHi  = 0x01,
+	kDarkSIDV1PwLo    = 0x02,
+	kDarkSIDV1PwHi    = 0x03,
+	kDarkSIDV1Ctrl    = 0x04,
+	kDarkSIDV1AD      = 0x05,
+	kDarkSIDV1SR      = 0x06,
+
+	kDarkSIDV2FreqLo  = 0x07,
+	kDarkSIDV2FreqHi  = 0x08,
+	kDarkSIDV2PwLo    = 0x09,
+	kDarkSIDV2PwHi    = 0x0A,
+	kDarkSIDV2Ctrl    = 0x0B,
+	kDarkSIDV2AD      = 0x0C,
+	kDarkSIDV2SR      = 0x0D,
+
+	kDarkSIDV3FreqLo  = 0x0E,
+	kDarkSIDV3FreqHi  = 0x0F,
+	kDarkSIDV3PwLo    = 0x10,
+	kDarkSIDV3PwHi    = 0x11,
+	kDarkSIDV3Ctrl    = 0x12,
+	kDarkSIDV3AD      = 0x13,
+	kDarkSIDV3SR      = 0x14,
+
+	kDarkSIDFilterLo  = 0x15,
+	kDarkSIDFilterHi  = 0x16,
+	kDarkSIDFilterCtrl = 0x17,
+	kDarkSIDVolume    = 0x18
+};
+
+// 40-byte SFX descriptor from the data table at $C802 in dark2.prg
+struct DarkSideSFXData {
+	uint8 numNotes;        // Number of frequency transitions
+	uint8 repeatCount;     // Times to replay the full sequence
+	uint8 reserved;
+	uint8 freqWaypoints[20]; // Up to 10 frequency waypoints (lo,hi pairs)
+	uint8 padding;         // Offset 23
+	uint8 durations[9];    // Duration for each transition (in speed units)
+	uint8 speed;           // Frames per speed unit
+	uint8 padding2;        // Offset 34
+	uint8 pwLo;            // Pulse Width low byte
+	uint8 pwHi;            // Pulse Width high byte
+	uint8 waveform;        // SID control register (gate bit managed separately)
+	uint8 attackDecay;     // SID Attack/Decay register
+	uint8 sustainRelease;  // SID Sustain/Release register
+};
+
+class DarkSideC64SFXPlayer {
+public:
+	DarkSideC64SFXPlayer();
+	~DarkSideC64SFXPlayer();
+
+	void playSfx(int sfxIndex);
+	void sfxTick();
+	void stopAllSfx();
+
+	bool isSfxActive() const;
+
+private:
+	SID::SID *_sid;
+
+	void sidWrite(int reg, uint8 data);
+	void initSID();
+	void onTimer();
+
+	// State machine ($C801 equivalent)
+	uint8 _state;          // 0=off, 1=start, 2=slide
+
+	// Work buffer (copied from SFX data table)
+	uint8 _numNotes;
+	uint8 _repeatCount;
+	uint8 _waveform;
+	uint8 _speed;
+
+	// Runtime state
+	uint8 _repeatLeft;     // $CE52: remaining sequence repeats
+	uint8 _notesLeft;      // $CE55: remaining notes in this pass
+	uint8 _freqIndex;      // $CE54: index into freq/delta arrays (by 2)
+	uint8 _durIndex;       // $CE53: index into duration array (by 1)
+	uint8 _durCounter;     // $CE4F: remaining ticks for current note
+	uint8 _speedCounter;   // $CE56: frames until next freq update
+
+	// Current frequency (16-bit)
+	uint8 _curFreqLo;      // $CE50
+	uint8 _curFreqHi;      // $CE51
+
+	// Precomputed start frequencies and deltas per note
+	int16 _startFreqs[16]; // $CE97: starting frequency for each note (lo,hi)
+	int16 _deltas[16];     // $CEA6: per-speed-unit frequency delta per note
+	uint8 _durCopies[9];   // $CEB5: copy of durations per note
+
+	void silenceV1();
+	void silenceAll();
+	void setupSfx(int index);
+	void tickStart();
+	void tickSlide();
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DARK_C64_SFX_H
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index f84873213f2..06dcc0c8904 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -34,6 +34,9 @@
 namespace Freescape {
 
 DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
+	_playerC64Sfx = nullptr;
+	_playerC64Music = nullptr;
+
 	// These sounds can be overriden by the class of each platform
 	_soundIndexShoot = 1;
 	_soundIndexCollide = -1;
@@ -88,6 +91,11 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 	_jetFuelSeconds = _initialEnergy * 6;
 }
 
+DarkEngine::~DarkEngine() {
+	delete _playerC64Sfx;
+	delete _playerC64Music;
+}
+
 void DarkEngine::addECDs(Area *area) {
 	if (!area->entranceWithID(255))
 		return;
@@ -304,6 +312,9 @@ void DarkEngine::initGameState() {
 				&_musicHandle, musicStream);
 		}
 	}
+
+	if (isC64() && _playerC64Music)
+		_playerC64Music->startMusic();
 }
 
 void DarkEngine::loadAssets() {
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index b2af7cd7923..b81d4649c14 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -21,6 +21,8 @@
 
 #include "audio/mixer.h"
 #include "common/array.h"
+#include "freescape/games/dark/c64.music.h"
+#include "freescape/games/dark/c64.sfx.h"
 
 namespace Freescape {
 
@@ -49,6 +51,7 @@ enum DarkFontSize {
 class DarkEngine : public FreescapeEngine {
 public:
 	DarkEngine(OSystem *syst, const ADGameDescription *gd);
+	~DarkEngine();
 
 	uint32 _initialEnergy;
 	uint32 _initialShield;
@@ -105,6 +108,10 @@ public:
 	int _soundIndexDestroyECD;
 	Audio::SoundHandle _soundFxHandleJetpack;
 
+	DarkSideC64SFXPlayer *_playerC64Sfx;
+	DarkSideC64MusicPlayer *_playerC64Music;
+	void playSoundC64(int index) override;
+
 	Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
 
 	void drawString(const DarkFontSize size, const Common::String &str, int x, int y, uint32 primaryColor, uint32 secondaryColor, uint32 backColor, Graphics::Surface *surface);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 321ad4e2140..b4ba06e75ad 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -17,6 +17,8 @@ MODULE_OBJS := \
 	games/dark/amiga.o \
 	games/dark/atari.o \
 	games/dark/c64.o \
+	games/dark/c64.music.o \
+	games/dark/c64.sfx.o \
 	games/dark/cpc.o \
 	games/dark/dark.o \
 	games/dark/dos.o \


Commit: 1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f
    https://github.com/scummvm/scummvm/commit/1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00

Commit Message:
FREESCAPE: allow to switch between music/sound in c64

Changed paths:
    engines/freescape/games/dark/c64.cpp
    engines/freescape/games/dark/c64.music.cpp
    engines/freescape/games/dark/c64.music.h
    engines/freescape/games/dark/c64.sfx.cpp
    engines/freescape/games/dark/c64.sfx.h
    engines/freescape/games/dark/dark.cpp
    engines/freescape/games/dark/dark.h
    engines/freescape/games/driller/c64.cpp
    engines/freescape/games/driller/c64.music.cpp
    engines/freescape/games/driller/c64.music.h
    engines/freescape/games/driller/c64.sfx.cpp
    engines/freescape/games/driller/c64.sfx.h
    engines/freescape/games/driller/driller.cpp
    engines/freescape/games/driller/driller.h


diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index bd5f973747a..1d4b1d521d9 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -163,18 +163,38 @@ void DarkEngine::loadAssetsC64FullGame() {
 
 	_title = loadAndConvertDoodleImage(&file, &colorFile1, &colorFile2, (byte *)&kC64Palette);
 
-	// TODO: SFX and music both need a SID instance, but only one can be active at a time.
-	// Disable SFX for now so the music player can work.
-	//_playerC64Sfx = new DarkSideC64SFXPlayer();
+	// Only one SID instance can be active at a time; music is the default.
+	// Create the inactive player first so its SID is destroyed before
+	// the active player's SID is created.
+	_playerC64Sfx = new DarkSideC64SFXPlayer();
+	_playerC64Sfx->destroySID();
 	_playerC64Music = new DarkSideC64MusicPlayer();
 }
 
 void DarkEngine::playSoundC64(int index) {
 	debugC(1, kFreescapeDebugMedia, "Playing Dark Side C64 SFX %d", index);
-	if (_playerC64Sfx)
+	if (_playerC64Sfx && _c64UseSFX)
 		_playerC64Sfx->playSfx(index);
 }
 
+void DarkEngine::toggleC64Sound() {
+	if (_c64UseSFX) {
+		if (_playerC64Sfx)
+			_playerC64Sfx->destroySID();
+		if (_playerC64Music) {
+			_playerC64Music->initSID();
+			_playerC64Music->startMusic();
+		}
+		_c64UseSFX = false;
+	} else {
+		if (_playerC64Music)
+			_playerC64Music->destroySID();
+		if (_playerC64Sfx)
+			_playerC64Sfx->initSID();
+		_c64UseSFX = true;
+	}
+}
+
 void DarkEngine::drawC64UI(Graphics::Surface *surface) {
 	uint8 r, g, b;
 	uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xAA, 0xAA, 0xAA);
diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
index 9310b2d80a2..bc0a39a2e52 100644
--- a/engines/freescape/games/dark/c64.music.cpp
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -213,6 +213,7 @@ void DarkSideC64MusicPlayer::ChannelState::reset() {
 }
 
 DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
+	_sid = nullptr;
 	_musicActive = false;
 	_speedDiv = 1;
 	_speedCounter = 1;
@@ -222,12 +223,23 @@ DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
 }
 
 DarkSideC64MusicPlayer::~DarkSideC64MusicPlayer() {
-	if (_sid)
+	destroySID();
+}
+
+void DarkSideC64MusicPlayer::destroySID() {
+	if (_sid) {
 		_sid->stop();
-	delete _sid;
+		delete _sid;
+		_sid = nullptr;
+	}
 }
 
 void DarkSideC64MusicPlayer::initSID() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+
 	_sid = SID::Config::create(SID::Config::kSidPAL);
 	if (!_sid || !_sid->init()) {
 		warning("DarkSideC64MusicPlayer: Failed to create SID emulator");
diff --git a/engines/freescape/games/dark/c64.music.h b/engines/freescape/games/dark/c64.music.h
index 78d2d2f7185..ffbd7a8427b 100644
--- a/engines/freescape/games/dark/c64.music.h
+++ b/engines/freescape/games/dark/c64.music.h
@@ -36,12 +36,13 @@ public:
 	void startMusic();
 	void stopMusic();
 	bool isPlaying() const;
+	void initSID();
+	void destroySID();
 
 private:
 	SID::SID *_sid;
 
 	void sidWrite(int reg, uint8 data);
-	void initSID();
 	void onTimer();
 
 	// Song setup
diff --git a/engines/freescape/games/dark/c64.sfx.cpp b/engines/freescape/games/dark/c64.sfx.cpp
index ee796b47d54..9ba591e692b 100644
--- a/engines/freescape/games/dark/c64.sfx.cpp
+++ b/engines/freescape/games/dark/c64.sfx.cpp
@@ -221,9 +221,14 @@ DarkSideC64SFXPlayer::DarkSideC64SFXPlayer()
 }
 
 DarkSideC64SFXPlayer::~DarkSideC64SFXPlayer() {
+	destroySID();
+}
+
+void DarkSideC64SFXPlayer::destroySID() {
 	if (_sid) {
 		_sid->stop();
 		delete _sid;
+		_sid = nullptr;
 	}
 }
 
diff --git a/engines/freescape/games/dark/c64.sfx.h b/engines/freescape/games/dark/c64.sfx.h
index 297347931d4..58ec8fe9172 100644
--- a/engines/freescape/games/dark/c64.sfx.h
+++ b/engines/freescape/games/dark/c64.sfx.h
@@ -85,12 +85,13 @@ public:
 	void stopAllSfx();
 
 	bool isSfxActive() const;
+	void initSID();
+	void destroySID();
 
 private:
 	SID::SID *_sid;
 
 	void sidWrite(int reg, uint8 data);
-	void initSID();
 	void onTimer();
 
 	// State machine ($C801 equivalent)
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 06dcc0c8904..25198558757 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -36,6 +36,7 @@ namespace Freescape {
 DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
 	_playerC64Sfx = nullptr;
 	_playerC64Music = nullptr;
+	_c64UseSFX = false;
 
 	// These sounds can be overriden by the class of each platform
 	_soundIndexShoot = 1;
@@ -971,12 +972,17 @@ void DarkEngine::drawInfoMenu() {
 					_eventManager->purgeKeyboardEvents();
 					saveGameDialog();
 					_gfx->setViewport(_viewArea);
+				} else if (isC64() && event.customType == kActionToggleSound) {
+					toggleC64Sound();
+					_eventManager->purgeKeyboardEvents();
 				} else if (isDOS() && event.customType == kActionToggleSound) {
 					playSound(6, true, _soundFxHandle);
+					_eventManager->purgeKeyboardEvents();
 				} else if (event.customType == kActionEscape) {
 					_forceEndGame = true;
 					cont = false;
-				}
+				} else
+					cont = false;
 				break;
 				case Common::EVENT_KEYDOWN:
 					cont = false;
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index b81d4649c14..ea2f1e733a9 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -110,7 +110,9 @@ public:
 
 	DarkSideC64SFXPlayer *_playerC64Sfx;
 	DarkSideC64MusicPlayer *_playerC64Music;
+	bool _c64UseSFX;
 	void playSoundC64(int index) override;
+	void toggleC64Sound();
 
 	Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
 
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index a12e2733b4c..0af0b4588e3 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -158,10 +158,12 @@ void DrillerEngine::loadAssetsC64FullGame() {
 	} else
 		error("Unknown C64 release");
 
-	// TODO: Re-enable music player once SFX coexistence is resolved.
-	// The SID emulator enforces a singleton, so only one instance can exist.
-	// _playerSid = new DrillerSIDPlayer();
+	// Only one SID instance can be active at a time; music is the default.
+	// Create the inactive player first so its SID is destroyed before
+	// the active player's SID is created.
 	_playerC64Sfx = new DrillerC64SFXPlayer();
+	_playerC64Sfx->destroySID();
+	_playerSid = new DrillerSIDPlayer();
 
 	// C64 SFX index mapping
 	// Based on analysis of the C64 binary SFX routines
@@ -185,10 +187,27 @@ void DrillerEngine::loadAssetsC64FullGame() {
 
 void DrillerEngine::playSoundC64(int index) {
 	debugC(1, kFreescapeDebugMedia, "Playing C64 SFX %d", index);
-	if (_playerC64Sfx)
+	if (_playerC64Sfx && _c64UseSFX)
 		_playerC64Sfx->playSfx(index);
 }
 
+void DrillerEngine::toggleC64Sound() {
+	if (_c64UseSFX) {
+		if (_playerC64Sfx)
+			_playerC64Sfx->destroySID();
+		if (_playerSid) {
+			_playerSid->initSID();
+			_playerSid->startMusic();
+		}
+		_c64UseSFX = false;
+	} else {
+		if (_playerSid)
+			_playerSid->destroySID();
+		if (_playerC64Sfx)
+			_playerC64Sfx->initSID();
+		_c64UseSFX = true;
+	}
+}
 
 void DrillerEngine::drawC64UI(Graphics::Surface *surface) {
 
diff --git a/engines/freescape/games/driller/c64.music.cpp b/engines/freescape/games/driller/c64.music.cpp
index 68661ca076d..90eac0d1968 100644
--- a/engines/freescape/games/driller/c64.music.cpp
+++ b/engines/freescape/games/driller/c64.music.cpp
@@ -192,12 +192,16 @@ DrillerSIDPlayer::DrillerSIDPlayer() : _sid(nullptr),
 }
 
 DrillerSIDPlayer::~DrillerSIDPlayer() {
+	destroySID();
+	debug(DEBUG_LEVEL >= 1, "Driller SID Player Destroyed");
+}
+
+void DrillerSIDPlayer::destroySID() {
 	if (_sid) {
 		_sid->stop();
 		delete _sid;
+		_sid = nullptr;
 	}
-
-	debug(DEBUG_LEVEL >= 1, "Driller SID Player Destroyed");
 }
 
 // Tune 0 seems unused, Tune 1 is the main theme
diff --git a/engines/freescape/games/driller/c64.music.h b/engines/freescape/games/driller/c64.music.h
index 57168d0e2d1..4cdcc587f5d 100644
--- a/engines/freescape/games/driller/c64.music.h
+++ b/engines/freescape/games/driller/c64.music.h
@@ -220,10 +220,11 @@ public:
 	~DrillerSIDPlayer();
 	void startMusic(int tuneIndex = 1);
 	void stopMusic();
+	void initSID();
+	void destroySID();
 
 private:
 	void SID_Write(int reg, uint8_t data);
-	void initSID();
 	void onTimer();
 	void handleChangeTune(int tuneIndex);
 	void handleResetVoices();
diff --git a/engines/freescape/games/driller/c64.sfx.cpp b/engines/freescape/games/driller/c64.sfx.cpp
index 2a6cf712a0c..5a494e0f7e1 100644
--- a/engines/freescape/games/driller/c64.sfx.cpp
+++ b/engines/freescape/games/driller/c64.sfx.cpp
@@ -39,9 +39,14 @@ DrillerC64SFXPlayer::DrillerC64SFXPlayer()
 }
 
 DrillerC64SFXPlayer::~DrillerC64SFXPlayer() {
+	destroySID();
+}
+
+void DrillerC64SFXPlayer::destroySID() {
 	if (_sid) {
 		_sid->stop();
 		delete _sid;
+		_sid = nullptr;
 	}
 }
 
diff --git a/engines/freescape/games/driller/c64.sfx.h b/engines/freescape/games/driller/c64.sfx.h
index 325cb4b3a7f..e63b4201323 100644
--- a/engines/freescape/games/driller/c64.sfx.h
+++ b/engines/freescape/games/driller/c64.sfx.h
@@ -68,12 +68,13 @@ public:
 	void stopAllSfx();
 
 	bool isSfxActive() const;
+	void initSID();
+	void destroySID();
 
 private:
 	SID::SID *_sid;
 
 	void sidWrite(int reg, uint8 data);
-	void initSID();
 
 	// Voice 1 pitch slide state ($CC5B-$CC61)
 	uint8 _v1Counter;      // 0xFF=inactive, 0=expired (marked 0xFF next tick)
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 3a1fa4de6f8..fa4691b37a5 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -98,6 +98,7 @@ DrillerEngine::DrillerEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 	_borderExtraTexture = nullptr;
 	_playerSid = nullptr;
 	_playerC64Sfx = nullptr;
+	_c64UseSFX = false;
 }
 
 DrillerEngine::~DrillerEngine() {
@@ -265,8 +266,8 @@ void DrillerEngine::gotoArea(uint16 areaID, int entranceID) {
 
 	if (areaID == _startArea && entranceID == _startEntrance) {
 		if (isC64()) {
-			// TODO: Re-enable music once SFX coexistence is resolved
-			// _playerSid->startMusic();
+			if (!_c64UseSFX && _playerSid)
+				_playerSid->startMusic();
 			playSound(_soundIndexStart, true, _soundFxHandle);
 		} else {
 			playSound(_soundIndexStart, true, _soundFxHandle);
@@ -427,8 +428,12 @@ void DrillerEngine::drawInfoMenu() {
 	} else if (isSpectrum()) {
 		drawStringInSurface("l-load s-save 1-abort", 76, 97, front, black, surface);
 		drawStringInSurface("any other key-continue", 76, 105, front, black, surface);
-	} else if (isAmiga() || isAtariST())
+	} else if (isAmiga() || isAtariST()) {
 		drawStringInSurface("press any key to continue", 66, 97, front, black, surface);
+	} else if (isC64())	{
+		drawStringInSurface("l-load s-save run/stop-abort", 76, 97, front, black, surface);
+		drawStringInSurface("t-toggle effect/music", 76, 105, front, black, surface);
+	}
 
 	Texture *menuTexture = _gfx->createTexture(surface);
 	Common::Event event;
@@ -449,6 +454,8 @@ void DrillerEngine::drawInfoMenu() {
 					_eventManager->purgeKeyboardEvents();
 					saveGameDialog();
 					_gfx->setViewport(_viewArea);
+				} else if (isC64() && event.customType == kActionToggleSound) {
+					toggleC64Sound();
 				} else if (isDOS() && event.customType == kActionToggleSound) {
 					// TODO
 				} else if ((isDOS() || isCPC() || isSpectrum()) && event.customType == kActionEscape) {
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index 167ba9d2bcb..52ea8da286d 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -47,8 +47,10 @@ public:
 
 	DrillerSIDPlayer *_playerSid;
 	DrillerC64SFXPlayer *_playerC64Sfx;
+	bool _c64UseSFX;
 
 	void playSoundC64(int index) override;
+	void toggleC64Sound();
 
 	// Only used for Amiga and Atari ST
 	Font _fontSmall;




More information about the Scummvm-git-logs mailing list