[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