[Scummvm-git-logs] scummvm master -> 4ae803a79697792c81c43c9a6a53447312cd5a19

neuromancer noreply at scummvm.org
Wed Apr 1 14:54:52 UTC 2026


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

Summary:
42232cd705 FREESCAPE: added some code to parse castle c64
d30f1cda6a FREESCAPE: experimental support for music in eclipse cpc from c64
e4f75e6c6f FREESCAPE: reduced data used in music for eclipse cpc
4ae803a796 FREESCAPE: extended optional music for eclipse zx


Commit: 42232cd705a7fc81f1184c38f998599b56d56b98
    https://github.com/scummvm/scummvm/commit/42232cd705a7fc81f1184c38f998599b56d56b98
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-01T16:54:23+02:00

Commit Message:
FREESCAPE: added some code to parse castle c64

Changed paths:
  A engines/freescape/games/castle/c64.cpp
    engines/freescape/detection.cpp
    engines/freescape/games/castle/castle.cpp
    engines/freescape/games/castle/castle.h
    engines/freescape/loaders/8bitBinaryLoader.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index 32a016db10e..c2c2d679a83 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -879,6 +879,16 @@ static const ADGameDescription gameDescriptions[] = {
 		ADGF_NO_FLAGS,
 		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
 	},
+	// C64 tape release
+	{
+		"castlemaster",
+		"",
+		AD_ENTRY1s("CASTLEMASTER.C64.DATA", "d433af0fc854d91fb22f986e274d809b", 51198),
+		Common::EN_ANY,
+		Common::kPlatformC64,
+		ADGF_UNSTABLE | GF_C64_TAPE,
+		GUIO4(GUIO_NOMIDI, GUIO_RENDERC64, GAMEOPTION_TRAVEL_ROCK, GAMEOPTION_WASD_CONTROLS)
+	},
 	{
 		"castlemaster",
 		"",
diff --git a/engines/freescape/games/castle/c64.cpp b/engines/freescape/games/castle/c64.cpp
new file mode 100644
index 00000000000..41bb61c9bf0
--- /dev/null
+++ b/engines/freescape/games/castle/c64.cpp
@@ -0,0 +1,55 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+
+#include "freescape/freescape.h"
+#include "freescape/games/castle/castle.h"
+#include "freescape/language/8bitDetokeniser.h"
+
+namespace Freescape {
+
+void CastleEngine::initC64() {
+	_viewArea = Common::Rect(32, 32, 288, 136);
+}
+
+extern byte kC64Palette[16][3];
+
+void CastleEngine::loadAssetsC64FullGame() {
+	Common::File file;
+	file.open("castlemaster.c64.data");
+
+	if (!file.isOpen())
+		error("Failed to open castlemaster.c64.data");
+
+	loadMessagesVariableSize(&file, 0x13a9, 75);
+	// TODO: riddles need C64-specific parsing (embedded control bytes differ from CPC)
+	//loadRiddles(&file, 0x1811, 9);
+	load8bitBinary(&file, 0x9951, 16);
+
+	// TODO: extract border from game data or add bundled image
+	// TODO: title screen is in BASIC loader (file 009) - not yet extracted
+}
+
+void CastleEngine::drawC64UI(Graphics::Surface *surface) {
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/castle/castle.cpp b/engines/freescape/games/castle/castle.cpp
index 2b88fef5c20..286f76ac8ec 100644
--- a/engines/freescape/games/castle/castle.cpp
+++ b/engines/freescape/games/castle/castle.cpp
@@ -58,6 +58,8 @@ CastleEngine::CastleEngine(OSystem *syst, const ADGameDescription *gd) : Freesca
 		initZX();
 	else if (isCPC())
 		initCPC();
+	else if (isC64())
+		initC64();
 
 	// Messages are assigned after loading in initGameState()
 
@@ -1950,7 +1952,7 @@ void CastleEngine::borderScreen() {
 	if (isAmiga() && isDemo())
 		return; // Skip character selection
 
-	if (isSpectrum() || isCPC())
+	if (isSpectrum() || isCPC() || isC64())
 		FreescapeEngine::borderScreen();
 	else {
 		uint32 color = _gfx->_texturePixelFormat.ARGBToColor(0x00, 0x00, 0x00, 0x00);
diff --git a/engines/freescape/games/castle/castle.h b/engines/freescape/games/castle/castle.h
index 62e1a08d44c..d5f734a45be 100644
--- a/engines/freescape/games/castle/castle.h
+++ b/engines/freescape/games/castle/castle.h
@@ -74,6 +74,10 @@ public:
 	void initZX();
 	void initDOS();
 	void initCPC();
+	void initC64();
+
+	void loadAssetsC64FullGame() override;
+	void drawC64UI(Graphics::Surface *surface) override;
 
 	void drawDOSUI(Graphics::Surface *surface) override;
 	void drawZXUI(Graphics::Surface *surface) override;
diff --git a/engines/freescape/loaders/8bitBinaryLoader.cpp b/engines/freescape/loaders/8bitBinaryLoader.cpp
index 12b9b55f4c6..870a90bf01a 100644
--- a/engines/freescape/loaders/8bitBinaryLoader.cpp
+++ b/engines/freescape/loaders/8bitBinaryLoader.cpp
@@ -743,7 +743,7 @@ Area *FreescapeEngine::load8bitArea(Common::SeekableReadStream *file, uint16 nco
 		byte idx = readField(file, 8);
 		if (isAmiga())
 			name = _messagesList[idx + 51];
-		else if (isSpectrum() || isCPC())
+		else if (isSpectrum() || isCPC() || isC64())
 			name = areaNumber == 255 ? "GLOBAL" : _messagesList[idx + 16];
 		else
 			name = _messagesList[idx + 41];
@@ -875,7 +875,7 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
 	uint8 initialEnergy2 = 0;
 	uint8 initialShield2 = 0;
 
-	if (isCastle() && (isSpectrum() || isCPC())) {
+	if (isCastle() && (isSpectrum() || isCPC() || isC64())) {
 		initialShield1 = readField(file, 8);
 	} else {
 		readField(file, 8); // Unknown
@@ -889,7 +889,7 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
 	debugC(1, kFreescapeDebugParser, "Initial levels of energy: %d and shield: %d", initialEnergy1, initialShield1);
 	debugC(1, kFreescapeDebugParser, "Initial levels of energy: %d and shield: %d", initialEnergy2, initialShield2);
 
-	if (isCastle() && (isSpectrum() || isCPC()))
+	if (isCastle() && (isSpectrum() || isCPC() || isC64()))
 		file->seek(offset + 0x6);
 	else if (isAmiga() || isAtariST())
 		file->seek(offset + 0x14);
@@ -923,6 +923,8 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
 
 	if (isCastle() && (isSpectrum() || isCPC()))
 		file->seek(offset + 0x42);
+	else if (isCastle() && isC64())
+		file->seek(offset + 0x3e);
 	else if (isAmiga() || isAtariST())
 		file->seek(offset + 0x8c);
 	else
@@ -998,6 +1000,8 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
 
 	if (isCastle() && (isSpectrum() || isCPC()))
 		file->seek(offset + 0x4f);
+	else if (isCastle() && isC64())
+		file->seek(offset + 0x4b);
 	else if (isAmiga() || isAtariST())
 		file->seek(offset + 0x190);
 	else
@@ -1015,7 +1019,10 @@ void FreescapeEngine::load8bitBinary(Common::SeekableReadStream *file, int offse
 	for (uint16 area = 0; area < numberOfAreas; area++) {
 		debugC(1, kFreescapeDebugParser, "Starting to parse area index %d at offset %x", area, fileOffsetForArea[area]);
 
-		file->seek(offset + fileOffsetForArea[area]);
+		if (isCastle() && isC64())
+			file->seek(offset + fileOffsetForArea[area] - 4);
+		else
+			file->seek(offset + fileOffsetForArea[area]);
 		newArea = load8bitArea(file, ncolors);
 
 		if (newArea) {
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 7e671a7d4b7..1074112fef8 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -11,6 +11,7 @@ MODULE_OBJS := \
 	freescape.o \
 	games/castle/castle.o \
 	games/castle/amiga.o \
+	games/castle/c64.o \
 	games/castle/cpc.o \
 	games/castle/dos.o \
 	games/castle/zx.o \


Commit: d30f1cda6a86521b859a9ce4afd498770e54cc37
    https://github.com/scummvm/scummvm/commit/d30f1cda6a86521b859a9ce4afd498770e54cc37
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-01T16:54:23+02:00

Commit Message:
FREESCAPE: experimental support for music in eclipse cpc from c64

Changed paths:
  A engines/freescape/games/eclipse/cpc.music.cpp
  A engines/freescape/games/eclipse/cpc.music.h
    engines/freescape/detection.cpp
    engines/freescape/detection.h
    engines/freescape/games/eclipse/cpc.cpp
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/games/eclipse/eclipse.h
    engines/freescape/metaengine.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index c2c2d679a83..f1963851f12 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -570,7 +570,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_DEMO,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
 	},
 	{
 		"totaleclipse",
@@ -630,7 +630,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
 	},
 	{
 		"totaleclipse2",
@@ -644,7 +644,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
 	},
 	{
 		"totaleclipse2",
@@ -658,7 +658,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
 	},
 	{
 		"totaleclipse2", // Tape release
diff --git a/engines/freescape/detection.h b/engines/freescape/detection.h
index 5e0a562c11f..15824bbbfe1 100644
--- a/engines/freescape/detection.h
+++ b/engines/freescape/detection.h
@@ -39,6 +39,7 @@
 
 #define GAMEOPTION_TRAVEL_ROCK   GUIO_GAMEOPTIONS9
 #define GAMEOPTION_WASD_CONTROLS GUIO_GAMEOPTIONS11
+#define GAMEOPTION_CPC_MUSIC     GUIO_GAMEOPTIONS12
 
 
 #endif
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index b6e8129413e..5b1c718242c 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -19,10 +19,12 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/file.h"
 #include "common/memstream.h"
 
 #include "freescape/freescape.h"
+#include "freescape/games/eclipse/cpc.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -142,6 +144,9 @@ void EclipseEngine::loadAssetsCPCFullGame() {
 
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
+
+	if (ConfMan.getBool("cpc_music"))
+		_playerCPCMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::loadAssetsCPCDemo() {
@@ -180,6 +185,9 @@ void EclipseEngine::loadAssetsCPCDemo() {
 
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
+
+	if (ConfMan.getBool("cpc_music"))
+		_playerCPCMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::updateHeartFramesCPC() {
diff --git a/engines/freescape/games/eclipse/cpc.music.cpp b/engines/freescape/games/eclipse/cpc.music.cpp
new file mode 100644
index 00000000000..5b8fd135d7c
--- /dev/null
+++ b/engines/freescape/games/eclipse/cpc.music.cpp
@@ -0,0 +1,799 @@
+/* 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/cpc.music.h"
+
+#include "common/textconsole.h"
+#include "freescape/wb.h"
+
+namespace Freescape {
+
+// ============================================================================
+// Embedded music data (extracted from Total Eclipse C64)
+// ============================================================================
+
+// AY-3-8910 period table (95 entries, derived from SID frequency table)
+// AY clock = 1MHz, period = clock / (16 * freq_hz)
+static const uint16 kAYPeriods[] = {
+	    0,  3657,  3455,  3265,  3076,  2908,  2743,  2589,
+	 2447,  2309,  2176,  2055,  1939,  1832,  1728,  1632,
+	 1540,  1454,  1371,  1295,  1222,  1153,  1088,  1027,
+	  970,   915,   864,   816,   770,   726,   686,   647,
+	  611,   577,   544,   514,   485,   458,   432,   406,
+	  385,   363,   343,   324,   305,   288,   272,   257,
+	  242,   229,   216,   204,   192,   182,   171,   162,
+	  153,   144,   136,   128,   121,   114,   108,   102,
+	   96,    91,    86,    81,    76,    72,    68,    64,
+	   61,    57,    54,    51,    48,    45,    43,    40,
+	   38,    36,    34,    32,    30,    29,    27,    25,
+	   24,    23,    21,    20,    19,    18,    17
+};
+
+// 12 instruments, 8 bytes each
+// Format: ctrl, AD, SR, PW, vibrato, PWM, autoArp, flags
+static const byte kInstruments[] = {
+	0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0: rest
+	0x41, 0x42, 0x24, 0x02, 0x23, 0x0F, 0x00, 0x04,  // 1: bass pulse
+	0x11, 0x8A, 0xAC, 0x00, 0x64, 0x0B, 0x00, 0x00,  // 2: triangle lead
+	0x11, 0x6C, 0x4F, 0x64, 0x63, 0x2F, 0x00, 0x04,  // 3: triangle pad
+	0x41, 0x3A, 0xAC, 0x20, 0x00, 0x08, 0x00, 0x04,  // 4: pulse arpeggio
+	0x81, 0x42, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00,  // 5: noise percussion
+	0x11, 0x3D, 0x1C, 0x22, 0x35, 0x00, 0x00, 0x00,  // 6: triangle melody
+	0x41, 0x4C, 0x2C, 0x43, 0x45, 0x00, 0x00, 0x00,  // 7: pulse melody
+	0x11, 0x5D, 0xBC, 0x44, 0x65, 0x22, 0x00, 0x04,  // 8: triangle sustain
+	0x41, 0x4C, 0xAF, 0x30, 0x00, 0x00, 0x00, 0x04,  // 9: pulse sustain
+	0x21, 0x4A, 0x2A, 0x80, 0x64, 0x22, 0x00, 0x04,  // 10: sawtooth lead
+	0x41, 0x6A, 0x6B, 0x41, 0x00, 0x40, 0x80, 0x04,  // 11: pulse w/ arpeggio
+};
+
+static const byte kOrderList0[] = {
+	0xE0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01,
+	0x0B, 0x10, 0x0B, 0x0B, 0x01, 0x10, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x13, 0x13,
+	0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x10,
+	0x0B, 0x0B, 0x01, 0x13, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0xFF
+};
+
+static const byte kOrderList1[] = {
+	0xE0, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x07, 0x07, 0x07, 0x09, 0x09, 0x07, 0x07, 0xE0, 0x08,
+	0x08, 0x08, 0x08, 0x18, 0x0A, 0x0A, 0x18, 0x0C, 0x0D, 0x0C, 0x0D, 0x14, 0x14, 0x14, 0x14, 0x16,
+	0x17, 0x0C, 0x0D, 0xD4, 0x0C, 0x0D, 0xE0, 0x05, 0x05, 0x05, 0x05, 0x17, 0x0C, 0x0D, 0xD4, 0x0C,
+	0x0D, 0xEC, 0x08, 0x08, 0x08, 0x08, 0xFF
+};
+
+static const byte kOrderList2[] = {
+	0xE0, 0x05, 0x05, 0x05, 0x05, 0xEC, 0x08, 0xE0, 0x02, 0x02, 0x03, 0x02, 0x04, 0xEC, 0x04, 0xE0,
+	0x0E, 0x0F, 0x11, 0x12, 0xEC, 0x02, 0x02, 0x03, 0x02, 0xE0, 0x15, 0x1B, 0x19, 0x1B, 0x1A, 0x1C,
+	0x19, 0x1C, 0x1A, 0x1D, 0x1E, 0x07, 0x00, 0x07, 0x00, 0x04, 0xEC, 0x04, 0xE0, 0x0E, 0x0F, 0x11,
+	0x12, 0xEC, 0x02, 0x02, 0x03, 0x02, 0xFF
+};
+
+// Pattern offset table (31 patterns into kPatternData)
+static const uint16 kPatternOffsets[] = {
+	    0,     4,    40,    65,    94,   122,   138,   148,
+	  173,   178,   192,   200,   235,   249,   263,   295,
+	  334,   369,   406,   450,   485,   490,   526,   562,
+	  564,   566,   618,   670,   672,   674,   718
+};
+
+static const byte kPatternData[] = {
+	0xC0, 0xBF, 0x00, 0xFF, 0xF3, 0xC1, 0x83, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
+	0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
+	0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0xFF, 0xC2, 0x97, 0x30, 0x87, 0x2D, 0x97, 0x30, 0x87,
+	0x2D, 0x30, 0x83, 0x2E, 0x30, 0x80, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x30, 0x2E, 0x30, 0xA7, 0x2D,
+	0xFF, 0xC2, 0x97, 0x30, 0x87, 0x32, 0x33, 0x85, 0x32, 0x30, 0x83, 0x2D, 0x80, 0x2D, 0x2E, 0x2D,
+	0x2E, 0x2D, 0x2E, 0x2D, 0x2E, 0x97, 0x2B, 0x87, 0x2D, 0x97, 0x2B, 0x87, 0x2D, 0xFF, 0xC3, 0x97,
+	0x3E, 0x81, 0x3D, 0x3E, 0x3D, 0x39, 0x97, 0x3C, 0x81, 0x3A, 0x3C, 0x3A, 0x35, 0x80, 0x39, 0x37,
+	0x9D, 0x39, 0x80, 0x39, 0x3A, 0x39, 0x38, 0x9B, 0x39, 0xFF, 0xF3, 0xC4, 0xBF, 0x7D, 0x89, 0x3E,
+	0x7D, 0x8A, 0x3C, 0x7D, 0x94, 0x39, 0x7D, 0x8A, 0x3C, 0xFF, 0xC3, 0xBF, 0x26, 0x28, 0x29, 0xB7,
+	0x28, 0x87, 0x24, 0xFF, 0xC0, 0x87, 0x00, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87, 0x3C, 0xC0,
+	0x00, 0x00, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87, 0x3C, 0xC0, 0x00, 0xFF, 0xC3, 0xBF, 0x1A,
+	0x26, 0xFF, 0xC3, 0xB7, 0x32, 0x81, 0x31, 0x32, 0x31, 0x2D, 0xB7, 0x30, 0x83, 0x2D, 0x30, 0xFF,
+	0xBF, 0x7D, 0x89, 0x3E, 0x7D, 0x89, 0x3E, 0xFF, 0xC1, 0x83, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26,
+	0x1A, 0x21, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26,
+	0x1A, 0x21, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0xFF, 0xBF, 0x7D, 0x89, 0x3E, 0x7D,
+	0x8A, 0x3C, 0x7D, 0x89, 0x39, 0x7D, 0x8A, 0x37, 0xFF, 0xBF, 0x7D, 0x89, 0x3E, 0x7D, 0x8A, 0x3F,
+	0x7D, 0x89, 0x3C, 0x7D, 0x89, 0x3E, 0xFF, 0xC6, 0xB7, 0x3E, 0x81, 0x3C, 0x3E, 0x3C, 0x39, 0x8B,
+	0x3C, 0xAB, 0x39, 0x83, 0x3C, 0x3E, 0x81, 0x39, 0x37, 0xA3, 0x39, 0x87, 0x3C, 0x39, 0x83, 0x37,
+	0x39, 0x81, 0x37, 0x39, 0xBB, 0x37, 0xFF, 0xA7, 0x3E, 0x87, 0x3E, 0x85, 0x3F, 0x3E, 0x83, 0x3C,
+	0x81, 0x3F, 0x3E, 0xA3, 0x3C, 0x83, 0x3F, 0x3E, 0x87, 0x3C, 0x83, 0x3E, 0x3C, 0x9B, 0x39, 0x83,
+	0x39, 0x8F, 0x3A, 0x37, 0x97, 0x39, 0x83, 0x3C, 0x81, 0x3E, 0x3F, 0x9F, 0x3E, 0xFF, 0xC1, 0x83,
+	0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x1A, 0x21, 0x26, 0x1A, 0x21, 0x26, 0x1A, 0x21,
+	0x1A, 0x1F, 0x26, 0x1A, 0x1F, 0x26, 0x1A, 0x1F, 0x1A, 0x1F, 0x26, 0x1A, 0x1F, 0x26, 0x1A, 0x1F,
+	0xFF, 0xC7, 0xB1, 0x3E, 0x81, 0x3F, 0x3E, 0x3C, 0x3E, 0x3F, 0x3E, 0x3F, 0x8F, 0x3C, 0xA7, 0x39,
+	0x83, 0x3C, 0x3E, 0x81, 0x37, 0x39, 0x3A, 0xB1, 0x39, 0x81, 0x37, 0x39, 0x37, 0x39, 0x9F, 0x37,
+	0x8F, 0x3E, 0x87, 0x3F, 0x3C, 0xFF, 0xC7, 0x9F, 0x3E, 0x8B, 0x45, 0x83, 0x45, 0x87, 0x46, 0x83,
+	0x45, 0x81, 0x43, 0x45, 0x97, 0x46, 0x81, 0x46, 0x48, 0x46, 0x45, 0x9B, 0x46, 0x81, 0x3F, 0x3E,
+	0x97, 0x3C, 0x87, 0x3E, 0x8F, 0x3F, 0x3C, 0xAF, 0x39, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87,
+	0x3C, 0xFF, 0xC1, 0x83, 0x1A, 0x1A, 0x26, 0x1A, 0x1A, 0x26, 0x1A, 0x26, 0x1A, 0x1A, 0x26, 0x1A,
+	0x1A, 0x26, 0x1A, 0x26, 0x1A, 0x1A, 0x26, 0x1A, 0x1A, 0x26, 0x1A, 0x26, 0x1A, 0x1A, 0x26, 0x1A,
+	0x1A, 0x26, 0x1A, 0x26, 0xFF, 0xC6, 0xBF, 0x26, 0x32, 0xFF, 0xC8, 0x97, 0x35, 0x87, 0x37, 0x39,
+	0x97, 0x39, 0x81, 0x38, 0x39, 0x38, 0x39, 0xA7, 0x35, 0x8F, 0x32, 0x81, 0x33, 0x35, 0x33, 0x35,
+	0x33, 0x35, 0x33, 0xB1, 0x32, 0x97, 0x30, 0x87, 0x32, 0x97, 0x30, 0x87, 0x32, 0xFF, 0xC8, 0x97,
+	0x32, 0x87, 0x34, 0x35, 0x97, 0x35, 0x81, 0x34, 0x35, 0x34, 0x35, 0xA7, 0x32, 0x8F, 0x2D, 0x81,
+	0x2E, 0x2F, 0x2E, 0x2F, 0x2E, 0x2F, 0x2E, 0xB1, 0x2D, 0x97, 0x2D, 0x87, 0x2D, 0x97, 0x2D, 0x87,
+	0x2D, 0xFF, 0xC9, 0xFF, 0xC4, 0xFF, 0x97, 0x32, 0x87, 0x30, 0x97, 0x32, 0x81, 0x32, 0x34, 0x32,
+	0x34, 0x87, 0x30, 0x9F, 0x2D, 0x83, 0x30, 0x32, 0x34, 0x35, 0x34, 0x35, 0x8B, 0x34, 0x81, 0x32,
+	0x30, 0x97, 0x2D, 0x83, 0x2F, 0x30, 0x32, 0x30, 0x2F, 0x30, 0x97, 0x32, 0x83, 0x2F, 0x30, 0x8F,
+	0x2F, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87, 0x3C, 0xFF, 0x81, 0x3E, 0x3C, 0x3E, 0x3F, 0xA3,
+	0x3E, 0x83, 0x3E, 0x87, 0x3F, 0x83, 0x3E, 0x3C, 0x81, 0x3F, 0x3E, 0x3F, 0x41, 0x97, 0x3F, 0x83,
+	0x3C, 0x87, 0x3E, 0x8B, 0x3F, 0x87, 0x3E, 0x81, 0x3C, 0x3E, 0xA3, 0x3C, 0x83, 0x3C, 0x3E, 0x3C,
+	0x3A, 0x39, 0x3A, 0xAF, 0x39, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87, 0x3C, 0xFF, 0xC7, 0xFF,
+	0xCA, 0xFF, 0xCB, 0x8B, 0x3E, 0x83, 0x3C, 0x9B, 0x3E, 0x83, 0x3C, 0x87, 0x3E, 0x3F, 0x8F, 0x3C,
+	0x97, 0x39, 0x83, 0x3C, 0x3E, 0x87, 0x3C, 0x83, 0x3A, 0x39, 0x8F, 0x39, 0x9F, 0x3E, 0x87, 0x3E,
+	0x40, 0x8F, 0x3C, 0x9F, 0x39, 0xC5, 0x81, 0x3C, 0x3C, 0x3C, 0x3C, 0x87, 0x3C, 0xFF, 0xCB, 0x97,
+	0x39, 0x83, 0x3E, 0x97, 0x40, 0x83, 0x39, 0x3E, 0x41, 0x8B, 0x40, 0x81, 0x3E, 0x3C, 0xA7, 0x39,
+	0x81, 0x3C, 0x3E, 0x3C, 0x3E, 0x89, 0x40, 0x81, 0x41, 0x40, 0x41, 0xAF, 0x3E, 0x8F, 0x3E, 0x3C,
+	0x9F, 0x39, 0xFF
+};
+
+static const byte kEmbeddedArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0C };
+
+// ============================================================================
+// Software ADSR rate tables (8.8 fixed point, per-frame at 50Hz)
+// ============================================================================
+
+// Attack rate: 0x0F00 / (time_ms / 20) in 8.8 fixed point at 50Hz
+// SID attack times: 2, 8, 16, 24, 38, 56, 68, 80, 100, 250, 500, 800, 1000, 3000, 5000, 8000 ms
+static const uint16 kAttackRate[16] = {
+	0x0F00, 0x0F00, 0x0F00, 0x0C80, // 0-2: <1 frame (instant), 3: 1.2 frames
+	0x07E5, 0x055B, 0x0469, 0x03C0, // 4-7: 1.9-4.0 frames
+	0x0300, 0x0133, 0x009A, 0x0060, // 8-11: 5-40 frames
+	0x004D, 0x001A, 0x000F, 0x000A  // 12-15: 50-400 frames
+};
+
+// Decay/Release rate: 0x0F00 / (time_ms / 20) in 8.8 fixed point at 50Hz
+// SID decay/release times: 6, 24, 48, 72, 114, 168, 204, 240, 300, 750, 1500, 2400, 3000, 9000, 15000, 24000 ms
+static const uint16 kDecayReleaseRate[16] = {
+	0x0F00, 0x0C80, 0x0640, 0x042B, // 0-3: instant to 3.6 frames
+	0x02A2, 0x01C9, 0x0178, 0x0140, // 4-7: 5.7-12 frames
+	0x0100, 0x0066, 0x0033, 0x0020, // 8-11: 15-120 frames
+	0x001A, 0x0009, 0x0005, 0x0003  // 12-15: 150-1200 frames
+};
+
+// ============================================================================
+// ChannelState
+// ============================================================================
+
+void EclipseAYMusicPlayer::ChannelState::reset() {
+	orderList = nullptr;
+	orderPos = 0;
+	patternDataOffset = 0;
+	patternOffset = 0;
+	instrumentOffset = 0;
+	currentNote = 0;
+	transpose = 0;
+	currentPeriod = 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;
+	delayValue = 0;
+	delayCounter = 0;
+	waveform = 0;
+	instrumentFlags = 0;
+	gateOffDisabled = false;
+	adsrPhase = kPhaseOff;
+	adsrVolume = 0;
+	attackRate = 0;
+	decayRate = 0;
+	sustainLevel = 0;
+	releaseRate = 0;
+}
+
+// ============================================================================
+// Constructor / Destructor
+// ============================================================================
+
+EclipseAYMusicPlayer::EclipseAYMusicPlayer(Audio::Mixer *mixer)
+	: AY8912Stream(44100, 1000000),
+	  _mixer(mixer),
+	  _musicActive(false),
+	  _speedDivider(1),
+	  _speedCounter(0),
+	  _mixerRegister(0x38),
+	  _tickSampleCount(0) {
+	memcpy(_arpeggioIntervals, kEmbeddedArpeggioIntervals, 8);
+}
+
+EclipseAYMusicPlayer::~EclipseAYMusicPlayer() {
+	stopMusic();
+}
+
+// ============================================================================
+// Public interface
+// ============================================================================
+
+void EclipseAYMusicPlayer::startMusic() {
+	_mixer->playStream(Audio::Mixer::kMusicSoundType, &_handle, toAudioStream(),
+	                   -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+	setupSong();
+}
+
+void EclipseAYMusicPlayer::stopMusic() {
+	_musicActive = false;
+	silenceAll();
+	if (_mixer)
+		_mixer->stopHandle(_handle);
+}
+
+bool EclipseAYMusicPlayer::isPlaying() const {
+	return _musicActive;
+}
+
+// ============================================================================
+// AudioStream
+// ============================================================================
+
+int EclipseAYMusicPlayer::readBuffer(int16 *buffer, const int numSamples) {
+	if (!_musicActive) {
+		memset(buffer, 0, numSamples * sizeof(int16));
+		return numSamples;
+	}
+
+	int samplesGenerated = 0;
+	int samplesPerTick = (getRate() / 50) * 2; // stereo: 2 int16 per frame
+
+	while (samplesGenerated < numSamples) {
+		int remaining = samplesPerTick - _tickSampleCount;
+		int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+		if (toGenerate > 0) {
+			generateSamples(buffer + samplesGenerated, toGenerate);
+			samplesGenerated += toGenerate;
+			_tickSampleCount += toGenerate;
+		}
+
+		if (_tickSampleCount >= samplesPerTick) {
+			_tickSampleCount -= samplesPerTick;
+			onTimer();
+		}
+	}
+
+	return samplesGenerated;
+}
+
+// ============================================================================
+// Timer / sequencer core
+// ============================================================================
+
+void EclipseAYMusicPlayer::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 EclipseAYMusicPlayer::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++;
+			}
+			loadCurrentPeriod(channel);
+			finalizeChannel(channel);
+			return;
+		}
+	} else if (_channels[channel].stepDownCounter != 0) {
+		_channels[channel].stepDownCounter--;
+		if (_channels[channel].currentNote > 0)
+			_channels[channel].currentNote--;
+		loadCurrentPeriod(channel);
+		finalizeChannel(channel);
+		return;
+	}
+
+	applyFrameEffects(channel);
+	finalizeChannel(channel);
+}
+
+void EclipseAYMusicPlayer::finalizeChannel(int channel) {
+	// Gate off at half duration triggers release phase
+	if (_channels[channel].durationReload != 0 &&
+	    !_channels[channel].gateOffDisabled &&
+	    ((_channels[channel].durationReload >> 1) == _channels[channel].durationCounter)) {
+		releaseADSR(channel);
+	}
+
+	updateADSR(channel);
+}
+
+// ============================================================================
+// Song setup
+// ============================================================================
+
+void EclipseAYMusicPlayer::setupSong() {
+	silenceAll();
+
+	// Initialize AY: all tone enabled, noise disabled, default noise period
+	_mixerRegister = 0x38;
+	setReg(7, _mixerRegister);
+	setReg(6, 0x07);
+
+	_speedDivider = 1;
+	_speedCounter = 0;
+
+	const byte *orderLists[3] = { kOrderList0, kOrderList1, kOrderList2 };
+
+	for (int i = 0; i < kChannelCount; i++) {
+		_channels[i].reset();
+		_channels[i].orderList = orderLists[i];
+		loadNextPattern(i);
+	}
+
+	_musicActive = true;
+}
+
+void EclipseAYMusicPlayer::silenceAll() {
+	for (int r = 0; r < 14; r++)
+		setReg(r, 0);
+	_mixerRegister = 0x38;
+	setReg(7, _mixerRegister);
+}
+
+// ============================================================================
+// Order list / pattern navigation
+// ============================================================================
+
+void EclipseAYMusicPlayer::loadNextPattern(int channel) {
+	int safety = 200;
+	while (safety-- > 0) {
+		byte value = _channels[channel].orderList[_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;
+		}
+
+		if (value < ARRAYSIZE(kPatternOffsets)) {
+			_channels[channel].patternDataOffset = kPatternOffsets[value];
+			_channels[channel].patternOffset = 0;
+		}
+		break;
+	}
+}
+
+byte EclipseAYMusicPlayer::readPatternByte(int channel) {
+	byte value = kPatternData[_channels[channel].patternDataOffset + _channels[channel].patternOffset];
+	_channels[channel].patternOffset++;
+	return value;
+}
+
+byte EclipseAYMusicPlayer::clampNote(byte note) const {
+	return note > kMaxNote ? kMaxNote : note;
+}
+
+// ============================================================================
+// Pattern command parser
+// ============================================================================
+
+void EclipseAYMusicPlayer::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;
+		}
+
+		// Filter command: consume the byte (no filters on AY), continue
+		if (cmd == 0xFD) {
+			readPatternByte(channel); // discard filter value
+			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 < 12)
+				_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;
+	}
+}
+
+// ============================================================================
+// Note application
+// ============================================================================
+
+void EclipseAYMusicPlayer::applyNote(int channel, byte note) {
+	byte instrumentOffset = _channels[channel].instrumentOffset;
+	byte ctrl = kInstruments[instrumentOffset + 0];
+	byte attackDecay = kInstruments[instrumentOffset + 1];
+	byte sustainRelease = kInstruments[instrumentOffset + 2];
+	byte autoEffect = kInstruments[instrumentOffset + 6];
+	byte flags = kInstruments[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);
+	}
+
+	loadCurrentPeriod(channel);
+
+	// Configure AY mixer for this channel: tone or noise
+	if (actualNote == 0) {
+		// Rest: disable both tone and noise
+		_mixerRegister |= (1 << channel);
+		_mixerRegister |= (1 << (channel + 3));
+	} else if (ctrl & 0x80) {
+		// Noise waveform
+		_mixerRegister |= (1 << channel);        // disable tone
+		_mixerRegister &= ~(1 << (channel + 3)); // enable noise
+	} else {
+		// Tone waveform (pulse, triangle, sawtooth all map to AY square)
+		_mixerRegister &= ~(1 << channel);       // enable tone
+		_mixerRegister |= (1 << (channel + 3));  // disable noise
+	}
+	setReg(7, _mixerRegister);
+
+	// Trigger ADSR envelope if gate bit is set
+	_channels[channel].gateOffDisabled = (sustainRelease & 0x0F) == 0x0F;
+	if (ctrl & 0x01) {
+		triggerADSR(channel, attackDecay, sustainRelease);
+	} else {
+		_channels[channel].adsrPhase = kPhaseOff;
+		_channels[channel].adsrVolume = 0;
+		setReg(8 + channel, 0);
+	}
+
+	_channels[channel].durationCounter = _channels[channel].durationReload;
+	_channels[channel].delayCounter = _channels[channel].delayValue;
+	_channels[channel].arpeggioSequencePos = 0;
+}
+
+// ============================================================================
+// Period / frequency helpers
+// ============================================================================
+
+void EclipseAYMusicPlayer::writeChannelPeriod(int channel, uint16 period) {
+	_channels[channel].currentPeriod = period;
+	setReg(channel * 2, period & 0xFF);
+	setReg(channel * 2 + 1, (period >> 8) & 0x0F);
+}
+
+void EclipseAYMusicPlayer::loadCurrentPeriod(int channel) {
+	byte note = clampNote(_channels[channel].currentNote);
+	writeChannelPeriod(channel, kAYPeriods[note]);
+}
+
+// ============================================================================
+// Effects
+// ============================================================================
+
+void EclipseAYMusicPlayer::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 EclipseAYMusicPlayer::applyFrameEffects(int channel) {
+	if (_channels[channel].currentNote == 0)
+		return;
+
+	if (applyInstrumentVibrato(channel))
+		return;
+
+	applyEffectArpeggio(channel);
+	applyTimedSlide(channel);
+}
+
+bool EclipseAYMusicPlayer::applyInstrumentVibrato(int channel) {
+	byte vibrato = kInstruments[_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 notePeriod = kAYPeriods[_channels[channel].currentNote];
+	uint16 nextPeriod = kAYPeriods[_channels[channel].currentNote + 1];
+
+	if (notePeriod <= nextPeriod)
+		return false;
+
+	uint16 delta = notePeriod - nextPeriod;
+
+	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;
+	}
+
+	// Modulate period: higher period = lower pitch
+	// Start low (high period), sweep toward high (low period)
+	int32 period = _channels[channel].currentPeriod;
+	for (byte i = 0; i < (span >> 1); i++)
+		period += delta;  // go down (increase period)
+	for (byte i = 0; i < _channels[channel].vibratoCounter; i++)
+		period -= delta;  // go up (decrease period)
+
+	if (period < 1)
+		period = 1;
+	if (period > 4095)
+		period = 4095;
+
+	setReg(channel * 2, period & 0xFF);
+	setReg(channel * 2 + 1, (period >> 8) & 0x0F);
+	return true;
+}
+
+void EclipseAYMusicPlayer::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++;
+
+	uint16 period = kAYPeriods[note];
+	setReg(channel * 2, period & 0xFF);
+	setReg(channel * 2 + 1, (period >> 8) & 0x0F);
+}
+
+void EclipseAYMusicPlayer::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 currentPeriod = _channels[channel].currentPeriod;
+	uint16 sourcePeriod = kAYPeriods[currentNote];
+	uint16 targetPeriod = kAYPeriods[targetNote];
+	uint16 difference = sourcePeriod > targetPeriod ?
+	                    sourcePeriod - targetPeriod :
+	                    targetPeriod - sourcePeriod;
+	uint16 divisor = span * (_speedDivider + 1);
+	if (divisor == 0)
+		return;
+
+	uint16 delta = difference / divisor;
+	if (delta == 0)
+		return;
+
+	if (targetPeriod > sourcePeriod)
+		currentPeriod += delta;
+	else
+		currentPeriod -= delta;
+
+	writeChannelPeriod(channel, currentPeriod);
+}
+
+// ============================================================================
+// Software ADSR envelope
+// ============================================================================
+
+void EclipseAYMusicPlayer::triggerADSR(int channel, byte ad, byte sr) {
+	_channels[channel].adsrPhase = kPhaseAttack;
+	// Don't reset volume: SID re-gate (gate-off + gate-on in same frame)
+	// starts the attack from the current volume, not from zero.
+	// This avoids audible clicks between consecutive notes.
+	_channels[channel].attackRate = kAttackRate[ad >> 4];
+	_channels[channel].decayRate = kDecayReleaseRate[ad & 0x0F];
+	_channels[channel].sustainLevel = sr >> 4;
+	_channels[channel].releaseRate = kDecayReleaseRate[sr & 0x0F];
+}
+
+void EclipseAYMusicPlayer::releaseADSR(int channel) {
+	if (_channels[channel].adsrPhase != kPhaseRelease &&
+	    _channels[channel].adsrPhase != kPhaseOff) {
+		_channels[channel].adsrPhase = kPhaseRelease;
+	}
+}
+
+void EclipseAYMusicPlayer::updateADSR(int channel) {
+	switch (_channels[channel].adsrPhase) {
+	case kPhaseAttack:
+		_channels[channel].adsrVolume += _channels[channel].attackRate;
+		if (_channels[channel].adsrVolume >= 0x0F00) {
+			_channels[channel].adsrVolume = 0x0F00;
+			_channels[channel].adsrPhase = kPhaseDecay;
+		}
+		break;
+
+	case kPhaseDecay: {
+		uint16 sustainTarget = (uint16)_channels[channel].sustainLevel << 8;
+		if (_channels[channel].adsrVolume > _channels[channel].decayRate + sustainTarget) {
+			_channels[channel].adsrVolume -= _channels[channel].decayRate;
+		} else {
+			_channels[channel].adsrVolume = sustainTarget;
+			_channels[channel].adsrPhase = kPhaseSustain;
+		}
+		break;
+	}
+
+	case kPhaseSustain:
+		// Volume stays at sustain level
+		break;
+
+	case kPhaseRelease:
+		if (_channels[channel].adsrVolume > _channels[channel].releaseRate) {
+			_channels[channel].adsrVolume -= _channels[channel].releaseRate;
+		} else {
+			_channels[channel].adsrVolume = 0;
+			_channels[channel].adsrPhase = kPhaseOff;
+		}
+		break;
+
+	case kPhaseOff:
+		_channels[channel].adsrVolume = 0;
+		break;
+	}
+
+	setReg(8 + channel, _channels[channel].adsrVolume >> 8);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/eclipse/cpc.music.h b/engines/freescape/games/eclipse/cpc.music.h
new file mode 100644
index 00000000000..c1e9633afa9
--- /dev/null
+++ b/engines/freescape/games/eclipse/cpc.music.h
@@ -0,0 +1,151 @@
+/* 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_CPC_MUSIC_H
+#define FREESCAPE_ECLIPSE_CPC_MUSIC_H
+
+#include "audio/softsynth/ay8912.h"
+#include "audio/mixer.h"
+
+namespace Freescape {
+
+/**
+ * AY-3-8910 music player for Total Eclipse CPC.
+ *
+ * Ports the Wally Beben C64 SID music to the CPC's AY chip by:
+ * - Reusing the same sequencer format (order lists, patterns, instruments)
+ * - Converting SID frequencies to AY periods
+ * - Replacing SID hardware ADSR with a software envelope
+ * - Dropping SID-specific features (pulse width, filters)
+ *
+ * The music data is extracted from the C64 version and embedded
+ * directly so no C64-specific game files are required.
+ */
+class EclipseAYMusicPlayer : public Audio::AY8912Stream {
+public:
+	EclipseAYMusicPlayer(Audio::Mixer *mixer);
+	~EclipseAYMusicPlayer() override;
+
+	void startMusic();
+	void stopMusic();
+	bool isPlaying() const;
+
+	// AudioStream overrides
+	int readBuffer(int16 *buffer, const int numSamples) override;
+	bool endOfData() const override { return !_musicActive; }
+	bool endOfStream() const override { return false; }
+
+private:
+	static const byte kChannelCount = 3;
+	static const byte kMaxNote = 94;
+
+	enum ADSRPhase {
+		kPhaseOff,
+		kPhaseAttack,
+		kPhaseDecay,
+		kPhaseSustain,
+		kPhaseRelease
+	};
+
+	struct ChannelState {
+		const byte *orderList;
+		byte orderPos;
+
+		uint16 patternDataOffset;
+		uint16 patternOffset;
+
+		byte instrumentOffset;
+		byte currentNote;
+		byte transpose;
+
+		uint16 currentPeriod;
+
+		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 delayValue;
+		byte delayCounter;
+
+		byte waveform;
+		byte instrumentFlags;
+		bool gateOffDisabled;
+
+		ADSRPhase adsrPhase;
+		uint16 adsrVolume; // 8.8 fixed point (0x0000 - 0x0F00)
+		uint16 attackRate;
+		uint16 decayRate;
+		byte sustainLevel;
+		uint16 releaseRate;
+
+		void reset();
+	};
+
+	Audio::Mixer *_mixer;
+	Audio::SoundHandle _handle;
+	bool _musicActive;
+	byte _speedDivider;
+	byte _speedCounter;
+	byte _mixerRegister;
+	int _tickSampleCount;
+	ChannelState _channels[kChannelCount];
+	byte _arpeggioIntervals[8];
+
+	void onTimer();
+	void setupSong();
+	void silenceAll();
+	void loadNextPattern(int channel);
+	void buildEffectArpeggio(int channel);
+	void loadCurrentPeriod(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 triggerADSR(int channel, byte ad, byte sr);
+	void releaseADSR(int channel);
+	void updateADSR(int channel);
+
+	byte readPatternByte(int channel);
+	byte clampNote(byte note) const;
+	void writeChannelPeriod(int channel, uint16 period);
+};
+
+} // namespace Freescape
+
+#endif
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 7bebd6313b4..93b3424bf1b 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -32,6 +32,7 @@
 #include "freescape/freescape.h"
 #include "freescape/games/eclipse/c64.music.h"
 #include "freescape/games/eclipse/c64.sfx.h"
+#include "freescape/games/eclipse/cpc.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -44,6 +45,7 @@ Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSiz
 EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
 	_playerC64Music = nullptr;
 	_playerC64Sfx = nullptr;
+	_playerCPCMusic = nullptr;
 	_c64UseSFX = false;
 
 	// These sounds can be overriden by the class of each platform
@@ -106,6 +108,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 }
 
 EclipseEngine::~EclipseEngine() {
+	delete _playerCPCMusic;
 	delete _playerC64Music;
 	delete _playerC64Sfx;
 }
@@ -129,6 +132,8 @@ void EclipseEngine::initGameState() {
 
 	if (isC64() && _playerC64Music)
 		_playerC64Music->startMusic();
+	else if (isCPC() && _playerCPCMusic)
+		_playerCPCMusic->startMusic();
 	else
 		playMusic("Total Eclipse Theme");
 }
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index 32f0317f8a6..e2f829532df 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -25,6 +25,7 @@
 
 namespace Freescape {
 
+class EclipseAYMusicPlayer;
 class EclipseC64MusicPlayer;
 class EclipseC64SFXPlayer;
 
@@ -121,6 +122,8 @@ public:
 	void playSoundC64(int index) override;
 	void toggleC64Sound();
 
+	EclipseAYMusicPlayer *_playerCPCMusic;
+
 	// Atari ST UI sprites (extracted from binary, pre-converted to target format)
 	Font _fontScore; // Font B (10 score digit glyphs, 4-plane at $249BE)
 	Common::Array<Graphics::ManagedSurface *> _eclipseSprites; // 2 eclipse animation frames (16x13)
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 4b9bb8685d5..101c84a7d14 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -158,6 +158,18 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 			0
 		}
 	},
+	{
+		GAMEOPTION_CPC_MUSIC,
+		{
+			// I18N: Enable background music on CPC versions using AY chip emulation
+			_s("Backported music from C64 releases"),
+			_s("Enable background music ported from the C64 version"),
+			"cpc_music",
+			false,
+			0,
+			0
+		}
+	},
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 1074112fef8..c008d1919dc 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -39,6 +39,7 @@ MODULE_OBJS := \
 	games/eclipse/c64.o \
 	games/eclipse/c64.music.o \
 	games/eclipse/c64.sfx.o \
+	games/eclipse/cpc.music.o \
 	games/eclipse/dos.o \
 	games/eclipse/eclipse.o \
 	games/eclipse/cpc.o \


Commit: e4f75e6c6fa665456aa17853665b8e6c03a80417
    https://github.com/scummvm/scummvm/commit/e4f75e6c6fa665456aa17853665b8e6c03a80417
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-01T16:54:23+02:00

Commit Message:
FREESCAPE: reduced data used in music for eclipse cpc

Changed paths:
    engines/freescape/games/eclipse/cpc.music.cpp


diff --git a/engines/freescape/games/eclipse/cpc.music.cpp b/engines/freescape/games/eclipse/cpc.music.cpp
index 5b8fd135d7c..470f1dfb393 100644
--- a/engines/freescape/games/eclipse/cpc.music.cpp
+++ b/engines/freescape/games/eclipse/cpc.music.cpp
@@ -32,7 +32,7 @@ namespace Freescape {
 
 // AY-3-8910 period table (95 entries, derived from SID frequency table)
 // AY clock = 1MHz, period = clock / (16 * freq_hz)
-static const uint16 kAYPeriods[] = {
+const uint16 kAYPeriods[] = {
 	    0,  3657,  3455,  3265,  3076,  2908,  2743,  2589,
 	 2447,  2309,  2176,  2055,  1939,  1832,  1728,  1632,
 	 1540,  1454,  1371,  1295,  1222,  1153,  1088,  1027,
@@ -47,38 +47,40 @@ static const uint16 kAYPeriods[] = {
 	   24,    23,    21,    20,    19,    18,    17
 };
 
-// 12 instruments, 8 bytes each
-// Format: ctrl, AD, SR, PW, vibrato, PWM, autoArp, flags
+// 12 instruments, 6 bytes each (SID-only fields PW and PWM stripped)
+// Format: ctrl, AD, SR, vibrato, autoArp, flags
 static const byte kInstruments[] = {
-	0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0: rest
-	0x41, 0x42, 0x24, 0x02, 0x23, 0x0F, 0x00, 0x04,  // 1: bass pulse
-	0x11, 0x8A, 0xAC, 0x00, 0x64, 0x0B, 0x00, 0x00,  // 2: triangle lead
-	0x11, 0x6C, 0x4F, 0x64, 0x63, 0x2F, 0x00, 0x04,  // 3: triangle pad
-	0x41, 0x3A, 0xAC, 0x20, 0x00, 0x08, 0x00, 0x04,  // 4: pulse arpeggio
-	0x81, 0x42, 0x00, 0x20, 0x00, 0x01, 0x00, 0x00,  // 5: noise percussion
-	0x11, 0x3D, 0x1C, 0x22, 0x35, 0x00, 0x00, 0x00,  // 6: triangle melody
-	0x41, 0x4C, 0x2C, 0x43, 0x45, 0x00, 0x00, 0x00,  // 7: pulse melody
-	0x11, 0x5D, 0xBC, 0x44, 0x65, 0x22, 0x00, 0x04,  // 8: triangle sustain
-	0x41, 0x4C, 0xAF, 0x30, 0x00, 0x00, 0x00, 0x04,  // 9: pulse sustain
-	0x21, 0x4A, 0x2A, 0x80, 0x64, 0x22, 0x00, 0x04,  // 10: sawtooth lead
-	0x41, 0x6A, 0x6B, 0x41, 0x00, 0x40, 0x80, 0x04,  // 11: pulse w/ arpeggio
+	0x40, 0x00, 0x00, 0x00, 0x00, 0x00,  // 0
+	0x41, 0x42, 0x24, 0x23, 0x00, 0x04,  // 1
+	0x11, 0x8A, 0xAC, 0x64, 0x00, 0x00,  // 2
+	0x11, 0x6C, 0x4F, 0x63, 0x00, 0x04,  // 3
+	0x41, 0x3A, 0xAC, 0x00, 0x00, 0x04,  // 4
+	0x81, 0x42, 0x00, 0x00, 0x00, 0x00,  // 5
+	0x11, 0x3D, 0x1C, 0x35, 0x00, 0x00,  // 6
+	0x41, 0x4C, 0x2C, 0x45, 0x00, 0x00,  // 7
+	0x11, 0x5D, 0xBC, 0x65, 0x00, 0x04,  // 8
+	0x41, 0x4C, 0xAF, 0x00, 0x00, 0x04,  // 9
+	0x21, 0x4A, 0x2A, 0x64, 0x00, 0x04,  // 10
+	0x41, 0x6A, 0x6B, 0x00, 0x80, 0x04,  // 11
 };
+static const byte kInstrumentSize = 6;
+static const byte kInstrumentCount = 12;
 
-static const byte kOrderList0[] = {
+const byte kOrderList0[] = {
 	0xE0, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01,
 	0x0B, 0x10, 0x0B, 0x0B, 0x01, 0x10, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x13, 0x13,
 	0x13, 0x13, 0x13, 0x13, 0x13, 0x13, 0x0B, 0x0B, 0x0B, 0x0B, 0x01, 0x01, 0x01, 0x01, 0x0B, 0x10,
 	0x0B, 0x0B, 0x01, 0x13, 0x01, 0x01, 0x13, 0x13, 0x13, 0x13, 0xFF
 };
 
-static const byte kOrderList1[] = {
+const byte kOrderList1[] = {
 	0xE0, 0x00, 0x00, 0x00, 0x00, 0x06, 0x07, 0x07, 0x07, 0x07, 0x09, 0x09, 0x07, 0x07, 0xE0, 0x08,
 	0x08, 0x08, 0x08, 0x18, 0x0A, 0x0A, 0x18, 0x0C, 0x0D, 0x0C, 0x0D, 0x14, 0x14, 0x14, 0x14, 0x16,
 	0x17, 0x0C, 0x0D, 0xD4, 0x0C, 0x0D, 0xE0, 0x05, 0x05, 0x05, 0x05, 0x17, 0x0C, 0x0D, 0xD4, 0x0C,
 	0x0D, 0xEC, 0x08, 0x08, 0x08, 0x08, 0xFF
 };
 
-static const byte kOrderList2[] = {
+const byte kOrderList2[] = {
 	0xE0, 0x05, 0x05, 0x05, 0x05, 0xEC, 0x08, 0xE0, 0x02, 0x02, 0x03, 0x02, 0x04, 0xEC, 0x04, 0xE0,
 	0x0E, 0x0F, 0x11, 0x12, 0xEC, 0x02, 0x02, 0x03, 0x02, 0xE0, 0x15, 0x1B, 0x19, 0x1B, 0x1A, 0x1C,
 	0x19, 0x1C, 0x1A, 0x1D, 0x1E, 0x07, 0x00, 0x07, 0x00, 0x04, 0xEC, 0x04, 0xE0, 0x0E, 0x0F, 0x11,
@@ -86,14 +88,14 @@ static const byte kOrderList2[] = {
 };
 
 // Pattern offset table (31 patterns into kPatternData)
-static const uint16 kPatternOffsets[] = {
+const uint16 kPatternOffsets[] = {
 	    0,     4,    40,    65,    94,   122,   138,   148,
 	  173,   178,   192,   200,   235,   249,   263,   295,
 	  334,   369,   406,   450,   485,   490,   526,   562,
 	  564,   566,   618,   670,   672,   674,   718
 };
 
-static const byte kPatternData[] = {
+const byte kPatternData[] = {
 	0xC0, 0xBF, 0x00, 0xFF, 0xF3, 0xC1, 0x83, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
 	0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A, 0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0x1A,
 	0x21, 0x21, 0x1A, 0x1A, 0x21, 0x1A, 0x21, 0xFF, 0xC2, 0x97, 0x30, 0x87, 0x2D, 0x97, 0x30, 0x87,
@@ -144,7 +146,7 @@ static const byte kPatternData[] = {
 	0x9F, 0x39, 0xFF
 };
 
-static const byte kEmbeddedArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0C };
+const byte kEmbeddedArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08, 0x09, 0x0A, 0x0C };
 
 // ============================================================================
 // Software ADSR rate tables (8.8 fixed point, per-frame at 50Hz)
@@ -152,7 +154,7 @@ static const byte kEmbeddedArpeggioIntervals[] = { 0x03, 0x04, 0x05, 0x07, 0x08,
 
 // Attack rate: 0x0F00 / (time_ms / 20) in 8.8 fixed point at 50Hz
 // SID attack times: 2, 8, 16, 24, 38, 56, 68, 80, 100, 250, 500, 800, 1000, 3000, 5000, 8000 ms
-static const uint16 kAttackRate[16] = {
+const uint16 kAttackRate[16] = {
 	0x0F00, 0x0F00, 0x0F00, 0x0C80, // 0-2: <1 frame (instant), 3: 1.2 frames
 	0x07E5, 0x055B, 0x0469, 0x03C0, // 4-7: 1.9-4.0 frames
 	0x0300, 0x0133, 0x009A, 0x0060, // 8-11: 5-40 frames
@@ -161,7 +163,7 @@ static const uint16 kAttackRate[16] = {
 
 // Decay/Release rate: 0x0F00 / (time_ms / 20) in 8.8 fixed point at 50Hz
 // SID decay/release times: 6, 24, 48, 72, 114, 168, 204, 240, 300, 750, 1500, 2400, 3000, 9000, 15000, 24000 ms
-static const uint16 kDecayReleaseRate[16] = {
+const uint16 kDecayReleaseRate[16] = {
 	0x0F00, 0x0C80, 0x0640, 0x042B, // 0-3: instant to 3.6 frames
 	0x02A2, 0x01C9, 0x0178, 0x0140, // 4-7: 5.7-12 frames
 	0x0100, 0x0066, 0x0033, 0x0020, // 8-11: 15-120 frames
@@ -464,8 +466,8 @@ void EclipseAYMusicPlayer::parseCommands(int channel) {
 
 		if (cmd >= 0xC0) {
 			byte instrument = cmd & 0x1F;
-			if (instrument < 12)
-				_channels[channel].instrumentOffset = instrument * 8;
+			if (instrument < kInstrumentCount)
+				_channels[channel].instrumentOffset = instrument * kInstrumentSize;
 			continue;
 		}
 
@@ -526,8 +528,8 @@ void EclipseAYMusicPlayer::applyNote(int channel, byte note) {
 	byte ctrl = kInstruments[instrumentOffset + 0];
 	byte attackDecay = kInstruments[instrumentOffset + 1];
 	byte sustainRelease = kInstruments[instrumentOffset + 2];
-	byte autoEffect = kInstruments[instrumentOffset + 6];
-	byte flags = kInstruments[instrumentOffset + 7];
+	byte autoEffect = kInstruments[instrumentOffset + 4];
+	byte flags = kInstruments[instrumentOffset + 5];
 	byte actualNote = note;
 
 	if (actualNote != 0)
@@ -622,7 +624,7 @@ void EclipseAYMusicPlayer::applyFrameEffects(int channel) {
 }
 
 bool EclipseAYMusicPlayer::applyInstrumentVibrato(int channel) {
-	byte vibrato = kInstruments[_channels[channel].instrumentOffset + 4];
+	byte vibrato = kInstruments[_channels[channel].instrumentOffset + 3];
 	if (vibrato == 0 || _channels[channel].currentNote >= kMaxNote)
 		return false;
 


Commit: 4ae803a79697792c81c43c9a6a53447312cd5a19
    https://github.com/scummvm/scummvm/commit/4ae803a79697792c81c43c9a6a53447312cd5a19
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-04-01T16:54:23+02:00

Commit Message:
FREESCAPE: extended optional music for eclipse zx

Changed paths:
  A engines/freescape/games/eclipse/ay.music.cpp
  A engines/freescape/games/eclipse/ay.music.h
  R engines/freescape/games/eclipse/cpc.music.cpp
  R engines/freescape/games/eclipse/cpc.music.h
    engines/freescape/detection.cpp
    engines/freescape/detection.h
    engines/freescape/games/eclipse/cpc.cpp
    engines/freescape/games/eclipse/eclipse.cpp
    engines/freescape/games/eclipse/eclipse.h
    engines/freescape/games/eclipse/zx.cpp
    engines/freescape/metaengine.cpp
    engines/freescape/module.mk


diff --git a/engines/freescape/detection.cpp b/engines/freescape/detection.cpp
index f1963851f12..c76c21d76b7 100644
--- a/engines/freescape/detection.cpp
+++ b/engines/freescape/detection.cpp
@@ -570,7 +570,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_DEMO,
-		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse",
@@ -584,7 +584,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_DEMO | GF_ZX_DEMO_MICROHOBBY,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse",
@@ -598,7 +598,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_DEMO | GF_ZX_DEMO_CRASH,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse",
@@ -607,7 +607,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse2",
@@ -616,7 +616,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformZX,
 		ADGF_NO_FLAGS,
-		GUIO4(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERZX, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse",
@@ -630,7 +630,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse2",
@@ -644,7 +644,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse2",
@@ -658,7 +658,7 @@ static const ADGameDescription gameDescriptions[] = {
 		Common::EN_ANY,
 		Common::kPlatformAmstradCPC,
 		ADGF_NO_FLAGS,
-		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_CPC_MUSIC)
+		GUIO5(GUIO_NOMIDI, GUIO_RENDERCPC, GAMEOPTION_MODERN_MOVEMENT, GAMEOPTION_WASD_CONTROLS, GAMEOPTION_AY_MUSIC)
 	},
 	{
 		"totaleclipse2", // Tape release
diff --git a/engines/freescape/detection.h b/engines/freescape/detection.h
index 15824bbbfe1..6aa9a32d617 100644
--- a/engines/freescape/detection.h
+++ b/engines/freescape/detection.h
@@ -39,7 +39,7 @@
 
 #define GAMEOPTION_TRAVEL_ROCK   GUIO_GAMEOPTIONS9
 #define GAMEOPTION_WASD_CONTROLS GUIO_GAMEOPTIONS11
-#define GAMEOPTION_CPC_MUSIC     GUIO_GAMEOPTIONS12
+#define GAMEOPTION_AY_MUSIC      GUIO_GAMEOPTIONS12
 
 
 #endif
diff --git a/engines/freescape/games/eclipse/cpc.music.cpp b/engines/freescape/games/eclipse/ay.music.cpp
similarity index 99%
rename from engines/freescape/games/eclipse/cpc.music.cpp
rename to engines/freescape/games/eclipse/ay.music.cpp
index 470f1dfb393..1a058b3b221 100644
--- a/engines/freescape/games/eclipse/cpc.music.cpp
+++ b/engines/freescape/games/eclipse/ay.music.cpp
@@ -19,7 +19,7 @@
  *
  */
 
-#include "engines/freescape/games/eclipse/cpc.music.h"
+#include "engines/freescape/games/eclipse/ay.music.h"
 
 #include "common/textconsole.h"
 #include "freescape/wb.h"
diff --git a/engines/freescape/games/eclipse/cpc.music.h b/engines/freescape/games/eclipse/ay.music.h
similarity index 94%
rename from engines/freescape/games/eclipse/cpc.music.h
rename to engines/freescape/games/eclipse/ay.music.h
index c1e9633afa9..bf8d61434c5 100644
--- a/engines/freescape/games/eclipse/cpc.music.h
+++ b/engines/freescape/games/eclipse/ay.music.h
@@ -19,8 +19,8 @@
  *
  */
 
-#ifndef FREESCAPE_ECLIPSE_CPC_MUSIC_H
-#define FREESCAPE_ECLIPSE_CPC_MUSIC_H
+#ifndef FREESCAPE_ECLIPSE_AY_MUSIC_H
+#define FREESCAPE_ECLIPSE_AY_MUSIC_H
 
 #include "audio/softsynth/ay8912.h"
 #include "audio/mixer.h"
@@ -36,8 +36,6 @@ namespace Freescape {
  * - Replacing SID hardware ADSR with a software envelope
  * - Dropping SID-specific features (pulse width, filters)
  *
- * The music data is extracted from the C64 version and embedded
- * directly so no C64-specific game files are required.
  */
 class EclipseAYMusicPlayer : public Audio::AY8912Stream {
 public:
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index 5b1c718242c..48078bd65ff 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -24,7 +24,7 @@
 #include "common/memstream.h"
 
 #include "freescape/freescape.h"
-#include "freescape/games/eclipse/cpc.music.h"
+#include "freescape/games/eclipse/ay.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -145,8 +145,8 @@ void EclipseEngine::loadAssetsCPCFullGame() {
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
 
-	if (ConfMan.getBool("cpc_music"))
-		_playerCPCMusic = new EclipseAYMusicPlayer(_mixer);
+	if (ConfMan.getBool("ay_music"))
+		_playerAYMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::loadAssetsCPCDemo() {
@@ -186,8 +186,8 @@ void EclipseEngine::loadAssetsCPCDemo() {
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
 
-	if (ConfMan.getBool("cpc_music"))
-		_playerCPCMusic = new EclipseAYMusicPlayer(_mixer);
+	if (ConfMan.getBool("ay_music"))
+		_playerAYMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::updateHeartFramesCPC() {
diff --git a/engines/freescape/games/eclipse/eclipse.cpp b/engines/freescape/games/eclipse/eclipse.cpp
index 93b3424bf1b..4fc28d015b1 100644
--- a/engines/freescape/games/eclipse/eclipse.cpp
+++ b/engines/freescape/games/eclipse/eclipse.cpp
@@ -32,7 +32,7 @@
 #include "freescape/freescape.h"
 #include "freescape/games/eclipse/c64.music.h"
 #include "freescape/games/eclipse/c64.sfx.h"
-#include "freescape/games/eclipse/cpc.music.h"
+#include "freescape/games/eclipse/ay.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -45,7 +45,7 @@ Audio::AudioStream *makeEclipseAtariMusicStream(const byte *data, uint32 dataSiz
 EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
 	_playerC64Music = nullptr;
 	_playerC64Sfx = nullptr;
-	_playerCPCMusic = nullptr;
+	_playerAYMusic = nullptr;
 	_c64UseSFX = false;
 
 	// These sounds can be overriden by the class of each platform
@@ -108,7 +108,7 @@ EclipseEngine::EclipseEngine(OSystem *syst, const ADGameDescription *gd) : Frees
 }
 
 EclipseEngine::~EclipseEngine() {
-	delete _playerCPCMusic;
+	delete _playerAYMusic;
 	delete _playerC64Music;
 	delete _playerC64Sfx;
 }
@@ -132,8 +132,8 @@ void EclipseEngine::initGameState() {
 
 	if (isC64() && _playerC64Music)
 		_playerC64Music->startMusic();
-	else if (isCPC() && _playerCPCMusic)
-		_playerCPCMusic->startMusic();
+	else if ((isCPC() || isSpectrum()) && _playerAYMusic)
+		_playerAYMusic->startMusic();
 	else
 		playMusic("Total Eclipse Theme");
 }
diff --git a/engines/freescape/games/eclipse/eclipse.h b/engines/freescape/games/eclipse/eclipse.h
index e2f829532df..6de63bda278 100644
--- a/engines/freescape/games/eclipse/eclipse.h
+++ b/engines/freescape/games/eclipse/eclipse.h
@@ -122,7 +122,7 @@ public:
 	void playSoundC64(int index) override;
 	void toggleC64Sound();
 
-	EclipseAYMusicPlayer *_playerCPCMusic;
+	EclipseAYMusicPlayer *_playerAYMusic;
 
 	// 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/games/eclipse/zx.cpp b/engines/freescape/games/eclipse/zx.cpp
index 6b25aae7d68..2753a4c8823 100644
--- a/engines/freescape/games/eclipse/zx.cpp
+++ b/engines/freescape/games/eclipse/zx.cpp
@@ -19,9 +19,11 @@
  *
  */
 
+#include "common/config-manager.h"
 #include "common/file.h"
 
 #include "freescape/freescape.h"
+#include "freescape/games/eclipse/ay.music.h"
 #include "freescape/games/eclipse/eclipse.h"
 #include "freescape/language/8bitDetokeniser.h"
 
@@ -124,6 +126,9 @@ void EclipseEngine::loadAssetsZXFullGame() {
 
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
+
+	if (ConfMan.getBool("ay_music"))
+		_playerAYMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::loadAssetsZXDemo() {
@@ -166,6 +171,9 @@ void EclipseEngine::loadAssetsZXDemo() {
 
 	for (auto &it : _indicators)
 		it->convertToInPlace(_gfx->_texturePixelFormat);
+
+	if (ConfMan.getBool("ay_music"))
+		_playerAYMusic = new EclipseAYMusicPlayer(_mixer);
 }
 
 void EclipseEngine::drawZXUI(Graphics::Surface *surface) {
diff --git a/engines/freescape/metaengine.cpp b/engines/freescape/metaengine.cpp
index 101c84a7d14..aba59923e52 100644
--- a/engines/freescape/metaengine.cpp
+++ b/engines/freescape/metaengine.cpp
@@ -159,12 +159,12 @@ static const ADExtraGuiOptionsMap optionsList[] = {
 		}
 	},
 	{
-		GAMEOPTION_CPC_MUSIC,
+		GAMEOPTION_AY_MUSIC,
 		{
-			// I18N: Enable background music on CPC versions using AY chip emulation
+			// I18N: Enable background music using AY chip emulation
 			_s("Backported music from C64 releases"),
 			_s("Enable background music ported from the C64 version"),
-			"cpc_music",
+			"ay_music",
 			false,
 			0,
 			0
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index c008d1919dc..17c97329b97 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -38,8 +38,8 @@ MODULE_OBJS := \
 	games/eclipse/atari.music.o \
 	games/eclipse/c64.o \
 	games/eclipse/c64.music.o \
+	games/eclipse/ay.music.o \
 	games/eclipse/c64.sfx.o \
-	games/eclipse/cpc.music.o \
 	games/eclipse/dos.o \
 	games/eclipse/eclipse.o \
 	games/eclipse/cpc.o \




More information about the Scummvm-git-logs mailing list