[Scummvm-git-logs] scummvm master -> 0e0db53cc20b2d319377268747d02bfdbb7ba62d

neuromancer noreply at scummvm.org
Mon Mar 30 09:32:53 UTC 2026


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

Summary:
17a050bcf2 FREESCAPE: restored thunder for castle cpc
f1a8a11bc9 FREESCAPE: enable multi language support in castle cpc
91c3070977 FREESCAPE: only run fillColorPairArray for CPC
83eb589944 FREESCAPE: improved driller music for C64
d9f552b451 FREESCAPE: improved dark music for C64
cceaa7b31d FREESCAPE: implement standing/flying indicator in dark for C64
49abb97a8a FREESCAPE: more indicators in dark for C64
0e0db53cc2 FREESCAPE: added music for eclipse c64


Commit: 17a050bcf264b9d1f0ab9a64021b84a5640f4377
    https://github.com/scummvm/scummvm/commit/17a050bcf264b9d1f0ab9a64021b84a5640f4377
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: restored thunder for castle cpc

Changed paths:
    engines/freescape/games/castle/cpc.cpp


diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index cc8dc6d0ead..47b6fe8fac8 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -115,6 +115,28 @@ byte mountainsData[288] {
 	0xaa, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
 };
 
+// Data for the thunder frames. This is not included in the original game for some reason
+// but all the other releases have it. This is coming from the ZX Spectrum version.
+// Each row stores two 2-byte variants side by side, so we decode it as a
+// 4-byte-wide bitmap and split it into the two thunder frames.
+byte thunderData[172] {
+	0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40, 0x00, 0x40,
+	0x00, 0x40, 0x00, 0x80, 0x01, 0x80, 0x01, 0x00, 0x01, 0x00, 0x02, 0x00,
+	0x04, 0x00, 0x08, 0x00, 0x18, 0x00, 0x10, 0x00, 0x30, 0x00, 0x20, 0x00,
+	0x20, 0x00, 0x20, 0x00, 0x70, 0x00, 0x50, 0x00, 0x50, 0x00, 0x88, 0x00,
+	0x08, 0x00, 0x04, 0x00, 0x02, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00,
+	0x01, 0x00, 0x00, 0x80, 0x00, 0xc0, 0x00, 0x40, 0x00, 0x20, 0x00, 0x10,
+	0x00, 0x08, 0x00, 0x0c, 0x00, 0x1c, 0x00, 0x32, 0x00, 0x22, 0x00, 0xc2,
+	0x01, 0x81, 0x02, 0x01, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00,
+	0x02, 0x00, 0x02, 0x00, 0x06, 0x00, 0x04, 0x00, 0x04, 0x00, 0x0c, 0x00,
+	0x18, 0x00, 0x30, 0x00, 0x20, 0x00, 0x20, 0x00, 0x70, 0x00, 0x4c, 0x00,
+	0x86, 0x00, 0x02, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x80, 0x00, 0x80,
+	0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x80, 0x00, 0x60,
+	0x00, 0x30, 0x00, 0x08, 0x00, 0x08, 0x00, 0x06, 0x00, 0x02, 0x00, 0x02,
+	0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x02, 0x00, 0x03, 0x00, 0x01,
+	0x00, 0x01, 0x00, 0x00
+};
+
 
 
 // Expand a 5-byte CPC riddle frame row definition into a 240-pixel CLUT8 row.
@@ -224,6 +246,21 @@ void CastleEngine::loadAssetsCPCFullGame() {
 
 	_background = loadFrame(&mountainsStream, background, backgroundWidth, backgroundHeight, front);
 
+	Common::MemoryReadStream thunderStream(thunderData, sizeof(thunderData));
+	Graphics::ManagedSurface *thunderFrame = new Graphics::ManagedSurface();
+	thunderFrame->create(4 * 8, 43, _gfx->_texturePixelFormat);
+	thunderFrame->fillRect(Common::Rect(0, 0, 4 * 8, 43), 0);
+	thunderFrame = loadFrame(&thunderStream, thunderFrame, 4, 43, front);
+
+	_thunderFrames.push_back(new Graphics::ManagedSurface);
+	_thunderFrames.push_back(new Graphics::ManagedSurface);
+	_thunderFrames[0]->create(2 * 8, 43, _gfx->_texturePixelFormat);
+	_thunderFrames[1]->create(2 * 8, 43, _gfx->_texturePixelFormat);
+	_thunderFrames[0]->copyRectToSurface(*thunderFrame, 0, 0, Common::Rect(0, 0, 2 * 8, 43));
+	_thunderFrames[1]->copyRectToSurface(*thunderFrame, 0, 0, Common::Rect(2 * 8, 0, 4 * 8, 43));
+	thunderFrame->free();
+	delete thunderFrame;
+
 	// CPC UI Sprites stored as CLUT8 (indexed by ink 0-3).
 	// On real CPC hardware, the 4-color palette changes per area, automatically
 	// recoloring everything. We store CLUT8 sprites and setPalette + convert


Commit: f1a8a11bc9cee36fc87fc2c3b7a0701f8757b412
    https://github.com/scummvm/scummvm/commit/f1a8a11bc9cee36fc87fc2c3b7a0701f8757b412
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: enable multi language support in castle cpc

Changed paths:
    engines/freescape/detection.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/cpc.cpp
    engines/freescape/ui.cpp


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 7a27cfd9174..63778cce8fc 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -874,7 +874,7 @@ static const ADGameDescription gameDescriptions[] = {
 			{"CMSCR.BIN", 0, "75fe4a8af0ca797c51922f0ceeb8d383", 16512},
 			AD_LISTEND
 		},
-		Common::EN_ANY,
+		Common::UNK_LANG, // Multi-language
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
 		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 6cc0ba831c4..7fdfc7f1ad5 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -1011,7 +1011,36 @@ void CastleEngine::drawInfoMenu() {
 		Common::Array<Common::String> lines;
 		lines.push_back(centerAndPadString("********************", 21));
 
-		if (_language == Common::EN_ANY) {
+		if (isCPC() && _messagesList.size() > 74) {
+			Common::String commandLine = _messagesList[68];
+			Common::String keysLabel = _messagesList[69];
+			Common::String spiritsLabel = _messagesList[70];
+			Common::String strengthLabel = _messagesList[71];
+			Common::String keysText = _messagesList[72];
+			Common::String spiritsText = _messagesList[73];
+			Common::String scoreText = _messagesList[74];
+			Common::String strengthText = _messagesList[62 + shield / 6];
+
+			commandLine.trim();
+			keysLabel.trim();
+			spiritsLabel.trim();
+			strengthLabel.trim();
+			keysText.trim();
+			spiritsText.trim();
+			scoreText.trim();
+			strengthText.trim();
+
+			Common::replace(keysText, "XX", Common::String::format("%2d", _keysCollected.size()));
+			Common::replace(spiritsText, "XX", Common::String::format("%2d", spiritsDestroyed));
+			Common::replace(scoreText, "XXXXXXX", Common::String::format("%07d", score));
+
+			lines.push_back(centerAndPadString(commandLine, 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString(Common::String::format("%s %s", keysLabel.c_str(), keysText.c_str()), 21));
+			lines.push_back(centerAndPadString(Common::String::format("%s %s", spiritsLabel.c_str(), spiritsText.c_str()), 21));
+			lines.push_back(centerAndPadString(Common::String::format("%s %s", strengthLabel.c_str(), strengthText.c_str()), 21));
+			lines.push_back(centerAndPadString(scoreText, 21));
+		} else if (_language == Common::EN_ANY) {
 			lines.push_back(centerAndPadString("s-save l-load q-quit", 21));
 			lines.push_back("");
 			lines.push_back(centerAndPadString(Common::String::format("keys   %d collected", _keysCollected.size()), 21));
@@ -1026,7 +1055,12 @@ void CastleEngine::drawInfoMenu() {
 			lines.push_back(centerAndPadString(Common::String::format("fuerza  %s", _messagesList[62 + shield / 6].c_str()), 21));
 			lines.push_back(centerAndPadString(Common::String::format("puntos   %07d", score), 21));
 		} else {
-			error("Language not supported");
+			lines.push_back(centerAndPadString("s-save l-load q-quit", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString(Common::String::format("keys   %d collected", _keysCollected.size()), 21));
+			lines.push_back(centerAndPadString(Common::String::format("spirits  %d destroyed", spiritsDestroyed), 21));
+			lines.push_back(centerAndPadString(Common::String::format("strength  %s", _messagesList[62 + shield / 6].c_str()), 21));
+			lines.push_back(centerAndPadString(Common::String::format("score   %07d", score), 21));
 		}
 
 		lines.push_back("");
@@ -1258,7 +1292,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
 		else if (_language == Common::ES_ESP)
 			scoreString = "PUNTOS XXXXXXX";
 		else
-			error("Language not supported");
+			scoreString = "SCORE XXXXXXX";
 	}
 
 	Common::replace(scoreString, "XXXXXXX", Common::String::format("%07d", score));
@@ -1273,7 +1307,7 @@ void CastleEngine::drawFullscreenGameOverAndWait() {
 		else if (_language == Common::ES_ESP)
 			spiritsDestroyedString = "X DESTRUIDOS";
 		else
-			error("Language not supported");
+			spiritsDestroyedString = "X DESTROYED";
 	}
 
 	Common::replace(spiritsDestroyedString, "X", Common::String::format("%d", spiritsDestroyed));
@@ -1479,8 +1513,14 @@ void CastleEngine::loadRiddles(Common::SeekableReadStream *file, int offset, int
 			x = file->readByte();
 			y = file->readByte();
 			int size = file->readByte();
+			const uint32 recordOffset = file->pos() - 3;
 			debugC(1, kFreescapeDebugParser, "size: %d (max %d?)", size, maxLineSize);
 
+			// Castle CPC French has one malformed riddle record in CM.BIN where the
+			// stored size for "NOUS PARVIENT" is 11 instead of 13.
+			if (isCPC() && _language == Common::FR_FRA && recordOffset == 0x86d && size == 11)
+				size = 13;
+
 			Common::String message = "";
 			if (size == 255) {
 				size = 19;
@@ -1968,6 +2008,39 @@ void CastleEngine::selectCharacterScreen() {
 			lines.push_back(centerAndPadString("2. Princesa", 21));
 			lines.push_back("");
 			lines.push_back(centerAndPadString("*******************", 21));
+		} else if (isCPC() && _language == Common::FR_FRA) {
+			// Original Castle Master CPC multilingual strings from CM.BIN @ 0x143.
+			lines.push_back(centerAndPadString("*******************", 21));
+			lines.push_back(centerAndPadString("SELECTIONNEZ LE", 21));
+			lines.push_back(centerAndPadString("PERSONNAGE DESIRE ET", 21));
+			lines.push_back(centerAndPadString("APPUYEZ SUR RETURN", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("1. PRINCE", 21));
+			lines.push_back(centerAndPadString("2. PRINCESSE", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("*******************", 21));
+		} else if (isCPC() && _language == Common::DE_DEU) {
+			// Original Castle Master CPC multilingual strings from CM.BIN @ 0x1ac.
+			lines.push_back(centerAndPadString("*******************", 21));
+			lines.push_back(centerAndPadString("GEWUNSCHTE FIGUR", 21));
+			lines.push_back(centerAndPadString("WAHLEN UND", 21));
+			lines.push_back(centerAndPadString("RETURN DRUCKEN", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("1. PRINZ", 21));
+			lines.push_back(centerAndPadString("2. PRINZESSIN", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("*******************", 21));
+		} else if (isCPC()) {
+			// Original Castle Master CPC English strings from CM.BIN @ 0xda.
+			lines.push_back(centerAndPadString("*******************", 21));
+			lines.push_back(centerAndPadString("SELECT THE CHARACTER", 21));
+			lines.push_back(centerAndPadString("YOU WISH TO PLAY", 21));
+			lines.push_back(centerAndPadString("AND PRESS RETURN", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("1. PRINCE", 21));
+			lines.push_back(centerAndPadString("2. PRINCESS", 21));
+			lines.push_back("");
+			lines.push_back(centerAndPadString("*******************", 21));
 		} else {
 			lines.push_back(centerAndPadString("*******************", 21));
 			lines.push_back(centerAndPadString("Select the character", 21));
diff --git a/engines/freescape/games/castle/cpc.cpp b/engines/freescape/games/castle/cpc.cpp
index 47b6fe8fac8..f19a2a775dc 100644
--- a/engines/freescape/games/castle/cpc.cpp
+++ b/engines/freescape/games/castle/cpc.cpp
@@ -192,7 +192,10 @@ void CastleEngine::loadAssetsCPCFullGame() {
 	if (!file.isOpen())
 		error("Failed to open TECODE.BIN/TE2.BI2");
 
-	loadMessagesVariableSize(&file, 0x16c6, 71);
+	int messagesOffset = -1;
+	int riddlesOffset = -1;
+	// Multi-language CM.BIN keeps per-language message/riddle blocks in-place:
+	// FR at 0x027B/0x0716, DE at 0x0A47/0x0EE2, EN at 0x16C6/0x1B61.
 	switch (_language) {
 		/*case Common::ES_ESP:
 			loadRiddles(&file, 0x1470 - 4 - 2 - 9 * 2, 9);
@@ -211,27 +214,40 @@ void CastleEngine::loadAssetsCPCFullGame() {
 			_fontLoaded = true;
 
 			break;*/
+		case Common::FR_FRA:
+			messagesOffset = 0x027b;
+			riddlesOffset = 0x0716;
+			break;
+		case Common::DE_DEU:
+			messagesOffset = 0x0a47;
+			riddlesOffset = 0x0ee2;
+			break;
 		case Common::EN_ANY:
-			loadRiddles(&file, 0x1b75 - 2 - 9 * 2, 9);
-			load8bitBinary(&file, 0x791a, 16);
-			loadSoundsCPC(&file, 0x21E2, 48, 0x2212, 204, 0x2179, 105);
-
-			file.seek(0x2724);
-			for (int i = 0; i < 90; i++) {
-				Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
-				surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
-				chars.push_back(loadFrame(&file, surface, 1, 8, 1));
-			}
-			_font = Font(chars);
-			_font.setCharWidth(9);
-			_fontLoaded = true;
-
+			messagesOffset = 0x16c6;
+			riddlesOffset = 0x1b75 - 2 - 9 * 2;
 			break;
 		default:
 			error("Language not supported");
 			break;
 	}
 
+	// Castle Master CPC keeps the info-menu strings in entries 68..74, just before
+	// the tape/disk prompt block and before the riddle table for each language.
+	loadMessagesVariableSize(&file, messagesOffset, 75);
+	loadRiddles(&file, riddlesOffset, 9);
+	load8bitBinary(&file, 0x791a, 16);
+	loadSoundsCPC(&file, 0x21E2, 48, 0x2212, 204, 0x2179, 105);
+
+	file.seek(0x2724);
+	for (int i = 0; i < 90; i++) {
+		Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+		surface->create(8, 8, Graphics::PixelFormat::createFormatCLUT8());
+		chars.push_back(loadFrame(&file, surface, 1, 8, 1));
+	}
+	_font = Font(chars);
+	_font.setCharWidth(9);
+	_fontLoaded = true;
+
 	loadColorPalette();
 
 	int backgroundWidth = 16;
diff --git a/engines/freescape/ui.cpp b/engines/freescape/ui.cpp
index dca45969c04..30b948573d4 100644
--- a/engines/freescape/ui.cpp
+++ b/engines/freescape/ui.cpp
@@ -246,6 +246,26 @@ void FreescapeEngine::borderScreen() {
 				lines.push_back("");
 				lines.push_back(centerAndPadString("ENTER: EMPEZAR MISION", pad));
 				lines.push_back(centerAndPadString("(c) 1990 INCENTIVE", pad));
+			} else if (isCastle() && _language == Common::FR_FRA) {
+				lines.push_back(centerAndPadString("MENU CONFIGURATION", pad));
+				lines.push_back("");
+				lines.push_back(centerAndPadString("1 CLAVIER          ", pad));
+				lines.push_back(centerAndPadString("2 JOYSTICK SINCLAIR", pad));
+				lines.push_back(centerAndPadString("3 JOYSTICK KEMSTON ", pad));
+				lines.push_back(centerAndPadString("4 JOYSTICK CURSEUR ", pad));
+				lines.push_back("");
+				lines.push_back(centerAndPadString("RETURN: DEBUT MISSION", pad));
+				lines.push_back(centerAndPadString("(c) 1990 INCENTIVE", pad));
+			} else if (isCastle() && _language == Common::DE_DEU) {
+				lines.push_back(centerAndPadString("AUSWAHL-MENUE", pad));
+				lines.push_back("");
+				lines.push_back(centerAndPadString("1 TASTATUR         ", pad));
+				lines.push_back(centerAndPadString("2 SINCLAIR JOYSTICK", pad));
+				lines.push_back(centerAndPadString("3 KEMSTON JOYSTICK ", pad));
+				lines.push_back(centerAndPadString("4 CURSOR JOYSTICK  ", pad));
+				lines.push_back("");
+				lines.push_back(centerAndPadString("RETURN: MISSION START", pad));
+				lines.push_back(centerAndPadString("(c) 1990 INCENTIVE", pad));
 			} else {
 				lines.push_back(centerAndPadString("CONTROL OPTIONS", pad));
 				lines.push_back("");


Commit: 91c3070977dca9257d93e876c857e869c96eb714
    https://github.com/scummvm/scummvm/commit/91c3070977dca9257d93e876c857e869c96eb714
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: only run fillColorPairArray for CPC

Changed paths:
    engines/freescape/games/castle/castle.cpp


diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 7fdfc7f1ad5..2b88fef5c20 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -598,7 +598,8 @@ void CastleEngine::gotoArea(uint16 areaID, int entranceID) {
 	// Ignore sky/ground fields
 	_gfx->_keyColor = 0;
 	_gfx->clearColorPairArray();
-	_gfx->fillColorPairArray();
+	if (isCPC())
+		_gfx->fillColorPairArray();
 
 	swapPalette(areaID);
 


Commit: 83eb589944b5bda7ef2afd2e75c2fc13f4db6660
    https://github.com/scummvm/scummvm/commit/83eb589944b5bda7ef2afd2e75c2fc13f4db6660
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: improved driller music for C64

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


diff --git a/engines/freescape/games/driller/c64.music.cpp b/engines/freescape/games/driller/c64.music.cpp
index 90eac0d1968..e595f713eb8 100644
--- a/engines/freescape/games/driller/c64.music.cpp
+++ b/engines/freescape/games/driller/c64.music.cpp
@@ -108,9 +108,9 @@ const uint8_t arpeggio_data[] = {0x00, 0x0C, 0x18};
 // In a real implementation, these would point into _musicData
 
 // Tracks (0x1057 - 0x118A)
-const uint8_t voice1_track_data[] = {0x01, 0x01, 0x07, 0x09, 0x09, 0x09, 0x01, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x03, 0x03, 0x0F, 0x0F, 0x13, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x1B, 0x1D, 0x1E, 0x0F, 0x1B, 0x1D, 0x1E, 0x0F, 0x1B, 0x1D, 0x1E, 0x12, 0x12, 0x12, 0x12, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x08, 0x08, 0x28, 0x00, 0x00, 0x00, 0x00, 0xFF};
+const uint8_t voice1_track_data[] = {0x01, 0x01, 0x07, 0x09, 0x09, 0x09, 0x01, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x03, 0x03, 0x0F, 0x0F, 0x13, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x13, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x0F, 0x1B, 0x1D, 0x1E, 0x0F, 0x1B, 0x1D, 0x1E, 0x0F, 0x1B, 0x1D, 0x1E, 0x12, 0x12, 0x12, 0x12, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x24, 0x24, 0x21, 0x21, 0x08, 0x08, 0x28, 0x00, 0x00, 0x00, 0x00, 0xFF};
 const uint8_t voice2_track_data[] = {0x03, 0x03, 0x08, 0x0A, 0x0D, 0x0D, 0x0D, 0x0D, 0x08, 0x07, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x05, 0x12, 0x12, 0x12, 0x12, 0x14, 0x15, 0x14, 0x15, 0x14, 0x15, 0x14, 0x15, 0x08, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x17, 0x07, 0x07, 0x1F, 0x1F, 0x1F, 0x1F, 0x07, 0x07, 0x00, 0x00, 0x25, 0x25, 0x26, 0x25, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x27, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x28, 0x00, 0x00, 0x00, 0x00, 0xFF};
-const uint8_t voice3_track_data[] = {0x00, 0x00, 0x00, 0x00, 0x04, 0x06, 0x06, 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0F, 0x0F, 0x10, 0x11, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x16, 0x07, 0x07, 0x07, 0x18, 0x19, 0x19, 0x1A, 0x1A, 0x08, 0x08, 0x1C, 0x08, 0x08, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x29, 0x00, 0x00, 0x00, 0x00, 0xFF};
+const uint8_t voice3_track_data[] = {0x00, 0x00, 0x00, 0x00, 0x04, 0x06, 0x06, 0x0C, 0x0B, 0x0C, 0x0B, 0x0C, 0x0B, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x06, 0x0F, 0x0F, 0x10, 0x11, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x0E, 0x16, 0x07, 0x07, 0x07, 0x18, 0x19, 0x19, 0x1A, 0x1A, 0x08, 0x08, 0x1C, 0x08, 0x08, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x23, 0x23, 0x22, 0x22, 0x07, 0x07, 0x0F, 0x0F, 0x0F, 0x0F, 0x29, 0x00, 0x00, 0x00, 0x00, 0xFF};
 
 // Patterns (0x118B - 0x1579) - Need to define these based on disassembly
 const uint8_t pattern_00[] = {0xFD, 0x3F, 0xFA, 0x04, 0x00, 0xFF};
@@ -145,7 +145,7 @@ const uint8_t pattern_28[] = {0xFA, 0x01, 0xFD, 0x1F, 0x3B, 0xFD, 0x0F, 0x3A, 0x
 const uint8_t pattern_29[] = {0xFA, 0x0B, 0xFD, 0x01, 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};
 const uint8_t pattern_30[] = {0xFA, 0x0B, 0xFD, 0x01, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0x1E, 0xFF};
 const uint8_t pattern_31[] = {0xFA, 0x09, 0xFD, 0x3F, 0x23, 0x1B, 0x1C, 0x1E, 0xFF};
-const uint8_t pattern_32[] = {0xFA, 0x01, 0xFD, 0x7F, 0x17, 0x17, 0xFF}; // Note: Simplified, removed data after FF
+const uint8_t pattern_32[] = {0xFA, 0x01, 0xFD, 0x7F, 0x17, 0x17, 0xFF, 0x21, 0x26, 0xFD, 0x11, 0x28, 0xFF};
 const uint8_t pattern_33[] = {0xFA, 0x15, 0xFD, 0x01, 0x1F, 0x1F, 0xFD, 0x03, 0x1F, 0xFA, 0x0F, 0xFD, 0x01, 0x2E, 0x27, 0xFA, 0x15, 0x1F, 0xFD, 0x03, 0x1F, 0xFD, 0x01, 0x1F, 0xFD, 0x03, 0x1F, 0xFD, 0x01, 0xFA, 0x0F, 0x2F, 0xFA, 0x15, 0x1A, 0x1D, 0x1F, 0xFF};
 const uint8_t pattern_34[] = {0xFA, 0x09, 0xFD, 0x01, 0x13, 0x13, 0xFD, 0x03, 0x13, 0xFD, 0x01, 0xFA, 0x00, 0x2E, 0x27, 0xFA, 0x09, 0x13, 0xFD, 0x03, 0x13, 0xFD, 0x01, 0x13, 0xFD, 0x03, 0x13, 0xFD, 0x01, 0x13, 0x10, 0x11, 0x13, 0xFF};
 const uint8_t pattern_35[] = {0xFA, 0x09, 0xFD, 0x01, 0x17, 0x17, 0xFD, 0x03, 0x17, 0xFD, 0x01, 0xFA, 0x00, 0x2E, 0x27, 0xFA, 0x09, 0x17, 0xFD, 0x03, 0x17, 0xFD, 0x01, 0x17, 0xFD, 0x03, 0x17, 0xFD, 0x01, 0x17, 0x12, 0x15, 0x17, 0xFF};
@@ -172,11 +172,41 @@ const uint8_t *const tune_track_data[][3] = {
 	{nullptr, nullptr, nullptr},                               // Tune 0 (null pointers = stop)
 	{voice1_track_data, voice2_track_data, voice3_track_data}, // Tune 1
 };
-const int NUM_TUNES = ARRAYSIZE(tune_tempo_data);
+const int NUM_TUNES = ARRAYSIZE(tune_track_data);
 
 // SID Base Addresses for Voices
 const int voice_sid_offset[] = {0, 7, 14};
 
+const uint8_t initialSomethingData[][3] = {
+	{0x00, 0x00, 0x3F},
+	{0x00, 0x00, 0x3F},
+	{0x00, 0x00, 0x3F}
+};
+
+const uint8_t initialInstrumentIndex[] = {0x08, 0x08, 0x20};
+
+const uint8_t initialSomethingElseData[][3] = {
+	{0xBB, 0x90, 0x02},
+	{0xBB, 0x90, 0x02},
+	{0xF0, 0x90, 0x0C}
+};
+
+const uint8_t initialCtrl0[] = {0x00, 0x00, 0x06};
+const uint8_t initialPwDirection[] = {0x00, 0x00, 0x01};
+const uint8_t initialStuffData[][7] = {
+	{0x47, 0x47, 0x06, 0x1F, 0x06, 0x00, 0x00},
+	{0x23, 0x23, 0x03, 0x13, 0x03, 0x00, 0x00},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+};
+
+const uint8_t initialThingsData[][7] = {
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x13},
+	{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}
+};
+
+const uint8_t initialTwoCtr[] = {0x02, 0x02, 0x02};
+
 // Debug log levels
 #define DEBUG_LEVEL 4 // 0: Minimal, 1: Basic Flow, 2: Detailed State
 
@@ -312,43 +342,10 @@ void DrillerSIDPlayer::handleChangeTune(int tuneIndex) {
 		debug(DEBUG_LEVEL >= 0, "Driller: Invalid tune index %d in handleChangeTune, using 1", tuneIndex);
 		tuneIndex = 1; // Default to tune 1 if invalid
 	}
-
-	// *** ADD THIS LOG - BEFORE ASSIGNMENT ***
-	debug(DEBUG_LEVEL >= 1, "Driller: Tune %d - Accessing tune_track_data[%d]...", tuneIndex, tuneIndex);
-
 	const uint8_t *const *currentTuneTracks = tune_track_data[tuneIndex];
 
-	// *** ADD THIS LOG - AFTER ACCESSING THE TUNE'S TRACK ARRAY ***
-	// Check if the pointer to the array itself is valid
-	if (!currentTuneTracks) {
-		debug(DEBUG_LEVEL >= 0, "Driller: FATAL - tune_track_data[%d] is NULL!", tuneIndex);
-		// Optional: Handle this error more gracefully, maybe stop music?
-	} else {
-		debug(DEBUG_LEVEL >= 2, "Driller: tune_track_data[%d] pointer is valid.", tuneIndex);
-	}
-
 	for (int i = 0; i < 3; ++i) {
-		// *** ADD THIS LOG - BEFORE ASSIGNING TO voiceState ***
-		const uint8_t *trackPtr = nullptr; // Temp variable
-		if (currentTuneTracks) {           // Check if the tune array pointer is valid
-			trackPtr = currentTuneTracks[i];
-			debug(DEBUG_LEVEL >= 1, "Driller: V%d - Got track pointer %p from currentTuneTracks[%d]", i, (const void *)trackPtr, i);
-		} else {
-			debug(DEBUG_LEVEL >= 0, "Driller: V%d - Cannot get track pointer because currentTuneTracks is NULL", i);
-		}
-
-		// Assign the pointer
-		_voiceState[i].trackDataPtr = trackPtr;
-
-		// *** ADD THIS LOG - AFTER ASSIGNING TO voiceState ***
-		debug(DEBUG_LEVEL >= 1, "Driller: V%d - Assigned _voiceState[%d].trackDataPtr = %p", i, i, (const void *)_voiceState[i].trackDataPtr);
-
-		if (!_voiceState[i].trackDataPtr) {
-			// This block now just confirms the assignment result
-			debug(DEBUG_LEVEL >= 1, "Driller: Voice %d has null track data assigned for tune %d.", i, tuneIndex);
-			// Don't reset here, handleResetVoices will do it.
-		}
-		// Pointers setup in resetVoices below
+		_voiceState[i].trackDataPtr = currentTuneTracks ? currentTuneTracks[i] : nullptr;
 	}
 
 	_globalTempo = tune_tempo_data[tuneIndex];
@@ -368,67 +365,59 @@ void DrillerSIDPlayer::handleResetVoices() {
 	SID_Write(0x18, 0x0F); // Volume Max
 
 	for (int i = 0; i < 3; ++i) {
-		// *** DO NOT CALL _voiceState[i].reset() HERE ***
-		// The trackDataPtr was just assigned in handleChangeTune.
-		// Reset only the playback state relevant for starting a tune/track.
-
-		debug(DEBUG_LEVEL >= 1, "Driller: Reset Voice %d - Checking _voiceState[%d].trackDataPtr (%p)...", i, i, (const void *)_voiceState[i].trackDataPtr);
-
-		if (_voiceState[i].trackDataPtr != nullptr) {
-			debug(DEBUG_LEVEL >= 1, "Driller: Reset Voice %d - Track pointer OK. Initializing playback state.", i);
-
-			// Reset playback state, keep trackDataPtr
-			_voiceState[i].trackIndex = 0;
-			_voiceState[i].patternDataPtr = nullptr; // Will be set by pattern lookup
-			_voiceState[i].patternIndex = 0;
-			_voiceState[i].instrumentIndex = 0; // Default instrument? Or should tune load set this? Let's keep 0.
-			_voiceState[i].delayCounter = -1;   // Ready for first note
-			_voiceState[i].noteDuration = 0;
-			_voiceState[i].gateMask = 0xFF;
-			_voiceState[i].currentNote = 0;
-			_voiceState[i].portaTargetNote = 0;
-			_voiceState[i].currentFreq = 0;
-			_voiceState[i].baseFreq = 0;
-			_voiceState[i].targetFreq = 0;
-			_voiceState[i].pulseWidth = 0;     // Default PW?
-			_voiceState[i].attackDecay = 0x00; // Default ADSR?
-			_voiceState[i].sustainRelease = 0x00;
-			_voiceState[i].effect = 0;
-			_voiceState[i].hardRestartActive = false;
-			_voiceState[i].waveform = 0x10; // Default waveform (Triangle)
-			_voiceState[i].keyOn = false;
-			_voiceState[i].currentNoteSlideTarget = 0;
-			_voiceState[i].glideDownTimer = 0; // Reset glide timer
-
-			// Reset other potentially problematic state variables from the struct
-			_voiceState[i].whatever0 = 0;
-			_voiceState[i].whatever1 = 0;
-			_voiceState[i].whatever2 = 0;
-			_voiceState[i].whatever3 = 0;
-			_voiceState[i].whatever4 = 0;
-			_voiceState[i].whatever2_vibDirToggle = 0;
-			_voiceState[i].portaStepRaw = 0;
-			memset(_voiceState[i].something_else, 0, sizeof(_voiceState[i].something_else));
-			_voiceState[i].ctrl0 = 0;
-			_voiceState[i].arpTableIndex = 0;
-			_voiceState[i].arpSpeedHiNibble = 0;
-			_voiceState[i].stuff_freq_porta_vib = 0;
-			_voiceState[i].stuff_freq_base = 0;
-			_voiceState[i].stuff_freq_hard_restart = 0;
-			_voiceState[i].stuff_arp_counter = 0;
-			_voiceState[i].stuff_arp_note_index = 0;
-			_voiceState[i].things_vib_state = 0;
-			_voiceState[i].things_vib_depth = 0;
-			_voiceState[i].things_vib_delay_reload = 0;
-			_voiceState[i].things_vib_delay_ctr = 0;
-			_voiceState[i].portaSpeed = 0;
-
-		} else {
-			debug(DEBUG_LEVEL >= 0, "Driller: Reset Voice %d - Check FAILED. trackDataPtr is NULL here!", i);
-			// Ensure voice is silent if no track data
-			int sidOffset = voice_sid_offset[i];
-			SID_Write(sidOffset + 4, 0); // Gate off
+		VoiceState &v = _voiceState[i];
+		if (!v.trackDataPtr) {
+			SID_Write(voice_sid_offset[i] + 4, 0);
+			continue;
 		}
+
+		v.trackIndex = 0;
+		v.patternDataPtr = nullptr;
+		v.patternIndex = 0;
+		v.instrumentIndex = initialInstrumentIndex[i];
+		v.delayCounter = 0;
+		v.noteDuration = initialSomethingData[i][2];
+		v.gateMask = 0xFF;
+		v.currentNote = initialStuffData[i][3];
+		v.portaTargetNote = 0;
+		v.currentFreq = 0;
+		v.baseFreq = 0;
+		v.targetFreq = 0;
+		v.pulseWidth = initialSomethingElseData[i][0] | (initialSomethingElseData[i][2] << 8);
+		v.attackDecay = 0;
+		v.sustainRelease = 0;
+		v.effect = 0;
+		v.hardRestartValue = 0;
+		v.hardRestartDelay = 0;
+		v.hardRestartCounter = 0;
+		v.hardRestartActive = false;
+		v.waveform = 0x10;
+		v.keyOn = false;
+		v.sync = false;
+		v.ringMod = false;
+
+		v.whatever0 = 0;
+		v.whatever1 = 0;
+		v.whatever2 = 0;
+		v.whatever3 = 0;
+		v.whatever4 = 0;
+		v.whatever2_vibDirToggle = initialPwDirection[i];
+		v.portaStepRaw = initialSomethingData[i][0];
+		memcpy(v.something_else, initialSomethingElseData[i], sizeof(v.something_else));
+		v.ctrl0 = initialCtrl0[i];
+		v.arpTableIndex = 0;
+		v.arpSpeedHiNibble = initialStuffData[i][5];
+		v.stuff_freq_porta_vib = initialStuffData[i][0] | (initialStuffData[i][4] << 8);
+		v.stuff_freq_base = initialStuffData[i][1] | (initialStuffData[i][2] << 8);
+		v.stuff_freq_hard_restart = 0;
+		v.stuff_arp_counter = initialStuffData[i][6];
+		v.stuff_arp_note_index = 0;
+		v.things_vib_state = initialThingsData[i][0];
+		v.things_vib_depth = initialThingsData[i][1];
+		v.things_vib_delay_reload = initialThingsData[i][2];
+		v.things_vib_delay_ctr = initialThingsData[i][3];
+		v.currentNoteSlideTarget = initialThingsData[i][6];
+		v.glideDownTimer = initialTwoCtr[i];
 	}
 
 	// Reset global tempo counter (0x093D)
@@ -437,606 +426,353 @@ void DrillerSIDPlayer::handleResetVoices() {
 
 // --- Voice Processing ---
 void DrillerSIDPlayer::playVoice(int voiceIndex) {
-	// debug(DEBUG_LEVEL >= 2, "Driller: Processing Voice %d", voiceIndex);
 	VoiceState &v = _voiceState[voiceIndex];
 	int sidOffset = voice_sid_offset[voiceIndex];
 
-	// If track data is null, this voice is inactive for the current tune
-	if (!v.trackDataPtr) {
+	if (!v.trackDataPtr)
 		return;
-	}
-
-	// --- Effect application before note processing (Tempo independent) ---
-	// Corresponds roughly to L0944 - L0964 (instrument specific effects)
-	// And L0B33 onwards (general effects like vibrato, portamento, arpeggio)
 
 	int instBase = v.instrumentIndex; // Already scaled by 8
-	// Safety check for instrument index
 	if (instBase < 0 || (size_t)instBase >= sizeof(instrumentDataA0)) {
-		instBase = 0; // Default to instrument 0 if invalid
+		instBase = 0;
 		v.instrumentIndex = 0;
 	}
 	const uint8_t *instA0 = &instrumentDataA0[instBase];
 	const uint8_t *instA1 = &instrumentDataA1[instBase];
 
-	// Waveform transition effect (L0944-L095E) - Inst A0[7] & 0x04
-	// This logic updates ctrl register $D404, likely wave or gate
 	if (instA0[7] & 0x04) {
-		if (v.glideDownTimer > 0) { // voice1_two_ctr,x (0xD3E)
+		if (v.glideDownTimer != 0) {
 			v.glideDownTimer--;
-			uint8_t ctrlVal = instA1[2]; // possibly_instrument_a1+2,y
-			SID_Write(sidOffset + 4, ctrlVal);
-			// bne L0964 - skip waveform reset if timer > 0
+			if (instA1[2] != 0)
+				SID_Write(sidOffset + 4, instA1[2]);
+			else
+				SID_Write(sidOffset + 4, instA0[1]);
 		} else {
-			// L095E: timer is 0
-			uint8_t ctrlVal = instA0[1]; // possibly_instrument_a0+1,y
-			SID_Write(sidOffset + 4, ctrlVal);
-			// Resets waveform/gate based on inst A0[1]
+			SID_Write(sidOffset + 4, instA0[1]);
 		}
 	}
 
-	// Corresponds to lda tempo_ctr; bne L096E (0x0964)
-	// The per-voice processing is gated by the global tempo counter being zero.
-	if (_globalTempoCounter == 0) {
-		// dec voice1_ctrl2,x (0x0969)
-		if (v.delayCounter >= 0) {
-			v.delayCounter--;
+	if (_globalTempoCounter != 0) {
+		applyContinuousEffects(v, sidOffset, instA0, instA1, false);
+		return;
+	}
+
+	v.delayCounter--;
+	if (v.delayCounter >= 0) {
+		applyContinuousEffects(v, sidOffset, instA0, instA1, false);
+		return;
+	}
+
+	uint8_t patternNum = v.trackDataPtr[v.trackIndex];
+	if (patternNum == 0xFE) {
+		stopMusic();
+		return;
+	}
+	if (patternNum == 0xFF) {
+		v.trackIndex = 0;
+		patternNum = v.trackDataPtr[v.trackIndex];
+		if (patternNum == 0xFF || patternNum == 0xFE) {
+			stopMusic();
+			return;
 		}
+	}
+	if (patternNum >= NUM_PATTERNS) {
+		debug(DEBUG_LEVEL >= 0, "Driller V%d: Invalid pattern number %d", voiceIndex, patternNum);
+		stopMusic();
+		return;
+	}
 
-		// If delay counter has expired, read new data from the pattern.
-		if (v.delayCounter < 0) {
-			debug(DEBUG_LEVEL >= 1, "Driller V%d: Delay Counter Expired - Reading new pattern data", voiceIndex);
-
-			// --- Start of inlined pattern reading logic ---
-			// Get current pattern index from track (09C0-09CE)
-			uint8_t patternNum = v.trackDataPtr[v.trackIndex];
-
-			// Handle track end/loop markers (0AE7, 0AF2)
-			if (patternNum == 0xFF) { // End of track list
-				debug(DEBUG_LEVEL >= 1, "Driller V%d: Track %d end marker (FF), looping.", voiceIndex, v.trackIndex);
-				v.trackIndex = 0; // Loop to start
-				patternNum = v.trackDataPtr[v.trackIndex];
-				if (patternNum == 0xFF || patternNum == 0xFE || !tune_track_data[_targetTuneIndex][voiceIndex]) { // Check again after loop or if track is null initially
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Stopping music after track loop (FF/FE/Null).", voiceIndex);
-					stopMusic(); // Stop if loop points to end marker or track is invalid
-					return;
-				}
-			} else if (patternNum == 0xFE) { // Stop playback command
-				debug(DEBUG_LEVEL >= 0, "Driller V%d: Stopping music due to track marker FE.", voiceIndex);
-				stopMusic();
-				return;
-			}
+	v.patternDataPtr = pattern_addresses[patternNum];
+	v.gateMask = 0xFF;
+	v.whatever2 = 0;
+	v.whatever1 = 0;
+	v.whatever0 = 0;
 
-			if (patternNum >= NUM_PATTERNS) {
-				debug(DEBUG_LEVEL >= 0, "Driller V%d: Invalid pattern number %d at track index %d", voiceIndex, patternNum, v.trackIndex);
-				v.trackIndex++; // Skip invalid entry
-				// Fetch next pattern number immediately to avoid getting stuck in invalid state for a frame
-				size_t trackSize = (voiceIndex == 0) ? sizeof(voice1_track_data) : ((voiceIndex == 1) ? sizeof(voice2_track_data) : sizeof(voice3_track_data));
-				if (v.trackIndex >= trackSize) { // Check for track end
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Stopping music, track index out of bounds after skipping invalid pattern.", voiceIndex);
-					stopMusic();
-					return;
-				}
-				patternNum = v.trackDataPtr[v.trackIndex];
-				if (patternNum == 0xFF || patternNum == 0xFE) {
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Stopping music, encountered FF/FE after skipping invalid pattern.", voiceIndex);
-					stopMusic();
-					return;
-				}
-				if (patternNum >= NUM_PATTERNS) { // Still invalid? Stop.
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Stopping music, encountered second invalid pattern.", voiceIndex);
-					stopMusic();
-					return;
-				}
-				// Continue with the new valid patternNum
-			}
+	while (true) {
+		uint8_t cmd = v.patternDataPtr[v.patternIndex];
 
-			// Only update pattern pointer if it changed or wasn't set
-			if (v.patternDataPtr != pattern_addresses[patternNum]) {
-				v.patternDataPtr = pattern_addresses[patternNum];
-				v.patternIndex = 0; // Reset index when pattern changes
-				debug(DEBUG_LEVEL >= 2, "Driller V%d: Switched to Pattern %d", voiceIndex, patternNum);
-			}
+		if (cmd >= 0xFD) {
+			v.patternIndex++;
+			v.noteDuration = v.patternDataPtr[v.patternIndex];
+			v.patternIndex++;
+			continue;
+		}
 
-			// Reset state related to previous note/effects for gate control
-			v.gateMask = 0xFF; // Reset gate mask (0x09D0: lda #$FF; sta control3)
-			v.whatever0 = 0;   // Reset effect states (0x09D5 onwards)
+		if (cmd >= 0xFB) {
+			v.patternIndex++;
+			v.whatever2 = (cmd == 0xFB) ? 1 : 2;
+			v.portaStepRaw = v.patternDataPtr[v.patternIndex];
 			v.whatever1 = 0;
-			v.whatever2 = 0;
-
-			// --- Read Pattern Data Loop (0x09E0 read_note_or_ctrl) ---
-			bool noteProcessed = false;
-			while (!noteProcessed) {
-				if (!v.patternDataPtr) { // Safety check
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Pattern pointer is null!", voiceIndex);
-					v.trackIndex++;       // Advance track to avoid getting stuck
-					noteProcessed = true; // Exit loop, try next track index next frame
-					break;
-				}
-
-				// Check pattern bounds - Use FF as terminator
-				if (v.patternIndex >= 255) { // Sanity check pattern length
-					debug(DEBUG_LEVEL >= 0, "Driller V%d: Pattern index overflow (>255), resetting.", voiceIndex);
-					v.patternIndex = 0;   // Reset pattern index
-					v.trackIndex++;       // Advance track index
-					noteProcessed = true; // Exit loop
-					break;                // Go to next track entry
-				}
-
-				uint8_t cmd = v.patternDataPtr[v.patternIndex];
-				debug(DEBUG_LEVEL >= 3, "Driller V%d: Reading Pat %d Idx %d: Cmd $%02X", voiceIndex, patternNum, v.patternIndex, cmd);
-
-				if (cmd == 0xFF) { // End of pattern marker (0x0AD6)
-					debug(DEBUG_LEVEL >= 2, "Driller V%d: End of Pattern %d detected.", voiceIndex, patternNum);
-					v.patternIndex = 0;   // Reset pattern index
-					v.trackIndex++;       // Advance track index (0x0ADF)
-					noteProcessed = true; // Exit inner loop, done processing for this tick
-					break;                // Exit pattern loop, next tick will get next pattern index from track
-				}
-
-				if (cmd >= 0xFD) {                                                       // --- Control Commands ---
-					v.patternIndex++;                                                    // Consume command byte
-					if (!v.patternDataPtr || v.patternDataPtr[v.patternIndex] == 0xFF) { // Check bounds before reading data
-						debug(DEBUG_LEVEL >= 1, "Driller V%d: Pattern ended unexpectedly after Fx command.", voiceIndex);
-						noteProcessed = true;
-						break;
-					}
-					uint8_t dataByte = v.patternDataPtr[v.patternIndex]; // Read data byte
-
-					// Effect FD/FE: Set Note Duration (0x09E5 + 0x09ED)
-					// Any command >= FD that is not FF (end of pattern) sets the duration.
-					v.noteDuration = dataByte; // Store duration (0x09EF)
-					debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd $%02X, Set Duration = %d", voiceIndex, cmd, v.noteDuration);
-
-					// Continue reading pattern (next_note_or_ctrl 09F2/0A15)
-					v.patternIndex++;
-
-				} else if (cmd >= 0xFB) {                                                // Effect FB/FC
-					v.patternIndex++;                                                    // Consume command byte
-					if (!v.patternDataPtr || v.patternDataPtr[v.patternIndex] == 0xFF) { // Check bounds before reading data
-						debug(DEBUG_LEVEL >= 1, "Driller V%d: Pattern ended unexpectedly after FB/FC command.", voiceIndex);
-						noteProcessed = true;
-						break;
-					}
-					uint8_t portaParam = v.patternDataPtr[v.patternIndex]; // Consume data byte
-
-					// FB = porta type 1 (down lo), FC = porta type 2 (up lo)
-					// Assembly: FB -> lda #$01 (0x09FF), FC -> lda #$02 (0x0A17)
-					if (cmd == 0xFB) {
-						v.whatever2 = 1; // (0x09FF: lda #$01; sta whatever+2)
-					} else { // FC
-						v.whatever2 = 2; // (0x0A17: lda #$02)
-					}
-					v.portaStepRaw = portaParam; // sta voice1_something (0x0A0A)
-					v.whatever1 = 0;             // sta whatever+1 (0x0A0F)
-					v.whatever0 = 0;             // sta whatever (0x0A12)
-					v.portaSpeed = 0;            // Force recalc
-					v.patternIndex++; // Continue reading pattern (0A15)
-
-				} else if (cmd == 0xFA) { // --- Effect FA: Set Instrument --- (0x0A1B)
-					v.patternIndex++;
-					if (!v.patternDataPtr || v.patternDataPtr[v.patternIndex] == 0xFF) { // Check bounds before reading data
-						debug(DEBUG_LEVEL >= 1, "Driller V%d: Pattern ended unexpectedly after FA command.", voiceIndex);
-						noteProcessed = true;
-						break;
-					}
-					uint8_t instNum = v.patternDataPtr[v.patternIndex];
-					if (instNum >= NUM_INSTRUMENTS) {
-						debug(DEBUG_LEVEL >= 0, "Driller V%d: Invalid instrument number %d, using 0.", voiceIndex, instNum);
-						instNum = 0;
-					}
-					v.instrumentIndex = instNum * 8; // Store base offset (0A28)
-					debug(DEBUG_LEVEL >= 2, "Driller V%d: Cmd FA, Set Instrument = %d", voiceIndex, instNum);
-
-					// Update local pointers for instrument data
-					instBase = v.instrumentIndex;
-					if (instBase < 0 || (size_t)instBase >= sizeof(instrumentDataA0))
-						instBase = 0; // Bounds check
-					instA0 = &instrumentDataA0[instBase];
-					instA1 = &instrumentDataA1[instBase];
-
-					// Set ADSR based on instrument (0A2C - 0A3E)
-					uint8_t adsrByte = instA0[0];       // 0A2C
-					v.sustainRelease = adsrByte & 0x0F; // Low nibble to SR (0A32) -> ctrl0
-					v.attackDecay = adsrByte & 0xF0;    // High nibble to AD (0A3B/0A3E) -> something_else[0/1]
-					// Store in voice state for SID write later
-					v.ctrl0 = v.sustainRelease;
-					v.something_else[0] = v.attackDecay;
-					v.something_else[1] = v.attackDecay; // Seems duplicated in disassembly?
-					// Also set PW from instA0[0]? Disassembly sets something_else[0] and [1] to AD (hi nibble)
-					// Pulse width seems set later from something_else[0] and [2] ? Let's use [0] for AD.
-					// Let's assume instA0[2] (often xx) and instA0[3] (often 00) are PW lo/hi nibble?
-					// Or maybe something_else[0]/[2] ARE PW and ADSR needs separate vars?
-					// Revisit PW setting in applyNote based on L0AC2. It uses something_else[0] and [2].
-					// Let's store ADSR in dedicated vars, and use something_else for PW based on instrument.
-					// What part of instrument sets PW? L0AC2 uses something_else[0/2]. FA command sets something_else[0/1/2].
-					// FA: pla -> and #F0 -> sta something_else[0] / [1]
-					// FA: pha -> and #0F -> sta something_else[2] / ctrl0
-					// This means: AD Hi Nibble -> PW Lo Byte? AD Hi Nibble -> something_else[1]? SR Lo Nibble -> PW Hi Nibble? SR Lo Nibble -> ctrl0?
-					// Let's follow the variable names:
-					v.attackDecay = instA0[0] & 0xF0;    // Stored in something_else[0] & [1]
-					v.sustainRelease = instA0[0] & 0x0F; // Stored in something_else[2] & ctrl0
-					v.something_else[0] = v.attackDecay;
-					v.something_else[1] = v.attackDecay;    // ???
-					v.something_else[2] = v.sustainRelease; // PW Hi?
-					v.ctrl0 = v.sustainRelease;             // SR?
-
-					debug(DEBUG_LEVEL >= 3, "Driller V%d: Inst %d - ADSR Byte: $%02X -> AD: $%02X, SR: $%02X", voiceIndex, instNum, adsrByte, v.attackDecay, v.sustainRelease);
-
-					// Continue reading pattern (0A41 -> 09F2)
-					v.patternIndex++;
-
-				} else {                 // --- Plain Note --- (0x0A1D -> 0A44)
-					v.currentNote = cmd; // Store note value (0A44: sta stuff+3)
-					// Set delay counter based on previously read duration (0A47-0A4A)
-					v.delayCounter = v.noteDuration;
-
-					// Reset hard restart counters (0A4D-0A52)
-					v.whatever3 = 0;
-					v.whatever4 = 0;
-
-					// Reset glide down timer (0A55-0A57)
-					v.glideDownTimer = 2; // voice1_two_ctr = 2
-
-					// Apply Note Data (0A5D-0AB3)
-					applyNote(v, sidOffset, instA0, instA1, voiceIndex);
-
-					// Continue reading pattern
-					v.patternIndex++;
-					noteProcessed = true;
-				}
-
-			} // End while(!noteProcessed)
-			// --- End of inlined pattern reading logic ---
-
-			// L0AFC: Post-note effect setup - determine which continuous effect is active
-			postNoteEffectSetup(v, sidOffset, instA0, instA1);
+			v.whatever0 = 0;
+			v.patternIndex++;
+			continue;
+		}
+
+		if (cmd >= 0xFA) {
+			v.patternIndex++;
+			uint8_t instNum = v.patternDataPtr[v.patternIndex];
+			if (instNum >= NUM_INSTRUMENTS)
+				instNum = 0;
+			v.instrumentIndex = instNum * 8;
+			instBase = v.instrumentIndex;
+			instA0 = &instrumentDataA0[instBase];
+			instA1 = &instrumentDataA1[instBase];
+
+			uint8_t instrumentByte = instA0[0];
+			v.attackDecay = instrumentByte & 0xF0;
+			v.sustainRelease = instrumentByte & 0x0F;
+			v.something_else[0] = v.attackDecay;
+			v.something_else[1] = v.attackDecay;
+			v.something_else[2] = v.sustainRelease;
+			v.ctrl0 = v.sustainRelease;
+			v.patternIndex++;
+			continue;
 		}
-	}
 
-	// ALWAYS apply continuous effects (L0B33+) for the current state of the voice.
-	// This runs every frame: on tempo ticks after note processing, and on non-tempo ticks directly.
-	applyContinuousEffects(v, sidOffset, instA0, instA1);
+		v.currentNote = cmd;
+		v.delayCounter = v.noteDuration;
+		v.whatever3 = 0;
+		v.whatever4 = 0;
+		v.glideDownTimer = 2;
+
+		applyNote(v, sidOffset, instA0);
+
+		v.patternIndex++;
+		if (v.patternDataPtr[v.patternIndex] == 0xFF) {
+			v.patternIndex = 0;
+			v.trackIndex++;
+			uint8_t trackCmd = v.trackDataPtr[v.trackIndex];
+			if (trackCmd == 0xFF) {
+				v.trackIndex = 0;
+			} else if (trackCmd == 0xFE) {
+				stopMusic();
+				return;
+			}
+		}
+
+		ContinuousEffectEntry entry = postNoteEffectSetup(v, instA0, instA1);
+		if (entry == kFullEffectPath)
+			applyContinuousEffects(v, sidOffset, instA0, instA1, false);
+		else if (entry == kPortamentoOnlyPath)
+			applyContinuousEffects(v, sidOffset, instA0, instA1, true);
+		return;
+	}
 }
 
 // --- Note Application ---
 // Corresponds to @plain_note (0x0A44) through L0AAD (0x0AB3)
-void DrillerSIDPlayer::applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex) {
-	uint8_t note = v.currentNote; // Already stored at @plain_note (0x0A44)
-	// v.delayCounter already set from v.noteDuration (0x0A47-0x0A4A)
-	// v.whatever3/4 already reset to 0 (0x0A4D-0x0A52)
-	// v.glideDownTimer already set to 2 (0x0A55-0x0A57)
+void DrillerSIDPlayer::applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0) {
+	uint8_t note = v.currentNote;
 
-	// Check legato bit instA0[7] & 0x02 (0x0A5D-0x0A6D)
 	if (instA0[7] & 0x02) {
-		// Legato: restore PW values from FA command backup
-		v.something_else[0] = v.something_else[1]; // something_else+1 -> something_else+0
-		v.something_else[2] = v.ctrl0;              // ctrl0 -> something_else+2
+		v.something_else[0] = v.something_else[1];
+		v.something_else[2] = v.ctrl0;
 	}
 
-	// Handle note=0 (rest) at L0A70
 	if (note == 0) {
-		// Load previous note from things+6 (0x0A75)
 		note = v.currentNoteSlideTarget;
-		v.currentNote = note; // Update stuff+3 equivalent
-		v.currentNoteSlideTarget = 0; // Clear things+6 (0x0A7B-0x0A7D)
-
-		// dec control3 (0x0A83)
+		v.currentNote = note;
+		v.currentNoteSlideTarget = 0;
 		v.gateMask--;
-
-		// bne L0AAD - since control3 was 0xFF, now 0xFE, always branches
-		// Skip frequency write entirely for rests, jump to L0AAD
 	} else {
-		// Non-zero note: store as previous note (L0A88)
-		v.currentNoteSlideTarget = note; // things+6 = note
-
-		// Set frequency from note (L0A8C-L0AA1)
-		if (note >= 96) note = 95;
-		SID_Write(sidOffset + 1, frq_hi[note]); // $D401
-		SID_Write(sidOffset + 0, frq_lo[note]); // $D400
-		// Store in stuff variables: stuff[0]=lo, stuff[1]=lo, stuff[2]=hi, stuff[4]=hi
+		v.currentNoteSlideTarget = note;
+		if (note >= 96)
+			note = 95;
+
 		uint8_t fLo = frq_lo[note];
 		uint8_t fHi = frq_hi[note];
-		v.stuff_freq_porta_vib = fLo | (fHi << 8); // stuff[0]/[4]
-		v.stuff_freq_base = fLo | (fHi << 8);       // stuff[1]/[2]
-		v.stuff_freq_hard_restart = fLo | (fHi << 8);
-		v.currentFreq = v.stuff_freq_porta_vib;
-
-		// Write initial waveform (gate-on transient): instA0[6] -> $D404 (L0AA7-L0AAA)
+		SID_Write(sidOffset + 1, fHi);
+		SID_Write(sidOffset + 0, fLo);
+		v.stuff_freq_porta_vib = fLo | (fHi << 8);
+		v.stuff_freq_base = fLo | (fHi << 8);
 		SID_Write(sidOffset + 4, instA0[6]);
 	}
 
-	// L0AAD: Write control register with gate mask
-	// lda instA0[1]; AND control3; sta $D404 (0x0AAD-0x0AB3)
 	SID_Write(sidOffset + 4, instA0[1] & v.gateMask);
-
-	// Write ADSR from instrument (0x0AB6-0x0ABF)
-	SID_Write(sidOffset + 5, instA0[2]); // Attack / Decay
-	SID_Write(sidOffset + 6, instA0[3]); // Sustain / Release
-
-	// Write Pulse Width from something_else (0x0AC2-0x0ACB)
-	SID_Write(sidOffset + 2, v.something_else[0]); // PW Lo
-	SID_Write(sidOffset + 3, v.something_else[2]); // PW Hi
-
+	SID_Write(sidOffset + 5, instA0[2]);
+	SID_Write(sidOffset + 6, instA0[3]);
+	SID_Write(sidOffset + 2, v.something_else[0]);
+	SID_Write(sidOffset + 3, v.something_else[2]);
 	v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
 }
 
-// --- Post-Note Effect Setup ---
-// Corresponds to L0AFC in the assembly. Runs after each note is applied.
-// Determines which continuous effect should be active based on instrument data.
-void DrillerSIDPlayer::postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1) {
-	// L0AFC: lda voice1_things+6,x; beq L0B33
+DrillerSIDPlayer::ContinuousEffectEntry DrillerSIDPlayer::postNoteEffectSetup(VoiceState &v, const uint8_t *instA0, const uint8_t *instA1) {
 	if (v.currentNoteSlideTarget == 0)
-		return; // No note stored, skip to continuous effects (PW LFO etc.)
+		return kFullEffectPath;
 
-	// Check if portamento already active from FB/FC pattern command (L0B04)
-	// lda voice1_whatever+2,x; bne L0B17
-	if (v.whatever2 != 0) {
-		// Already have porta from pattern command, go to porta processing
-		return; // Porta will run in applyContinuousEffects
+	if (v.whatever2 != 0)
+		return kPortamentoOnlyPath;
+
+	if (instA1[4] != 0) {
+		v.whatever2 = instA1[4];
+		v.portaStepRaw = instA1[3];
+		return kPortamentoOnlyPath;
 	}
 
-	int instBase = v.instrumentIndex;
-	if (instBase < 0 || (size_t)instBase >= sizeof(instrumentDataA0))
-		instBase = 0;
+	if (instA1[0] != 0) {
+		if (instA0[5] != 0) {
+			v.arpTableIndex = instA0[5] & 0x0F;
+			v.arpSpeedHiNibble = (instA0[5] & 0xF0) >> 4;
+			v.stuff_arp_counter = 0;
+			v.whatever1 = 1;
+			v.whatever0 = 0;
+			return kVoiceDone;
+		}
 
-	// Check instA1[4] for instrument-level portamento (L0B09)
-	// lda possibly_instrument_a1+4,y; beq L0B1A
-	if (instA1[4] != 0) {
-		v.whatever2 = instA1[4];       // Store as porta type (L0B0E)
-		v.portaStepRaw = instA1[3];    // Store porta speed (L0B11-L0B14)
-		v.portaSpeed = 0;              // Force recalc
-		return; // jmp L0C5A - porta will run in applyContinuousEffects
+		v.whatever1 = 0;
+		v.things_vib_depth = instA1[0];
+		v.things_vib_delay_reload = instA1[1];
+		v.things_vib_delay_ctr = instA1[1];
+		v.things_vib_state = 0;
+		v.whatever0 = 1;
+		return kVoiceDone;
 	}
 
-	// Check instA0[5] for arpeggio (L0B1A -> L0E67)
-	// lda possibly_instrument_a0+5,y; beq L0B22
 	if (instA0[5] != 0) {
-		// L0E67: arpeggio setup
-		uint8_t arpData = instA0[5];
-		v.arpTableIndex = arpData & 0x0F;                    // and #$0F -> ctrl1
-		v.arpSpeedHiNibble = (arpData & 0xF0) >> 4;         // and #$F0; lsr*4 -> stuff+5
-		v.stuff_arp_counter = 0;                             // sta stuff+6
-		v.whatever1 = 1;                                     // sta whatever+1
-		v.whatever0 = 0;                                     // sta whatever
-		return; // jmp voice_done
+		v.arpTableIndex = instA0[5] & 0x0F;
+		v.arpSpeedHiNibble = (instA0[5] & 0xF0) >> 4;
+		v.stuff_arp_counter = 0;
+		v.whatever1 = 1;
+		v.whatever0 = 0;
+		return kVoiceDone;
 	}
 
-	// L0B22: Clear arpeggio flag (A=0 from beq)
 	v.whatever1 = 0;
-
-	// Check instA1[0] for vibrato (L0B25 -> L0E89)
-	// lda possibly_instrument_a1,y; beq L0B2D
-	if (instA1[0] != 0) {
-		// L0E89: vibrato setup
-		v.things_vib_depth = instA1[0];                      // sta things+1
-		v.things_vib_delay_reload = instA1[1];               // sta things+2
-		v.things_vib_delay_ctr = v.things_vib_delay_reload;  // sta things+3
-		v.things_vib_state = 0;                              // sta things
-		v.whatever1 = 0;                                     // sta whatever+1
-		v.whatever0 = 1;                                     // sta whatever
-		return; // jmp voice_done
-	}
-
-	// L0B2D: Clear vibrato flag (A=0 from beq)
 	v.whatever0 = 0;
-	// jmp voice_done
+	return kVoiceDone;
 }
 
-// --- Continuous Effect Application (Vibrato, Porta, Arp) ---
-void DrillerSIDPlayer::applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1) {
-	// Corresponds to logic starting around L0B33 / L0B82 / L0BC0 / L0C5A
-
-	uint16_t freq = v.stuff_freq_porta_vib; // Start with base freq + porta/vib from previous step
-	bool freqDirty = false;                 // Track if frequency needs writing
-
-	// PW LFO (L0B33-L0B82) - instA0[4] = modulation speed (stored in control1)
-	uint8_t lfoSpeed = instA0[4];
-	if (lfoSpeed != 0) {
-		// Operates on something_else[0] (lo byte) and something_else[2] (hi nibble)
-		if (v.whatever2_vibDirToggle == 0) {
-			// Add phase (L0B40-L0B5D): clc; adc control1 on lo; adc #0 on hi
-			uint16_t sum = (uint16_t)v.something_else[0] + lfoSpeed;
-			v.something_else[0] = sum & 0xFF;
-			v.something_else[2] = v.something_else[2] + (sum >> 8);
-			SID_Write(sidOffset + 2, v.something_else[0]); // $D402
-			SID_Write(sidOffset + 3, v.something_else[2]); // $D403
-			// clc; cmp #$0E - check hi byte >= 0x0E (L0B59)
-			if (v.something_else[2] >= 0x0E) {
-				v.whatever2_vibDirToggle = 1; // inc whatever2 (L0B5D)
+void DrillerSIDPlayer::applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, bool startAtPortamento) {
+	if (!startAtPortamento) {
+		uint8_t lfoSpeed = instA0[4];
+		if (lfoSpeed != 0) {
+			if (v.whatever2_vibDirToggle == 0) {
+				uint16_t sum = (uint16_t)v.something_else[0] + lfoSpeed;
+				v.something_else[0] = sum & 0xFF;
+				v.something_else[2] = (v.something_else[2] + (sum >> 8)) & 0xFF;
+				SID_Write(sidOffset + 2, v.something_else[0]);
+				SID_Write(sidOffset + 3, v.something_else[2]);
+				if (v.something_else[2] >= 0x0E)
+					v.whatever2_vibDirToggle++;
+			} else {
+				uint16_t diff = (uint16_t)v.something_else[0] - lfoSpeed;
+				uint8_t borrow = (diff > 0xFF) ? 1 : 0;
+				v.something_else[0] = diff & 0xFF;
+				v.something_else[2] = (v.something_else[2] - borrow) & 0xFF;
+				SID_Write(sidOffset + 2, v.something_else[0]);
+				SID_Write(sidOffset + 3, v.something_else[2]);
+				if (v.something_else[2] < 0x08)
+					v.whatever2_vibDirToggle--;
 			}
-		} else {
-			// Subtract phase (L0B62-L0B7F): sec; sbc control1 on lo; sbc #0 on hi
-			uint16_t diff = (uint16_t)v.something_else[0] + 0x100 - lfoSpeed;
-			v.something_else[0] = diff & 0xFF;
-			if (diff < 0x100) // borrow
-				v.something_else[2]--;
-			SID_Write(sidOffset + 2, v.something_else[0]); // $D402
-			SID_Write(sidOffset + 3, v.something_else[2]); // $D403
-			// clc; cmp #$08 - check hi byte < 0x08 (L0B7B)
-			if (v.something_else[2] < 0x08) {
-				v.whatever2_vibDirToggle = 0; // dec whatever2 (L0B7F)
-			}
-		}
-		v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
-	}
-
-	// Arpeggio (L0B82) - Check 'whatever1' flag
-	if (v.whatever1) {
-		// Assembly: single counter (stuff+6) cycles 0..speed-1, used directly as arp table index
-		// lda stuff+6; cmp stuff+5; bne L0BA5; lda #0; sta stuff+6
-		uint8_t speed = v.arpSpeedHiNibble; // stuff+5: set from instA0[5] hi nibble
-		if (v.stuff_arp_counter == speed) {
-			v.stuff_arp_counter = 0; // Reset when counter == speed (L0BA0)
+			v.pulseWidth = v.something_else[0] | (v.something_else[2] << 8);
 		}
 
-		// tay; lda stuff+3; clc; adc arpeggio_0,y (L0BA5-L0BAD)
-		uint8_t baseNote = v.currentNote;
-		if (baseNote > 0 && baseNote < 96 && v.stuff_arp_counter < 3) {
-			uint8_t arpOffset = arpeggio_data[v.stuff_arp_counter]; // Counter IS the table index
-			uint8_t arpNote = baseNote + arpOffset;
-			if (arpNote >= 96)
-				arpNote = 95;
-
-			freq = frq_lo[arpNote] | (frq_hi[arpNote] << 8);
-			SID_Write(sidOffset + 0, frq_lo[arpNote]); // sta $D400 (L0BB1)
-			SID_Write(sidOffset + 1, frq_hi[arpNote]); // sta $D401 (L0BB7)
-			v.currentFreq = freq;
-		}
+		if (v.whatever1) {
+			if (v.stuff_arp_counter == v.arpSpeedHiNibble)
+				v.stuff_arp_counter = 0;
 
-		v.stuff_arp_counter++; // inc stuff+6 (L0BBA)
-		// jmp voice_done - arpeggio skips vibrato/porta
-		return;
-	}
-
-	// Vibrato (L0BC0 / L0BC8) - Check 'whatever0' flag
-	// Assembly applies frequency modification EVERY frame.
-	// The timer (things+3) only controls when the direction state advances.
-	if (v.whatever0) {
-		int state = v.things_vib_state;
-		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
-		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+			uint8_t arpNote = v.currentNote;
+			if (v.stuff_arp_counter < ARRAYSIZE(arpeggio_data)) {
+				uint16_t noteWithOffset = v.currentNote + arpeggio_data[v.stuff_arp_counter];
+				arpNote = noteWithOffset >= 96 ? 95 : noteWithOffset;
+			}
 
-		// Apply depth based on state (L0C06, L0C2F, L0BD1)
-		// States 0, 3, 4: subtract (down). States 1, 2: add (up).
-		// State 0: L0C06 (beq). States 1,2: L0C2F (cmp #3; bcc). States 3,4: fall through.
-		if (state == 1 || state == 2) {
-			// Add (L0C2F): clc; lda stuff,x; adc things+1,x
-			uint16_t sum = (uint16_t)freqLo + (v.things_vib_depth & 0xFF);
-			freqLo = sum & 0xFF;
-			freqHi = freqHi + (sum >> 8);
-		} else {
-			// Subtract (L0C06/L0BD1): sec; lda stuff,x; sbc things+1,x
-			uint16_t diff = (uint16_t)freqLo + 0x100 - (v.things_vib_depth & 0xFF);
-			freqLo = diff & 0xFF;
-			if (diff < 0x100) // borrow occurred
-				freqHi--;
+			SID_Write(sidOffset + 0, frq_lo[arpNote]);
+			SID_Write(sidOffset + 1, frq_hi[arpNote]);
+			v.stuff_arp_counter++;
+			return;
 		}
 
-		v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
-		freq = v.stuff_freq_porta_vib;
-		freqDirty = true;
-
-		// Decrement timer, advance state only when expired (dec things+3; bne done)
-		v.things_vib_delay_ctr--;
-		if (v.things_vib_delay_ctr == 0) {
-			v.things_vib_delay_ctr = v.things_vib_delay_reload;
-			v.things_vib_state++;
-			if (v.things_vib_state >= 5) { // cmp #$05; bcc
-				v.things_vib_state = 1;    // Reset to state 1 (0BFE)
+		if (v.whatever0) {
+			uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+			uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+
+			if (v.things_vib_state == 0 || v.things_vib_state >= 3) {
+				uint16_t diff = (uint16_t)freqLo - v.things_vib_depth;
+				uint8_t borrow = (diff > 0xFF) ? 1 : 0;
+				freqLo = diff & 0xFF;
+				freqHi = (freqHi - borrow) & 0xFF;
+			} else {
+				uint16_t sum = (uint16_t)freqLo + v.things_vib_depth;
+				freqLo = sum & 0xFF;
+				freqHi = (freqHi + (sum >> 8)) & 0xFF;
 			}
-		}
-	} // end if(v.whatever0)
-
-	// Portamento (L0C5A) - Check 'whatever2' flag
-	// 4 distinct types matching assembly:
-	// Type 1 (L0C7B): CLC+SBC on lo byte (slide down, borrow to hi)
-	// Type 2 (L0CA6): CLC+ADC on lo byte (slide up, carry to hi)
-	// Type 3 (L0C96): SEC+SBC on hi byte only (fast slide down)
-	// Type >= 4 (L0C6B): CLC+ADC on hi byte only (fast slide up)
-	if (v.whatever2) {
-		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;       // stuff[0]
-		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF; // stuff[4]
-		uint8_t speed = v.portaStepRaw & 0xFF;                 // voice1_something
-
-		if (v.whatever2 == 1) {
-			// Type 1 (L0C7B): clc; sbc = subtract (speed+1) from lo, borrow to hi
-			uint16_t diff = (uint16_t)freqLo - speed; // CLC means borrow, so effectively -(speed+1)
-			freqLo = (diff - 1) & 0xFF; // CLC+SBC = subtract with extra borrow
-			if ((diff - 1) > 0xFF) freqHi--; // Propagate borrow
-			SID_Write(sidOffset + 0, freqLo);
-			SID_Write(sidOffset + 1, freqHi);
-		} else if (v.whatever2 == 2) {
-			// Type 2 (L0CA6): clc; adc on lo, carry to hi
-			uint16_t sum = (uint16_t)freqLo + speed;
-			freqLo = sum & 0xFF;
-			freqHi = freqHi + (sum >> 8);
+
+			v.stuff_freq_porta_vib = freqLo | (freqHi << 8);
 			SID_Write(sidOffset + 0, freqLo);
 			SID_Write(sidOffset + 1, freqHi);
-		} else if (v.whatever2 == 3) {
-			// Type 3 (L0C96): sec; sbc on hi byte only (fast slide down)
-			freqHi = freqHi - speed;
-			SID_Write(sidOffset + 1, freqHi);
-		} else {
-			// Type >= 4 (L0C6B): clc; adc on hi byte only (fast slide up)
-			freqHi = freqHi + speed;
-			SID_Write(sidOffset + 1, freqHi);
+			v.things_vib_delay_ctr--;
+			if (v.things_vib_delay_ctr == 0) {
+				v.things_vib_delay_ctr = v.things_vib_delay_reload;
+				v.things_vib_state++;
+				if (v.things_vib_state >= 5)
+					v.things_vib_state = 1;
+			}
+			return;
 		}
+	}
 
-		v.stuff_freq_porta_vib = (uint16_t)freqLo | ((uint16_t)freqHi << 8);
-		v.currentFreq = v.stuff_freq_porta_vib;
+	if (v.whatever2 == 1) {
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+		uint16_t diff = (uint16_t)freqLo - v.portaStepRaw - 1;
+		uint8_t borrow = (diff > 0xFF) ? 1 : 0;
+		freqLo = diff & 0xFF;
+		freqHi = (freqHi - borrow) & 0xFF;
+		v.stuff_freq_porta_vib = freqLo | (freqHi << 8);
+		SID_Write(sidOffset + 0, freqLo);
+		SID_Write(sidOffset + 1, freqHi);
+	} else if (v.whatever2 == 2) {
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+		uint8_t freqHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+		uint16_t sum = (uint16_t)freqLo + v.portaStepRaw;
+		freqLo = sum & 0xFF;
+		freqHi = (freqHi + (sum >> 8)) & 0xFF;
+		v.stuff_freq_porta_vib = freqLo | (freqHi << 8);
+		SID_Write(sidOffset + 0, freqLo);
+		SID_Write(sidOffset + 1, freqHi);
+	} else if (v.whatever2 == 3) {
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+		uint8_t freqHi = ((v.stuff_freq_porta_vib >> 8) - v.portaStepRaw) & 0xFF;
+		v.stuff_freq_porta_vib = freqLo | (freqHi << 8);
+		SID_Write(sidOffset + 1, freqHi);
+	} else if (v.whatever2 != 0) {
+		uint8_t freqLo = v.stuff_freq_porta_vib & 0xFF;
+		uint8_t freqHi = ((v.stuff_freq_porta_vib >> 8) + v.portaStepRaw) & 0xFF;
+		v.stuff_freq_porta_vib = freqLo | (freqHi << 8);
+		SID_Write(sidOffset + 1, freqHi);
 	}
 
-	// After porta, check for hard restart (L0CBE)
-	// lda instA0[7]; and #$01; beq voice_done; jmp L1005
-	if (instA0[7] & 0x01) {
-		applyHardRestart(v, sidOffset, instA0, instA1);
+	if (instA0[7] & 0x01)
+		applyHardRestart(v, sidOffset, instA1);
+}
+
+void DrillerSIDPlayer::applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA1) {
+	uint8_t storedHi = (v.stuff_freq_base >> 8) & 0xFF;
+	if (storedHi != 0) {
+		storedHi--;
+		v.stuff_freq_base = (v.stuff_freq_base & 0x00FF) | (storedHi << 8);
 	}
 
-	// Write final frequency if modified by vibrato (arp and porta write directly)
-	if (freqDirty && v.currentFreq != freq) {
-		v.currentFreq = freq;
-		SID_Write(sidOffset + 0, freq & 0xFF);
-		SID_Write(sidOffset + 1, (freq >> 8) & 0xFF);
+	if (v.whatever3 != 0) {
+		v.whatever3--;
+		SID_Write(sidOffset + 4, 0x81);
+		SID_Write(sidOffset + 1, storedHi ^ 0x23);
+		return;
 	}
-}
 
-// --- Hard Restart / Buzz Effect ---
-void DrillerSIDPlayer::applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1) {
-	// Corresponds to L1005 onwards
-	debug(DEBUG_LEVEL >= 2, "Driller 1: Applying Hard Restart (Delay=%d, Ctr=%d, Val=%d)", v.hardRestartDelay, v.hardRestartCounter, v.hardRestartValue);
-
-	// Check delay phase (L100D)
-	if (v.hardRestartDelay > 0) {
-		v.hardRestartDelay--;
-		// Set high bit of waveform? (L1015)
-		SID_Write(sidOffset + 4, 0x81); // Force waveform to noise? Or just toggle sync/ring? Or maybe $80 = Noise, $01 = Gate On
-		// Modify frequency slightly (L101A)
-		uint16_t freq = v.stuff_freq_hard_restart; // Use stored base freq
-		// freq ^= 0x2300; // EOR with #$23 on high byte? (L101D) - Check calculation
-		uint8_t hiByte = (freq >> 8) ^ 0x23; // EOR high byte only
-		SID_Write(sidOffset + 1, hiByte);    // Write modified high byte
-		// Keep low byte as is? Yes, original only writes high byte $D401.
-		// Keep current frequency updated? No, use stored base.
-		// v.currentFreq = (hiByte << 8) | (freq & 0xFF); // Update internal state if needed
+	uint8_t currentHi = (v.stuff_freq_porta_vib >> 8) & 0xFF;
+	if (v.whatever4 != instA1[5]) {
+		v.whatever3++;
+		v.whatever4++;
 	} else {
-		// Delay phase over, check frequency change phase (L103A)
-		if (v.hardRestartCounter < v.hardRestartValue) { // Compare with value from inst A1[5] (L103D)
-			v.hardRestartCounter++;                      // Increment counter (L1045)
-			v.hardRestartDelay++;                        // Also increment delay? Seems odd (L1042) - Maybe reloads delay? Yes, seems to reload.
-			// Reset frequency and waveform (L1048 -> L1028)
-			uint16_t freq = v.stuff_freq_hard_restart;
-			SID_Write(sidOffset + 1, (freq >> 8) & 0xFF); // Restore high byte (L1028)
-			SID_Write(sidOffset + 0, freq & 0xFF);        // Restore low byte (L102B implies sta $D401,x AND sta $D400,x ?) No, only $D401. Assume low byte restored too.
-			v.currentFreq = freq;                         // Update internal state
-
-			// Restore waveform from instrument? (L1031) - Uses instA1[2]? Needs Gate bit.
-			uint8_t ctrl = instA1[2];
-			if (v.keyOn)
-				ctrl |= 0x01;
-			else
-				ctrl &= 0xFE; // Add gate state
-			SID_Write(sidOffset + 4, ctrl);
-		} else {
-			// Effect finished (L104A)
-			debug(DEBUG_LEVEL >= 2, "Driller 1: Hard Restart Finished");
-			v.hardRestartActive = false;
-			v.hardRestartCounter = 0; // Reset counters
-			v.hardRestartDelay = 0;
-			// Restore frequency and waveform (L104A -> L1052 -> L1028)
-			uint16_t freq = v.stuff_freq_hard_restart;
-			SID_Write(sidOffset + 1, (freq >> 8) & 0xFF);
-			SID_Write(sidOffset + 0, freq & 0xFF);
-			v.currentFreq = freq; // Update internal state
-
-			uint8_t ctrl = instA1[2]; // Restore waveform from instA1[2]? Needs Gate bit.
-			if (v.keyOn)
-				ctrl |= 0x01;
-			else
-				ctrl &= 0xFE; // Add gate state
-			SID_Write(sidOffset + 4, ctrl);
-		}
+		v.whatever4 = 0;
+		v.whatever3 = 0;
 	}
+
+	SID_Write(sidOffset + 1, currentHi);
+	v.stuff_freq_base = (v.stuff_freq_base & 0x00FF) | (currentHi << 8);
+	SID_Write(sidOffset + 4, instA1[2]);
 }
 
 } // namespace Freescape
diff --git a/engines/freescape/games/driller/c64.music.h b/engines/freescape/games/driller/c64.music.h
index 4cdcc587f5d..f36fc86491f 100644
--- a/engines/freescape/games/driller/c64.music.h
+++ b/engines/freescape/games/driller/c64.music.h
@@ -203,6 +203,11 @@ class DrillerSIDPlayer {
 	enum PlayState { STOPPED,
 					 PLAYING,
 					 CHANGING_TUNE };
+	enum ContinuousEffectEntry {
+		kVoiceDone,
+		kFullEffectPath,
+		kPortamentoOnlyPath
+	};
 	PlayState _playState;
 	uint8_t _targetTuneIndex; // Tune index requested via startMusic
 
@@ -223,16 +228,16 @@ public:
 	void initSID();
 	void destroySID();
 
-private:
+	private:
 	void SID_Write(int reg, uint8_t data);
 	void onTimer();
 	void handleChangeTune(int tuneIndex);
 	void handleResetVoices();
 	void playVoice(int voiceIndex);
-	void applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, int voiceIndex);
-	void postNoteEffectSetup(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
-	void applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
-	void applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1);
-};
+	void applyNote(VoiceState &v, int sidOffset, const uint8_t *instA0);
+	ContinuousEffectEntry postNoteEffectSetup(VoiceState &v, const uint8_t *instA0, const uint8_t *instA1);
+	void applyContinuousEffects(VoiceState &v, int sidOffset, const uint8_t *instA0, const uint8_t *instA1, bool startAtPortamento);
+	void applyHardRestart(VoiceState &v, int sidOffset, const uint8_t *instA1);
+	};
 
 } // namespace Freescape


Commit: d9f552b45129a3afb87e8be9edd589945b842c1e
    https://github.com/scummvm/scummvm/commit/d9f552b45129a3afb87e8be9edd589945b842c1e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: improved dark music for C64

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


diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
index bc0a39a2e52..1943217b897 100644
--- a/engines/freescape/games/dark/c64.music.cpp
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -53,7 +53,7 @@ static const uint8 kFreqLo[96] = {
 };
 
 // Instrument table at $1010 (18 instruments x 8 bytes)
-// Bytes: ctrl, AD, SR, envCtl, vibrato, pwMod, autoFx, flags
+// Bytes: ctrl, AD, SR, initPW, vib/env mode, 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)
@@ -76,8 +76,8 @@ static const uint8 kInstruments[18 * 8] = {
 	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] = {
+// Auxiliary envelope data tables ($154B and $156B, 16 entries each)
+static const uint8 kEnvData[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 },
 };
@@ -183,7 +183,6 @@ void DarkSideC64MusicPlayer::ChannelState::reset() {
 	patData = nullptr;
 	patOffset = 0;
 	instIdx = 0;
-	noteActive = 0;
 	curNote = 0;
 	transpose = 0;
 	freqLo = 0;
@@ -199,24 +198,27 @@ void DarkSideC64MusicPlayer::ChannelState::reset() {
 	arpSeqPos = 0;
 	arpSeqLen = 0;
 	memset(arpSeqData, 0, sizeof(arpSeqData));
-	portaDelta = 0;
-	portaTarget = 0;
+	noteStepCommand = 0;
+	stepDownCounter = 0;
 	vibPhase = 0;
 	vibCounter = 0;
 	pwDirection = 0;
+	delayValue = 0;
 	delayCounter = 0;
 	envCounter = 0;
-	envTable = 0;
-	envSeqActive = false;
-	sustainMode = false;
+	gateOffDisabled = false;
+	gateModeControl = false;
+	specialAttack = false;
+	attackDone = false;
 	waveform = 0;
+	instFlags = 0;
 }
 
 DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
 	_sid = nullptr;
 	_musicActive = false;
 	_speedDiv = 1;
-	_speedCounter = 1;
+	_speedCounter = 0;
 	for (int i = 0; i < 3; i++)
 		_ch[i].reset();
 	initSID();
@@ -274,12 +276,12 @@ void DarkSideC64MusicPlayer::stopMusic() {
 void DarkSideC64MusicPlayer::setupSong() {
 	silenceAll();
 
-	// Set filter: LP+HP on, full volume ($5F)
+	// Init routine at $0FF6.
 	sidWrite(0x18, 0x5F);
 	sidWrite(0x17, 0x00);
 
 	_speedDiv = 1;
-	_speedCounter = 1;
+	_speedCounter = 0;
 
 	for (int ch = 0; ch < 3; ch++) {
 		_ch[ch].reset();
@@ -324,98 +326,145 @@ uint8 DarkSideC64MusicPlayer::readPatByte(int ch) {
 	return b;
 }
 
+void DarkSideC64MusicPlayer::buildEffectArpeggio(int ch) {
+	uint8 bits = _ch[ch].effectParam;
+	uint8 len = 1;
+
+	_ch[ch].arpSeqData[0] = 0;
+	for (int i = 0; i < 8 && len < sizeof(_ch[ch].arpSeqData); i++) {
+		if (bits & (1 << i))
+			_ch[ch].arpSeqData[len++] = kArpIntervals[i];
+	}
+
+	_ch[ch].arpSeqLen = len;
+	_ch[ch].arpSeqPos = 0;
+	_ch[ch].arpPattern = 0;
+}
+
+void DarkSideC64MusicPlayer::loadCurrentFrequency(int ch) {
+	int off = kSIDOffset[ch];
+	uint8 note = (_ch[ch].curNote > 94) ? 94 : _ch[ch].curNote;
+
+	_ch[ch].freqLo = kFreqLo[note];
+	_ch[ch].freqHi = kFreqHi[note];
+	sidWrite(off + 0, _ch[ch].freqLo);
+	sidWrite(off + 1, _ch[ch].freqHi);
+}
+
+void DarkSideC64MusicPlayer::finalizeChannel(int ch) {
+	int off = kSIDOffset[ch];
+
+	if (_ch[ch].durReload != 0 && !_ch[ch].gateOffDisabled) {
+		if ((_ch[ch].durReload >> 1) == _ch[ch].durCounter)
+			sidWrite(off + 4, _ch[ch].waveform & 0xFE);
+	}
+
+	applyPWModulation(ch);
+	sidWrite(off + 2, _ch[ch].pwLo);
+	sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
+}
+
 // ---- 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++;
-	}
+	for (int ch = 0; ch < 3; ch++)
+		_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];
+	if (!_musicActive)
+		return;
 
-	// 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)
+		_speedCounter = _speedDiv;
+	else
+		_speedCounter--;
+}
 
+void DarkSideC64MusicPlayer::processChannel(int ch, bool newBeat) {
 	if (newBeat) {
-		if (_ch[ch].durCounter > 0)
-			_ch[ch].durCounter--;
-
-		if (_ch[ch].durCounter == 0) {
+		_ch[ch].durCounter--;
+		if (_ch[ch].durCounter == 0xFF) {
 			parseCommands(ch);
+			if (!_musicActive)
+				return;
+			finalizeChannel(ch);
+			return;
 		}
-	}
 
-	// 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);
+		if (_ch[ch].noteStepCommand != 0) {
+			if (_ch[ch].noteStepCommand == 0xDE) {
+				if (_ch[ch].curNote > 0)
+					_ch[ch].curNote--;
+			} else if (_ch[ch].curNote < 94) {
+				_ch[ch].curNote++;
+			}
+			loadCurrentFrequency(ch);
+			finalizeChannel(ch);
+			return;
 		}
+	} else if (_ch[ch].stepDownCounter != 0) {
+		_ch[ch].stepDownCounter--;
+		if (_ch[ch].curNote > 0)
+			_ch[ch].curNote--;
+		loadCurrentFrequency(ch);
+		finalizeChannel(ch);
+		return;
 	}
+
+	applyFrameEffects(ch);
+	finalizeChannel(ch);
 }
 
 // ---- Command parser ----
 
 void DarkSideC64MusicPlayer::parseCommands(int ch) {
+	if (_ch[ch].effectMode != 2) {
+		_ch[ch].effectParam = 0;
+		_ch[ch].effectMode = 0;
+		_ch[ch].arpSeqLen = 0;
+		_ch[ch].arpSeqPos = 0;
+	}
+	_ch[ch].arpPattern = 0;
+	_ch[ch].noteStepCommand = 0;
+
 	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;
+			cmd = readPatByte(ch);
+			if (cmd == 0xFF) {
+				loadNextPattern(ch);
+				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;
@@ -423,66 +472,51 @@ void DarkSideC64MusicPlayer::parseCommands(int ch) {
 		}
 
 		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].noteStepCommand = 0xDE;
 			_ch[ch].effectMode = 0xDE;
 			continue;
 		}
 
 		if (cmd == 0x7E) {
-			// Portamento down
+			_ch[ch].noteStepCommand = 0xFE;
 			_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;
+			buildEffectArpeggio(ch);
 			continue;
 		}
 
 		if (cmd == 0x7C) {
-			// Vibrato mode 2
 			_ch[ch].effectMode = 2;
 			_ch[ch].effectParam = readPatByte(ch);
-			_ch[ch].vibPhase = 0;
-			_ch[ch].vibCounter = 0;
+			buildEffectArpeggio(ch);
 			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);
+			_ch[ch].effectMode = 1;
+			_ch[ch].arpPattern = (readPatByte(ch) + _ch[ch].transpose) & 0xFF;
+			_ch[ch].arpParam2 = readPatByte(ch);
 			continue;
 		}
 
 		if (cmd == 0x7A) {
-			// Delay: read 1 byte (delay value)
-			_ch[ch].delayCounter = readPatByte(ch);
-			continue;
+			_ch[ch].delayValue = readPatByte(ch);
+			cmd = readPatByte(ch);
 		}
 
-		// Note (0x00-0x5F)
 		applyNote(ch, cmd);
-		break;
+		return;
 	}
 }
 
@@ -491,226 +525,220 @@ void DarkSideC64MusicPlayer::parseCommands(int ch) {
 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 initialPW = kInstruments[instBase + 3];
 	uint8 autoFx = kInstruments[instBase + 6];
 	uint8 flags = kInstruments[instBase + 7];
+	uint8 actualNote = note;
 
-	_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);
-	}
+	if (actualNote != 0)
+		actualNote = (actualNote + _ch[ch].transpose) & 0xFF;
+	if (actualNote > 94)
+		actualNote = 94;
 
-	// Setup auto-effect from instrument
-	if (autoFx != 0 && _ch[ch].effectMode == 0) {
-		_ch[ch].effectMode = 1;
+	_ch[ch].curNote = actualNote;
+	_ch[ch].waveform = ctrl;
+	_ch[ch].instFlags = flags;
+	_ch[ch].attackDone = false;
+	_ch[ch].envCounter = 0xFF;
+	_ch[ch].gateModeControl = (flags & 0x80) != 0;
+	_ch[ch].specialAttack = (flags & 0x08) != 0;
+	_ch[ch].stepDownCounter = 0;
+
+	if (actualNote != 0 && _ch[ch].effectParam == 0 && autoFx != 0) {
 		_ch[ch].effectParam = autoFx;
+		buildEffectArpeggio(ch);
 	}
 
-	// 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);
+	if (actualNote != 0 && (flags & 0x02)) {
+		_ch[ch].stepDownCounter = 2;
+		_ch[ch].curNote = (_ch[ch].curNote > 92) ? 94 : _ch[ch].curNote + 2;
 	}
 
-	// Reset envelope counter
-	_ch[ch].envCounter = 0;
+	loadCurrentFrequency(ch);
 
-	// 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;
+	if (!_ch[ch].gateModeControl) {
+		_ch[ch].pwLo = initialPW & 0xF0;
+		_ch[ch].pwHi = initialPW & 0x0F;
+		sidWrite(off + 2, _ch[ch].pwLo);
+		sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
 	}
 
-	// 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);
+	sidWrite(off + 5, ad);
+	sidWrite(off + 6, sr);
+	_ch[ch].gateOffDisabled = (sr & 0x0F) == 0x0F;
+	sidWrite(off + 4, 0);
+	sidWrite(off + 4, ctrl);
+	_ch[ch].durCounter = _ch[ch].durReload;
+	_ch[ch].delayCounter = _ch[ch].delayValue;
+	_ch[ch].arpSeqPos = 0;
 }
 
-void DarkSideC64MusicPlayer::applyVibrato(int ch) {
-	if (!_ch[ch].noteActive)
+void DarkSideC64MusicPlayer::applyFrameEffects(int ch) {
+	if (_ch[ch].curNote == 0)
 		return;
 
-	uint8 vibParam = _ch[ch].effectParam;
-	if (vibParam == 0)
+	if (applySpecialAttack(ch))
 		return;
 
-	uint8 depth = vibParam >> 4;
-	uint8 speed = vibParam & 0x0F;
-	if (depth == 0 || speed == 0)
+	if (applyEnvelopeSequence(ch))
 		return;
 
-	uint8 note = _ch[ch].curNote;
-	if (note == 0 || note >= 94)
+	if (applyInstrumentVibrato(ch))
 		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);
+	applyEffectArpeggio(ch);
+	applyTimedSlide(ch);
+}
 
-	// Scale by depth
-	int16 vibDelta = semitoneDelta * depth / 16;
+bool DarkSideC64MusicPlayer::applySpecialAttack(int ch) {
+	if (!_ch[ch].specialAttack || _ch[ch].attackDone)
+		return false;
 
-	// 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;
+	int off = kSIDOffset[ch];
+	if (_ch[ch].envCounter < 1) {
+		sidWrite(off + 0, 0x00);
+		sidWrite(off + 1, 0x48);
+		sidWrite(off + 4, 0x81);
+	} else {
+		loadCurrentFrequency(ch);
+		_ch[ch].attackDone = true;
+		sidWrite(off + 4, _ch[ch].waveform);
 	}
+	return true;
+}
 
-	// Apply offset
-	int16 offset = _ch[ch].vibPhase ? vibDelta : -vibDelta;
-	int16 baseFreq = (int16)curFreq;
-	uint16 newFreq = (uint16)(baseFreq + offset);
+bool DarkSideC64MusicPlayer::applyEnvelopeSequence(int ch) {
+	if ((_ch[ch].instFlags & 0x01) == 0 || _ch[ch].envCounter >= 15)
+		return false;
 
-	_ch[ch].freqLo = newFreq & 0xFF;
-	_ch[ch].freqHi = (newFreq >> 8) & 0xFF;
+	int off = kSIDOffset[ch];
+	uint8 instBase = _ch[ch].instIdx;
+	uint8 envMode = kInstruments[instBase + 4];
+	uint8 table = (envMode & 0x0F) ? 1 : 0;
+	uint8 data = kEnvData[table][_ch[ch].envCounter];
+
+	sidWrite(off + 4, kEnvControl[table][_ch[ch].envCounter]);
+	if (envMode & 0x10) {
+		uint8 note = (_ch[ch].curNote + data > 94) ? 94 : _ch[ch].curNote + data;
+		sidWrite(off + 0, kFreqLo[note]);
+		sidWrite(off + 1, kFreqHi[note]);
+	} else {
+		sidWrite(off + 0, 0x00);
+		sidWrite(off + 1, data + 0x0D);
+	}
+	return true;
 }
 
-void DarkSideC64MusicPlayer::applyArpeggio(int ch) {
-	if (!_ch[ch].noteActive || _ch[ch].arpSeqLen == 0)
+bool DarkSideC64MusicPlayer::applyInstrumentVibrato(int ch) {
+	uint8 instBase = _ch[ch].instIdx;
+	uint8 vib = kInstruments[instBase + 4];
+	if (vib == 0 || _ch[ch].curNote >= 94)
+		return false;
+
+	uint8 shift = vib & 0x0F;
+	uint8 span = vib >> 4;
+	if (span == 0)
+		return false;
+
+	uint16 noteFreq = ((uint16)kFreqHi[_ch[ch].curNote] << 8) | kFreqLo[_ch[ch].curNote];
+	uint16 nextFreq = ((uint16)kFreqHi[_ch[ch].curNote + 1] << 8) | kFreqLo[_ch[ch].curNote + 1];
+	uint16 delta = nextFreq - noteFreq;
+
+	while (shift-- != 0)
+		delta >>= 1;
+
+	if (_ch[ch].vibPhase & 0x80) {
+		if (_ch[ch].vibCounter != 0)
+			_ch[ch].vibCounter--;
+		if (_ch[ch].vibCounter == 0)
+			_ch[ch].vibPhase = 0;
+	} else {
+		_ch[ch].vibCounter++;
+		if (_ch[ch].vibCounter >= span)
+			_ch[ch].vibPhase = 0xFF;
+	}
+
+	if (_ch[ch].delayCounter != 0) {
+		_ch[ch].delayCounter--;
+		return false;
+	}
+
+	int32 freq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+	for (uint8 i = 0; i < (span >> 1); i++)
+		freq -= delta;
+	for (uint8 i = 0; i < _ch[ch].vibCounter; i++)
+		freq += delta;
+
+	if (freq < 0)
+		freq = 0;
+	if (freq > 0xFFFF)
+		freq = 0xFFFF;
+
+	int off = kSIDOffset[ch];
+	sidWrite(off + 0, freq & 0xFF);
+	sidWrite(off + 1, (freq >> 8) & 0xFF);
+	return true;
+}
+
+void DarkSideC64MusicPlayer::applyEffectArpeggio(int ch) {
+	if (_ch[ch].effectParam == 0 || _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;
+	uint8 note = (_ch[ch].curNote + _ch[ch].arpSeqData[_ch[ch].arpSeqPos] > 94) ? 94 : _ch[ch].curNote + _ch[ch].arpSeqData[_ch[ch].arpSeqPos];
+	_ch[ch].arpSeqPos++;
 
-	_ch[ch].freqLo = kFreqLo[arpNote];
-	_ch[ch].freqHi = kFreqHi[arpNote];
+	int off = kSIDOffset[ch];
+	sidWrite(off + 0, kFreqLo[note]);
+	sidWrite(off + 1, kFreqHi[note]);
 }
 
-void DarkSideC64MusicPlayer::unpackArpeggio(int ch) {
-	uint8 bits = _ch[ch].arpPattern;
+void DarkSideC64MusicPlayer::applyTimedSlide(int ch) {
+	if (_ch[ch].arpPattern == 0)
+		return;
 
-	// First entry: base note (interval 0)
-	_ch[ch].arpSeqData[0] = 0;
-	uint8 len = 1;
+	uint8 total = _ch[ch].durReload;
+	uint8 remaining = _ch[ch].durCounter;
+	uint8 start = _ch[ch].arpParam2 >> 4;
+	uint8 span = _ch[ch].arpParam2 & 0x0F;
+	uint8 elapsed = total - remaining;
 
-	for (int i = 0; i < 8 && len < 19; i++) {
-		if (bits & (1 << i)) {
-			_ch[ch].arpSeqData[len] = kArpIntervals[i];
-			len++;
-		}
-	}
+	if (elapsed <= start || elapsed > start + span || span == 0)
+		return;
 
-	_ch[ch].arpSeqLen = len;
-	_ch[ch].arpSeqPos = 0;
-}
+	uint8 currentNote = (_ch[ch].curNote > 94) ? 94 : _ch[ch].curNote;
+	uint8 targetNote = (_ch[ch].arpPattern > 94) ? 94 : _ch[ch].arpPattern;
+	if (currentNote == targetNote)
+		return;
 
-void DarkSideC64MusicPlayer::applyPortamento(int ch) {
-	if (!_ch[ch].noteActive)
+	uint16 currentFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+	uint16 sourceFreq = ((uint16)kFreqHi[currentNote] << 8) | kFreqLo[currentNote];
+	uint16 targetFreq = ((uint16)kFreqHi[targetNote] << 8) | kFreqLo[targetNote];
+	uint16 diff = (sourceFreq > targetFreq) ? (sourceFreq - targetFreq) : (targetFreq - sourceFreq);
+	uint16 divisor = span * (_speedDiv + 1);
+	if (divisor == 0)
 		return;
 
-	uint16 curFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
-	int32 newFreq = (int32)curFreq + _ch[ch].portaDelta;
+	uint16 delta = diff / divisor;
+	if (delta == 0)
+		return;
 
-	// 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;
-	}
+	if (targetFreq > sourceFreq)
+		currentFreq += delta;
+	else
+		currentFreq -= delta;
 
-	_ch[ch].freqLo = (uint16)newFreq & 0xFF;
-	_ch[ch].freqHi = ((uint16)newFreq >> 8) & 0xFF;
+	_ch[ch].freqLo = currentFreq & 0xFF;
+	_ch[ch].freqHi = (currentFreq >> 8) & 0xFF;
+
+	int off = kSIDOffset[ch];
+	sidWrite(off + 0, _ch[ch].freqLo);
+	sidWrite(off + 1, _ch[ch].freqHi);
 }
 
 void DarkSideC64MusicPlayer::applyPWModulation(int ch) {
@@ -721,44 +749,23 @@ void DarkSideC64MusicPlayer::applyPWModulation(int ch) {
 
 	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;
+		_ch[ch].pwLo += pwMod;
 	} else {
-		// Triangle sweep between pwHi=$08 and pwHi=$0F
+		uint16 pw = ((uint16)_ch[ch].pwHi << 8) | _ch[ch].pwLo;
 		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;
-			}
+			if ((pw >> 8) >= 0x0F)
+				_ch[ch].pwDirection = 1;
 		} else {
 			pw -= pwMod;
-			if ((pw >> 8) <= 0x08)
+			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]);
+		_ch[ch].pwLo = pw & 0xFF;
+		_ch[ch].pwHi = (pw >> 8) & 0xFF;
+	}
 }
 
 } // namespace Freescape
diff --git a/engines/freescape/games/dark/c64.music.h b/engines/freescape/games/dark/c64.music.h
index ffbd7a8427b..19d61343e6b 100644
--- a/engines/freescape/games/dark/c64.music.h
+++ b/engines/freescape/games/dark/c64.music.h
@@ -49,18 +49,21 @@ private:
 	void setupSong();
 	void silenceAll();
 	void loadNextPattern(int ch);
-	void unpackArpeggio(int ch);
+	void buildEffectArpeggio(int ch);
+	void loadCurrentFrequency(int ch);
+	void finalizeChannel(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 applyFrameEffects(int ch);
+	bool applySpecialAttack(int ch);
+	bool applyEnvelopeSequence(int ch);
+	bool applyInstrumentVibrato(int ch);
+	void applyEffectArpeggio(int ch);
+	void applyTimedSlide(int ch);
 	void applyPWModulation(int ch);
-	void applyEnvelope(int ch);
 
 	uint8 readPatByte(int ch);
 
@@ -78,7 +81,6 @@ private:
 		int patOffset;
 
 		uint8 instIdx;         // $15F6: instrument# * 8
-		uint8 noteActive;      // $1599
 		uint8 curNote;         // $159C: note index (with transpose)
 		uint8 transpose;       // $15C4
 
@@ -90,7 +92,7 @@ private:
 		uint8 durReload;       // $15B8
 		uint8 durCounter;      // $15BB
 
-		uint8 effectMode;      // $15BE: 0=none, 1=arp/vib1, 2=vib2, 0xDE=portaUp, 0xFE=portaDn
+		uint8 effectMode;      // $15BE: mode 2 keeps effectParam alive across note loads
 		uint8 effectParam;     // $15C1
 
 		uint8 arpPattern;      // $15C7
@@ -99,23 +101,24 @@ private:
 		uint8 arpSeqData[20];  // $160C
 		uint8 arpSeqLen;
 
-		int16 portaDelta;      // $15CD/$15CE: per-tick freq delta
-		uint16 portaTarget;    // target frequency for portamento
+		uint8 noteStepCommand; // $15D0: self-modified INC/DEC opcode for beat step
+		uint8 stepDownCounter; // $15D3: short downward slide counter
 
 		uint8 vibPhase;        // $15DE
 		uint8 vibCounter;      // $15E1
 
 		uint8 pwDirection;     // $15D6: 0=up, 1=down
 
+		uint8 delayValue;      // $15E4
 		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)
+		bool gateOffDisabled;  // $1606: set when SR release nibble is $F
+		bool gateModeControl;  // $1600: flags bit 7
+		bool specialAttack;    // flags bit 3
+		bool attackDone;       // $1603
+		uint8 waveform;        // current instrument ctrl byte
+		uint8 instFlags;       // current instrument flags byte
 
 		void reset();
 	};


Commit: cceaa7b31d72d96ff1e9e41571b1f1a241247741
    https://github.com/scummvm/scummvm/commit/cceaa7b31d72d96ff1e9e41571b1f1a241247741
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: implement standing/flying indicator in dark for C64

Changed paths:
    engines/freescape/games/dark/c64.cpp
    engines/freescape/games/dark/dark.cpp


diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index 1d4b1d521d9..d3ce0bd2555 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -28,12 +28,60 @@
 
 namespace Freescape {
 
+extern byte kC64Palette[16][3];
+
+static const byte *getDarkC64IndicatorSprite(const byte *spriteData, byte pointer) {
+	assert(pointer >= 3 && pointer <= 10);
+	return spriteData + (pointer - 3) * 64;
+}
+
+static void blitDarkC64IndicatorSprite(Graphics::Surface *surface, const byte *sprite, byte color, const Graphics::PixelFormat &pixelFormat) {
+	uint32 pixel = pixelFormat.ARGBToColor(0xFF, kC64Palette[color][0], kC64Palette[color][1], kC64Palette[color][2]);
+	for (int y = 0; y < 21; y++) {
+		for (int byteIndex = 0; byteIndex < 3; byteIndex++) {
+			byte value = sprite[y * 3 + byteIndex];
+			for (int bit = 0; bit < 8; bit++) {
+				if ((value & (0x80 >> bit)) == 0)
+					continue;
+				surface->setPixel(byteIndex * 8 + bit, y, pixel);
+			}
+		}
+	}
+}
+
+static Graphics::Surface *composeDarkC64Indicator(const byte *spriteData, byte basePointer, byte overlayPointer1, byte overlayColor1, byte overlayPointer2, byte overlayColor2, bool includeSprite6, const Graphics::PixelFormat &pixelFormat) {
+	Graphics::Surface *surface = new Graphics::Surface();
+	surface->create(24, 21, pixelFormat);
+	surface->fillRect(Common::Rect(0, 0, surface->w, surface->h), pixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00));
+
+	blitDarkC64IndicatorSprite(surface, getDarkC64IndicatorSprite(spriteData, basePointer), 2, pixelFormat);
+	blitDarkC64IndicatorSprite(surface, getDarkC64IndicatorSprite(spriteData, 4), 12, pixelFormat);
+	if (overlayPointer1)
+		blitDarkC64IndicatorSprite(surface, getDarkC64IndicatorSprite(spriteData, overlayPointer1), overlayColor1, pixelFormat);
+	if (overlayPointer2)
+		blitDarkC64IndicatorSprite(surface, getDarkC64IndicatorSprite(spriteData, overlayPointer2), overlayColor2, pixelFormat);
+	if (includeSprite6)
+		blitDarkC64IndicatorSprite(surface, getDarkC64IndicatorSprite(spriteData, 6), 8, pixelFormat);
+	return surface;
+}
+
+static void loadDarkC64Indicators(Common::SeekableReadStream *file, uint32 offset, Common::Array<Graphics::Surface *> &indicators, const Graphics::PixelFormat &pixelFormat) {
+	byte spriteData[8 * 64];
+	file->seek(offset);
+	file->read(spriteData, sizeof(spriteData));
+
+	// $8161 has three distinct body states: crawl (pointer 3 with layers 5/7),
+	// walk (pointer 3 with layers disabled), and fly (pointer 8 with 5/7 then 9/10).
+	indicators.push_back(composeDarkC64Indicator(spriteData, 3, 0, 0, 0, 0, false, pixelFormat));
+	indicators.push_back(composeDarkC64Indicator(spriteData, 3, 0, 0, 0, 0, false, pixelFormat));
+	indicators.push_back(composeDarkC64Indicator(spriteData, 8, 5, 7, 7, 8, true, pixelFormat));
+	indicators.push_back(composeDarkC64Indicator(spriteData, 8, 9, 7, 10, 8, true, pixelFormat));
+}
+
 void DarkEngine::initC64() {
 	_viewArea = Common::Rect(32, 24, 288, 127);
 }
 
-extern byte kC64Palette[16][3];
-
 void DarkEngine::loadAssetsC64FullGame() {
 	Common::File file;
 	file.open("darkside.c64.data");
@@ -52,6 +100,7 @@ void DarkEngine::loadAssetsC64FullGame() {
 		loadFonts(&dfile, 0xc3e);
 		loadGlobalObjects(&dfile, 0x20bd, 23);
 		load8bitBinary(&dfile, 0x9b3e, 16);
+		loadDarkC64Indicators(&dfile, 0xadba, _indicators, _gfx->_texturePixelFormat);
 	} else if (_variant & GF_C64_DISC) {
 		loadMessagesFixedSize(&file, 0x16a3, 16, 27);
 		loadFonts(&file, 0x402);
@@ -66,6 +115,7 @@ void DarkEngine::loadAssetsC64FullGame() {
 		Common::MemoryReadStream stream(_extraBuffer, 0x300, DisposeAfterUse::NO);
 		loadGlobalObjects(&stream, 0x0, 23);
 		load8bitBinary(&file, 0x8914, 16);
+		loadDarkC64Indicators(&file, 0xb2d4, _indicators, _gfx->_texturePixelFormat);
 	} else
 		error("Unknown C64 variant %x", _variant);
 
@@ -274,6 +324,8 @@ void DarkEngine::drawC64UI(Graphics::Surface *surface) {
 	}
 	drawBinaryClock(surface, 304, 124, front, back);
 	drawVerticalCompass(surface, 17, 77, _pitch, front);
+	surface->fillRect(Common::Rect(152, 148 - 5, 184, 176 - 15), _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00));
+	drawIndicator(surface, 160 - 1, 148 - 1);
 }
 
 } // End of namespace Freescape
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index c16c082cb89..068b14a9789 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -986,6 +986,17 @@ void DarkEngine::drawIndicator(Graphics::Surface *surface, int xPosition, int yP
 		return;
 	}
 
+	if (isC64() && _indicators.size() >= 4) {
+		if (_hasFallen)
+			return;
+
+		const Graphics::Surface *indicator = _indicators[1];
+		if (_flyMode)
+			indicator = _indicators[2 + ((g_system->getMillis() / 40) & 1)];
+		surface->copyRectToSurfaceWithKey(*indicator, xPosition, yPosition, Common::Rect(indicator->w, indicator->h), 0);
+		return;
+	}
+
 	if (_indicators.size() == 0)
 		return;
 	if (_hasFallen)


Commit: 49abb97a8ad394d669a6cff4be2e08d0b5fd243a
    https://github.com/scummvm/scummvm/commit/49abb97a8ad394d669a6cff4be2e08d0b5fd243a
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: more indicators in dark for C64

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


diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index d3ce0bd2555..6b882b15fd4 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -30,6 +30,26 @@ namespace Freescape {
 
 extern byte kC64Palette[16][3];
 
+static const byte kDarkC64CompassMask[] = {0xfe, 0xfa, 0x69, 0xf5, 0xea};
+static const byte kDarkC64CompassColor1[] = {0xfc, 0x1f, 0xf1, 0xcb, 0x3b};
+static const byte kDarkC64CompassColor2[] = {0x0b, 0x0c, 0x0c, 0x0f, 0x0c};
+static const byte kDarkC64ModeIndicatorPalette[2][3][4] = {
+	{{0, 11, 15, 1}, {0, 11, 15, 2}, {0, 15, 1, 11}},
+	{{0, 15, 12, 11}, {0, 15, 12, 11}, {0, 15, 2, 11}}
+};
+
+static bool isDarkC64BrokenCompassArea(uint16 areaID) {
+	return areaID == 1 || areaID == 18 || areaID == 27 || areaID == 28;
+}
+
+static int getDarkC64CompassTarget(float yaw) {
+	int target = int(yaw / 5.0f + 0.5f);
+	target = (target + 18) % 72;
+	if (target < 0)
+		target += 72;
+	return target;
+}
+
 static const byte *getDarkC64IndicatorSprite(const byte *spriteData, byte pointer) {
 	assert(pointer >= 3 && pointer <= 10);
 	return spriteData + (pointer - 3) * 64;
@@ -65,6 +85,12 @@ static Graphics::Surface *composeDarkC64Indicator(const byte *spriteData, byte b
 	return surface;
 }
 
+static void loadDarkC64CompassTable(Common::SeekableReadStream *file, uint32 offset, Common::Array<byte> &table) {
+	table.resize(80);
+	file->seek(offset);
+	file->read(table.data(), table.size());
+}
+
 static void loadDarkC64Indicators(Common::SeekableReadStream *file, uint32 offset, Common::Array<Graphics::Surface *> &indicators, const Graphics::PixelFormat &pixelFormat) {
 	byte spriteData[8 * 64];
 	file->seek(offset);
@@ -78,10 +104,112 @@ static void loadDarkC64Indicators(Common::SeekableReadStream *file, uint32 offse
 	indicators.push_back(composeDarkC64Indicator(spriteData, 8, 9, 7, 10, 8, true, pixelFormat));
 }
 
+static void loadDarkC64ModeFrames(Common::SeekableReadStream *file, uint32 offset, Common::Array<Graphics::ManagedSurface *> &frames, const Graphics::PixelFormat &pixelFormat) {
+	byte data[6 * 48];
+	file->seek(offset);
+	file->read(data, sizeof(data));
+
+	for (int frameIndex = 0; frameIndex < 6; frameIndex++) {
+		Graphics::ManagedSurface *surface = new Graphics::ManagedSurface();
+		surface->create(24, 16, pixelFormat);
+		surface->fillRect(Common::Rect(0, 0, surface->w, surface->h),
+			pixelFormat.ARGBToColor(0xFF, kC64Palette[0][0], kC64Palette[0][1], kC64Palette[0][2]));
+
+		for (int band = 0; band < 2; band++) {
+			for (int cell = 0; cell < 3; cell++) {
+				for (int row = 0; row < 8; row++) {
+					byte bitmapByte = data[frameIndex * 48 + band * 24 + cell * 8 + row];
+					for (int pair = 0; pair < 4; pair++) {
+						byte color = kDarkC64ModeIndicatorPalette[band][cell][(bitmapByte >> (6 - pair * 2)) & 0x03];
+						uint32 pixel = pixelFormat.ARGBToColor(0xFF, kC64Palette[color][0], kC64Palette[color][1], kC64Palette[color][2]);
+						int x = cell * 8 + pair * 2;
+						int y = band * 8 + row;
+						surface->setPixel(x, y, pixel);
+						surface->setPixel(x + 1, y, pixel);
+					}
+				}
+			}
+		}
+		frames.push_back(surface);
+	}
+}
+
 void DarkEngine::initC64() {
 	_viewArea = Common::Rect(32, 24, 288, 127);
 }
 
+void DarkEngine::drawC64Compass(Graphics::Surface *surface) {
+	if (!_currentArea || _c64CompassTable.size() < 80)
+		return;
+
+	int target = getDarkC64CompassTarget(_yaw);
+	if (!_c64CompassInitialized) {
+		_c64CompassPosition = target;
+		_c64CompassInitialized = true;
+	}
+
+	if (isDarkC64BrokenCompassArea(_currentArea->getAreaID())) {
+		if ((_ticks & 1) == 0)
+			_c64CompassPosition = (_c64CompassPosition + 71) % 72;
+	} else {
+		int delta = (_c64CompassPosition - target + 72) % 72;
+		if (delta != 0) {
+			if (delta < 37)
+				_c64CompassPosition = (_c64CompassPosition + 71) % 72;
+			else
+				_c64CompassPosition = (_c64CompassPosition + 1) % 72;
+		}
+	}
+
+	int start = (_c64CompassPosition + 72 - 20) % 72;
+	int tableOffset = start & 0xf8;
+	int shift = (start & 0x07) >> 1;
+
+	for (int cell = 0; cell < 5; cell++) {
+		byte color1 = kDarkC64CompassColor2[cell] & 0x0f;
+		byte color2 = kDarkC64CompassColor1[cell] >> 4;
+		byte color3 = kDarkC64CompassColor1[cell] & 0x0f;
+		byte mask = kDarkC64CompassMask[cell];
+
+		for (int row = 0; row < 8; row++) {
+			int index = tableOffset + cell * 8 + row;
+			if (index >= 72)
+				index -= 72;
+
+			byte bitmapByte = _c64CompassTable[index];
+			if (shift > 0) {
+				byte nextByte = _c64CompassTable[index + 8];
+				bitmapByte = ((bitmapByte << (2 * shift)) | (nextByte >> (8 - 2 * shift))) & 0xff;
+			}
+			bitmapByte &= mask;
+
+			for (int pair = 0; pair < 4; pair++) {
+				byte pixelPair = (bitmapByte >> (6 - pair * 2)) & 0x03;
+				byte color = 0;
+				if (pixelPair == 1)
+					color = color2;
+				else if (pixelPair == 2)
+					color = color3;
+				else if (pixelPair == 3)
+					color = color1;
+
+				uint32 pixel = _gfx->_texturePixelFormat.ARGBToColor(0xFF, kC64Palette[color][0], kC64Palette[color][1], kC64Palette[color][2]);
+				surface->setPixel(256 + cell * 8 + pair * 2, 144 + row, pixel);
+				surface->setPixel(256 + cell * 8 + pair * 2 + 1, 144 + row, pixel);
+			}
+		}
+	}
+}
+
+void DarkEngine::drawC64ModeIndicator(Graphics::Surface *surface) {
+	if (_c64ModeFrames.size() < 6)
+		return;
+
+	const Graphics::ManagedSurface *frame = _shootMode ? _c64ModeFrames[0] : _c64ModeFrames[1];
+
+	surface->copyRectToSurface(*frame, 264, 176, Common::Rect(frame->w, frame->h));
+}
+
 void DarkEngine::loadAssetsC64FullGame() {
 	Common::File file;
 	file.open("darkside.c64.data");
@@ -100,7 +228,9 @@ void DarkEngine::loadAssetsC64FullGame() {
 		loadFonts(&dfile, 0xc3e);
 		loadGlobalObjects(&dfile, 0x20bd, 23);
 		load8bitBinary(&dfile, 0x9b3e, 16);
+		loadDarkC64CompassTable(&dfile, 0x7e37, _c64CompassTable);
 		loadDarkC64Indicators(&dfile, 0xadba, _indicators, _gfx->_texturePixelFormat);
+		loadDarkC64ModeFrames(&dfile, 0xd2f6, _c64ModeFrames, _gfx->_texturePixelFormat);
 	} else if (_variant & GF_C64_DISC) {
 		loadMessagesFixedSize(&file, 0x16a3, 16, 27);
 		loadFonts(&file, 0x402);
@@ -115,7 +245,9 @@ void DarkEngine::loadAssetsC64FullGame() {
 		Common::MemoryReadStream stream(_extraBuffer, 0x300, DisposeAfterUse::NO);
 		loadGlobalObjects(&stream, 0x0, 23);
 		load8bitBinary(&file, 0x8914, 16);
+		loadDarkC64CompassTable(&file, 0x7c5a, _c64CompassTable);
 		loadDarkC64Indicators(&file, 0xb2d4, _indicators, _gfx->_texturePixelFormat);
+		loadDarkC64ModeFrames(&file, 0xc0d1, _c64ModeFrames, _gfx->_texturePixelFormat);
 	} else
 		error("Unknown C64 variant %x", _variant);
 
@@ -323,9 +455,11 @@ void DarkEngine::drawC64UI(Graphics::Surface *surface) {
 			surface->drawLine(64, 147 + 8, 127 - (_maxEnergy - energy) - 1, 155, lineColor);
 	}
 	drawBinaryClock(surface, 304, 124, front, back);
+	drawC64Compass(surface);
 	drawVerticalCompass(surface, 17, 77, _pitch, front);
 	surface->fillRect(Common::Rect(152, 148 - 5, 184, 176 - 15), _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0x00, 0x00, 0x00));
 	drawIndicator(surface, 160 - 1, 148 - 1);
+	drawC64ModeIndicator(surface);
 }
 
 } // End of namespace Freescape
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 068b14a9789..b8d3988e021 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -37,6 +37,8 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
 	_playerC64Sfx = nullptr;
 	_playerC64Music = nullptr;
 	_c64UseSFX = false;
+	_c64CompassInitialized = false;
+	_c64CompassPosition = 0;
 
 	// These sounds can be overriden by the class of each platform
 	_soundIndexShoot = 1;
@@ -118,6 +120,10 @@ DarkEngine::~DarkEngine() {
 		indicator->free();
 		delete indicator;
 	}
+	for (auto &frame : _c64ModeFrames) {
+		frame->free();
+		delete frame;
+	}
 }
 
 void DarkEngine::addECDs(Area *area) {
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index 984bed34cf5..e60926e1522 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -101,6 +101,7 @@ public:
 	void drawZXUI(Graphics::Surface *surface) override;
 	void drawCPCUI(Graphics::Surface *surface) override;
 	void drawAmigaAtariSTUI(Graphics::Surface *surface) override;
+	void drawC64Compass(Graphics::Surface *surface);
 
 	Font _fontBig;
 	Font _fontMedium;
@@ -109,6 +110,7 @@ public:
 	Common::Array<Graphics::ManagedSurface *> _cpcJetpackIndicators;
 	Common::Array<Graphics::ManagedSurface *> _cpcActionIndicators;
 	uint32 _cpcActionIndicatorUntilMillis;
+	Common::Array<Graphics::ManagedSurface *> _c64ModeFrames;
 
 	// Dark Side Amiga stores the grounded jetpack indicator states as raw
 	// 4-plane bitplane data. The executable drives those frames through a tiny
@@ -143,6 +145,9 @@ public:
 	DarkSideC64SFXPlayer *_playerC64Sfx;
 	DarkSideC64MusicPlayer *_playerC64Music;
 	bool _c64UseSFX;
+	bool _c64CompassInitialized;
+	int _c64CompassPosition;
+	Common::Array<byte> _c64CompassTable;
 	void playSoundC64(int index) override;
 	void toggleC64Sound();
 
@@ -165,6 +170,7 @@ private:
 	void loadCPCIndicator(Common::SeekableReadStream *file, uint32 offset, Common::Array<Graphics::ManagedSurface *> &target);
 	void loadCPCIndicatorData(const byte *data, int widthBytes, int height, Common::Array<Graphics::ManagedSurface *> &target);
 	void loadCPCIndicators(Common::SeekableReadStream *file);
+	void drawC64ModeIndicator(Graphics::Surface *surface);
 	void drawCPCSprite(Graphics::Surface *surface, const Graphics::ManagedSurface *indicator, int xPosition, int yPosition);
 	void drawCPCIndicator(Graphics::Surface *surface, int xPosition, int yPosition);
 	void drawVerticalCompass(Graphics::Surface *surface, int x, int y, float angle, uint32 color);


Commit: 0e0db53cc20b2d319377268747d02bfdbb7ba62d
    https://github.com/scummvm/scummvm/commit/0e0db53cc20b2d319377268747d02bfdbb7ba62d
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-30T11:32:36+02:00

Commit Message:
FREESCAPE: added music for eclipse c64

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


diff --git a/engines/freescape/games/eclipse/c64.cpp b/engines/freescape/games/eclipse/c64.cpp
index e9e670585f5..cded7fcc8a8 100644
--- a/engines/freescape/games/eclipse/c64.cpp
+++ b/engines/freescape/games/eclipse/c64.cpp
@@ -22,6 +22,7 @@
 #include "common/file.h"
 
 #include "freescape/freescape.h"
+#include "freescape/games/eclipse/c64.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -83,6 +84,18 @@ void EclipseEngine::loadAssetsC64FullGame() {
 
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
+
+	Common::File musicFile;
+	musicFile.open("totec1.prg");
+	if (musicFile.isOpen()) {
+		uint16 loadAddress = musicFile.readUint16LE();
+		if (loadAddress == 0x0410) {
+			_c64MusicData.resize(musicFile.size() - 2);
+			musicFile.read(_c64MusicData.data(), _c64MusicData.size());
+			delete _playerC64Music;
+			_playerC64Music = new EclipseC64MusicPlayer(_c64MusicData);
+		}
+	}
 }
 
 
diff --git a/engines/freescape/games/eclipse/c64.music.cpp b/engines/freescape/games/eclipse/c64.music.cpp
new file mode 100644
index 00000000000..82fae5526ca
--- /dev/null
+++ b/engines/freescape/games/eclipse/c64.music.cpp
@@ -0,0 +1,569 @@
+/* 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 "engines/freescape/games/eclipse/c64.music.h"
+
+#include "common/textconsole.h"
+#include "freescape/wb.h"
+
+namespace Freescape {
+
+static const int kSIDOffset[] = {0, 7, 14};
+
+void EclipseC64MusicPlayer::ChannelState::reset() {
+	orderAddr = 0;
+	orderPos = 0;
+	patternAddr = 0;
+	patternOffset = 0;
+	instrumentOffset = 0;
+	currentNote = 0;
+	transpose = 0;
+	frequencyLow = 0;
+	frequencyHigh = 0;
+	pulseWidthLow = 0;
+	pulseWidthHigh = 0;
+	durationReload = 0;
+	durationCounter = 0;
+	effectMode = 0;
+	effectParam = 0;
+	arpeggioTarget = 0;
+	arpeggioParam = 0;
+	arpeggioSequencePos = 0;
+	memset(arpeggioSequence, 0, sizeof(arpeggioSequence));
+	arpeggioSequenceLen = 0;
+	noteStepCommand = 0;
+	stepDownCounter = 0;
+	vibratoPhase = 0;
+	vibratoCounter = 0;
+	pulseWidthDirection = 0;
+	delayValue = 0;
+	delayCounter = 0;
+	waveform = 0;
+	instrumentFlags = 0;
+	gateOffDisabled = false;
+}
+
+EclipseC64MusicPlayer::EclipseC64MusicPlayer(const Common::Array<byte> &musicData)
+	: _sid(nullptr),
+	  _musicData(musicData),
+	  _musicActive(false),
+	  _speedDivider(1),
+	  _speedCounter(0) {
+	memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
+	for (int i = 0; i < 8; i++)
+		_arpeggioIntervals[i] = readByte(kArpeggioIntervalTable + i);
+	initSID();
+}
+
+EclipseC64MusicPlayer::~EclipseC64MusicPlayer() {
+	destroySID();
+}
+
+void EclipseC64MusicPlayer::destroySID() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+		_sid = nullptr;
+	}
+}
+
+void EclipseC64MusicPlayer::initSID() {
+	if (_sid) {
+		_sid->stop();
+		delete _sid;
+	}
+
+	_sid = SID::Config::create(SID::Config::kSidPAL);
+	if (!_sid || !_sid->init()) {
+		warning("EclipseC64MusicPlayer: Failed to create SID emulator");
+		return;
+	}
+
+	_sid->start(new Common::Functor0Mem<void, EclipseC64MusicPlayer>(this, &EclipseC64MusicPlayer::onTimer), 50);
+}
+
+bool EclipseC64MusicPlayer::isPlaying() const {
+	return _musicActive;
+}
+
+void EclipseC64MusicPlayer::sidWrite(int reg, byte data) {
+	if (_sid)
+		_sid->writeReg(reg, data);
+}
+
+void EclipseC64MusicPlayer::silenceAll() {
+	for (int i = 0; i <= 0x18; i++)
+		sidWrite(i, 0);
+}
+
+byte EclipseC64MusicPlayer::readByte(uint16 address) const {
+	if (address < kLoadAddress)
+		return 0;
+	uint32 offset = address - kLoadAddress;
+	if (offset >= _musicData.size())
+		return 0;
+	return _musicData[offset];
+}
+
+uint16 EclipseC64MusicPlayer::readWordLE(uint16 address) const {
+	return readByte(address) | (readByte(address + 1) << 8);
+}
+
+uint16 EclipseC64MusicPlayer::readPatternPointer(byte index) const {
+	if (index >= kPatternCount)
+		return 0;
+	return readByte(kPatternPointerLowTable + index) | (readByte(kPatternPointerHighTable + index) << 8);
+}
+
+byte EclipseC64MusicPlayer::readInstrumentByte(byte instrumentOffset, byte field) const {
+	return readByte(kInstrumentTable + instrumentOffset + field);
+}
+
+byte EclipseC64MusicPlayer::readPatternByte(int channel) {
+	byte value = readByte(_channels[channel].patternAddr + _channels[channel].patternOffset);
+	_channels[channel].patternOffset++;
+	return value;
+}
+
+byte EclipseC64MusicPlayer::clampNote(byte note) const {
+	return note > kMaxNote ? kMaxNote : note;
+}
+
+void EclipseC64MusicPlayer::startMusic() {
+	if (_musicData.empty())
+		return;
+	setupSong();
+}
+
+void EclipseC64MusicPlayer::stopMusic() {
+	_musicActive = false;
+	silenceAll();
+}
+
+void EclipseC64MusicPlayer::setupSong() {
+	silenceAll();
+
+	sidWrite(0x15, 0x00);
+	sidWrite(0x16, 0x00);
+	sidWrite(0x17, 0x77);
+	sidWrite(0x18, 0x5F);
+
+	_speedDivider = 1;
+	_speedCounter = 0;
+
+	for (int i = 0; i < kChannelCount; i++) {
+		_channels[i].reset();
+		_channels[i].orderAddr = readWordLE(kOrderPointerTable + i * 2);
+		loadNextPattern(i);
+	}
+
+	_musicActive = true;
+}
+
+void EclipseC64MusicPlayer::loadNextPattern(int channel) {
+	int safety = 200;
+	while (safety-- > 0) {
+		byte value = readByte(_channels[channel].orderAddr + _channels[channel].orderPos);
+		_channels[channel].orderPos++;
+
+		if (value == 0xFF) {
+			_channels[channel].orderPos = 0;
+			continue;
+		}
+
+		if (value >= 0xC0) {
+			_channels[channel].transpose = (byte)WBCommon::decodeOrderTranspose(value);
+			continue;
+		}
+
+		_channels[channel].patternAddr = readPatternPointer(value);
+		_channels[channel].patternOffset = 0;
+		break;
+	}
+}
+
+void EclipseC64MusicPlayer::buildEffectArpeggio(int channel) {
+	_channels[channel].arpeggioSequenceLen = WBCommon::buildArpeggioTable(
+		_arpeggioIntervals,
+		_channels[channel].effectParam,
+		_channels[channel].arpeggioSequence,
+		sizeof(_channels[channel].arpeggioSequence),
+		true);
+	_channels[channel].arpeggioSequencePos = 0;
+}
+
+void EclipseC64MusicPlayer::loadCurrentFrequency(int channel) {
+	byte note = clampNote(_channels[channel].currentNote);
+
+	_channels[channel].frequencyLow = readByte(kFrequencyLowTable + note);
+	_channels[channel].frequencyHigh = readByte(kFrequencyHighTable + note);
+	sidWrite(kSIDOffset[channel] + 0, _channels[channel].frequencyLow);
+	sidWrite(kSIDOffset[channel] + 1, _channels[channel].frequencyHigh);
+}
+
+void EclipseC64MusicPlayer::finalizeChannel(int channel) {
+	if (_channels[channel].durationReload != 0 &&
+		!_channels[channel].gateOffDisabled &&
+		((_channels[channel].durationReload >> 1) == _channels[channel].durationCounter)) {
+		sidWrite(kSIDOffset[channel] + 4, _channels[channel].waveform & 0xFE);
+	}
+
+	applyPulseWidthModulation(channel);
+	sidWrite(kSIDOffset[channel] + 2, _channels[channel].pulseWidthLow);
+	sidWrite(kSIDOffset[channel] + 3, _channels[channel].pulseWidthHigh & 0x0F);
+}
+
+void EclipseC64MusicPlayer::onTimer() {
+	if (!_musicActive)
+		return;
+
+	bool newBeat = (_speedCounter == 0);
+
+	for (int channel = kChannelCount - 1; channel >= 0; channel--)
+		processChannel(channel, newBeat);
+
+	if (!_musicActive)
+		return;
+
+	if (newBeat)
+		_speedCounter = _speedDivider;
+	else
+		_speedCounter--;
+}
+
+void EclipseC64MusicPlayer::processChannel(int channel, bool newBeat) {
+	if (newBeat) {
+		_channels[channel].durationCounter--;
+		if (_channels[channel].durationCounter == 0xFF) {
+			parseCommands(channel);
+			if (!_musicActive)
+				return;
+			finalizeChannel(channel);
+			return;
+		}
+
+		if (_channels[channel].noteStepCommand != 0) {
+			if (_channels[channel].noteStepCommand == 0xDE) {
+				if (_channels[channel].currentNote > 0)
+					_channels[channel].currentNote--;
+			} else if (_channels[channel].currentNote < kMaxNote) {
+				_channels[channel].currentNote++;
+			}
+			loadCurrentFrequency(channel);
+			finalizeChannel(channel);
+			return;
+		}
+	} else if (_channels[channel].stepDownCounter != 0) {
+		_channels[channel].stepDownCounter--;
+		if (_channels[channel].currentNote > 0)
+			_channels[channel].currentNote--;
+		loadCurrentFrequency(channel);
+		finalizeChannel(channel);
+		return;
+	}
+
+	applyFrameEffects(channel);
+	finalizeChannel(channel);
+}
+
+void EclipseC64MusicPlayer::parseCommands(int channel) {
+	if (_channels[channel].effectMode != 2) {
+		_channels[channel].effectParam = 0;
+		_channels[channel].effectMode = 0;
+		_channels[channel].arpeggioSequenceLen = 0;
+		_channels[channel].arpeggioSequencePos = 0;
+	}
+
+	_channels[channel].arpeggioTarget = 0;
+	_channels[channel].noteStepCommand = 0;
+
+	int safety = 200;
+	while (safety-- > 0) {
+		byte cmd = readPatternByte(channel);
+
+		if (cmd == 0xFF) {
+			loadNextPattern(channel);
+			continue;
+		}
+
+		if (cmd == 0xFE) {
+			stopMusic();
+			return;
+		}
+
+		if (cmd == 0xFD) {
+			sidWrite(0x18, readPatternByte(channel));
+			cmd = readPatternByte(channel);
+			if (cmd == 0xFF) {
+				loadNextPattern(channel);
+				continue;
+			}
+		}
+
+		if (cmd >= 0xF0) {
+			_speedDivider = cmd & 0x0F;
+			continue;
+		}
+
+		if (cmd >= 0xC0) {
+			byte instrument = cmd & 0x1F;
+			if (instrument < kInstrumentCount)
+				_channels[channel].instrumentOffset = instrument * 8;
+			continue;
+		}
+
+		if (cmd >= 0x80) {
+			_channels[channel].durationReload = cmd & 0x3F;
+			continue;
+		}
+
+		if (cmd == 0x7F) {
+			_channels[channel].noteStepCommand = 0xDE;
+			_channels[channel].effectMode = 0xDE;
+			continue;
+		}
+
+		if (cmd == 0x7E) {
+			_channels[channel].effectMode = 0xFE;
+			continue;
+		}
+
+		if (cmd == 0x7D) {
+			_channels[channel].effectMode = 1;
+			_channels[channel].effectParam = readPatternByte(channel);
+			buildEffectArpeggio(channel);
+			continue;
+		}
+
+		if (cmd == 0x7C) {
+			_channels[channel].effectMode = 2;
+			_channels[channel].effectParam = readPatternByte(channel);
+			buildEffectArpeggio(channel);
+			continue;
+		}
+
+		if (cmd == 0x7B) {
+			_channels[channel].effectParam = 0;
+			_channels[channel].effectMode = 1;
+			_channels[channel].arpeggioTarget = readPatternByte(channel) + _channels[channel].transpose;
+			_channels[channel].arpeggioParam = readPatternByte(channel);
+			continue;
+		}
+
+		if (cmd == 0x7A) {
+			_channels[channel].delayValue = readPatternByte(channel);
+			cmd = readPatternByte(channel);
+		}
+
+		applyNote(channel, cmd);
+		return;
+	}
+}
+
+void EclipseC64MusicPlayer::applyNote(int channel, byte note) {
+	byte instrumentOffset = _channels[channel].instrumentOffset;
+	byte ctrl = readInstrumentByte(instrumentOffset, 0);
+	byte attackDecay = readInstrumentByte(instrumentOffset, 1);
+	byte sustainRelease = readInstrumentByte(instrumentOffset, 2);
+	byte initialPulseWidth = readInstrumentByte(instrumentOffset, 3);
+	byte autoEffect = readInstrumentByte(instrumentOffset, 6);
+	byte flags = readInstrumentByte(instrumentOffset, 7);
+	byte actualNote = note;
+
+	if (actualNote != 0)
+		actualNote = clampNote(actualNote + _channels[channel].transpose);
+
+	_channels[channel].currentNote = actualNote;
+	_channels[channel].waveform = ctrl;
+	_channels[channel].instrumentFlags = flags;
+	_channels[channel].stepDownCounter = 0;
+
+	if (actualNote != 0 && _channels[channel].effectParam == 0 && autoEffect != 0) {
+		_channels[channel].effectParam = autoEffect;
+		buildEffectArpeggio(channel);
+	}
+
+	if (actualNote != 0 && (flags & 0x02) != 0) {
+		_channels[channel].stepDownCounter = 2;
+		_channels[channel].currentNote = clampNote(_channels[channel].currentNote + 2);
+	}
+
+	loadCurrentFrequency(channel);
+
+	_channels[channel].pulseWidthLow = initialPulseWidth & 0xF0;
+	_channels[channel].pulseWidthHigh = initialPulseWidth & 0x0F;
+	sidWrite(kSIDOffset[channel] + 2, _channels[channel].pulseWidthLow);
+	sidWrite(kSIDOffset[channel] + 3, _channels[channel].pulseWidthHigh & 0x0F);
+
+	sidWrite(kSIDOffset[channel] + 5, attackDecay);
+	sidWrite(kSIDOffset[channel] + 6, sustainRelease);
+	_channels[channel].gateOffDisabled = (sustainRelease & 0x0F) == 0x0F;
+	sidWrite(kSIDOffset[channel] + 4, 0x00);
+	sidWrite(kSIDOffset[channel] + 4, ctrl);
+
+	_channels[channel].durationCounter = _channels[channel].durationReload;
+	_channels[channel].delayCounter = _channels[channel].delayValue;
+	_channels[channel].arpeggioSequencePos = 0;
+}
+
+void EclipseC64MusicPlayer::applyFrameEffects(int channel) {
+	if (_channels[channel].currentNote == 0)
+		return;
+
+	if (applyInstrumentVibrato(channel))
+		return;
+
+	applyEffectArpeggio(channel);
+	applyTimedSlide(channel);
+}
+
+bool EclipseC64MusicPlayer::applyInstrumentVibrato(int channel) {
+	byte vibrato = readInstrumentByte(_channels[channel].instrumentOffset, 4);
+	if (vibrato == 0 || _channels[channel].currentNote >= kMaxNote)
+		return false;
+
+	byte shift = vibrato & 0x0F;
+	byte span = vibrato >> 4;
+	if (span == 0)
+		return false;
+
+	uint16 noteFrequency = (readByte(kFrequencyHighTable + _channels[channel].currentNote) << 8) |
+		readByte(kFrequencyLowTable + _channels[channel].currentNote);
+	uint16 nextFrequency = (readByte(kFrequencyHighTable + _channels[channel].currentNote + 1) << 8) |
+		readByte(kFrequencyLowTable + _channels[channel].currentNote + 1);
+	uint16 delta = nextFrequency - noteFrequency;
+
+	while (shift-- != 0)
+		delta >>= 1;
+
+	if ((_channels[channel].vibratoPhase & 0x80) != 0) {
+		if (_channels[channel].vibratoCounter != 0)
+			_channels[channel].vibratoCounter--;
+		if (_channels[channel].vibratoCounter == 0)
+			_channels[channel].vibratoPhase = 0;
+	} else {
+		_channels[channel].vibratoCounter++;
+		if (_channels[channel].vibratoCounter >= span)
+			_channels[channel].vibratoPhase = 0xFF;
+	}
+
+	if (_channels[channel].delayCounter != 0) {
+		_channels[channel].delayCounter--;
+		return false;
+	}
+
+	int32 frequency = (_channels[channel].frequencyHigh << 8) | _channels[channel].frequencyLow;
+	for (byte i = 0; i < (span >> 1); i++)
+		frequency -= delta;
+	for (byte i = 0; i < _channels[channel].vibratoCounter; i++)
+		frequency += delta;
+
+	if (frequency < 0)
+		frequency = 0;
+	if (frequency > 0xFFFF)
+		frequency = 0xFFFF;
+
+	sidWrite(kSIDOffset[channel] + 0, frequency & 0xFF);
+	sidWrite(kSIDOffset[channel] + 1, (frequency >> 8) & 0xFF);
+	return true;
+}
+
+void EclipseC64MusicPlayer::applyEffectArpeggio(int channel) {
+	if (_channels[channel].effectParam == 0 || _channels[channel].arpeggioSequenceLen == 0)
+		return;
+
+	if (_channels[channel].arpeggioSequencePos >= _channels[channel].arpeggioSequenceLen)
+		_channels[channel].arpeggioSequencePos = 0;
+
+	byte note = clampNote(_channels[channel].currentNote + _channels[channel].arpeggioSequence[_channels[channel].arpeggioSequencePos]);
+	_channels[channel].arpeggioSequencePos++;
+
+	sidWrite(kSIDOffset[channel] + 0, readByte(kFrequencyLowTable + note));
+	sidWrite(kSIDOffset[channel] + 1, readByte(kFrequencyHighTable + note));
+}
+
+void EclipseC64MusicPlayer::applyTimedSlide(int channel) {
+	if (_channels[channel].arpeggioTarget == 0)
+		return;
+
+	byte total = _channels[channel].durationReload;
+	byte remaining = _channels[channel].durationCounter;
+	byte start = _channels[channel].arpeggioParam >> 4;
+	byte span = _channels[channel].arpeggioParam & 0x0F;
+	byte elapsed = total - remaining;
+
+	if (elapsed <= start || elapsed > start + span || span == 0)
+		return;
+
+	byte currentNote = clampNote(_channels[channel].currentNote);
+	byte targetNote = clampNote(_channels[channel].arpeggioTarget);
+	if (currentNote == targetNote)
+		return;
+
+	uint16 currentFrequency = (_channels[channel].frequencyHigh << 8) | _channels[channel].frequencyLow;
+	uint16 sourceFrequency = (readByte(kFrequencyHighTable + currentNote) << 8) | readByte(kFrequencyLowTable + currentNote);
+	uint16 targetFrequency = (readByte(kFrequencyHighTable + targetNote) << 8) | readByte(kFrequencyLowTable + targetNote);
+	uint16 difference = sourceFrequency > targetFrequency ? sourceFrequency - targetFrequency : targetFrequency - sourceFrequency;
+	uint16 divisor = span * (_speedDivider + 1);
+	if (divisor == 0)
+		return;
+
+	uint16 delta = difference / divisor;
+	if (delta == 0)
+		return;
+
+	if (targetFrequency > sourceFrequency)
+		currentFrequency += delta;
+	else
+		currentFrequency -= delta;
+
+	_channels[channel].frequencyLow = currentFrequency & 0xFF;
+	_channels[channel].frequencyHigh = (currentFrequency >> 8) & 0xFF;
+	sidWrite(kSIDOffset[channel] + 0, _channels[channel].frequencyLow);
+	sidWrite(kSIDOffset[channel] + 1, _channels[channel].frequencyHigh);
+}
+
+void EclipseC64MusicPlayer::applyPulseWidthModulation(int channel) {
+	byte pulseWidthMod = readInstrumentByte(_channels[channel].instrumentOffset, 5);
+	if (pulseWidthMod == 0)
+		return;
+
+	if ((_channels[channel].instrumentFlags & 0x04) != 0) {
+		_channels[channel].pulseWidthLow += pulseWidthMod;
+		return;
+	}
+
+	uint16 pulseWidth = (_channels[channel].pulseWidthHigh << 8) | _channels[channel].pulseWidthLow;
+	if (_channels[channel].pulseWidthDirection == 0) {
+		pulseWidth += pulseWidthMod;
+		if ((pulseWidth >> 8) >= 0x0F)
+			_channels[channel].pulseWidthDirection = 1;
+	} else {
+		pulseWidth -= pulseWidthMod;
+		if ((pulseWidth >> 8) < 0x08)
+			_channels[channel].pulseWidthDirection = 0;
+	}
+
+	_channels[channel].pulseWidthLow = pulseWidth & 0xFF;
+	_channels[channel].pulseWidthHigh = (pulseWidth >> 8) & 0xFF;
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/eclipse/c64.music.h b/engines/freescape/games/eclipse/c64.music.h
new file mode 100644
index 00000000000..8496dff7f35
--- /dev/null
+++ b/engines/freescape/games/eclipse/c64.music.h
@@ -0,0 +1,135 @@
+/* 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_ECLIPSE_C64_MUSIC_H
+#define FREESCAPE_ECLIPSE_C64_MUSIC_H
+
+#include "audio/sid.h"
+#include "common/array.h"
+
+namespace Freescape {
+
+class EclipseC64MusicPlayer {
+public:
+	EclipseC64MusicPlayer(const Common::Array<byte> &musicData);
+	~EclipseC64MusicPlayer();
+
+	void startMusic();
+	void stopMusic();
+	bool isPlaying() const;
+	void initSID();
+	void destroySID();
+
+private:
+	static const uint16 kLoadAddress = 0x0410;
+	static const uint16 kOrderPointerTable = 0x1041;
+	static const uint16 kInstrumentTable = 0x1047;
+	static const uint16 kPatternPointerLowTable = 0x10A7;
+	static const uint16 kPatternPointerHighTable = 0x10C6;
+	static const uint16 kFrequencyHighTable = 0x0F5F;
+	static const uint16 kFrequencyLowTable = 0x0FBE;
+	static const uint16 kArpeggioIntervalTable = 0x148B;
+
+	static const byte kChannelCount = 3;
+	static const byte kInstrumentCount = 12;
+	static const byte kPatternCount = 31;
+	static const byte kMaxNote = 94;
+
+	struct ChannelState {
+		uint16 orderAddr;
+		byte orderPos;
+
+		uint16 patternAddr;
+		byte patternOffset;
+
+		byte instrumentOffset;
+		byte currentNote;
+		byte transpose;
+
+		byte frequencyLow;
+		byte frequencyHigh;
+		byte pulseWidthLow;
+		byte pulseWidthHigh;
+
+		byte durationReload;
+		byte durationCounter;
+
+		byte effectMode;
+		byte effectParam;
+		byte arpeggioTarget;
+		byte arpeggioParam;
+		byte arpeggioSequencePos;
+		byte arpeggioSequence[9];
+		byte arpeggioSequenceLen;
+
+		byte noteStepCommand;
+		byte stepDownCounter;
+
+		byte vibratoPhase;
+		byte vibratoCounter;
+		byte pulseWidthDirection;
+
+		byte delayValue;
+		byte delayCounter;
+
+		byte waveform;
+		byte instrumentFlags;
+		bool gateOffDisabled;
+
+		void reset();
+	};
+
+	SID::SID *_sid;
+	Common::Array<byte> _musicData;
+	byte _arpeggioIntervals[8];
+	bool _musicActive;
+	byte _speedDivider;
+	byte _speedCounter;
+	ChannelState _channels[kChannelCount];
+
+	byte readByte(uint16 address) const;
+	uint16 readWordLE(uint16 address) const;
+	uint16 readPatternPointer(byte index) const;
+	byte readInstrumentByte(byte instrumentOffset, byte field) const;
+	byte readPatternByte(int channel);
+	byte clampNote(byte note) const;
+
+	void sidWrite(int reg, byte data);
+	void onTimer();
+	void silenceAll();
+	void setupSong();
+	void loadNextPattern(int channel);
+	void buildEffectArpeggio(int channel);
+	void loadCurrentFrequency(int channel);
+	void finalizeChannel(int channel);
+	void processChannel(int channel, bool newBeat);
+	void parseCommands(int channel);
+	void applyNote(int channel, byte note);
+	void applyFrameEffects(int channel);
+	bool applyInstrumentVibrato(int channel);
+	void applyEffectArpeggio(int channel);
+	void applyTimedSlide(int channel);
+	void applyPulseWidthModulation(int channel);
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index b984bffca00..dc74dcebb30 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -30,6 +30,7 @@
 #include "common/translation.h"
 
 #include "freescape/freescape.h"
+#include "freescape/games/eclipse/c64.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -40,6 +41,8 @@ Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSiz
                                                   int songNum = 1, int rate = 44100);
 
 EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
+	_playerC64Music = nullptr;
+
 	// These sounds can be overriden by the class of each platform
 	_soundIndexStartFalling = -1;
 	_soundIndexEndFalling = -1;
@@ -99,6 +102,10 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 	_flashlightOn = false;
 }
 
+EclipseEngine::~EclipseEngine() {
+	delete _playerC64Music;
+}
+
 void EclipseEngine::initGameState() {
 	FreescapeEngine::initGameState();
 
@@ -116,8 +123,10 @@ void EclipseEngine::initGameState() {
 	_resting = false;
 	_flashlightOn = false;
 
-	// Start playing music, if any, in any supported format
-	playMusic("Total Eclipse Theme");
+	if (isC64() && _playerC64Music)
+		_playerC64Music->startMusic();
+	else
+		playMusic("Total Eclipse Theme");
 }
 
 void EclipseEngine::loadAssets() {
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 9d902e65795..88eadf1862d 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -25,6 +25,8 @@
 
 namespace Freescape {
 
+class EclipseC64MusicPlayer;
+
 enum EclipseReleaseFlags {
 	GF_ZX_DEMO_CRASH = (1 << 0),
 	GF_ZX_DEMO_MICROHOBBY = (1 << 1),
@@ -37,6 +39,7 @@ enum {
 class EclipseEngine : public FreescapeEngine {
 public:
 	EclipseEngine(OSystem *syst, const ADGameDescription *gd);
+	~EclipseEngine() override;
 
 	void gotoArea(uint16 areaID, int entranceID) override;
 
@@ -105,6 +108,8 @@ public:
 	void drawHeartIndicator(Graphics::Surface *surface, int x, int y);
 
 	Common::Array<byte> _musicData; // TEMUSIC.ST TEXT segment (Atari ST)
+	Common::Array<byte> _c64MusicData;
+	EclipseC64MusicPlayer *_playerC64Music;
 
 	// Atari ST UI sprites (extracted from binary, pre-converted to target format)
 	Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index b4ba06e75ad..914af1a0015 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -36,6 +36,7 @@ MODULE_OBJS := \
 	games/eclipse/atari.o \
 	games/eclipse/atari.music.o \
 	games/eclipse/c64.o \
+	games/eclipse/c64.music.o \
 	games/eclipse/dos.o \
 	games/eclipse/eclipse.o \
 	games/eclipse/cpc.o \




More information about the Scummvm-git-logs mailing list