[Scummvm-git-logs] scummvm master -> 1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f
neuromancer
noreply at scummvm.org
Tue Mar 3 07:23:04 UTC 2026
This automated email contains information about 6 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
0146ecd5af FREESCAPE: restored rise/fall keys
14102847c9 FREESCAPE: fixed celestial bodies when using shaders
c3cbb7025e FREESCAPE: better wb-based music for dark and eclipse
c229bdfea8 FREESCAPE: added sound playback for driller (c64)
3233011b5c FREESCAPE: first pass on the music/sound for dark side (c64)
1dc0f0b189 FREESCAPE: allow to switch between music/sound in c64
Commit: 0146ecd5afaf8b06b343e146a3b854e55f1d67c3
https://github.com/scummvm/scummvm/commit/0146ecd5afaf8b06b343e146a3b854e55f1d67c3
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: restored rise/fall keys
Changed paths:
engines/freescape/freescape.cpp
engines/freescape/movement.cpp
diff --git a/engines/freescape/freescape.cpp b/engines/freescape/freescape.cpp
index 610d4f98cdb..ef0e4654650 100644
--- a/engines/freescape/freescape.cpp
+++ b/engines/freescape/freescape.cpp
@@ -616,12 +616,6 @@ void FreescapeEngine::processInput() {
case kActionMoveRight:
_strafeRight = true;
break;
- case kActionRiseOrFlyUp:
- _moveUp = true;
- break;
- case kActionLowerOrFlyDown:
- _moveDown = true;
- break;
case kActionShoot:
shoot();
break;
diff --git a/engines/freescape/movement.cpp b/engines/freescape/movement.cpp
index 39374d6cb36..56b47946853 100644
--- a/engines/freescape/movement.cpp
+++ b/engines/freescape/movement.cpp
@@ -312,6 +312,7 @@ bool FreescapeEngine::rise() {
debugC(1, kFreescapeDebugMove, "playerHeightNumber: %d", _playerHeightNumber);
int previousAreaID = _currentArea->getAreaID();
if (_flyMode) {
+ _moveUp = true;
Math::Vector3d destination = _position;
destination.y() = destination.y() + _playerSteps[_playerStepIndex];
resolveCollisions(destination);
@@ -345,6 +346,7 @@ bool FreescapeEngine::rise() {
void FreescapeEngine::lower() {
debugC(1, kFreescapeDebugMove, "playerHeightNumber: %d", _playerHeightNumber);
if (_flyMode) {
+ _moveDown = true;
Math::Vector3d destination = _position;
destination.y() = destination.y() - _playerSteps[_playerStepIndex];
resolveCollisions(destination);
Commit: 14102847c9ffc9464a5744b15fc6e37cc5deeee9
https://github.com/scummvm/scummvm/commit/14102847c9ffc9464a5744b15fc6e37cc5deeee9
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: fixed celestial bodies when using shaders
Changed paths:
engines/freescape/gfx_opengl_shaders.cpp
diff --git a/engines/freescape/gfx_opengl_shaders.cpp b/engines/freescape/gfx_opengl_shaders.cpp
index b689f982f95..16976d074ec 100644
--- a/engines/freescape/gfx_opengl_shaders.cpp
+++ b/engines/freescape/gfx_opengl_shaders.cpp
@@ -490,23 +490,30 @@ void OpenGLShaderRenderer::drawCelestialBody(const Math::Vector3d position, floa
verts.push_back(z);
}
- // === Apply billboard effect to MVP matrix ===
- // Replicate the legacy code's matrix modification
- Math::Matrix4 billboardMVP = _mvpMatrix;
-
- // Zero out rotation for rows 1, 3 (skip row 2), set diagonal to 1.0
- // This matches: for (int i = 1; i < 4; i++) for (int j = 0; j < 4; j++)
+ // === Apply billboard effect to modelview matrix only ===
+ // Matrix4 operator()(i,j) uses (column, row) convention, matching
+ // OpenGL column-major m[col*4+row]. Zero columns 1 and 3 (skip 2),
+ // set diagonal to 1.0 â same as legacy glGetFloatv billboard.
+ Math::Matrix4 billboardMV = _modelViewMatrix;
for (int i = 1; i < 4; i++) {
for (int j = 0; j < 4; j++) {
if (i == 2)
continue;
if (i == j)
- billboardMVP(i, j) = 2.5f;
+ billboardMV(i, j) = 1.0f;
else
- billboardMVP(i, j) = 0.0f;
+ billboardMV(i, j) = 0.0f;
}
}
+ // Recombine with projection (same pattern as positionCamera)
+ Math::Matrix4 proj = _projectionMatrix;
+ Math::Matrix4 model = billboardMV;
+ proj.transpose();
+ model.transpose();
+ Math::Matrix4 billboardMVP = proj * model;
+ billboardMVP.transpose();
+
// === Bind VBO ===
glBindBuffer(GL_ARRAY_BUFFER, _triangleVBO);
glBufferData(GL_ARRAY_BUFFER, verts.size() * sizeof(float), verts.data(), GL_DYNAMIC_DRAW);
Commit: c3cbb7025ecc16d7e7b1b4a089df7fc9e573c361
https://github.com/scummvm/scummvm/commit/c3cbb7025ecc16d7e7b1b4a089df7fc9e573c361
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: better wb-based music for dark and eclipse
Changed paths:
engines/freescape/games/eclipse/atari.music.cpp
engines/freescape/wb.cpp
engines/freescape/wb.h
diff --git a/engines/freescape/games/eclipse/atari.music.cpp b/engines/freescape/games/eclipse/atari.music.cpp
index b3518d87c43..e4760ce4138 100644
--- a/engines/freescape/games/eclipse/atari.music.cpp
+++ b/engines/freescape/games/eclipse/atari.music.cpp
@@ -38,6 +38,7 @@
#include "audio/softsynth/ay8912.h"
#include "freescape/freescape.h"
+#include "freescape/wb.h"
#include "common/endian.h"
#include "common/debug.h"
@@ -121,16 +122,27 @@ private:
byte decayTarget; // Target volume to hold
byte attackRate; // Increment per tick
byte releaseRate; // Decrement per tick
- byte envelopePhase; // 0=attack, 1=decay, 2=sustain, 3=release
+ byte envelopeFlags; // Instrument byte 4
+ byte envelopeToggle; // Bit7 envelope direction toggle
+ bool envelopeDone; // Mirrors original per-note envelope completion flag
+ bool useHardwareEnvelope;
// Effects
- byte effectMode; // 0=none, 1=vibrato, 2=arpeggio
+ byte effectMode; // 0=none, 1=pattern FX ($7D), 2=instrument FX ($7C)
bool portaUp;
bool portaDown;
int16 portaStep;
int16 portaTarget;
byte arpeggioMask;
byte arpeggioPos;
+ byte arpeggioTable[16];
+ byte arpeggioTableLen;
+ byte effect7BBaseNote;
+ byte effect7BParam;
+ bool effect7BActive;
+ int16 effect7BPeriod;
+ byte delay;
+ byte delayCounter;
// Vibrato
byte vibratoSpeed; // Phase increment per tick
@@ -139,8 +151,19 @@ private:
int8 vibratoDir; // +1 or -1
// Noise
- bool noiseEnabled; // Instrument flags bit 1: noise mode
+ bool noiseEnabled; // Instrument flags bit 0/1: noise mode
+ bool toneEnabled; // If false, channel uses noise-only mode
+ bool skipTranspose; // Noise-only mode bypasses order-list transpose
bool freqSweep; // Instrument flags bit 2: frequency sweep
+ byte noisePeriod; // YM noise period source from instrument byte 5
+ byte noiseCounter; // Instrument flags high nibble countdown ($54)
+
+ // Instrument byte-5 period modulation ($04A2..$05C8 path)
+ byte modParam; // Raw instrument byte 5
+ byte modSpan; // High nibble
+ byte modPos; // Running position (mirrors +$30)
+ int8 modDir; // -1/1 (mirrors sign of +$2D)
+ int16 modStep; // Derived note-step delta (mirrors $C9A)
// Period
int16 basePeriod;
@@ -155,10 +178,9 @@ private:
byte _tickSpeed;
byte _tickCounter;
int _tickSampleCount;
-
- // Arpeggio working table
- byte _arpeggioTable[16];
- int _arpeggioTableLen;
+ bool _hwEnvelopeDirty;
+ uint16 _hwEnvelopePeriod;
+ byte _hwEnvelopeShape;
// --- Methods ---
void loadTables();
@@ -169,7 +191,7 @@ private:
void triggerNote(int ch);
void processEffects(int ch);
void processEnvelope(int ch);
- void buildArpeggioTable(byte mask);
+ void buildArpeggioTable(ChannelState &c, byte mask);
void tickUpdate();
void writeYMRegisters();
@@ -207,8 +229,8 @@ EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSi
: AY8912Stream(rate, 2000000), // YM2149 at 2 MHz on Atari ST
_data(data), _dataSize(dataSize),
_musicActive(false), _tickSpeed(6), _tickCounter(0),
- _tickSampleCount(0),
- _arpeggioTableLen(0), _numPatterns(0) {
+ _tickSampleCount(0), _hwEnvelopeDirty(false), _hwEnvelopePeriod(0), _hwEnvelopeShape(0),
+ _numPatterns(0) {
memset(_periods, 0, sizeof(_periods));
memset(_instruments, 0, sizeof(_instruments));
@@ -216,7 +238,6 @@ EclipseAtariMusicStream::EclipseAtariMusicStream(const byte *data, uint32 dataSi
memset(_patternPtrs, 0, sizeof(_patternPtrs));
memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
memset(_channels, 0, sizeof(_channels));
- memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
// Reset all YM registers
for (int r = 0; r < 14; r++)
@@ -305,7 +326,9 @@ void EclipseAtariMusicStream::startSong(int songNum) {
int songIdx = songNum - 1;
_tickSpeed = 6;
_tickCounter = 0;
- _arpeggioTableLen = 0;
+ _hwEnvelopeDirty = false;
+ _hwEnvelopePeriod = 0;
+ _hwEnvelopeShape = 0;
// Silence all YM channels
for (int r = 0; r < 14; r++)
@@ -334,9 +357,11 @@ void EclipseAtariMusicStream::initChannel(int ch) {
memset(&c, 0, sizeof(ChannelState));
c.duration = 1;
c.durationCounter = 0;
- c.envelopePhase = 3; // Start in release (silent)
c.attackLevel = 0x36; // Default from instrument 1
c.decayTarget = 0x36;
+ c.toneEnabled = true;
+ c.envelopeDone = true;
+ c.useHardwareEnvelope = false;
}
// ---------------------------------------------------------------------------
@@ -360,7 +385,7 @@ void EclipseAtariMusicStream::readOrderList(int ch) {
}
if (cmd > 0xC0) {
- c.transpose = (int8)((cmd + 0x20) & 0xFF);
+ c.transpose = WBCommon::decodeOrderTranspose(cmd);
continue;
}
@@ -407,19 +432,22 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
}
if (cmd == 0xFC) {
- // Song change command â read parameter, restart
- byte newSong = readDataByte(c.patternOffset + c.patternPos);
+ // Song jump command: mirror TEMUSIC mailbox semantics.
+ byte command = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
- if (newSong >= 1 && newSong <= 2) {
- startSong(newSong);
+ if (command == 0) {
+ _musicActive = false;
+ for (int r = 0; r < 14; r++)
+ setReg(r, 0);
+ setReg(7, 0x3F);
+ } else if (command >= 1 && command <= 2) {
+ startSong(command);
}
return;
}
if (cmd >= 0xF0) {
- _tickSpeed = cmd & 0x0F;
- if (_tickSpeed == 0)
- _tickSpeed = 1;
+ _tickSpeed = WBCommon::decodeTickSpeed(cmd);
continue;
}
@@ -433,36 +461,52 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
c.decayTarget = inst.targetVol;
c.attackRate = inst.attackRate;
c.releaseRate = inst.releaseRate;
-
- // Instrument effect type: high nibble = vibrato speed,
- // low nibble = vibrato depth (in period units)
- if (inst.effectType != 0) {
- c.effectMode = 1; // vibrato
- c.vibratoSpeed = (inst.effectType >> 4) & 0x0F;
- c.vibratoDepth = inst.effectType & 0x0F;
- c.vibratoPos = 0;
- c.vibratoDir = 1;
- c.portaUp = false;
- c.portaDown = false;
- } else if (inst.arpeggioData != 0) {
- c.effectMode = 2; // arpeggio
+ c.envelopeFlags = inst.envFlags;
+
+ // Instrument byte 5 is used as a PSG/noise control parameter.
+ c.modParam = inst.effectType;
+ c.modSpan = (inst.effectType >> 4) & 0x0F;
+ c.modPos = 0;
+ c.modDir = 1;
+ c.noisePeriod = inst.effectType & 0x3F;
+ c.toneEnabled = true;
+ c.noiseEnabled = false;
+ c.skipTranspose = false;
+ c.freqSweep = false;
+ c.noiseCounter = inst.flags >> 4;
+
+ // Instrument byte 6 preloads the channel interval table and forces mode $7C.
+ if (inst.arpeggioData != 0) {
+ c.effectMode = 2;
c.arpeggioMask = inst.arpeggioData;
- buildArpeggioTable(inst.arpeggioData);
- } else {
- c.effectMode = 0;
+ c.arpeggioPos = 0;
+ buildArpeggioTable(c, inst.arpeggioData);
}
- // Noise flags from instrument byte 7
- c.noiseEnabled = (inst.flags & 0x02) != 0;
- c.freqSweep = (inst.flags & 0x04) != 0;
+ // Instrument byte 7 flags:
+ // bit0/1 noise modes, bit2 frequency sweep, bit3 retrigger.
+ if (inst.flags & 0x01) {
+ // Bit 0: tone + noise with fixed noise seed period.
+ c.noiseEnabled = true;
+ c.noisePeriod = 0x20;
+ } else if (inst.flags & 0x02) {
+ // Bit 1: note-relative noise-only mode.
+ c.noiseEnabled = true;
+ c.toneEnabled = false;
+ c.skipTranspose = true;
+ if (c.noisePeriod == 0)
+ c.noisePeriod = 1;
+ } else if (inst.flags & 0x04) {
+ // Bit 2: tone + noise mode with frequency sweep.
+ c.noiseEnabled = true;
+ c.freqSweep = true;
+ }
}
continue;
}
if (cmd >= 0x80) {
- c.duration = cmd & 0x3F;
- if (c.duration == 0)
- c.duration = 1;
+ c.duration = WBCommon::decodeDuration(cmd);
continue;
}
@@ -470,40 +514,81 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
c.portaUp = true;
c.portaDown = false;
c.effectMode = 1;
- continue;
+ // Original parser consumes the next byte and uses it as the immediate note.
+ if (c.patternOffset + c.patternPos >= _dataSize)
+ return;
+ c.prevNote = c.note;
+ c.note = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.durationCounter = c.duration;
+ triggerNote(ch);
+ return;
}
if (cmd == 0x7E) {
c.portaDown = true;
c.portaUp = false;
c.effectMode = 1;
- continue;
+ // Original parser consumes the next byte and uses it as the immediate note.
+ if (c.patternOffset + c.patternPos >= _dataSize)
+ return;
+ c.prevNote = c.note;
+ c.note = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ c.durationCounter = c.duration;
+ triggerNote(ch);
+ return;
}
if (cmd == 0x7D) {
- // Arpeggio / effect mode 1 â param is bitmask into interval table
+ // Pattern effect 1: parameterized interval table (channel-local)
byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
- c.effectMode = 2; // arpeggio
+ c.effectMode = 1;
c.arpeggioMask = param;
c.arpeggioPos = 0;
- buildArpeggioTable(param);
+ buildArpeggioTable(c, param);
continue;
}
if (cmd == 0x7C) {
- // Effect mode 2 â alternate arpeggio/vibrato
- byte param = readDataByte(c.patternOffset + c.patternPos);
- c.patternPos++;
- c.effectMode = 2; // arpeggio
- c.arpeggioMask = param;
+ // Pattern effect 2: switch mode only (no parameter byte consumed).
+ c.effectMode = 2;
+ continue;
+ }
+
+ if (cmd == 0x7B) {
+ // TEMUSIC delayed slide command:
+ // byte1 = base note (+transpose), byte2 low nibble = delay window.
+ c.arpeggioTableLen = 0;
c.arpeggioPos = 0;
- buildArpeggioTable(param);
+ c.effectMode = 1;
+ c.effect7BActive = false;
+
+ if (c.patternOffset + c.patternPos < _dataSize) {
+ byte base = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ base = (byte)(base + c.transpose);
+ c.effect7BBaseNote = base;
+
+ int baseNote = c.effect7BBaseNote;
+ if (baseNote < 1)
+ baseNote = 1;
+ if (baseNote >= kTENumPeriods)
+ baseNote = kTENumPeriods - 1;
+ c.effect7BPeriod = getPeriod(baseNote);
+ }
+ if (c.patternOffset + c.patternPos < _dataSize) {
+ c.effect7BParam = readDataByte(c.patternOffset + c.patternPos);
+ c.patternPos++;
+ }
+ c.effect7BActive = true;
continue;
}
if (cmd == 0x7A) {
- // Delay command â skip parameter byte (same as wb.cpp)
+ // Delay command â consumed and copied to per-note delay counter.
+ c.delay = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
continue;
}
@@ -526,37 +611,86 @@ void EclipseAtariMusicStream::readPatternCommands(int ch) {
void EclipseAtariMusicStream::triggerNote(int ch) {
ChannelState &c = _channels[ch];
- if (c.note == 0) {
- // Rest â silence channel
- c.outputPeriod = 0;
- c.volume = 0;
- c.envelopePhase = 3;
- return;
- }
-
// Apply transpose and clamp
- int note = c.note + c.transpose;
- if (note < 1) note = 1;
- if (note >= kTENumPeriods) note = kTENumPeriods - 1;
+ int note = c.note;
+ if (!c.skipTranspose)
+ note += c.transpose;
+ bool isRest = (note == 0);
+ if (!isRest) {
+ if (note < 1)
+ note = 1;
+ if (note >= kTENumPeriods)
+ note = kTENumPeriods - 1;
+ }
- c.basePeriod = getPeriod(note);
+ c.basePeriod = isRest ? 0 : getPeriod(note);
c.outputPeriod = c.basePeriod;
+ c.useHardwareEnvelope = false;
- if (c.basePeriod == 0) {
+ if (!isRest && c.basePeriod == 0) {
warning("TE-Atari: ch%d note %d has period 0", ch, note);
return;
}
// Reset envelope
- c.envelopePhase = 0;
+ c.envelopeToggle = 0;
+ c.envelopeDone = false;
c.volume = c.attackLevel;
+ c.delayCounter = c.delay;
+ c.arpeggioPos = 0;
+ c.modStep = 0;
+ const InstrumentDesc &inst = _instruments[c.instrumentIdx];
+ c.noiseCounter = inst.flags >> 4;
+
+ // Reapply byte-7 mixer mode on every note trigger.
+ c.toneEnabled = true;
+ c.noiseEnabled = false;
+ c.skipTranspose = false;
+ c.freqSweep = false;
+ if (inst.flags & 0x01) {
+ c.noiseEnabled = true;
+ c.noisePeriod = 0x20;
+ } else if (inst.flags & 0x02) {
+ c.noiseEnabled = true;
+ c.toneEnabled = false;
+ c.skipTranspose = true;
+ if (c.noisePeriod == 0)
+ c.noisePeriod = 1;
+ } else if (inst.flags & 0x04) {
+ c.noiseEnabled = true;
+ c.freqSweep = true;
+ }
+
+ if (!isRest && c.modParam != 0 && note + 1 < kTENumPeriods) {
+ int16 periodDelta = ABS((int16)getPeriod(note) - (int16)getPeriod(note + 1));
+ byte depth = c.modParam & 0x0F;
+ while (depth > 0) {
+ periodDelta = (periodDelta >> 1) | 1;
+ depth--;
+ }
+ c.modStep = periodDelta;
+ }
+
+ // TEMUSIC instrument byte 4 bit7 selects YM hardware envelope mode.
+ if (!isRest && (c.envelopeFlags & 0x80)) {
+ c.useHardwareEnvelope = true;
+ c.envelopeDone = true;
+
+ // Envelope style comes from low nibble; period follows note pitch scale.
+ _hwEnvelopeShape = c.envelopeFlags & 0x0F;
+ uint16 envPeriod = (c.basePeriod > 0) ? (uint16)MAX(1, c.basePeriod >> 4) : 1;
+ _hwEnvelopePeriod = envPeriod;
+ _hwEnvelopeDirty = true;
+ }
debugC(3, kFreescapeDebugParser, "TE-Atari: ch%d NOTE note=%d(+%d) period=%d inst=%d vol=%d",
ch, c.note, c.transpose, c.basePeriod, c.instrumentIdx, c.volume);
// Set up portamento if active
- if (c.portaUp || c.portaDown) {
- int prevNote = c.prevNote + c.transpose;
+ if (!isRest && (c.portaUp || c.portaDown)) {
+ int prevNote = c.prevNote;
+ if (!c.skipTranspose)
+ prevNote += c.transpose;
if (prevNote < 1) prevNote = 1;
if (prevNote >= kTENumPeriods) prevNote = kTENumPeriods - 1;
@@ -579,6 +713,28 @@ void EclipseAtariMusicStream::triggerNote(int ch) {
void EclipseAtariMusicStream::processEffects(int ch) {
ChannelState &c = _channels[ch];
+ // Noise gate: in TEMUSIC, instrument high nibble is a countdown that
+ // can auto-disable channel noise after N ticks.
+ if (c.noiseEnabled) {
+ if (c.noiseCounter > 0) {
+ c.noiseCounter--;
+ if (c.noiseCounter == 0)
+ c.noiseEnabled = false;
+ }
+ }
+
+ int16 period = c.basePeriod;
+
+ // Instrument flag bit2 enables the original per-tick sweep path (+$64, 12-bit wrap),
+ // and advances the shared noise period source.
+ if (c.freqSweep) {
+ c.basePeriod = (c.basePeriod + 0x64) & 0x0FFF;
+ if (c.basePeriod == 0)
+ c.basePeriod = 1;
+ c.noisePeriod = (c.noisePeriod + 1) & 0x1F;
+ period = c.basePeriod;
+ }
+
// Portamento takes priority (active during porta regardless of effectMode)
if (c.portaUp) {
c.basePeriod -= c.portaStep;
@@ -586,52 +742,83 @@ void EclipseAtariMusicStream::processEffects(int ch) {
c.basePeriod = c.portaTarget;
c.portaUp = false;
}
- c.outputPeriod = c.basePeriod;
- return;
- }
-
- if (c.portaDown) {
+ period = c.basePeriod;
+ } else if (c.portaDown) {
c.basePeriod += c.portaStep;
if (c.basePeriod >= c.portaTarget) {
c.basePeriod = c.portaTarget;
c.portaDown = false;
}
- c.outputPeriod = c.basePeriod;
- return;
- }
-
- if (c.effectMode == 1 && c.vibratoDepth > 0) {
- // Vibrato: triangle wave pitch oscillation around basePeriod
- c.vibratoPos += c.vibratoDir;
- if (c.vibratoPos >= (int8)c.vibratoDepth) {
- c.vibratoPos = c.vibratoDepth;
- c.vibratoDir = -1;
- } else if (c.vibratoPos <= -(int8)c.vibratoDepth) {
- c.vibratoPos = -(int8)c.vibratoDepth;
- c.vibratoDir = 1;
- }
- // Scale vibrato offset by speed (higher speed = faster oscillation)
- int16 offset = c.vibratoPos * c.vibratoSpeed;
- c.outputPeriod = c.basePeriod + offset;
- if (c.outputPeriod < 1) c.outputPeriod = 1;
- return;
- }
-
- if (c.effectMode == 2 && _arpeggioTableLen > 0) {
- // Arpeggio: cycle through note offsets from interval table
- int note = c.note + c.transpose;
- int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+ period = c.basePeriod;
+ } else if (c.effectMode != 0 && c.arpeggioTableLen > 0) {
+ // Channel-local interval cycling (used by $7D/$7C paths).
+ int note = c.note;
+ if (!c.skipTranspose)
+ note += c.transpose;
+ int offset = c.arpeggioTable[c.arpeggioPos % c.arpeggioTableLen];
note += offset;
if (note < 1) note = 1;
if (note >= kTENumPeriods) note = kTENumPeriods - 1;
- c.outputPeriod = getPeriod(note);
+ period = getPeriod(note);
c.arpeggioPos++;
- if (c.arpeggioPos >= _arpeggioTableLen)
+ if (c.arpeggioPos >= c.arpeggioTableLen)
c.arpeggioPos = 0;
- return;
+ } else if (c.effect7BActive) {
+ // $7B path: delayed slide from a base note period toward current note period.
+ byte window = c.effect7BParam & 0x0F;
+ if ((int)c.durationCounter + (int)window <= (int)c.duration) {
+ int16 target = c.basePeriod;
+ int steps = (_tickSpeed > 0) ? _tickSpeed : 1;
+ int16 delta = ABS(target - c.effect7BPeriod) / steps;
+ if (delta == 0)
+ delta = 1;
+
+ if (c.effect7BPeriod < target) {
+ c.effect7BPeriod += delta;
+ if (c.effect7BPeriod > target)
+ c.effect7BPeriod = target;
+ } else if (c.effect7BPeriod > target) {
+ c.effect7BPeriod -= delta;
+ if (c.effect7BPeriod < target)
+ c.effect7BPeriod = target;
+ }
+ period = c.effect7BPeriod;
+ }
}
- c.outputPeriod = c.basePeriod;
+ // TEMUSIC enters byte-5 modulation only while mode is clear.
+ if (c.effectMode == 0 && c.modParam != 0 && c.modStep > 0 && c.modSpan > 0) {
+ // $7A delay applies to this modulation path, not to all effects.
+ if (c.delayCounter > 0) {
+ c.delayCounter--;
+ } else {
+ if (c.modDir < 0) {
+ if (c.modPos > 0) {
+ c.modPos--;
+ } else {
+ c.modDir = 1;
+ if (c.modPos < c.modSpan)
+ c.modPos++;
+ }
+ } else {
+ c.modPos++;
+ if (c.modPos > c.modSpan) {
+ c.modPos = c.modSpan;
+ c.modDir = -1;
+ if (c.modPos > 0)
+ c.modPos--;
+ }
+ }
+
+ int center = c.modSpan >> 1;
+ int offset = (center - c.modPos) * c.modStep;
+ period += offset;
+ if (period < 1)
+ period = 1;
+ }
+ }
+
+ c.outputPeriod = period;
}
// ---------------------------------------------------------------------------
@@ -641,41 +828,71 @@ void EclipseAtariMusicStream::processEffects(int ch) {
void EclipseAtariMusicStream::processEnvelope(int ch) {
ChannelState &c = _channels[ch];
+ // Noise-only instruments may validly run with zero tone period.
+ if (c.outputPeriod == 0 && !c.noiseEnabled)
+ return;
+ if (c.useHardwareEnvelope)
+ return;
- switch (c.envelopePhase) {
- case 0: // Attack â volume set to attackLevel in triggerNote
- if (c.attackRate == 0) {
- c.volume = c.decayTarget;
- c.envelopePhase = 2;
- } else {
- c.envelopePhase = 1;
- }
- break;
+ byte env = c.envelopeFlags;
- case 1: // Decay â fade toward target
- if (c.volume < c.decayTarget) {
- c.volume++;
- } else if (c.volume > c.decayTarget) {
- c.volume--;
+ // Instrument env byte bit7: oscillating level between attackLevel and target.
+ if (env & 0x80) {
+ byte step = env & 0x0F;
+ if (c.envelopeToggle == 0) {
+ if (c.volume == c.decayTarget) {
+ c.envelopeToggle = 1;
+ if (c.volume == c.attackLevel) {
+ c.envelopeToggle = 0;
+ } else {
+ c.volume = (byte)(c.volume + step);
+ }
+ } else {
+ c.volume = (byte)(c.volume - step);
+ c.envelopeToggle = 0;
+ }
} else {
- c.envelopePhase = 2;
+ if (c.volume == c.attackLevel) {
+ c.envelopeToggle = 0;
+ } else {
+ c.volume = (byte)(c.volume + step);
+ }
}
- break;
+ } else {
+ c.envelopeToggle = 0;
+ // Original routine skips non-bit7 envelope work only when duration counter is exactly zero.
+ if (c.durationCounter == 0)
+ return;
+
+ if (!c.envelopeDone) {
+ if (env == 0x00) {
+ // Special case: immediate jump to target, mark envelope done.
+ c.envelopeDone = true;
+ c.volume = c.decayTarget;
+ if (c.volume > 63)
+ c.volume = 63;
+ return;
+ }
- case 2: // Sustain â hold
- break;
+ if (env != 0xFF) {
+ byte div = env & 0x7F;
+ byte triggerPoint = (div != 0) ? (c.duration / div) : 0;
- case 3: // Release
- if (c.releaseRate > 0) {
- if (c.volume > c.releaseRate) {
- c.volume -= c.releaseRate;
- } else {
- c.volume = 0;
+ if (c.durationCounter == triggerPoint) {
+ c.envelopeDone = true;
+ } else if (c.volume != c.decayTarget) {
+ c.volume = (byte)(c.volume + c.attackRate);
+ }
}
- } else {
- c.volume = 0;
}
- break;
+
+ if (c.envelopeDone) {
+ byte next = (byte)(c.volume - c.releaseRate);
+ if ((int8)next < 0)
+ c.volume = 0;
+ else
+ c.volume = next;
+ }
}
if (c.volume > 63)
@@ -686,18 +903,9 @@ void EclipseAtariMusicStream::processEnvelope(int ch) {
// Arpeggio table builder
// ---------------------------------------------------------------------------
-void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
- _arpeggioTableLen = 0;
- _arpeggioTable[_arpeggioTableLen++] = 0; // Base note
-
- for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
- if (mask & (1 << i)) {
- _arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
- }
- }
-
- if (_arpeggioTableLen <= 1)
- _arpeggioTableLen = 0;
+void EclipseAtariMusicStream::buildArpeggioTable(ChannelState &c, byte mask) {
+ c.arpeggioTableLen = WBCommon::buildArpeggioTable(_arpeggioIntervals, mask, c.arpeggioTable, 16, false);
+ c.arpeggioPos = 0;
}
// ---------------------------------------------------------------------------
@@ -706,37 +914,56 @@ void EclipseAtariMusicStream::buildArpeggioTable(byte mask) {
void EclipseAtariMusicStream::writeYMRegisters() {
byte mixer = 0x3F; // Start with all disabled (bits 0-2=tone, bits 3-5=noise)
+ if (_hwEnvelopeDirty) {
+ setReg(11, _hwEnvelopePeriod & 0xFF);
+ setReg(12, (_hwEnvelopePeriod >> 8) & 0xFF);
+ setReg(13, _hwEnvelopeShape & 0x0F);
+ _hwEnvelopeDirty = false;
+ }
- for (int ch = 0; ch < kTENumChannels; ch++) {
+ // TEMUSIC channel loop runs 2 -> 0; keep that order so global noise register ownership matches.
+ for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
ChannelState &c = _channels[ch];
- if (!c.active || c.outputPeriod == 0 || c.volume == 0) {
+ bool hasTone = c.toneEnabled && (c.outputPeriod > 0);
+ bool hasNoise = c.noiseEnabled;
+ bool usesHwEnvelope = c.useHardwareEnvelope;
+ if (!c.active || (!usesHwEnvelope && c.volume == 0) || (!hasTone && !hasNoise)) {
// Channel silent
setReg(8 + ch, 0); // Volume = 0
continue;
}
- // Enable tone for this channel (bits 0-2)
- mixer &= ~(1 << ch);
+ // Enable tone for this channel (bits 0-2), unless in noise-only mode.
+ if (hasTone)
+ mixer &= ~(1 << ch);
// Enable noise for this channel if instrument has noise flag (bits 3-5)
- if (c.noiseEnabled) {
+ if (hasNoise) {
mixer &= ~(1 << (ch + 3));
- // Set noise period from note (lower period = higher pitched noise)
- byte noisePeriod = (c.outputPeriod >> 4) & 0x1F;
- if (noisePeriod == 0) noisePeriod = 1;
+ byte noisePeriod = c.noisePeriod;
+ if (noisePeriod == 0 && c.outputPeriod > 0)
+ noisePeriod = (c.outputPeriod >> 4) & 0x1F;
+ if (noisePeriod == 0)
+ noisePeriod = 1;
setReg(6, noisePeriod);
}
- // Set tone period (2 registers per channel)
- uint16 period = (uint16)c.outputPeriod;
- setReg(ch * 2, period & 0xFF); // Fine tune
- setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+ // Set tone period (2 registers per channel) when tone path is active.
+ if (hasTone) {
+ uint16 period = (uint16)c.outputPeriod;
+ setReg(ch * 2, period & 0xFF); // Fine tune
+ setReg(ch * 2 + 1, (period >> 8) & 0x0F); // Coarse tune
+ }
// Set volume (internal 0-63 â YM 0-15)
- byte ymVol = c.volume >> 2;
- if (ymVol > 15) ymVol = 15;
- setReg(8 + ch, ymVol);
+ if (usesHwEnvelope) {
+ setReg(8 + ch, 0x10); // Enable YM hardware envelope on this channel
+ } else {
+ byte ymVol = c.volume >> 2;
+ if (ymVol > 15) ymVol = 15;
+ setReg(8 + ch, ymVol);
+ }
}
setReg(7, mixer);
@@ -750,35 +977,41 @@ void EclipseAtariMusicStream::tickUpdate() {
if (!_musicActive)
return;
- // Tick speed gating: the 68000 code uses subq.b #1; bpl; reset which
- // gives a period of (tickSpeed + 1) ticks before the sequencer advances.
- // Counter counts: tickSpeed â tickSpeed-1 â ... â 0 â -1(wrap&reset)
- _tickCounter++;
- bool sequencerTick = false;
- if (_tickCounter > _tickSpeed) {
- _tickCounter = 0;
- sequencerTick = true;
- }
+ // Sequencer step occurs when tick counter is zero, then the counter advances.
+ // This matches the original TEMUSIC update loop and wb.cpp behavior.
+ bool sequencerTick = (_tickCounter == 0);
if (sequencerTick) {
- for (int ch = 0; ch < kTENumChannels; ch++) {
+ for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
if (!_channels[ch].active)
continue;
- if (_channels[ch].durationCounter > 0) {
- _channels[ch].durationCounter--;
- }
+ _channels[ch].durationCounter--;
- if (_channels[ch].durationCounter == 0) {
- if (_channels[ch].envelopePhase < 3)
- _channels[ch].envelopePhase = 3;
+ if (_channels[ch].durationCounter < 0) {
+ // Original step boundary reset: clear transient note/effect flags
+ // before parsing the next command stream, except mode 2 persistence.
+ ChannelState &c = _channels[ch];
+ if (c.effectMode != 2) {
+ c.effectMode = 0;
+ c.arpeggioMask = 0;
+ c.arpeggioPos = 0;
+ c.arpeggioTableLen = 0;
+ }
+ c.effect7BActive = false;
+ c.portaUp = false;
+ c.portaDown = false;
+ c.noiseEnabled = false;
+ c.freqSweep = false;
+ c.envelopeDone = false;
+ c.useHardwareEnvelope = false;
readPatternCommands(ch);
}
}
}
// Every tick: process effects and envelope
- for (int ch = 0; ch < kTENumChannels; ch++) {
+ for (int ch = kTENumChannels - 1; ch >= 0; ch--) {
if (!_channels[ch].active)
continue;
@@ -786,6 +1019,10 @@ void EclipseAtariMusicStream::tickUpdate() {
processEnvelope(ch);
}
+ _tickCounter++;
+ if (_tickCounter >= _tickSpeed)
+ _tickCounter = 0;
+
writeYMRegisters();
}
diff --git a/engines/freescape/wb.cpp b/engines/freescape/wb.cpp
index e64e90cc922..dfa2ce8d429 100644
--- a/engines/freescape/wb.cpp
+++ b/engines/freescape/wb.cpp
@@ -54,6 +54,39 @@
namespace Freescape {
+namespace WBCommon {
+
+int8 decodeOrderTranspose(byte cmd) {
+ return (int8)((cmd + 0x20) & 0xFF);
+}
+
+byte decodeTickSpeed(byte cmd) {
+ byte speed = cmd & 0x0F;
+ return speed == 0 ? 1 : speed;
+}
+
+byte decodeDuration(byte cmd) {
+ byte duration = cmd & 0x3F;
+ return duration == 0 ? 1 : duration;
+}
+
+byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte maxLen, bool includeBase) {
+ byte len = 0;
+ if (includeBase && maxLen > 0)
+ outTable[len++] = 0;
+
+ for (int i = 0; i < 8 && len < maxLen; i++) {
+ if (mask & (1 << i))
+ outTable[len++] = intervals[i];
+ }
+
+ if (includeBase && len <= 1)
+ return 0;
+ return len;
+}
+
+} // End of namespace WBCommon
+
// TEXT-relative offsets for data tables within HDSMUSIC.AM
// All addresses verified against disassembly of the 68000 code.
static const uint32 kPeriodTableOffset = 0x0AAE; // 48 x uint16 BE (note 0=silence, 1-47=C-1..B-3)
@@ -154,6 +187,13 @@ private:
int16 portaTarget;
byte arpeggioMask;
byte arpeggioPos;
+ byte arpeggioTable[16];
+ byte arpeggioTableLen;
+ byte effect7BBaseNote;
+ byte effect7BRate;
+ byte effect7BCounter;
+ bool effect7BUseBase;
+ bool effect7BActive;
byte vibratoPos;
// Period
@@ -163,6 +203,7 @@ private:
// Delay
byte delay;
byte delayCounter;
+ bool pendingNoteOn;
bool active;
};
@@ -173,10 +214,7 @@ private:
bool _musicActive;
byte _tickSpeed; // Ticks between sequencer steps
byte _tickCounter; // Current tick count (counts up to _tickSpeed)
-
- // Arpeggio working table (built by buildArpeggioTable)
- byte _arpeggioTable[16];
- int _arpeggioTableLen;
+ int _pendingSongCommand; // -1=no pending, else mailbox command from $DD
// --- Methods ---
@@ -188,7 +226,7 @@ private:
void triggerNote(int ch);
void processEffects(int ch);
void processEnvelope(int ch);
- void buildArpeggioTable(byte mask);
+ void buildArpeggioTable(int ch, byte mask);
uint16 getPeriod(int note) const;
byte readDataByte(uint32 offset) const {
@@ -219,7 +257,7 @@ WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
: Paula(stereo, rate, rate / 50), // 50 Hz PAL VBI interrupt rate
_data(data), _dataSize(dataSize),
_musicActive(false), _tickSpeed(6), _tickCounter(0),
- _arpeggioTableLen(0), _numPatterns(0), _firstSampleOffset(0) {
+ _pendingSongCommand(-1), _numPatterns(0), _firstSampleOffset(0) {
memset(_periods, 0, sizeof(_periods));
memset(_instruments, 0, sizeof(_instruments));
@@ -229,7 +267,6 @@ WallyBebenStream::WallyBebenStream(const byte *data, uint32 dataSize,
memset(_patternPtrs, 0, sizeof(_patternPtrs));
memset(_arpeggioIntervals, 0, sizeof(_arpeggioIntervals));
memset(_channels, 0, sizeof(_channels));
- memset(_arpeggioTable, 0, sizeof(_arpeggioTable));
// Standard Amiga panning: channels 0,3 left â channels 1,2 right
setChannelPanning(0, PANNING_LEFT);
@@ -366,7 +403,7 @@ void WallyBebenStream::startSong(int songNum) {
int songIdx = songNum - 1;
_tickSpeed = 6;
_tickCounter = 0;
- _arpeggioTableLen = 0;
+ _pendingSongCommand = -1;
// Silence all Paula channels
for (int ch = 0; ch < NUM_VOICES; ch++) {
@@ -374,7 +411,7 @@ void WallyBebenStream::startSong(int songNum) {
}
// Initialize each channel from the song table
- for (int ch = 0; ch < 4; ch++) {
+ for (int ch = 3; ch >= 0; ch--) {
initChannel(ch);
_channels[ch].orderListOffset = _songOrderPtrs[songIdx][ch];
_channels[ch].orderListPos = 0;
@@ -388,7 +425,7 @@ void WallyBebenStream::startSong(int songNum) {
startPaula();
debug(3, "WB: Song %d started, tickSpeed=%d", songNum, _tickSpeed);
- for (int ch = 0; ch < 4; ch++) {
+ for (int ch = 3; ch >= 0; ch--) {
debug(3, "WB: ch%d orderList=$%X pattern=$%X",
ch, _channels[ch].orderListOffset, _channels[ch].patternOffset);
}
@@ -405,7 +442,7 @@ void WallyBebenStream::initChannel(int ch) {
// $C0 envelope command still produce sound (Env 0 has all zeros = silence)
c.attackLevel = 64;
c.decayTarget = 64;
- c.sustainLevel = 0; // Instant transition (no fade)
+ c.sustainLevel = 64;
c.releaseRate = 0;
}
@@ -434,7 +471,7 @@ void WallyBebenStream::readOrderList(int ch) {
if (cmd > 0xC0) {
// Transpose command: transpose = (cmd + 0x20) & 0xFF
// Stored as signed offset
- c.transpose = (int8)((cmd + 0x20) & 0xFF);
+ c.transpose = WBCommon::decodeOrderTranspose(cmd);
continue;
}
@@ -462,7 +499,7 @@ void WallyBebenStream::readOrderList(int ch) {
// Pattern format: $FF=end-pattern, $FE=end-song, $F0-$FD=speed,
// $E0-$EF=instrument, $C0-$DF=envelope, $80-$BF=duration,
// $7F/$7E=portamento, $7D/$7C=vibrato, $7B=arpeggio,
-// $7A=delay, $00-$79=note (0=rest, 1-47=C-1..B-3)
+// $7A=delay, $00-$5F=note (0=rest, 1-47=C-1..B-3)
// ---------------------------------------------------------------------------
void WallyBebenStream::readPatternCommands(int ch) {
@@ -488,16 +525,16 @@ void WallyBebenStream::readPatternCommands(int ch) {
}
if (cmd == 0xDD) {
- // Song change â read parameter byte, ignore for now
+ // Song change command: hand off through mailbox-equivalent path.
+ byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
- continue;
+ _pendingSongCommand = param;
+ return;
}
if (cmd >= 0xF0) {
// Set speed: low nibble
- _tickSpeed = cmd & 0x0F;
- if (_tickSpeed == 0)
- _tickSpeed = 1;
+ _tickSpeed = WBCommon::decodeTickSpeed(cmd);
continue;
}
@@ -533,16 +570,14 @@ void WallyBebenStream::readPatternCommands(int ch) {
if (env.arpeggioMask != 0) {
c.effectMode = 2;
c.arpeggioMask = env.arpeggioMask;
- buildArpeggioTable(env.arpeggioMask);
+ buildArpeggioTable(ch, env.arpeggioMask);
}
continue;
}
if (cmd >= 0x80) {
// Set duration: low 6 bits (0-63)
- c.duration = cmd & 0x3F;
- if (c.duration == 0)
- c.duration = 1;
+ c.duration = WBCommon::decodeDuration(cmd);
continue;
}
@@ -551,6 +586,7 @@ void WallyBebenStream::readPatternCommands(int ch) {
c.portaUp = true;
c.portaDown = false;
c.effectMode = 1;
+ c.effect7BActive = false;
continue;
}
@@ -559,6 +595,7 @@ void WallyBebenStream::readPatternCommands(int ch) {
c.portaDown = true;
c.portaUp = false;
c.effectMode = 1;
+ c.effect7BActive = false;
continue;
}
@@ -567,8 +604,9 @@ void WallyBebenStream::readPatternCommands(int ch) {
byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
c.effectMode = 1;
+ c.effect7BActive = false;
c.arpeggioMask = param;
- buildArpeggioTable(param);
+ buildArpeggioTable(ch, param);
continue;
}
@@ -577,13 +615,15 @@ void WallyBebenStream::readPatternCommands(int ch) {
byte param = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
c.effectMode = 2;
+ c.effect7BActive = false;
c.arpeggioMask = param;
- buildArpeggioTable(param);
+ buildArpeggioTable(ch, param);
continue;
}
if (cmd == 0x7B) {
- // Arpeggio
+ // Dedicated arpeggio/slide mode:
+ // xx + transpose -> base note, yy -> rate.
byte base = readDataByte(c.patternOffset + c.patternPos);
c.patternPos++;
byte rate = readDataByte(c.patternOffset + c.patternPos);
@@ -591,10 +631,18 @@ void WallyBebenStream::readPatternCommands(int ch) {
c.effectMode = 1;
c.arpeggioMask = 0;
c.arpeggioPos = 0;
- // Store arpeggio base note with transpose applied
- _arpeggioTable[0] = base;
- _arpeggioTable[1] = rate;
- _arpeggioTableLen = 2;
+ c.arpeggioTableLen = 0;
+ c.effect7BActive = true;
+ c.effect7BCounter = 0;
+ c.effect7BUseBase = true;
+
+ int baseNote = base + c.transpose;
+ if (baseNote < 1)
+ baseNote = 1;
+ if (baseNote > 47)
+ baseNote = 47;
+ c.effect7BBaseNote = (byte)baseNote;
+ c.effect7BRate = rate;
continue;
}
@@ -606,11 +654,24 @@ void WallyBebenStream::readPatternCommands(int ch) {
continue;
}
- // Note value (0x00-0x79)
+ if (cmd > 0x5F) {
+ // Original command parser accepts notes in range 0x00-0x5F.
+ warning("WallyBeben: ch%d unknown pattern command $%02X", ch, cmd);
+ continue;
+ }
+
+ // Note value (0x00-0x5F)
c.prevNote = c.note;
c.note = cmd;
- c.durationCounter = c.duration;
- triggerNote(ch);
+ if (c.note != 0 && c.delay > 0) {
+ // Delay note-on by c.delay frames.
+ c.delayCounter = c.delay;
+ c.pendingNoteOn = true;
+ } else {
+ c.durationCounter = c.duration;
+ c.pendingNoteOn = false;
+ triggerNote(ch);
+ }
return; // One note per sequencer step
}
@@ -624,12 +685,14 @@ void WallyBebenStream::readPatternCommands(int ch) {
void WallyBebenStream::triggerNote(int ch) {
ChannelState &c = _channels[ch];
+ c.pendingNoteOn = false;
if (c.note == 0) {
// Rest â silence channel
c.outputPeriod = 0;
c.volume = 0;
c.envelopePhase = 3;
+ c.effect7BActive = false;
setChannelVolume(ch, 0);
return;
}
@@ -643,6 +706,11 @@ void WallyBebenStream::triggerNote(int ch) {
c.basePeriod = getPeriod(note);
c.outputPeriod = c.basePeriod;
+ if (c.effect7BActive) {
+ c.effect7BCounter = 0;
+ c.effect7BUseBase = true;
+ }
+
if (c.basePeriod == 0) {
warning("WallyBeben: ch%d note %d (raw %d + transpose %d) has period 0",
ch, note, c.note, c.transpose);
@@ -681,11 +749,12 @@ void WallyBebenStream::triggerNote(int ch) {
// setChannelData calls enableChannel internally â do NOT call enableChannel again!
if (inst.loopFlag && loopLen > 2 && loopOff + loopLen <= totalLen) {
- // Looped sample: initial play of full sample, then loop region
+ uint32 initialLen = loopOff + loopLen;
+ // Looped sample: initial play through loop end, then loop region
setChannelData(ch,
sampleData, // initial data pointer
sampleData + loopOff, // loop start pointer
- totalLen, // initial length in bytes
+ initialLen, // initial length in bytes
loopLen); // loop length in bytes
} else {
// One-shot: play once, then 1-byte silence loop
@@ -731,6 +800,9 @@ void WallyBebenStream::triggerNote(int ch) {
void WallyBebenStream::processEffects(int ch) {
ChannelState &c = _channels[ch];
+ if (c.pendingNoteOn)
+ return;
+
if (c.effectMode == 0) {
c.outputPeriod = c.basePeriod;
return;
@@ -761,16 +833,37 @@ void WallyBebenStream::processEffects(int ch) {
return;
}
+ // Dedicated $7B effect: cycle between base note and current note.
+ if (c.effect7BActive) {
+ int rate = c.effect7BRate;
+ if (rate <= 0)
+ rate = 1;
+
+ c.effect7BCounter++;
+ if (c.effect7BCounter >= rate) {
+ c.effect7BCounter = 0;
+ c.effect7BUseBase = !c.effect7BUseBase;
+ }
+
+ int note = c.effect7BUseBase ? c.effect7BBaseNote : (c.note + c.transpose);
+ if (note < 1)
+ note = 1;
+ if (note > 47)
+ note = 47;
+ c.outputPeriod = getPeriod(note);
+ return;
+ }
+
// Arpeggio effect (effectMode 1 or 2)
- if (_arpeggioTableLen > 0) {
+ if (c.arpeggioTableLen > 0) {
int note = c.note + c.transpose;
- int offset = _arpeggioTable[c.arpeggioPos % _arpeggioTableLen];
+ int offset = c.arpeggioTable[c.arpeggioPos % c.arpeggioTableLen];
note += offset;
if (note < 1) note = 1;
if (note > 47) note = 47;
c.outputPeriod = getPeriod(note);
c.arpeggioPos++;
- if (c.arpeggioPos >= _arpeggioTableLen)
+ if (c.arpeggioPos >= c.arpeggioTableLen)
c.arpeggioPos = 0;
} else {
c.outputPeriod = c.basePeriod;
@@ -784,7 +877,7 @@ void WallyBebenStream::processEffects(int ch) {
// Envelope table bytes (per entry, 8 bytes at TEXT+$D0A):
// byte 0 (attackLevel): initial volume on note-on
// byte 1 (decayTarget): target volume to fade toward and hold
-// byte 2 (sustainLevel): fade rate (0 = instant jump to target, >0 = per-tick)
+// byte 2 (sustainLevel): sustain volume while note is active
// byte 3 (releaseRate): volume decrease per tick on note-off
// byte 4 (modDepth): modulation depth
// byte 5 (vibratoWave): vibrato waveform selector
@@ -796,28 +889,22 @@ void WallyBebenStream::processEnvelope(int ch) {
ChannelState &c = _channels[ch];
switch (c.envelopePhase) {
- case 0: // Attack â volume already set to attackLevel in triggerNote
- if (c.sustainLevel == 0) {
- // Instant transition to hold level
- c.volume = c.decayTarget;
- c.envelopePhase = 2; // Skip fade, go straight to hold
- } else {
- // Begin gradual fade toward target
- c.envelopePhase = 1;
- }
+ case 0: // Attack â start from attack level, then enter decay.
+ c.volume = MIN(c.attackLevel, (byte)64);
+ c.envelopePhase = 1;
break;
- case 1: // Fade â move volume toward hold level (decayTarget)
- if (c.volume < c.decayTarget) {
- c.volume++;
- } else if (c.volume > c.decayTarget) {
+ case 1: // Decay â decrease toward decay target.
+ if (c.volume > c.decayTarget) {
c.volume--;
} else {
- c.envelopePhase = 2; // Reached target
+ c.volume = c.sustainLevel;
+ c.envelopePhase = 2;
}
break;
- case 2: // Hold â maintain volume until note-off
+ case 2: // Sustain â hold sustain level until note-off.
+ c.volume = c.sustainLevel;
break;
case 3: // Release â decrease volume on note-off
@@ -842,20 +929,10 @@ void WallyBebenStream::processEnvelope(int ch) {
// Arpeggio table builder â unpacks a bitmask into interval offsets
// ---------------------------------------------------------------------------
-void WallyBebenStream::buildArpeggioTable(byte mask) {
- _arpeggioTableLen = 0;
-
- // Always include 0 (base note) as first entry
- _arpeggioTable[_arpeggioTableLen++] = 0;
-
- for (int i = 0; i < 8 && _arpeggioTableLen < 16; i++) {
- if (mask & (1 << i)) {
- _arpeggioTable[_arpeggioTableLen++] = _arpeggioIntervals[i];
- }
- }
-
- if (_arpeggioTableLen <= 1)
- _arpeggioTableLen = 0; // No arpeggio if only base note
+void WallyBebenStream::buildArpeggioTable(int ch, byte mask) {
+ ChannelState &c = _channels[ch];
+ c.arpeggioTableLen = WBCommon::buildArpeggioTable(_arpeggioIntervals, mask, c.arpeggioTable, 16, true);
+ c.arpeggioPos = 0;
}
// ---------------------------------------------------------------------------
@@ -879,9 +956,11 @@ void WallyBebenStream::interrupt() {
// Sequencer step: when tick counter reaches 0
if (_tickCounter == 0) {
- for (int ch = 0; ch < 4; ch++) {
+ for (int ch = 3; ch >= 0; ch--) {
if (!_channels[ch].active)
continue;
+ if (_channels[ch].pendingNoteOn)
+ continue;
if (_channels[ch].durationCounter > 0) {
_channels[ch].durationCounter--;
@@ -898,8 +977,41 @@ void WallyBebenStream::interrupt() {
}
}
+ // Apply pending song command ($DD xx) after parser step.
+ if (_pendingSongCommand != -1) {
+ byte command = (byte)_pendingSongCommand;
+ _pendingSongCommand = -1;
+
+ if (command == 0) {
+ _musicActive = false;
+ for (int ch = 0; ch < NUM_VOICES; ch++)
+ clearVoice(ch);
+ return;
+ }
+ if (command >= 1 && command <= 2) {
+ startSong(command);
+ return;
+ }
+ }
+
+ // Delay-before-note handling ($7A): countdown in frames, then trigger note-on.
+ for (int ch = 3; ch >= 0; ch--) {
+ ChannelState &c = _channels[ch];
+ if (!c.active || !c.pendingNoteOn)
+ continue;
+
+ if (c.delayCounter > 0)
+ c.delayCounter--;
+
+ if (c.delayCounter == 0) {
+ c.pendingNoteOn = false;
+ c.durationCounter = c.duration;
+ triggerNote(ch);
+ }
+ }
+
// Every frame: process effects and envelope, update Paula
- for (int ch = 0; ch < 4; ch++) {
+ for (int ch = 3; ch >= 0; ch--) {
if (!_channels[ch].active)
continue;
diff --git a/engines/freescape/wb.h b/engines/freescape/wb.h
index 1dcc22be052..f39ee5c6be6 100644
--- a/engines/freescape/wb.h
+++ b/engines/freescape/wb.h
@@ -27,6 +27,38 @@
namespace Freescape {
+namespace WBCommon {
+
+/**
+ * Decode order-list transpose command ($C1-$FE).
+ * Formula used by Wally Beben engines: (cmd + $20) & $FF.
+ */
+int8 decodeOrderTranspose(byte cmd);
+
+/**
+ * Decode speed command ($F0-$FD): low nibble, with 0 coerced to 1.
+ */
+byte decodeTickSpeed(byte cmd);
+
+/**
+ * Decode duration command ($80-$BF): low 6 bits, with 0 coerced to 1.
+ */
+byte decodeDuration(byte cmd);
+
+/**
+ * Expand an arpeggio bitmask using the 8-byte interval lookup table.
+ *
+ * @param intervals Source interval table (8 entries)
+ * @param mask Bitmask selecting entries from intervals
+ * @param outTable Output sequence buffer
+ * @param maxLen Capacity of output buffer
+ * @param includeBase Whether to prepend 0 (base note)
+ * @return Number of valid entries in outTable
+ */
+byte buildArpeggioTable(const byte intervals[8], byte mask, byte *outTable, byte maxLen, bool includeBase);
+
+} // End of namespace WBCommon
+
/**
* Create a music stream for the Wally Beben custom music engine
* used in the Amiga version of Dark Side.
Commit: c229bdfea8296e6d2de1abf5dd195cc28d9b123c
https://github.com/scummvm/scummvm/commit/c229bdfea8296e6d2de1abf5dd195cc28d9b123c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: added sound playback for driller (c64)
Changed paths:
A engines/freescape/games/driller/c64.sfx.cpp
A engines/freescape/games/driller/c64.sfx.h
engines/freescape/freescape.h
engines/freescape/games/driller/c64.cpp
engines/freescape/games/driller/driller.cpp
engines/freescape/games/driller/driller.h
engines/freescape/module.mk
engines/freescape/sound/common.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 0cd8bdeb912..4089b64173d 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -492,6 +492,7 @@ public:
void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
void playSoundCPC(int index, Audio::SoundHandle &handle);
+ virtual void playSoundC64(int index);
virtual void playSoundFx(int index, bool sync);
virtual void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number);
Common::HashMap<uint16, soundFx *> _soundsFx;
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index cca4fd71a0a..a12e2733b4c 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -158,7 +158,35 @@ void DrillerEngine::loadAssetsC64FullGame() {
} else
error("Unknown C64 release");
- _playerSid = new DrillerSIDPlayer();
+ // TODO: Re-enable music player once SFX coexistence is resolved.
+ // The SID emulator enforces a singleton, so only one instance can exist.
+ // _playerSid = new DrillerSIDPlayer();
+ _playerC64Sfx = new DrillerC64SFXPlayer();
+
+ // C64 SFX index mapping
+ // Based on analysis of the C64 binary SFX routines
+ _soundIndexShoot = 2; // SFX #2 - Dual-voice noise sweep (explosion/drilling)
+ _soundIndexCollide = 3; // SFX #3 - Noise pitch slide (collision)
+ _soundIndexStepUp = 5; // SFX #5 - Pulse slide up
+ _soundIndexStepDown = 4; // SFX #4 - Pulse slide down
+ _soundIndexFall = 11; // SFX #11 - Triangle slide down fast (falling)
+ _soundIndexStart = 8; // SFX #8 - Triangle slide up (teleporter/energy)
+ _soundIndexMenu = 6; // SFX #6 - Triangle blip
+ _soundIndexAreaChange = 8; // SFX #8 - Triangle slide up
+ _soundIndexHit = 7; // SFX #7 - Dual noise burst
+ _soundIndexNoShield = 9; // SFX #9 - Dual slide noise (damage)
+ _soundIndexNoEnergy = 9; // SFX #9 - Dual slide noise (damage)
+ _soundIndexFallen = 18; // SFX #18 - Major explosion
+ _soundIndexTimeout = 10; // SFX #10 - Programmed noise bursts (alarm)
+ _soundIndexForceEndGame = 18; // SFX #18 - Major explosion
+ _soundIndexCrushed = 18; // SFX #18 - Major explosion
+ _soundIndexMissionComplete = 14; // SFX #14 - 3-step chord
+}
+
+void DrillerEngine::playSoundC64(int index) {
+ debugC(1, kFreescapeDebugMedia, "Playing C64 SFX %d", index);
+ if (_playerC64Sfx)
+ _playerC64Sfx->playSfx(index);
}
diff --git a/engines/freescape/games/driller/c64.sfx.cpp b/engines/freescape/games/driller/c64.sfx.cpp
new file mode 100644
index 00000000000..2a6cf712a0c
--- /dev/null
+++ b/engines/freescape/games/driller/c64.sfx.cpp
@@ -0,0 +1,687 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/games/driller/c64.sfx.h"
+#include "freescape/freescape.h"
+
+#include "common/debug.h"
+#include "common/textconsole.h"
+
+namespace Freescape {
+
+DrillerC64SFXPlayer::DrillerC64SFXPlayer()
+ : _sid(nullptr),
+ _v1Counter(0xFF), _v1FreqLo(0), _v1FreqHi(0),
+ _v1DeltaLo(0), _v1DeltaHi(0), _v1TickCtr(0), _v1TickReload(0),
+ _v3Counter(0xFF), _v3FreqLo(0), _v3FreqHi(0),
+ _v3DeltaLo(0), _v3DeltaHi(0),
+ _noiseTimer(0), _noiseCounter(0), _noiseReload(0), _noiseDec(0),
+ _sfxPhase(0), _sfxPhaseTimer(0), _sfxActiveIndex(0) {
+ initSID();
+}
+
+DrillerC64SFXPlayer::~DrillerC64SFXPlayer() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ }
+}
+
+void DrillerC64SFXPlayer::initSID() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ }
+
+ _sid = SID::Config::create(SID::Config::kSidPAL);
+ if (!_sid || !_sid->init())
+ error("Failed to initialise SID emulator for SFX");
+
+ for (int i = 0; i < 0x18; i++)
+ sidWrite(i, 0);
+ sidWrite(kSIDVolume, 0x0F);
+
+ _sid->start(new Common::Functor0Mem<void, DrillerC64SFXPlayer>(this, &DrillerC64SFXPlayer::onTimer), 50);
+}
+
+void DrillerC64SFXPlayer::sidWrite(int reg, uint8 data) {
+ if (_sid) {
+ debugC(4, kFreescapeDebugMedia, "SFX SID Write: Reg $%02X = $%02X", reg, data);
+ _sid->writeReg(reg, data);
+ }
+}
+
+void DrillerC64SFXPlayer::onTimer() {
+ sfxTick();
+}
+
+bool DrillerC64SFXPlayer::isSfxActive() const {
+ return (_v1Counter != 0xFF) || (_v3Counter != 0xFF) || (_noiseTimer != 0) || (_sfxPhase != 0);
+}
+
+void DrillerC64SFXPlayer::stopAllSfx() {
+ _v1Counter = 0xFF;
+ _v3Counter = 0xFF;
+ _noiseTimer = 0;
+ _sfxPhase = 0;
+ silenceAllVoices();
+}
+
+void DrillerC64SFXPlayer::silenceAllVoices() {
+ for (int i = 0; i <= 0x17; i++)
+ sidWrite(i, 0);
+ sidWrite(kSIDVolume, 0x0F);
+}
+
+// --- Noise burst subroutine ($C818) ---
+void DrillerC64SFXPlayer::noiseBurst(uint8 param) {
+ sidWrite(kSIDV1FreqLo, 0);
+ sidWrite(kSIDV1FreqHi, param);
+ sidWrite(kSIDV1AD, 0x0A);
+ sidWrite(kSIDV1SR, 0x09);
+ sidWrite(kSIDV1Ctrl, 0x81); // Noise + gate
+}
+
+// --- Tick handler ($C96F) - called every frame (50Hz) ---
+
+void DrillerC64SFXPlayer::sfxTick() {
+ tickNoiseBurst();
+ tickVoice1Slide();
+ tickVoice3Slide();
+ tickPhase();
+}
+
+void DrillerC64SFXPlayer::tickNoiseBurst() {
+ if (_noiseTimer == 0)
+ return;
+
+ _noiseCounter--;
+ if (_noiseCounter == 0) {
+ noiseBurst(_noiseTimer);
+ _noiseCounter = _noiseReload;
+ _noiseReload--;
+ _noiseDec--;
+ if (_noiseDec == 0) {
+ _noiseTimer = 0;
+ }
+ }
+}
+
+// Tick handler for Voice 1 pitch slide.
+// Matches the original 6502 at $C97F:
+// - Check counter==0 BEFORE decrementing (expired last frame â mark 0xFF)
+// - On counter expiry: do NOT silence - ADSR release handles decay
+// - Write freq to SID THEN decrement counter
+void DrillerC64SFXPlayer::tickVoice1Slide() {
+ if (_v1Counter == 0xFF)
+ return;
+
+ // Counter expired last frame - silence V1.
+ // Matches the $C950 pattern: write 0 to ctrl (no waveform, no gate).
+ // The game is silent between SFX, so the voice must be stopped.
+ if (_v1Counter == 0) {
+ _v1Counter = 0xFF;
+ sidWrite(kSIDV1Ctrl, 0);
+ return;
+ }
+
+ // Tick prescaler: skip frames until prescaler expires
+ if (_v1TickCtr != 0) {
+ _v1TickCtr--;
+ return;
+ }
+ _v1TickCtr = _v1TickReload;
+
+ // 16-bit frequency addition (CLC; ADC)
+ uint16 freq = (_v1FreqHi << 8) | _v1FreqLo;
+ uint16 delta = (_v1DeltaHi << 8) | _v1DeltaLo;
+ freq += delta;
+ _v1FreqLo = freq & 0xFF;
+ _v1FreqHi = (freq >> 8) & 0xFF;
+
+ // Write to SID FIRST
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // THEN decrement counter
+ _v1Counter--;
+}
+
+// Tick handler for Voice 3 pitch slide (no tick prescaler).
+void DrillerC64SFXPlayer::tickVoice3Slide() {
+ if (_v3Counter == 0xFF)
+ return;
+
+ if (_v3Counter == 0) {
+ _v3Counter = 0xFF;
+ return;
+ }
+
+ uint16 freq = (_v3FreqHi << 8) | _v3FreqLo;
+ uint16 delta = (_v3DeltaHi << 8) | _v3DeltaLo;
+ freq += delta;
+ _v3FreqLo = freq & 0xFF;
+ _v3FreqHi = (freq >> 8) & 0xFF;
+
+ sidWrite(kSIDV3FreqLo, _v3FreqLo);
+ sidWrite(kSIDV3FreqHi, _v3FreqHi);
+
+ _v3Counter--;
+}
+
+// Phase state machine for multi-step SFX (#6, #14, #15).
+// Handles gate-off timing and chord/phase transitions that the
+// original implements via busy-wait loops.
+void DrillerC64SFXPlayer::tickPhase() {
+ if (_sfxPhase == 0)
+ return;
+
+ if (_sfxPhaseTimer > 0) {
+ _sfxPhaseTimer--;
+ if (_sfxPhaseTimer > 0)
+ return;
+ }
+
+ // Timer expired - handle phase transition
+ switch (_sfxActiveIndex) {
+ case 6:
+ // SFX #6: gate off after 1 frame
+ if (_sfxPhase == 1) {
+ sidWrite(kSIDV1Ctrl, 0x10); // Triangle, gate off
+ _sfxPhase = 0;
+ }
+ break;
+
+ case 14:
+ // SFX #14: 3-step chord using inline data from $C7AA
+ // V1 freqHi: $22, $22, $39
+ // V2 freqHi: $26, $30, $9A
+ // Timing: $0C, $08, $1E
+ switch (_sfxPhase) {
+ case 1: // Step 1 done â Step 2
+ sidWrite(kSIDV1FreqHi, 0x22);
+ sidWrite(kSIDV2FreqHi, 0x30);
+ _sfxPhase = 2;
+ _sfxPhaseTimer = 8; // $08 frames
+ break;
+ case 2: // Step 2 done â Step 3
+ sidWrite(kSIDV1FreqHi, 0x39);
+ sidWrite(kSIDV2FreqHi, 0x9A);
+ _sfxPhase = 3;
+ _sfxPhaseTimer = 30; // $1E frames
+ break;
+ case 3: // Step 3 done â gate off
+ sidWrite(kSIDV1Ctrl, 0x46); // Pulse+sync, gate off
+ sidWrite(kSIDV2Ctrl, 0x46);
+ _sfxPhase = 0;
+ break;
+ default:
+ _sfxPhase = 0;
+ break;
+ }
+ break;
+
+ case 15:
+ // SFX #15: two-phase pulse effect
+ switch (_sfxPhase) {
+ case 1: // Phase 1 slide done â gate off, short wait
+ sidWrite(kSIDV1Ctrl, 0x40); // Pulse, gate off
+ _sfxPhase = 2;
+ _sfxPhaseTimer = 3; // Approximate busy-wait delay
+ break;
+ case 2: // Wait done â Phase 2: new slide params
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x08;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0x00;
+ _v1Counter = 6;
+ _v1TickCtr = 1;
+ _v1TickReload = 8;
+ sidWrite(kSIDV1FreqLo, 0x00);
+ sidWrite(kSIDV1FreqHi, 0x08);
+ sidWrite(kSIDV1Ctrl, 0x41); // Pulse + gate
+ _sfxPhase = 0; // V1 slide handler takes over
+ break;
+ default:
+ _sfxPhase = 0;
+ break;
+ }
+ break;
+
+ default:
+ _sfxPhase = 0;
+ break;
+ }
+}
+
+// --- SFX dispatch ---
+
+void DrillerC64SFXPlayer::playSfx(int sfxIndex) {
+ debugC(1, kFreescapeDebugMedia, "DrillerC64SFX: Playing SFX %d", sfxIndex);
+
+ // Stop any ongoing SFX state before starting new one
+ _v1Counter = 0xFF;
+ _v3Counter = 0xFF;
+ _noiseTimer = 0;
+ _sfxPhase = 0;
+
+ // Reset all SID registers to a clean state.
+ // Music and SFX are mutually exclusive on C64; during gameplay only
+ // SFX play and the game is silent between them. The SID starts from
+ // a zeroed state (music stopped at game start), and many SFX routines
+ // don't write all registers (e.g. SFX #2 never writes V1 SR). Without
+ // this reset, stale values from a previous SFX (e.g. SR=$F8 from #8)
+ // would cause subsequent SFX to sustain indefinitely.
+ for (int i = 0; i < 0x18; i++)
+ sidWrite(i, 0);
+
+ switch (sfxIndex) {
+ case 2:
+ sfx2();
+ break;
+ case 3:
+ sfx3();
+ break;
+ case 4:
+ sfx4();
+ break;
+ case 5:
+ sfx5();
+ break;
+ case 6:
+ sfx6();
+ break;
+ case 7:
+ sfx7();
+ break;
+ case 8:
+ sfx8();
+ break;
+ case 9:
+ sfx9();
+ break;
+ case 10:
+ sfx10();
+ break;
+ case 11:
+ sfx11();
+ break;
+ case 14:
+ sfx14();
+ break;
+ case 15:
+ sfx15();
+ break;
+ case 16:
+ sfx16();
+ break;
+ case 17:
+ sfx17();
+ break;
+ case 18:
+ sfx18();
+ break;
+ default:
+ debugC(1, kFreescapeDebugMedia, "DrillerC64SFX: Unknown SFX index %d", sfxIndex);
+ break;
+ }
+}
+
+// --- Individual SFX routines ---
+// All verified against C64 binary (driller.bin, PRG load $0400)
+
+// SFX #2 ($C55A) - Dual-Voice Noise Sweep Down
+void DrillerC64SFXPlayer::sfx2() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ // V1: waveform $15 (triangle+ring+gate), AD=$0B
+ sidWrite(kSIDV1AD, 0x0B);
+ sidWrite(kSIDV1Ctrl, 0x15);
+ // V1 slide: start $4800, delta $FF00, 48 frames, no prescaler
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x48;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0xFF;
+ _v1Counter = 48;
+ _v1TickCtr = 0;
+ _v1TickReload = 0;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // V2: waveform $15, AD=$0B, SR=$09, freq=$0600
+ sidWrite(kSIDV2FreqLo, 0x00);
+ sidWrite(kSIDV2FreqHi, 0x06);
+ sidWrite(kSIDV2AD, 0x0B);
+ sidWrite(kSIDV2SR, 0x09);
+ sidWrite(kSIDV2Ctrl, 0x15);
+
+ // V3: freq=$0C00, no delta, 48 frames (ring modulation source)
+ _v3FreqLo = 0x00;
+ _v3FreqHi = 0x0C;
+ _v3DeltaLo = 0x00;
+ _v3DeltaHi = 0x00;
+ _v3Counter = 48;
+ sidWrite(kSIDV3FreqLo, _v3FreqLo);
+ sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #3 ($C5A6) - Noise Pitch Slide
+void DrillerC64SFXPlayer::sfx3() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ // V1: waveform $15, AD=$0F, no prescaler
+ sidWrite(kSIDV1AD, 0x0F);
+ sidWrite(kSIDV1Ctrl, 0x15);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0xAF;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0xF7;
+ _v1Counter = 12;
+ _v1TickCtr = 0;
+ _v1TickReload = 0;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // V3: freq=$2000, no delta, 12 frames
+ _v3FreqLo = 0x00;
+ _v3FreqHi = 0x20;
+ _v3DeltaLo = 0x00;
+ _v3DeltaHi = 0x00;
+ _v3Counter = 12;
+ sidWrite(kSIDV3FreqLo, _v3FreqLo);
+ sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #4 ($C5E3) - Pulse Slide Down
+void DrillerC64SFXPlayer::sfx4() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1PwLo, 0xB4);
+ sidWrite(kSIDV1PwHi, 0x0E);
+ sidWrite(kSIDV1AD, 0x2F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1Ctrl, 0x41);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x14;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0xFD;
+ _v1Counter = 2;
+ _v1TickCtr = 1; // Prescaler: tick every 7 frames
+ _v1TickReload = 6;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #5 ($C62A) - Pulse Slide Up
+void DrillerC64SFXPlayer::sfx5() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1PwLo, 0xB4);
+ sidWrite(kSIDV1PwHi, 0x0E);
+ sidWrite(kSIDV1AD, 0x2F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1Ctrl, 0x41);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x10;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0x03;
+ _v1Counter = 2;
+ _v1TickCtr = 1; // Prescaler: tick every 7 frames
+ _v1TickReload = 6;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #6 ($C681) - Triangle Blip
+// Original: gate on, ~250-cycle busy loop (~0.25ms), gate off.
+// We use the phase system to gate off after 1 frame.
+void DrillerC64SFXPlayer::sfx6() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1AD, 0x1F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1FreqLo, 0x00);
+ sidWrite(kSIDV1FreqHi, 0x77);
+ sidWrite(kSIDV1Ctrl, 0x11); // Triangle + gate on
+
+ _sfxActiveIndex = 6;
+ _sfxPhase = 1;
+ _sfxPhaseTimer = 1;
+}
+
+// SFX #7 ($C6B0) - Dual Noise Burst (one-shot)
+void DrillerC64SFXPlayer::sfx7() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1FreqLo, 0x00);
+ sidWrite(kSIDV1FreqHi, 0x01);
+ sidWrite(kSIDV1AD, 0x0A);
+ sidWrite(kSIDV1SR, 0x09);
+ sidWrite(kSIDV1Ctrl, 0x81);
+
+ sidWrite(kSIDV2FreqLo, 0x00);
+ sidWrite(kSIDV2FreqHi, 0x06);
+ sidWrite(kSIDV2AD, 0x0A);
+ sidWrite(kSIDV2SR, 0x09);
+ sidWrite(kSIDV2Ctrl, 0x81);
+}
+
+// SFX #8 ($C6DB) - Triangle Slide Up
+void DrillerC64SFXPlayer::sfx8() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1AD, 0x1F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1Ctrl, 0x11);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x1E;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0x03;
+ _v1Counter = 35;
+ _v1TickCtr = 0;
+ _v1TickReload = 0;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #9 ($C70F) - Dual Slide (V1+V3)
+void DrillerC64SFXPlayer::sfx9() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1AD, 0x0F);
+ sidWrite(kSIDV1Ctrl, 0x15);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x60;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0xF7;
+ _v1Counter = 25;
+ _v1TickCtr = 0;
+ _v1TickReload = 0;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // V3: freq=$5000, delta=$0000 (no slide), 25 frames
+ _v3FreqLo = 0x00;
+ _v3FreqHi = 0x50;
+ _v3DeltaLo = 0x00;
+ _v3DeltaHi = 0x00; // Fixed: was 0x07
+ _v3Counter = 25;
+ sidWrite(kSIDV3FreqLo, _v3FreqLo);
+ sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+// SFX #10 ($C74C) - Programmed Noise Bursts
+void DrillerC64SFXPlayer::sfx10() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ _noiseTimer = 13;
+ _noiseReload = 6;
+ _noiseCounter = 6; // Fixed: was 1 (first burst after 6 frames, not immediately)
+ _noiseDec = 5;
+}
+
+// SFX #11 ($C766) - Triangle Slide Down Fast
+void DrillerC64SFXPlayer::sfx11() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1AD, 0x1F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1Ctrl, 0x11);
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x46;
+ _v1DeltaLo = 0x00;
+ _v1DeltaHi = 0xFE;
+ _v1Counter = 30;
+ _v1TickCtr = 0;
+ _v1TickReload = 0;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+}
+
+// SFX #14 ($C7B3) - 3-Step Chord (V1+V2)
+// Inline data at $C7AA: V1 freqHi {$22,$22,$39}, V2 freqHi {$26,$30,$9A},
+// timing {$0C,$08,$1E}. Original blocks game loop via busy-wait.
+void DrillerC64SFXPlayer::sfx14() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ sidWrite(kSIDV1PwLo, 0xB4);
+ sidWrite(kSIDV1PwHi, 0x0E);
+ sidWrite(kSIDV1AD, 0x09);
+ sidWrite(kSIDV1SR, 0x00);
+
+ sidWrite(kSIDV2PwLo, 0xB4);
+ sidWrite(kSIDV2PwHi, 0x0E);
+ sidWrite(kSIDV2AD, 0x09);
+ sidWrite(kSIDV2SR, 0x00);
+
+ // Step 1: V1=$2200, V2=$2600
+ sidWrite(kSIDV1FreqLo, 0x00);
+ sidWrite(kSIDV1FreqHi, 0x22);
+ sidWrite(kSIDV2FreqLo, 0x00);
+ sidWrite(kSIDV2FreqHi, 0x26);
+ sidWrite(kSIDV1Ctrl, 0x47); // Pulse + sync + gate
+ sidWrite(kSIDV2Ctrl, 0x47);
+
+ _sfxActiveIndex = 14;
+ _sfxPhase = 1;
+ _sfxPhaseTimer = 12; // $0C frames
+}
+
+// SFX #15 ($C810) - Two-Phase Pulse Effect
+void DrillerC64SFXPlayer::sfx15() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ // Phase 1: pulse slide
+ sidWrite(kSIDV1PwLo, 0xB4);
+ sidWrite(kSIDV1PwHi, 0x32);
+ sidWrite(kSIDV1AD, 0x2F);
+ sidWrite(kSIDV1SR, 0xF8);
+ sidWrite(kSIDV1Ctrl, 0x41);
+
+ _v1FreqLo = 0x00;
+ _v1FreqHi = 0x0C;
+ _v1DeltaLo = 0x03; // Fixed: was 0x00 (delta is $0003, not $0300)
+ _v1DeltaHi = 0x00; // Fixed: was 0x03
+ _v1Counter = 11;
+ _v1TickCtr = 1; // Fixed: was 0
+ _v1TickReload = 1; // Fixed: was 0 (slide every 2 frames)
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // Phase system: 11 iterations à 2 frames/iteration = 22 frames
+ _sfxActiveIndex = 15;
+ _sfxPhase = 1;
+ _sfxPhaseTimer = 23; // Wait for slide to finish + 1 frame margin
+}
+
+// SFX #16 ($C896) - Filtered Effect (one-shot, no slide)
+void DrillerC64SFXPlayer::sfx16() {
+ // V1: waveform $15, freq=$080A, AD=$0A, SR=$09
+ sidWrite(kSIDV1FreqLo, 0x0A);
+ sidWrite(kSIDV1FreqHi, 0x08);
+ sidWrite(kSIDV1AD, 0x0A);
+ sidWrite(kSIDV1SR, 0x09);
+ sidWrite(kSIDV1Ctrl, 0x15);
+
+ // V3: freq=$010A (ring modulation source)
+ sidWrite(kSIDV3FreqLo, 0x0A);
+ sidWrite(kSIDV3FreqHi, 0x01);
+
+ // Filter: cutoff=$780, filter V1, LP enable
+ sidWrite(kSIDFilterLo, 0x00);
+ sidWrite(kSIDFilterHi, 0x78);
+ sidWrite(kSIDFilterCtrl, 0x01);
+ sidWrite(kSIDVolume, 0x1F); // Vol=$0F + LP filter enable bit
+}
+
+// SFX #17 ($C8C8) - Freq Echo (one-shot, no slide)
+void DrillerC64SFXPlayer::sfx17() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ // V1: waveform $15, freq=$001D, AD=$DD
+ sidWrite(kSIDV1FreqLo, 0x1D);
+ sidWrite(kSIDV1FreqHi, 0x00);
+ sidWrite(kSIDV1AD, 0xDD);
+ sidWrite(kSIDV1Ctrl, 0x15);
+
+ // V3: original reads V1.FreqHi ($D401) â V3.FreqLo, V3.FreqHi=$02
+ // Since V1.FreqHi is $00, V3 freq = $0200
+ sidWrite(kSIDV3FreqLo, 0x00);
+ sidWrite(kSIDV3FreqHi, 0x02);
+}
+
+// SFX #18 ($C8EF) - Dual Noise with Modulation (major explosion)
+void DrillerC64SFXPlayer::sfx18() {
+ sidWrite(kSIDVolume, 0x0F);
+
+ // V1: waveform $15, AD=$0B
+ sidWrite(kSIDV1AD, 0x0B);
+ sidWrite(kSIDV1Ctrl, 0x15);
+
+ // V2: waveform $15, AD=$0B, SR=$09, freq=$0600
+ sidWrite(kSIDV2FreqLo, 0x00);
+ sidWrite(kSIDV2FreqHi, 0x06);
+ sidWrite(kSIDV2AD, 0x0B);
+ sidWrite(kSIDV2SR, 0x09);
+ sidWrite(kSIDV2Ctrl, 0x15);
+
+ // V1 slide: start $C8C8, delta $00F0, 10 frames, prescaler=6
+ _v1FreqLo = 0xC8;
+ _v1FreqHi = 0xC8;
+ _v1DeltaLo = 0xF0; // Fixed: was 0x00 (delta is $00F0, not $F000)
+ _v1DeltaHi = 0x00; // Fixed: was 0xF0
+ _v1Counter = 10;
+ _v1TickCtr = 1; // Fixed: was 0
+ _v1TickReload = 6;
+ sidWrite(kSIDV1FreqLo, _v1FreqLo);
+ sidWrite(kSIDV1FreqHi, _v1FreqHi);
+
+ // V3: freq=$07C8 (ring modulation source), no delta, 10 frames
+ _v3FreqLo = 0xC8;
+ _v3FreqHi = 0x07; // Fixed: was 0x00
+ _v3DeltaLo = 0x00;
+ _v3DeltaHi = 0x00;
+ _v3Counter = 10;
+ sidWrite(kSIDV3FreqLo, _v3FreqLo);
+ sidWrite(kSIDV3FreqHi, _v3FreqHi);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/driller/c64.sfx.h b/engines/freescape/games/driller/c64.sfx.h
new file mode 100644
index 00000000000..325cb4b3a7f
--- /dev/null
+++ b/engines/freescape/games/driller/c64.sfx.h
@@ -0,0 +1,139 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DRILLER_C64_SFX_H
+#define FREESCAPE_DRILLER_C64_SFX_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// SID register offsets
+enum SIDRegs {
+ kSIDV1FreqLo = 0x00,
+ kSIDV1FreqHi = 0x01,
+ kSIDV1PwLo = 0x02,
+ kSIDV1PwHi = 0x03,
+ kSIDV1Ctrl = 0x04,
+ kSIDV1AD = 0x05,
+ kSIDV1SR = 0x06,
+
+ kSIDV2FreqLo = 0x07,
+ kSIDV2FreqHi = 0x08,
+ kSIDV2PwLo = 0x09,
+ kSIDV2PwHi = 0x0A,
+ kSIDV2Ctrl = 0x0B,
+ kSIDV2AD = 0x0C,
+ kSIDV2SR = 0x0D,
+
+ kSIDV3FreqLo = 0x0E,
+ kSIDV3FreqHi = 0x0F,
+ kSIDV3PwLo = 0x10,
+ kSIDV3PwHi = 0x11,
+ kSIDV3Ctrl = 0x12,
+ kSIDV3AD = 0x13,
+ kSIDV3SR = 0x14,
+
+ kSIDFilterLo = 0x15,
+ kSIDFilterHi = 0x16,
+ kSIDFilterCtrl = 0x17,
+ kSIDVolume = 0x18
+};
+
+class DrillerC64SFXPlayer {
+public:
+ DrillerC64SFXPlayer();
+ ~DrillerC64SFXPlayer();
+
+ void playSfx(int sfxIndex);
+ void sfxTick(); // Called every frame (50Hz) from onTimer
+ void stopAllSfx();
+
+ bool isSfxActive() const;
+
+private:
+ SID::SID *_sid;
+
+ void sidWrite(int reg, uint8 data);
+ void initSID();
+
+ // Voice 1 pitch slide state ($CC5B-$CC61)
+ uint8 _v1Counter; // 0xFF=inactive, 0=expired (marked 0xFF next tick)
+ uint8 _v1FreqLo;
+ uint8 _v1FreqHi;
+ uint8 _v1DeltaLo;
+ uint8 _v1DeltaHi;
+ uint8 _v1TickCtr;
+ uint8 _v1TickReload;
+
+ // Voice 3 pitch slide state ($CC62-$CC66)
+ uint8 _v3Counter;
+ uint8 _v3FreqLo;
+ uint8 _v3FreqHi;
+ uint8 _v3DeltaLo;
+ uint8 _v3DeltaHi;
+
+ // Noise burst timer ($CC67-$CC6A)
+ uint8 _noiseTimer;
+ uint8 _noiseCounter;
+ uint8 _noiseReload;
+ uint8 _noiseDec;
+
+ // Phase state machine for multi-step SFX (#6, #14, #15)
+ uint8 _sfxPhase; // 0=inactive
+ uint8 _sfxPhaseTimer; // frames until next phase transition
+ uint8 _sfxActiveIndex; // which SFX owns the phase state
+
+ // Tick handlers
+ void tickVoice1Slide();
+ void tickVoice3Slide();
+ void tickNoiseBurst();
+ void tickPhase();
+
+ // Helper to silence all voices
+ void silenceAllVoices();
+
+ // Noise burst subroutine ($C818)
+ void noiseBurst(uint8 param);
+
+ // Individual SFX routines
+ void sfx2(); // Dual-voice noise sweep down
+ void sfx3(); // Noise pitch slide
+ void sfx4(); // Pulse slide down
+ void sfx5(); // Pulse slide up
+ void sfx6(); // Triangle blip
+ void sfx7(); // Dual noise burst
+ void sfx8(); // Triangle slide up
+ void sfx9(); // Dual slide (V1+V3) noise
+ void sfx10(); // Programmed noise bursts
+ void sfx11(); // Triangle slide down (fast)
+ void sfx14(); // 3-step chord (V1+V2)
+ void sfx15(); // Two-phase pulse effect
+ void sfx16(); // Filtered effect (V1+V3)
+ void sfx17(); // Freq echo (V1->V3)
+ void sfx18(); // Dual noise with modulation
+
+ void onTimer();
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DRILLER_C64_SFX_H
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 6730a9afc3a..3a1fa4de6f8 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -97,10 +97,12 @@ DrillerEngine::DrillerEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_borderExtra = nullptr;
_borderExtraTexture = nullptr;
_playerSid = nullptr;
+ _playerC64Sfx = nullptr;
}
DrillerEngine::~DrillerEngine() {
delete _playerSid;
+ delete _playerC64Sfx;
delete _drillBase;
if (_borderExtra) {
@@ -262,9 +264,11 @@ void DrillerEngine::gotoArea(uint16 areaID, int entranceID) {
_gameStateVars[0x1f] = 0;
if (areaID == _startArea && entranceID == _startEntrance) {
- if (isC64())
- _playerSid->startMusic();
- else {
+ if (isC64()) {
+ // TODO: Re-enable music once SFX coexistence is resolved
+ // _playerSid->startMusic();
+ playSound(_soundIndexStart, true, _soundFxHandle);
+ } else {
playSound(_soundIndexStart, true, _soundFxHandle);
// Start playing music, if any, in any supported format
playMusic("Matt Gray - The Best Of Reformation - 07 Driller Theme");
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index e57f131fddc..167ba9d2bcb 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -23,6 +23,7 @@
#include "audio/mixer.h"
#include "engines/freescape/games/driller/c64.music.h"
+#include "engines/freescape/games/driller/c64.sfx.h"
namespace Freescape {
@@ -45,6 +46,9 @@ public:
bool _useAutomaticDrilling;
DrillerSIDPlayer *_playerSid;
+ DrillerC64SFXPlayer *_playerC64Sfx;
+
+ void playSoundC64(int index) override;
// Only used for Amiga and Atari ST
Font _fontSmall;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 413a85bfdf8..321ad4e2140 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -25,6 +25,7 @@ MODULE_OBJS := \
games/driller/atari.o \
games/driller/c64.o \
games/driller/c64.music.o \
+ games/driller/c64.sfx.o \
games/driller/cpc.o \
games/driller/dos.o \
games/driller/driller.o \
diff --git a/engines/freescape/sound/common.cpp b/engines/freescape/sound/common.cpp
index 8c59906ca41..5d69e261363 100644
--- a/engines/freescape/sound/common.cpp
+++ b/engines/freescape/sound/common.cpp
@@ -40,6 +40,11 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
_syncSound = sync;
debugC(1, kFreescapeDebugMedia, "Playing sound %d with sync: %d", index, sync);
+ if (isC64()) {
+ playSoundC64(index);
+ return;
+ }
+
if (isAmiga() || isAtariST()) {
playSoundFx(index, sync);
return;
@@ -67,6 +72,10 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
playWav(filename);
_syncSound = sync;
}
+void FreescapeEngine::playSoundC64(int index) {
+ debugC(1, kFreescapeDebugMedia, "C64 sound %d not implemented for this engine", index);
+}
+
void FreescapeEngine::playWav(const Common::Path &filename) {
Common::SeekableReadStream *s = _dataBundle->createReadStreamForMember(filename);
Commit: 3233011b5c52e8c87658f7eb4443c861cdd045e6
https://github.com/scummvm/scummvm/commit/3233011b5c52e8c87658f7eb4443c861cdd045e6
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: first pass on the music/sound for dark side (c64)
Changed paths:
A engines/freescape/games/dark/c64.music.cpp
A engines/freescape/games/dark/c64.music.h
A engines/freescape/games/dark/c64.sfx.cpp
A engines/freescape/games/dark/c64.sfx.h
engines/freescape/games/dark/c64.cpp
engines/freescape/games/dark/dark.cpp
engines/freescape/games/dark/dark.h
engines/freescape/module.mk
diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index 50fc9e4db9f..bd5f973747a 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -162,8 +162,18 @@ void DarkEngine::loadAssetsC64FullGame() {
colorFile2.open("darkside.c64.title.colors2");
_title = loadAndConvertDoodleImage(&file, &colorFile1, &colorFile2, (byte *)&kC64Palette);
+
+ // TODO: SFX and music both need a SID instance, but only one can be active at a time.
+ // Disable SFX for now so the music player can work.
+ //_playerC64Sfx = new DarkSideC64SFXPlayer();
+ _playerC64Music = new DarkSideC64MusicPlayer();
}
+void DarkEngine::playSoundC64(int index) {
+ debugC(1, kFreescapeDebugMedia, "Playing Dark Side C64 SFX %d", index);
+ if (_playerC64Sfx)
+ _playerC64Sfx->playSfx(index);
+}
void DarkEngine::drawC64UI(Graphics::Surface *surface) {
uint8 r, g, b;
diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
new file mode 100644
index 00000000000..9310b2d80a2
--- /dev/null
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -0,0 +1,752 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/debug.h"
+#include "common/endian.h"
+#include "common/textconsole.h"
+#include "freescape/games/dark/c64.music.h"
+
+namespace Freescape {
+
+// ============================================================
+// Data tables extracted from darkside.prg (load address $0400)
+// ============================================================
+
+// Frequency table: hi bytes at $0F38, lo bytes at $0F97 (95 entries each)
+// Index 0 = rest (freq 0), indices 1-94 = notes spanning 8 octaves
+static const uint8 kFreqHi[96] = {
+ 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x03, 0x03, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x06, 0x06,
+ 0x06, 0x07, 0x07, 0x08, 0x08, 0x09, 0x09, 0x0A, 0x0A, 0x0B, 0x0C, 0x0C, 0x0D, 0x0E, 0x0F, 0x10,
+ 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x18, 0x19, 0x1B, 0x1C, 0x1E, 0x20, 0x22, 0x24, 0x26, 0x28,
+ 0x2B, 0x2D, 0x30, 0x33, 0x36, 0x39, 0x3D, 0x40, 0x44, 0x48, 0x4C, 0x51, 0x56, 0x5B, 0x60, 0x66,
+ 0x6C, 0x73, 0x7A, 0x81, 0x89, 0x91, 0x99, 0xA3, 0xAC, 0xB7, 0xC1, 0xCD, 0xD9, 0xE6, 0xF4,
+ 0x00 // safety padding
+};
+
+static const uint8 kFreqLo[96] = {
+ 0x00, 0x23, 0x34, 0x46, 0x5A, 0x6E, 0x84, 0x9B, 0xB3, 0xCD, 0xE9, 0x06, 0x25, 0x45, 0x68, 0x8C,
+ 0xB3, 0xDC, 0x08, 0x36, 0x67, 0x9B, 0xD2, 0x0C, 0x49, 0x8B, 0xD0, 0x19, 0x67, 0xB9, 0x10, 0x6C,
+ 0xCE, 0x35, 0xA3, 0x17, 0x93, 0x15, 0x9F, 0x3C, 0xCD, 0x72, 0x20, 0xD8, 0x9C, 0x6B, 0x46, 0x2F,
+ 0x25, 0x2A, 0x3F, 0x64, 0x9A, 0xE3, 0x3F, 0xB1, 0x38, 0xD6, 0x8D, 0x5E, 0x4B, 0x55, 0x7E, 0xC8,
+ 0x34, 0xC6, 0x7F, 0x61, 0x6F, 0xAC, 0x7E, 0xBC, 0x95, 0xA9, 0xFC, 0xA1, 0x69, 0x8C, 0xFE, 0xC2,
+ 0xDF, 0x58, 0x34, 0x78, 0x2B, 0x53, 0xF7, 0x1F, 0xD2, 0x19, 0xFC, 0x85, 0xBD, 0xB0, 0x67,
+ 0x00 // safety padding
+};
+
+// Instrument table at $1010 (18 instruments x 8 bytes)
+// Bytes: ctrl, AD, SR, envCtl, vibrato, pwMod, autoFx, flags
+// Instruments 16-17 are stored between $1090-$109F (past the nominal 16-entry table)
+static const uint8 kInstruments[18 * 8] = {
+ 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0: Pulse (silence)
+ 0x41, 0x0C, 0xDC, 0x02, 0x00, 0x33, 0x00, 0x08, // 1: Pulse+G
+ 0x41, 0xDC, 0xDD, 0x00, 0x00, 0x15, 0x00, 0x80, // 2: Pulse+G
+ 0x41, 0x1A, 0x04, 0x00, 0x00, 0x05, 0x00, 0x04, // 3: Pulse+G
+ 0x11, 0x32, 0x3A, 0x00, 0x00, 0x11, 0x00, 0x80, // 4: Triangle+G
+ 0x41, 0x0B, 0xAC, 0x20, 0x00, 0x01, 0x00, 0x04, // 5: Pulse+G
+ 0x41, 0x0B, 0x6C, 0x40, 0x33, 0x10, 0x00, 0x00, // 6: Pulse+G
+ 0x45, 0x0A, 0x8B, 0x88, 0x46, 0x13, 0x00, 0x80, // 7: Pulse+Ring+G
+ 0x41, 0x16, 0x00, 0x60, 0x00, 0x17, 0x00, 0x80, // 8: Pulse+G
+ 0x21, 0x08, 0x17, 0x00, 0x00, 0x00, 0x80, 0x00, // 9: Sawtooth+G
+ 0x15, 0xCC, 0xDC, 0x40, 0x00, 0x11, 0x00, 0x80, // 10: Tri+Ring+G
+ 0x81, 0x42, 0x38, 0x10, 0x00, 0x01, 0x03, 0x02, // 11: Noise+G
+ 0x41, 0x8B, 0xAF, 0x80, 0x00, 0x00, 0x00, 0x04, // 12: Pulse+G
+ 0x41, 0x1B, 0x4A, 0x14, 0x53, 0x02, 0x00, 0x00, // 13: Pulse+G
+ 0x81, 0x04, 0x11, 0x00, 0x00, 0x00, 0x00, 0x00, // 14: Noise+G
+ 0x41, 0x0B, 0x6C, 0x62, 0x62, 0x42, 0x00, 0x00, // 15: Pulse+G
+ 0x41, 0x0C, 0x00, 0x88, 0x11, 0x00, 0x00, 0x01, // 16: Pulse+G (percussion, envelope seq)
+ 0x81, 0x0B, 0x20, 0x66, 0x00, 0x00, 0x00, 0x01, // 17: Noise+G (percussion, envelope seq)
+};
+
+// Envelope volume tables ($154B and $156B, 16 entries each)
+static const uint8 kEnvVolume[2][16] = {
+ { 0xFA, 0x01, 0xFF, 0x20, 0x0A, 0x12, 0x04, 0x16, 0x0E, 0x0C, 0x0A, 0x08, 0x06, 0x04, 0x02, 0x00 },
+ { 0x10, 0x0A, 0x06, 0x00, 0x04, 0x00, 0x00, 0x00, 0x10, 0x10, 0x10, 0x10, 0x00, 0x00, 0x00, 0x00 },
+};
+
+// Envelope waveform control tables ($155B and $157B, 16 entries each)
+static const uint8 kEnvControl[2][16] = {
+ { 0x81, 0x41, 0x81, 0x80, 0x80, 0x80, 0x80, 0x80, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
+ { 0x81, 0x41, 0x81, 0x80, 0x40, 0x40, 0x40, 0x40, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10 },
+};
+
+// Arpeggio semitone intervals ($1591)
+static const uint8 kArpIntervals[8] = { 3, 4, 5, 7, 8, 9, 10, 12 };
+
+// SID register base offset per channel
+static const int kSIDOffset[3] = { 0, 7, 14 };
+
+// ---- Pattern data (29 patterns from $122D-$1542) ----
+
+static const uint8 kPattern00[] = { 0xC0, 0xBF, 0x00, 0xFF };
+static const uint8 kPattern01[] = { 0xF5, 0xC2, 0xBF, 0x10, 0x10, 0xFF };
+static const uint8 kPattern02[] = { 0xC1, 0x8F, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0xFF };
+static const uint8 kPattern03[] = { 0xC4, 0x80, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0x34, 0x2F, 0x37, 0x36, 0x2F, 0x37, 0x34, 0x2F, 0xFF };
+static const uint8 kPattern04[] = { 0xC6, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xFF };
+static const uint8 kPattern05[] = { 0x81, 0x34, 0x36, 0x34, 0x2F, 0x32, 0x30, 0x2B, 0x91, 0x2F, 0xFF };
+static const uint8 kPattern06[] = { 0x83, 0x34, 0x37, 0x34, 0x8F, 0x3C, 0x83, 0x3B, 0x8F, 0x3A, 0x8F, 0x36, 0xFF };
+static const uint8 kPattern07[] = { 0xC1, 0x81, 0x10, 0x10, 0x12, 0x10, 0x13, 0x10, 0x15, 0x13, 0xFF };
+static const uint8 kPattern08[] = { 0xC5, 0x9F, 0x7D, 0x89, 0x40, 0x7D, 0x91, 0x40, 0x7D, 0xA1, 0x40, 0x7D, 0x91, 0x40, 0xFF };
+static const uint8 kPattern09[] = { 0xC0, 0x9F, 0x00, 0xFF };
+static const uint8 kPattern10[] = { 0xCA, 0x9F, 0x7E, 0x0A, 0x7F, 0x4C, 0x83, 0x1C, 0x23, 0x28, 0x2F, 0x1C, 0x23, 0x28, 0x2F, 0x8F, 0x7F, 0x40, 0x7E, 0x10, 0x9F, 0x7B, 0x40, 0x3B, 0x21, 0x8F, 0x34, 0x40, 0x9F, 0x7B, 0x0D, 0x3B, 0x4C, 0x7F, 0x3C, 0xFF };
+static const uint8 kPattern11[] = { 0xC7, 0xFF };
+static const uint8 kPattern12[] = { 0xC2, 0xBF, 0x10, 0xFF };
+static const uint8 kPattern13[] = { 0xC8, 0x80, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0x1C, 0xFF };
+static const uint8 kPattern14[] = { 0x80, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x28, 0x34, 0x28, 0x28, 0x34, 0x34, 0x28, 0x34, 0x28, 0x28, 0x34, 0xFF };
+static const uint8 kPattern15[] = { 0xCB, 0x81, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0x50, 0xC0, 0x00, 0xCB, 0x54, 0x52, 0xFF };
+static const uint8 kPattern16[] = { 0xC1, 0x87, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x1C, 0x10, 0x83, 0x1C, 0x81, 0x10, 0x0E, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0A, 0x87, 0x09, 0x15, 0x09, 0x15, 0x09, 0x15, 0x09, 0x83, 0x15, 0x81, 0x09, 0x0A, 0x87, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x17, 0x0B, 0x83, 0x17, 0x81, 0x0B, 0x0E, 0xFF };
+static const uint8 kPattern17[] = { 0xCC, 0xBF, 0x7D, 0x89, 0x40, 0x7C, 0xA2, 0x3E, 0x3C, 0x7D, 0x91, 0x3F, 0xFF };
+static const uint8 kPattern18[] = { 0x83, 0x1C, 0xFF };
+static const uint8 kPattern19[] = { 0xC0, 0x93, 0x00, 0xC1, 0x81, 0x23, 0x83, 0x21, 0x81, 0x1F, 0x83, 0x1E, 0xFF };
+static const uint8 kPattern20[] = { 0xC1, 0x8B, 0x1C, 0x83, 0x23, 0x97, 0x23, 0x83, 0x21, 0x23, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x1E, 0x83, 0x23, 0x9B, 0x23, 0x81, 0x21, 0x23, 0x83, 0x21, 0x1F, 0x1E, 0x1F, 0x8B, 0x21, 0x83, 0x23, 0x93, 0x21, 0x83, 0x21, 0x23, 0x24, 0x26, 0x24, 0x23, 0x21, 0x9B, 0x1E, 0x83, 0x23, 0x93, 0x23, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xD1, 0x30, 0x30, 0xD0, 0x7A, 0x18, 0x12, 0xFF };
+static const uint8 kPattern21[] = { 0xC8, 0xFF };
+static const uint8 kPattern22[] = { 0xC4, 0xFF };
+static const uint8 kPattern23[] = { 0xCF, 0xA3, 0x7B, 0x37, 0x02, 0x36, 0x83, 0x2F, 0x34, 0x36, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x34, 0x8B, 0x7B, 0x36, 0x02, 0x34, 0x83, 0x2F, 0x93, 0x2F, 0x83, 0x36, 0x36, 0x37, 0x7B, 0x39, 0x02, 0x37, 0x37, 0x36, 0x7B, 0x34, 0x04, 0x37, 0x8B, 0x34, 0x81, 0x36, 0x37, 0x93, 0x34, 0x83, 0x34, 0x36, 0x37, 0x39, 0x37, 0x36, 0x34, 0xA3, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x85, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xC0, 0x83, 0x00, 0xD1, 0x81, 0x30, 0x30, 0xD0, 0x12, 0xFF };
+static const uint8 kPattern24[] = { 0xC1, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0x87, 0x10, 0x85, 0x1C, 0x81, 0x10, 0x83, 0x10, 0x10, 0x81, 0x1C, 0x83, 0x10, 0x81, 0x1C, 0xFF };
+static const uint8 kPattern25[] = { 0xCF, 0x99, 0x7B, 0x34, 0x04, 0x32, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x7B, 0x34, 0x04, 0x32, 0x83, 0x7B, 0x37, 0x02, 0x36, 0x81, 0x37, 0x85, 0x7B, 0x32, 0x02, 0x34, 0x93, 0x32, 0x83, 0x32, 0x36, 0x7B, 0x36, 0x02, 0x37, 0x9B, 0x36, 0x83, 0x7B, 0x30, 0x03, 0x32, 0x30, 0x32, 0x81, 0x30, 0x85, 0x7B, 0x30, 0x02, 0x32, 0x93, 0x30, 0x83, 0x30, 0x32, 0x34, 0x8B, 0x7B, 0x36, 0x04, 0x37, 0x83, 0x34, 0x97, 0x7B, 0x36, 0x04, 0x34, 0x81, 0x36, 0x37, 0x36, 0x34, 0x99, 0x7B, 0x36, 0x04, 0x34, 0xD1, 0x81, 0x12, 0x83, 0x12, 0xFF };
+static const uint8 kPattern26[] = { 0xCC, 0xBF, 0x7D, 0x8A, 0x40, 0x7D, 0x91, 0x3F, 0x7D, 0x94, 0x3D, 0x7D, 0x91, 0x3F, 0xFF };
+static const uint8 kPattern27[] = { 0xCF, 0x83, 0x2F, 0x34, 0x91, 0x34, 0x85, 0x7B, 0x34, 0x12, 0x36, 0x2F, 0x81, 0x34, 0x34, 0x36, 0x8D, 0x34, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x93, 0x33, 0x83, 0x33, 0x81, 0x33, 0x85, 0x7B, 0x33, 0x12, 0x34, 0x99, 0x33, 0x81, 0x2F, 0x83, 0x7B, 0x2D, 0x12, 0x2F, 0x8B, 0x2D, 0x83, 0x34, 0x34, 0x8B, 0x7B, 0x37, 0x22, 0x36, 0x93, 0x37, 0x87, 0x7B, 0x34, 0x04, 0x36, 0x83, 0x34, 0xA1, 0x7B, 0x36, 0x12, 0x34, 0xD1, 0x81, 0x1C, 0x8D, 0x1C, 0x81, 0x1C, 0x83, 0x1C, 0x1C, 0x81, 0x1C, 0x1C, 0xFF };
+static const uint8 kPattern28[] = { 0xCF, 0x80, 0x7A, 0x08, 0x34, 0x36, 0x9D, 0x37, 0xD0, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0xCF, 0x80, 0x32, 0x34, 0x9D, 0x36, 0xD0, 0x81, 0x12, 0x83, 0x12, 0x12, 0x81, 0x0E, 0x83, 0x12, 0x1C, 0x85, 0x12, 0x81, 0x0E, 0x83, 0x12, 0xCF, 0x80, 0x30, 0x32, 0x9D, 0x34, 0xD1, 0x83, 0x1C, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x83, 0x1C, 0x81, 0x18, 0x1C, 0xCF, 0x87, 0x30, 0x80, 0x33, 0x34, 0x9D, 0x36, 0xC0, 0x9B, 0x00, 0xD1, 0x81, 0x18, 0x18, 0xFF };
+
+static const uint8 *kPatterns[29] = {
+ kPattern00, kPattern01, kPattern02, kPattern03, kPattern04,
+ kPattern05, kPattern06, kPattern07, kPattern08, kPattern09,
+ kPattern10, kPattern11, kPattern12, kPattern13, kPattern14,
+ kPattern15, kPattern16, kPattern17, kPattern18, kPattern19,
+ kPattern20, kPattern21, kPattern22, kPattern23, kPattern24,
+ kPattern25, kPattern26, kPattern27, kPattern28,
+};
+
+// ---- Order lists (song 0, 3 channels) ----
+
+static const uint8 kOrderList0[] = {
+ 0xC2, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x02, 0x02, 0x02, 0x02, 0x02, 0x02,
+ 0x02, 0x02, 0x02, 0x02, 0x01, 0x10, 0x10, 0x10, 0x10, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9,
+ 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9, 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18,
+ 0xC9, 0x18, 0xC2, 0x18, 0xC9, 0x18, 0xC7, 0x18, 0xC9, 0x18, 0xC2, 0x18, 0x18, 0x18, 0x18,
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x10, 0x10, 0x01,
+ 0x01, 0xFF
+};
+
+static const uint8 kOrderList1[] = {
+ 0xCE, 0x09, 0x01, 0x0C, 0x09, 0x01, 0x01, 0x01, 0x09, 0x01, 0x01, 0x09, 0x00, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0xC2,
+ 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x08, 0x08, 0x02, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1A, 0x1A, 0x08, 0x08, 0xB6,
+ 0x08, 0x08, 0xC2, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03, 0x03,
+ 0x03, 0x03, 0x03, 0x03, 0xCE, 0x0C, 0xC9, 0x0C, 0xC7, 0x0C, 0xC9, 0x0C, 0xC2, 0x16, 0x0E,
+ 0x0E, 0xBD, 0x0E, 0x0E, 0xBB, 0x0E, 0x0E, 0xBD, 0x0E, 0x0E, 0xCE, 0x09, 0x01, 0x0C, 0x09,
+ 0xFF
+};
+
+static const uint8 kOrderList2[] = {
+ 0xC2, 0x00, 0x00, 0x00, 0x00, 0x0A, 0x09, 0x04, 0x05, 0x09, 0x04, 0x06, 0x09, 0x08, 0x08,
+ 0x08, 0x08, 0x0B, 0x05, 0x06, 0x09, 0xAA, 0x04, 0xC2, 0x09, 0xB6, 0x04, 0xC2, 0x09, 0x0D,
+ 0x09, 0xC4, 0x0D, 0xC2, 0x09, 0xC5, 0x0D, 0xC2, 0x09, 0xC7, 0x0D, 0xC2, 0x09, 0xCE, 0x0D,
+ 0x12, 0xC0, 0x0F, 0xD0, 0x0D, 0x12, 0xC0, 0x0F, 0xD1, 0x0D, 0x12, 0xC0, 0x0F, 0xD3, 0x0D,
+ 0x12, 0xC0, 0x0F, 0xB6, 0x15, 0x0E, 0x0E, 0x15, 0x0E, 0x0E, 0xC2, 0x15, 0x0E, 0x0E, 0x16,
+ 0x0E, 0x0E, 0x09, 0xCE, 0x0C, 0xC2, 0x13, 0x14, 0x14, 0x17, 0xCE, 0x17, 0xC2, 0x19, 0xCE,
+ 0x19, 0xC2, 0x1B, 0xB6, 0x1B, 0xCE, 0x01, 0xC2, 0x01, 0xCE, 0x03, 0x03, 0x03, 0x03, 0xC2,
+ 0x03, 0x03, 0x03, 0x03, 0x0B, 0x05, 0x06, 0x09, 0xAA, 0x04, 0xC2, 0x09, 0xB6, 0x04, 0xC2,
+ 0x09, 0x0D, 0x12, 0x0F, 0xC4, 0x0D, 0x12, 0xC2, 0x0F, 0xC5, 0x0D, 0x12, 0xC2, 0x0F, 0xC7,
+ 0x0D, 0x12, 0xC2, 0x0F, 0x15, 0x0E, 0x0E, 0xBD, 0x0E, 0x0E, 0xBB, 0x0E, 0x0E, 0xBD, 0x0E,
+ 0x0E, 0xC2, 0x1C, 0x02, 0x02, 0xFF
+};
+
+static const uint8 *kOrderLists[3] = { kOrderList0, kOrderList1, kOrderList2 };
+
+// ============================================================
+// Implementation
+// ============================================================
+
+void DarkSideC64MusicPlayer::ChannelState::reset() {
+ orderData = nullptr;
+ orderPos = 0;
+ patData = nullptr;
+ patOffset = 0;
+ instIdx = 0;
+ noteActive = 0;
+ curNote = 0;
+ transpose = 0;
+ freqLo = 0;
+ freqHi = 0;
+ pwLo = 0;
+ pwHi = 0;
+ durReload = 1;
+ durCounter = 1;
+ effectMode = 0;
+ effectParam = 0;
+ arpPattern = 0;
+ arpParam2 = 0;
+ arpSeqPos = 0;
+ arpSeqLen = 0;
+ memset(arpSeqData, 0, sizeof(arpSeqData));
+ portaDelta = 0;
+ portaTarget = 0;
+ vibPhase = 0;
+ vibCounter = 0;
+ pwDirection = 0;
+ delayCounter = 0;
+ envCounter = 0;
+ envTable = 0;
+ envSeqActive = false;
+ sustainMode = false;
+ waveform = 0;
+}
+
+DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
+ _musicActive = false;
+ _speedDiv = 1;
+ _speedCounter = 1;
+ for (int i = 0; i < 3; i++)
+ _ch[i].reset();
+ initSID();
+}
+
+DarkSideC64MusicPlayer::~DarkSideC64MusicPlayer() {
+ if (_sid)
+ _sid->stop();
+ delete _sid;
+}
+
+void DarkSideC64MusicPlayer::initSID() {
+ _sid = SID::Config::create(SID::Config::kSidPAL);
+ if (!_sid || !_sid->init()) {
+ warning("DarkSideC64MusicPlayer: Failed to create SID emulator");
+ return;
+ }
+ _sid->start(new Common::Functor0Mem<void, DarkSideC64MusicPlayer>(this, &DarkSideC64MusicPlayer::onTimer), 50);
+}
+
+void DarkSideC64MusicPlayer::sidWrite(int reg, uint8 data) {
+ if (_sid)
+ _sid->writeReg(reg, data);
+}
+
+void DarkSideC64MusicPlayer::silenceAll() {
+ for (int i = 0; i <= 0x18; i++)
+ sidWrite(i, 0);
+}
+
+bool DarkSideC64MusicPlayer::isPlaying() const {
+ return _musicActive;
+}
+
+void DarkSideC64MusicPlayer::startMusic() {
+ setupSong();
+}
+
+void DarkSideC64MusicPlayer::stopMusic() {
+ _musicActive = false;
+ silenceAll();
+}
+
+void DarkSideC64MusicPlayer::setupSong() {
+ silenceAll();
+
+ // Set filter: LP+HP on, full volume ($5F)
+ sidWrite(0x18, 0x5F);
+ sidWrite(0x17, 0x00);
+
+ _speedDiv = 1;
+ _speedCounter = 1;
+
+ for (int ch = 0; ch < 3; ch++) {
+ _ch[ch].reset();
+ _ch[ch].orderData = kOrderLists[ch];
+ loadNextPattern(ch);
+ }
+
+ _musicActive = true;
+}
+
+void DarkSideC64MusicPlayer::loadNextPattern(int ch) {
+ int safety = 200;
+ while (safety-- > 0) {
+ uint8 byte = _ch[ch].orderData[_ch[ch].orderPos];
+
+ if (byte == 0xFF) {
+ // Loop to start
+ _ch[ch].orderPos = 0;
+ continue;
+ }
+
+ _ch[ch].orderPos++;
+
+ if (byte >= 0x80) {
+ // Transpose: (byte + $40) & $FF
+ _ch[ch].transpose = (byte + 0x40) & 0xFF;
+ continue;
+ }
+
+ // Pattern number
+ if (byte < 29) {
+ _ch[ch].patData = kPatterns[byte];
+ _ch[ch].patOffset = 0;
+ }
+ break;
+ }
+}
+
+uint8 DarkSideC64MusicPlayer::readPatByte(int ch) {
+ uint8 b = _ch[ch].patData[_ch[ch].patOffset];
+ _ch[ch].patOffset++;
+ return b;
+}
+
+// ---- Main timer callback (50 Hz) ----
+
+void DarkSideC64MusicPlayer::onTimer() {
+ if (!_musicActive)
+ return;
+
+ // Increment envelope counters for all channels
+ for (int ch = 0; ch < 3; ch++) {
+ if (_ch[ch].envCounter < 15)
+ _ch[ch].envCounter++;
+ }
+
+ // Speed counter
+ _speedCounter--;
+ bool newBeat = (_speedCounter == 0);
+ if (newBeat)
+ _speedCounter = _speedDiv;
+
+ // Process channels (2 down to 0, matching original)
+ for (int ch = 2; ch >= 0; ch--)
+ processChannel(ch, newBeat);
+}
+
+void DarkSideC64MusicPlayer::processChannel(int ch, bool newBeat) {
+ int off = kSIDOffset[ch];
+
+ // Handle delay countdown
+ if (_ch[ch].delayCounter > 0) {
+ _ch[ch].delayCounter--;
+ if (_ch[ch].delayCounter == 0 && _ch[ch].noteActive) {
+ // Delay expired: gate on now
+ sidWrite(off + 4, _ch[ch].waveform | 0x01);
+ }
+ }
+
+ if (newBeat) {
+ if (_ch[ch].durCounter > 0)
+ _ch[ch].durCounter--;
+
+ if (_ch[ch].durCounter == 0) {
+ parseCommands(ch);
+ }
+ }
+
+ // Apply continuous effects
+ applyContinuousEffects(ch);
+
+ // Gate-off at half duration (if not in sustain mode and not in envelope seq mode)
+ if (!_ch[ch].sustainMode && !_ch[ch].envSeqActive && _ch[ch].noteActive) {
+ if (_ch[ch].durReload > 1 && _ch[ch].durCounter == _ch[ch].durReload / 2) {
+ sidWrite(off + 4, _ch[ch].waveform & 0xFE);
+ }
+ }
+}
+
+// ---- Command parser ----
+
+void DarkSideC64MusicPlayer::parseCommands(int ch) {
+ int safety = 200;
+ while (safety-- > 0) {
+ uint8 cmd = readPatByte(ch);
+
+ if (cmd == 0xFF) {
+ // End of pattern: load next from order list
+ loadNextPattern(ch);
+ continue;
+ }
+
+ if (cmd == 0xFE) {
+ // End of song: stop
+ stopMusic();
+ return;
+ }
+
+ if (cmd == 0xFD) {
+ // Filter control: read 1 parameter byte
+ uint8 filterVal = readPatByte(ch);
+ sidWrite(0x18, filterVal);
+ sidWrite(0x17, (filterVal >> 4) | 0x07);
+ continue;
+ }
+
+ if (cmd >= 0xF0) {
+ // Speed: low nybble
+ _speedDiv = cmd & 0x0F;
+ if (_speedDiv == 0)
+ _speedDiv = 1;
+ continue;
+ }
+
+ if (cmd >= 0xC0) {
+ // Instrument: (cmd & 0x1F) * 8
+ uint8 instNum = cmd & 0x1F;
+ if (instNum < 18)
+ _ch[ch].instIdx = instNum * 8;
+ continue;
+ }
+
+ if (cmd >= 0x80) {
+ // Duration: cmd & 0x3F
+ _ch[ch].durReload = cmd & 0x3F;
+ if (_ch[ch].durReload == 0)
+ _ch[ch].durReload = 1;
+ continue;
+ }
+
+ if (cmd == 0x7F) {
+ // Portamento up
+ _ch[ch].effectMode = 0xDE;
+ continue;
+ }
+
+ if (cmd == 0x7E) {
+ // Portamento down
+ _ch[ch].effectMode = 0xFE;
+ continue;
+ }
+
+ if (cmd == 0x7D) {
+ // Vibrato mode 1
+ _ch[ch].effectMode = 1;
+ _ch[ch].effectParam = readPatByte(ch);
+ _ch[ch].arpPattern = 0;
+ _ch[ch].vibPhase = 0;
+ _ch[ch].vibCounter = 0;
+ continue;
+ }
+
+ if (cmd == 0x7C) {
+ // Vibrato mode 2
+ _ch[ch].effectMode = 2;
+ _ch[ch].effectParam = readPatByte(ch);
+ _ch[ch].vibPhase = 0;
+ _ch[ch].vibCounter = 0;
+ continue;
+ }
+
+ if (cmd == 0x7B) {
+ // Arpeggio: read 2 bytes
+ uint8 arpX = readPatByte(ch);
+ uint8 arpY = readPatByte(ch);
+ _ch[ch].effectMode = 1;
+ _ch[ch].effectParam = 0;
+ _ch[ch].arpPattern = (arpX + _ch[ch].transpose) & 0xFF;
+ _ch[ch].arpParam2 = arpY;
+ _ch[ch].arpSeqPos = 0;
+ unpackArpeggio(ch);
+ continue;
+ }
+
+ if (cmd == 0x7A) {
+ // Delay: read 1 byte (delay value)
+ _ch[ch].delayCounter = readPatByte(ch);
+ continue;
+ }
+
+ // Note (0x00-0x5F)
+ applyNote(ch, cmd);
+ break;
+ }
+}
+
+// ---- Note handling ----
+
+void DarkSideC64MusicPlayer::applyNote(int ch, uint8 note) {
+ int off = kSIDOffset[ch];
+ uint8 instBase = _ch[ch].instIdx;
+
+ if (note == 0) {
+ // Rest: gate off
+ _ch[ch].noteActive = 0;
+ sidWrite(off + 4, _ch[ch].waveform & 0xFE);
+ _ch[ch].durCounter = _ch[ch].durReload;
+ return;
+ }
+
+ // Compute actual note index with transpose
+ uint8 actualNote = (note + _ch[ch].transpose) & 0xFF;
+ if (actualNote > 94)
+ actualNote = 94;
+
+ // Previous frequency (for portamento)
+ uint16 prevFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+
+ // Look up new frequency
+ _ch[ch].freqLo = kFreqLo[actualNote];
+ _ch[ch].freqHi = kFreqHi[actualNote];
+ _ch[ch].curNote = actualNote;
+ _ch[ch].noteActive = 1;
+
+ // Load instrument parameters
+ uint8 ctrl = kInstruments[instBase + 0];
+ uint8 ad = kInstruments[instBase + 1];
+ uint8 sr = kInstruments[instBase + 2];
+ uint8 vibrato = kInstruments[instBase + 4];
+ uint8 autoFx = kInstruments[instBase + 6];
+ uint8 flags = kInstruments[instBase + 7];
+
+ _ch[ch].waveform = ctrl;
+ _ch[ch].sustainMode = (flags & 0x08) != 0;
+ _ch[ch].envSeqActive = (flags & 0x01) != 0;
+ _ch[ch].envTable = (vibrato >> 4) & 0x01;
+
+ // Write ADSR
+ sidWrite(off + 5, ad);
+ sidWrite(off + 6, sr);
+
+ // Write PW
+ sidWrite(off + 2, _ch[ch].pwLo);
+ sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
+
+ // Write frequency
+ sidWrite(off + 0, _ch[ch].freqLo);
+ sidWrite(off + 1, _ch[ch].freqHi);
+
+ // Setup portamento if active
+ if (_ch[ch].effectMode == 0xDE || _ch[ch].effectMode == 0xFE) {
+ uint16 newFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+ _ch[ch].portaTarget = newFreq;
+ // Slide from previous frequency
+ _ch[ch].freqLo = prevFreq & 0xFF;
+ _ch[ch].freqHi = (prevFreq >> 8) & 0xFF;
+ if (_ch[ch].durReload > 0) {
+ _ch[ch].portaDelta = (int16)(newFreq - prevFreq) / (int16)_ch[ch].durReload;
+ }
+ sidWrite(off + 0, _ch[ch].freqLo);
+ sidWrite(off + 1, _ch[ch].freqHi);
+ }
+
+ // Setup auto-effect from instrument
+ if (autoFx != 0 && _ch[ch].effectMode == 0) {
+ _ch[ch].effectMode = 1;
+ _ch[ch].effectParam = autoFx;
+ }
+
+ // Gate on (unless delay is active)
+ if (_ch[ch].delayCounter > 0) {
+ // Delay: don't gate on yet, gate off first
+ sidWrite(off + 4, ctrl & 0xFE);
+ } else if (_ch[ch].envSeqActive) {
+ // Envelope sequencer controls the gate
+ _ch[ch].envCounter = 0;
+ sidWrite(off + 4, kEnvControl[_ch[ch].envTable][0]);
+ } else {
+ sidWrite(off + 4, ctrl | 0x01);
+ }
+
+ // Reset envelope counter
+ _ch[ch].envCounter = 0;
+
+ // Set duration
+ _ch[ch].durCounter = _ch[ch].durReload;
+}
+
+// ---- Continuous effects ----
+
+void DarkSideC64MusicPlayer::applyContinuousEffects(int ch) {
+ // PW modulation
+ applyPWModulation(ch);
+
+ // Frequency effects (mutually exclusive based on effectMode)
+ switch (_ch[ch].effectMode) {
+ case 1:
+ if (_ch[ch].arpPattern != 0)
+ applyArpeggio(ch);
+ else
+ applyVibrato(ch);
+ break;
+ case 2:
+ applyVibrato(ch);
+ break;
+ case 0xDE:
+ case 0xFE:
+ applyPortamento(ch);
+ break;
+ default:
+ break;
+ }
+
+ // Envelope sequencer
+ if (_ch[ch].envSeqActive)
+ applyEnvelope(ch);
+
+ // Write frequency to SID
+ int off = kSIDOffset[ch];
+ sidWrite(off + 0, _ch[ch].freqLo);
+ sidWrite(off + 1, _ch[ch].freqHi);
+
+ // Write PW to SID
+ sidWrite(off + 2, _ch[ch].pwLo);
+ sidWrite(off + 3, _ch[ch].pwHi & 0x0F);
+}
+
+void DarkSideC64MusicPlayer::applyVibrato(int ch) {
+ if (!_ch[ch].noteActive)
+ return;
+
+ uint8 vibParam = _ch[ch].effectParam;
+ if (vibParam == 0)
+ return;
+
+ uint8 depth = vibParam >> 4;
+ uint8 speed = vibParam & 0x0F;
+ if (depth == 0 || speed == 0)
+ return;
+
+ uint8 note = _ch[ch].curNote;
+ if (note == 0 || note >= 94)
+ return;
+
+ // Compute frequency delta between this note and the next semitone
+ uint16 curFreq = ((uint16)kFreqHi[note] << 8) | kFreqLo[note];
+ uint16 nextFreq = ((uint16)kFreqHi[note + 1] << 8) | kFreqLo[note + 1];
+ int16 semitoneDelta = (int16)(nextFreq - curFreq);
+
+ // Scale by depth
+ int16 vibDelta = semitoneDelta * depth / 16;
+
+ // Oscillate: counter counts up to speed, then phase flips
+ _ch[ch].vibCounter++;
+ if (_ch[ch].vibCounter >= speed) {
+ _ch[ch].vibCounter = 0;
+ _ch[ch].vibPhase ^= 1;
+ }
+
+ // Apply offset
+ int16 offset = _ch[ch].vibPhase ? vibDelta : -vibDelta;
+ int16 baseFreq = (int16)curFreq;
+ uint16 newFreq = (uint16)(baseFreq + offset);
+
+ _ch[ch].freqLo = newFreq & 0xFF;
+ _ch[ch].freqHi = (newFreq >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyArpeggio(int ch) {
+ if (!_ch[ch].noteActive || _ch[ch].arpSeqLen == 0)
+ return;
+
+ // Cycle through unpacked arpeggio intervals
+ uint8 interval = _ch[ch].arpSeqData[_ch[ch].arpSeqPos];
+ _ch[ch].arpSeqPos++;
+ if (_ch[ch].arpSeqPos >= _ch[ch].arpSeqLen)
+ _ch[ch].arpSeqPos = 0;
+
+ // Compute arpeggiated note
+ uint8 arpNote = _ch[ch].curNote + interval;
+ if (arpNote > 94)
+ arpNote = 94;
+
+ _ch[ch].freqLo = kFreqLo[arpNote];
+ _ch[ch].freqHi = kFreqHi[arpNote];
+}
+
+void DarkSideC64MusicPlayer::unpackArpeggio(int ch) {
+ uint8 bits = _ch[ch].arpPattern;
+
+ // First entry: base note (interval 0)
+ _ch[ch].arpSeqData[0] = 0;
+ uint8 len = 1;
+
+ for (int i = 0; i < 8 && len < 19; i++) {
+ if (bits & (1 << i)) {
+ _ch[ch].arpSeqData[len] = kArpIntervals[i];
+ len++;
+ }
+ }
+
+ _ch[ch].arpSeqLen = len;
+ _ch[ch].arpSeqPos = 0;
+}
+
+void DarkSideC64MusicPlayer::applyPortamento(int ch) {
+ if (!_ch[ch].noteActive)
+ return;
+
+ uint16 curFreq = ((uint16)_ch[ch].freqHi << 8) | _ch[ch].freqLo;
+ int32 newFreq = (int32)curFreq + _ch[ch].portaDelta;
+
+ // Clamp and check if target reached
+ if (_ch[ch].portaDelta > 0 && (uint16)newFreq >= _ch[ch].portaTarget) {
+ newFreq = _ch[ch].portaTarget;
+ } else if (_ch[ch].portaDelta < 0 && (uint16)newFreq <= _ch[ch].portaTarget) {
+ newFreq = _ch[ch].portaTarget;
+ }
+
+ _ch[ch].freqLo = (uint16)newFreq & 0xFF;
+ _ch[ch].freqHi = ((uint16)newFreq >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyPWModulation(int ch) {
+ uint8 instBase = _ch[ch].instIdx;
+ uint8 pwMod = kInstruments[instBase + 5];
+ if (pwMod == 0)
+ return;
+
+ uint8 flags = kInstruments[instBase + 7];
+
+ uint16 pw = ((uint16)_ch[ch].pwHi << 8) | _ch[ch].pwLo;
+
+ if (flags & 0x04) {
+ // Direct add (one-directional sweep)
+ pw += pwMod;
+ } else {
+ // Triangle sweep between pwHi=$08 and pwHi=$0F
+ if (_ch[ch].pwDirection == 0) {
+ pw += pwMod;
+ if ((_ch[ch].pwHi + (pw >> 8)) >= 0x0F || (pw >> 8) >= 0x0F) {
+ pw = ((uint16)_ch[ch].pwHi << 8) | _ch[ch].pwLo;
+ pw += pwMod;
+ if ((pw >> 8) >= 0x0F)
+ _ch[ch].pwDirection = 1;
+ }
+ } else {
+ pw -= pwMod;
+ if ((pw >> 8) <= 0x08)
+ _ch[ch].pwDirection = 0;
+ }
+ }
+
+ _ch[ch].pwLo = pw & 0xFF;
+ _ch[ch].pwHi = (pw >> 8) & 0xFF;
+}
+
+void DarkSideC64MusicPlayer::applyEnvelope(int ch) {
+ if (_ch[ch].envCounter >= 15)
+ return;
+
+ int off = kSIDOffset[ch];
+ uint8 tbl = _ch[ch].envTable;
+
+ // Write waveform control from envelope table
+ sidWrite(off + 4, kEnvControl[tbl][_ch[ch].envCounter]);
+
+ // Write AD register from envelope volume table
+ sidWrite(off + 5, kEnvVolume[tbl][_ch[ch].envCounter]);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/games/dark/c64.music.h b/engines/freescape/games/dark/c64.music.h
new file mode 100644
index 00000000000..78d2d2f7185
--- /dev/null
+++ b/engines/freescape/games/dark/c64.music.h
@@ -0,0 +1,127 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DARK_C64_MUSIC_H
+#define FREESCAPE_DARK_C64_MUSIC_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// 3-channel SID music player for Dark Side C64.
+// Implements the Wally Beben byte-stream sequencer from darkside.prg ($0901).
+class DarkSideC64MusicPlayer {
+public:
+ DarkSideC64MusicPlayer();
+ ~DarkSideC64MusicPlayer();
+
+ void startMusic();
+ void stopMusic();
+ bool isPlaying() const;
+
+private:
+ SID::SID *_sid;
+
+ void sidWrite(int reg, uint8 data);
+ void initSID();
+ void onTimer();
+
+ // Song setup
+ void setupSong();
+ void silenceAll();
+ void loadNextPattern(int ch);
+ void unpackArpeggio(int ch);
+
+ // Per-tick processing
+ void processChannel(int ch, bool newBeat);
+ void parseCommands(int ch);
+ void applyNote(int ch, uint8 note);
+ void applyContinuousEffects(int ch);
+ void applyVibrato(int ch);
+ void applyArpeggio(int ch);
+ void applyPortamento(int ch);
+ void applyPWModulation(int ch);
+ void applyEnvelope(int ch);
+
+ uint8 readPatByte(int ch);
+
+ // Global state
+ bool _musicActive;
+ uint8 _speedDiv; // $15A1: speed divider
+ uint8 _speedCounter; // $15A2: frames until next beat
+
+ // Per-channel state
+ struct ChannelState {
+ const uint8 *orderData;
+ uint8 orderPos; // $15A3
+
+ const uint8 *patData;
+ int patOffset;
+
+ uint8 instIdx; // $15F6: instrument# * 8
+ uint8 noteActive; // $1599
+ uint8 curNote; // $159C: note index (with transpose)
+ uint8 transpose; // $15C4
+
+ uint8 freqLo; // $15A6
+ uint8 freqHi; // $15A9
+ uint8 pwLo; // $15AC
+ uint8 pwHi; // $15AF
+
+ uint8 durReload; // $15B8
+ uint8 durCounter; // $15BB
+
+ uint8 effectMode; // $15BE: 0=none, 1=arp/vib1, 2=vib2, 0xDE=portaUp, 0xFE=portaDn
+ uint8 effectParam; // $15C1
+
+ uint8 arpPattern; // $15C7
+ uint8 arpParam2; // $15CA
+ uint8 arpSeqPos; // $15D9
+ uint8 arpSeqData[20]; // $160C
+ uint8 arpSeqLen;
+
+ int16 portaDelta; // $15CD/$15CE: per-tick freq delta
+ uint16 portaTarget; // target frequency for portamento
+
+ uint8 vibPhase; // $15DE
+ uint8 vibCounter; // $15E1
+
+ uint8 pwDirection; // $15D6: 0=up, 1=down
+
+ uint8 delayCounter; // $15E7
+
+ uint8 envCounter; // $15FC
+ uint8 envTable; // envelope table set (0 or 1)
+ bool envSeqActive; // flags bit 0
+
+ bool sustainMode; // flags bit 3
+
+ uint8 waveform; // current waveform ctrl byte (from instrument)
+
+ void reset();
+ };
+
+ ChannelState _ch[3];
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DARK_C64_MUSIC_H
diff --git a/engines/freescape/games/dark/c64.sfx.cpp b/engines/freescape/games/dark/c64.sfx.cpp
new file mode 100644
index 00000000000..ee796b47d54
--- /dev/null
+++ b/engines/freescape/games/dark/c64.sfx.cpp
@@ -0,0 +1,473 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/games/dark/c64.sfx.h"
+#include "freescape/freescape.h"
+
+#include "common/debug.h"
+#include "common/textconsole.h"
+
+namespace Freescape {
+
+// 25 SFX entries extracted from dark2.prg at $C802 (address $C802-$CBEA).
+// Each entry is 40 bytes in the original 6502 format.
+// See SOUND_ANALYSIS.md for full documentation.
+static const DarkSideSFXData kDarkSideSFXData[25] = {
+ // SFX #1: Shoot (Noise, highâsilence)
+ {2, 1, 0,
+ {0x00, 0x38, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {21, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0xEE, 0x02, 0x80, 0x13, 0xF6},
+
+ // SFX #2: Hit (Noise, quick highâmid x3)
+ {2, 3, 0,
+ {0x00, 0x44, 0x00, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {4, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0xEA, 0x01, 0x80, 0x23, 0xDA},
+
+ // SFX #3: Step down (Pulse, highâsilence)
+ {2, 1, 0,
+ {0x00, 0x34, 0x00, 0x00, 0x00, 0x08, 0x00, 0x2C, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {50, 6, 2, 5, 0, 0, 0, 0, 0}, 1, 0,
+ 0x72, 0x06, 0x40, 0x35, 0xF4},
+
+ // SFX #4: Step up (Saw, silenceâmid ascending)
+ {2, 1, 0,
+ {0x00, 0x00, 0x00, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {46, 0, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0xA2, 0x03, 0x20, 0x34, 0xF4},
+
+ // SFX #5: Area change (Noise, flatâhigh sweep)
+ {3, 1, 0,
+ {0x00, 0x34, 0x00, 0x34, 0x00, 0xFC, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {14, 9, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0x64, 0x05, 0x80, 0x52, 0x6A},
+
+ // SFX #6: Menu / area select (Triangle, hiâloâloâhi bounce)
+ {3, 1, 0,
+ {0x00, 0x74, 0x00, 0x24, 0x00, 0x24, 0x00, 0x78, 0x00, 0x24,
+ 0x00, 0x94, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {2, 2, 8, 6, 1, 0, 0, 0, 0}, 2, 0,
+ 0xEA, 0x06, 0x10, 0x19, 0xF9},
+
+ // SFX #7: Noise burst sequence
+ {3, 1, 0,
+ {0x00, 0x1C, 0x00, 0x00, 0x00, 0x18, 0x00, 0x7C, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {4, 7, 2, 21, 0, 0, 0, 0, 0}, 1, 0,
+ 0x38, 0x0F, 0x80, 0x38, 0xFC},
+
+ // SFX #8: Triangle slide (repeat x5)
+ {2, 5, 0,
+ {0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {6, 0, 0, 0, 0, 0, 0, 0, 0}, 2, 0,
+ 0xD0, 0x02, 0x10, 0x19, 0xF7},
+
+ // SFX #9: Game start (Triangle, rising 4-note arpeggio)
+ {4, 1, 0,
+ {0x00, 0x90, 0x00, 0xC0, 0x00, 0x4C, 0x00, 0x4C, 0x00, 0x54,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {7, 10, 6, 8, 0, 0, 0, 0, 0}, 1, 0,
+ 0xC8, 0x00, 0x10, 0x32, 0xF4},
+
+ // SFX #10: Noise burst (repeat x2)
+ {3, 2, 0,
+ {0x00, 0x68, 0x00, 0x70, 0x00, 0x60, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {2, 1, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0x06, 0x04, 0x80, 0x15, 0xF9},
+
+ // SFX #11: Tri+Ring modulation
+ {2, 1, 0,
+ {0x00, 0x00, 0x00, 0x2C, 0x00, 0x18, 0x00, 0x24, 0x00, 0x90,
+ 0x00, 0x30, 0x00, 0x90, 0x00, 0xB0, 0x00, 0xC0, 0x00, 0x14}, 0,
+ {4, 8, 14, 19, 18, 4, 8, 3, 0x80}, 1, 0xB0,
+ 0x00, 0xCC, 0x14, 0x25, 0xF7},
+
+ // SFX #12: Long noise sequence (6 notes)
+ {6, 1, 0,
+ {0x00, 0x4C, 0x00, 0x28, 0x00, 0x34, 0x00, 0x14, 0x00, 0x30,
+ 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {5, 9, 4, 13, 14, 0, 0, 0, 0}, 1, 0,
+ 0x26, 0x0D, 0x80, 0x16, 0xFA},
+
+ // SFX #13: Noise, 4-note descending
+ {4, 1, 0,
+ {0x00, 0x7C, 0x00, 0x7C, 0x00, 0x50, 0x00, 0x44, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {2, 4, 4, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0x90, 0x01, 0x80, 0x33, 0xFA},
+
+ // SFX #14: Fall (Pulse, long highâsilence slide)
+ {2, 1, 0,
+ {0x00, 0xFC, 0x00, 0x00, 0x00, 0x90, 0x00, 0x6C, 0x00, 0x6C,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {43, 5, 4, 7, 0, 0, 0, 0, 0}, 2, 0,
+ 0x30, 0x07, 0x40, 0x33, 0x79},
+
+ // SFX #15: ECD destroy pre-noise (7-step rising sweep)
+ {7, 1, 0,
+ {0x00, 0x00, 0x00, 0x04, 0x00, 0x10, 0x00, 0x30, 0x00, 0x50,
+ 0x00, 0xA0, 0x00, 0xCC, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {9, 12, 11, 5, 4, 2, 0, 0, 0}, 1, 0,
+ 0x80, 0x02, 0x80, 0x35, 0xF9},
+
+ // SFX #16: Tri+Ring (identical to #11)
+ {2, 1, 0,
+ {0x00, 0x00, 0x00, 0x2C, 0x00, 0x18, 0x00, 0x24, 0x00, 0x90,
+ 0x00, 0x30, 0x00, 0x90, 0x00, 0xB0, 0x00, 0xC0, 0x00, 0x14}, 0,
+ {4, 8, 14, 19, 18, 4, 8, 3, 0x80}, 1, 0xB0,
+ 0x00, 0xCC, 0x14, 0x25, 0xF7},
+
+ // SFX #17: Triangle slide down
+ {2, 1, 0,
+ {0x00, 0xB0, 0x00, 0x40, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {5, 4, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0x90, 0x02, 0x10, 0x36, 0xF6},
+
+ // SFX #18: Sawtooth oscillation (repeat x3)
+ {4, 3, 0,
+ {0x00, 0xAC, 0x00, 0x84, 0x00, 0xAC, 0x00, 0x84, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {4, 2, 5, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0xAA, 0x05, 0x20, 0x16, 0xF8},
+
+ // SFX #19: Restore ECD (Noise, quick burst)
+ {3, 1, 0,
+ {0x00, 0x18, 0x00, 0x24, 0x00, 0x14, 0x00, 0x00, 0x00, 0x34,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {2, 3, 2, 9, 0, 0, 0, 0, 0}, 1, 0,
+ 0x62, 0x0D, 0x80, 0x16, 0xFA},
+
+ // SFX #20: No shield / depleted (Triangle, 4-note warning x2)
+ {4, 2, 0,
+ {0x00, 0x5C, 0x00, 0xA0, 0x00, 0xA0, 0x00, 0x60, 0x00, 0x8C,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {4, 4, 4, 5, 0, 0, 0, 0, 0}, 1, 0,
+ 0x88, 0x09, 0x10, 0x14, 0xF6},
+
+ // SFX #21: Sawtooth multi-note (speed=3)
+ {4, 1, 0,
+ {0x00, 0x00, 0x00, 0x00, 0x00, 0x8C, 0x00, 0xBC, 0x00, 0x8C,
+ 0x00, 0xC0, 0x00, 0x34, 0x00, 0x74, 0x00, 0x48, 0x00, 0x88}, 0,
+ {2, 6, 15, 4, 18, 4, 4, 5, 2}, 3, 3,
+ 0xEA, 0x0B, 0x20, 0x32, 0xF8},
+
+ // SFX #22: Noise ascendingâsilence
+ {3, 1, 0,
+ {0x00, 0xB8, 0x00, 0xD4, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {8, 7, 0, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0xEA, 0x06, 0x80, 0x32, 0xF7},
+
+ // SFX #23: Pulse pattern (repeat x5)
+ {5, 5, 0,
+ {0x00, 0x00, 0x00, 0x00, 0x00, 0x24, 0x00, 0x24, 0x00, 0x00,
+ 0x00, 0x40, 0x00, 0x40, 0x00, 0x30, 0x00, 0x28, 0x00, 0x00}, 0,
+ {7, 9, 8, 9, 13, 0, 17, 11, 0}, 1, 0,
+ 0x84, 0x08, 0x40, 0x15, 0xF1},
+
+ // SFX #24: Sawtooth sweep (repeat x5)
+ {2, 5, 0,
+ {0x00, 0xE0, 0x00, 0x44, 0x00, 0x68, 0x00, 0x40, 0x00, 0x40,
+ 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 0,
+ {6, 0, 5, 0, 0, 0, 0, 0, 0}, 1, 0,
+ 0x12, 0x01, 0x20, 0x39, 0xF8},
+
+ // SFX #25: Noise multi-bounce (speed=2)
+ {4, 1, 0,
+ {0x00, 0x7C, 0x00, 0x00, 0x00, 0x28, 0x00, 0x28, 0x00, 0x58,
+ 0x00, 0x68, 0x00, 0x6C, 0x00, 0x64, 0x00, 0x54, 0x00, 0x3C}, 0,
+ {6, 5, 7, 7, 4, 5, 14, 8, 3}, 2, 0,
+ 0x6E, 0x05, 0x80, 0x74, 0xF4},
+};
+
+DarkSideC64SFXPlayer::DarkSideC64SFXPlayer()
+ : _sid(nullptr),
+ _state(0),
+ _numNotes(0), _repeatCount(0), _waveform(0), _speed(1),
+ _repeatLeft(0), _notesLeft(0), _freqIndex(0), _durIndex(0),
+ _durCounter(0), _speedCounter(0),
+ _curFreqLo(0), _curFreqHi(0) {
+ memset(_startFreqs, 0, sizeof(_startFreqs));
+ memset(_deltas, 0, sizeof(_deltas));
+ memset(_durCopies, 0, sizeof(_durCopies));
+ initSID();
+}
+
+DarkSideC64SFXPlayer::~DarkSideC64SFXPlayer() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ }
+}
+
+void DarkSideC64SFXPlayer::initSID() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ }
+
+ _sid = SID::Config::create(SID::Config::kSidPAL);
+ if (!_sid || !_sid->init())
+ error("Failed to initialise SID emulator for Dark Side SFX");
+
+ for (int i = 0; i < 0x19; i++)
+ sidWrite(i, 0);
+ sidWrite(kDarkSIDVolume, 0x0F);
+
+ _sid->start(new Common::Functor0Mem<void, DarkSideC64SFXPlayer>(this, &DarkSideC64SFXPlayer::onTimer), 50);
+}
+
+void DarkSideC64SFXPlayer::sidWrite(int reg, uint8 data) {
+ if (_sid) {
+ debugC(4, kFreescapeDebugMedia, "Dark SFX SID Write: Reg $%02X = $%02X", reg, data);
+ _sid->writeReg(reg, data);
+ }
+}
+
+void DarkSideC64SFXPlayer::onTimer() {
+ sfxTick();
+}
+
+bool DarkSideC64SFXPlayer::isSfxActive() const {
+ return _state != 0;
+}
+
+void DarkSideC64SFXPlayer::stopAllSfx() {
+ _state = 0;
+ silenceAll();
+}
+
+void DarkSideC64SFXPlayer::silenceV1() {
+ // $CE5C: zero V1 SID registers ($D400-$D406), clear state
+ _state = 0;
+ for (int i = kDarkSIDV1FreqLo; i <= kDarkSIDV1SR; i++)
+ sidWrite(i, 0);
+}
+
+void DarkSideC64SFXPlayer::silenceAll() {
+ // $CE57: zero all SID registers ($D400-$D413), clear state
+ _state = 0;
+ for (int i = 0; i <= 0x13; i++)
+ sidWrite(i, 0);
+ sidWrite(kDarkSIDVolume, 0x0F);
+}
+
+// Signed 32÷16 division matching the original $2122 routine.
+// Computes (dividend / divisor) as a signed 16-bit result.
+static int16 signedDivide(int16 dividend, uint8 divisor) {
+ if (divisor == 0)
+ return 0;
+ return dividend / (int16)divisor;
+}
+
+void DarkSideC64SFXPlayer::setupSfx(int index) {
+ const DarkSideSFXData &sfx = kDarkSideSFXData[index];
+
+ debugC(1, kFreescapeDebugMedia, "Dark Side C64 SFX: setup #%d (notes=%d repeat=%d wf=$%02X)",
+ index + 1, sfx.numNotes, sfx.repeatCount, sfx.waveform);
+
+ // Call silence V1 first ($CE5C)
+ silenceV1();
+
+ // Gate off
+ sidWrite(kDarkSIDV1Ctrl, 0);
+
+ // Load SID registers from descriptor
+ sidWrite(kDarkSIDV1PwLo, sfx.pwLo);
+ sidWrite(kDarkSIDV1PwHi, sfx.pwHi);
+ sidWrite(kDarkSIDV1AD, sfx.attackDecay);
+ sidWrite(kDarkSIDV1SR, sfx.sustainRelease);
+
+ // Store working copies
+ _numNotes = sfx.numNotes;
+ _repeatCount = sfx.repeatCount;
+ _waveform = sfx.waveform;
+ _speed = sfx.speed;
+
+ // Compute per-note start frequencies, deltas, and duration copies.
+ // Matches the $CCFC loop: for each note transition, compute
+ // delta = (freq[i+1] - freq[i]) / duration[i]
+ int numTransitions = sfx.numNotes;
+ for (int i = 0; i < numTransitions && i < 9; i++) {
+ // Frequency waypoints are stored as (lo, hi) pairs at offsets 0,2,4...
+ uint16 freqStart = sfx.freqWaypoints[i * 2] | (sfx.freqWaypoints[i * 2 + 1] << 8);
+ uint16 freqEnd = sfx.freqWaypoints[(i + 1) * 2] | (sfx.freqWaypoints[(i + 1) * 2 + 1] << 8);
+
+ _startFreqs[i] = (int16)freqStart;
+
+ int16 diff = (int16)(freqEnd - freqStart);
+ _deltas[i] = signedDivide(diff, sfx.durations[i]);
+
+ _durCopies[i] = sfx.durations[i];
+
+ debugC(2, kFreescapeDebugMedia, " Note %d: freq $%04Xâ$%04X dur=%d delta=%d",
+ i, freqStart, freqEnd, sfx.durations[i], _deltas[i]);
+ }
+
+ // Set state to start phase
+ _state = 1;
+}
+
+void DarkSideC64SFXPlayer::playSfx(int sfxIndex) {
+ // Guard: SFX indices are 1-based, table is 0-based
+ if (sfxIndex < 1 || sfxIndex > 25) {
+ debugC(1, kFreescapeDebugMedia, "Dark Side C64 SFX: invalid index %d", sfxIndex);
+ return;
+ }
+
+ // Guard: if SFX is already active, drop the request ($CC90-$CC93)
+ if (_state != 0) {
+ debugC(2, kFreescapeDebugMedia, "Dark Side C64 SFX: busy, dropping #%d", sfxIndex);
+ return;
+ }
+
+ setupSfx(sfxIndex - 1);
+}
+
+void DarkSideC64SFXPlayer::sfxTick() {
+ if (_state == 0)
+ return;
+
+ if (_state == 1) {
+ tickStart();
+ return;
+ }
+
+ if (_state == 2) {
+ tickSlide();
+ return;
+ }
+
+ // State $FF equivalent: cleanup â inactive
+ _state = 0;
+}
+
+// State 1: Start phase ($CD8B-$CDD1)
+// Load repeat count, reset indices, load first note, gate on, transition to slide.
+void DarkSideC64SFXPlayer::tickStart() {
+ _repeatLeft = _repeatCount;
+
+ // Reset note counters and indices
+ _notesLeft = _numNotes - 1;
+ _freqIndex = 0;
+ _durIndex = 0;
+
+ // Load speed counter
+ _speedCounter = _speed;
+
+ // Load first note frequency
+ uint16 freq = (uint16)_startFreqs[0];
+ _curFreqLo = freq & 0xFF;
+ _curFreqHi = (freq >> 8) & 0xFF;
+ sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+ sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+ // Load first duration
+ _durCounter = _durCopies[0];
+
+ // Gate on: waveform | 0x01
+ sidWrite(kDarkSIDV1Ctrl, _waveform | 0x01);
+
+ // Transition to slide state
+ _state = 2;
+}
+
+// State 2: Slide phase ($CDD2-$CE4E)
+// Decrement speed counter, update frequency with delta, advance notes.
+void DarkSideC64SFXPlayer::tickSlide() {
+ // Speed prescaler
+ _speedCounter--;
+ if (_speedCounter != 0)
+ return;
+
+ // Reload speed counter
+ _speedCounter = _speed;
+
+ // Update frequency: add delta for current note
+ int noteIdx = _freqIndex / 2;
+ uint16 freq = (_curFreqHi << 8) | _curFreqLo;
+ freq = (uint16)((int16)freq + _deltas[noteIdx]);
+ _curFreqLo = freq & 0xFF;
+ _curFreqHi = (freq >> 8) & 0xFF;
+
+ sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+ sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+ // Decrement duration
+ _durCounter--;
+ if (_durCounter != 0)
+ return;
+
+ // Duration expired: advance to next note
+ _freqIndex += 2;
+ _durIndex++;
+ _notesLeft--;
+
+ if (_notesLeft != 0) {
+ // Load next note parameters
+ noteIdx = _freqIndex / 2;
+ _durCounter = _durCopies[_durIndex];
+ _speedCounter = _speed;
+
+ // Load start frequency for next note
+ freq = (uint16)_startFreqs[noteIdx];
+ _curFreqLo = freq & 0xFF;
+ _curFreqHi = (freq >> 8) & 0xFF;
+ sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+ sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+ return;
+ }
+
+ // All notes done: check repeat
+ _repeatLeft--;
+ if (_repeatLeft != 0) {
+ // Restart sequence ($CD91): reload counters, keep same SFX data
+ _notesLeft = _numNotes - 1;
+ _freqIndex = 0;
+ _durIndex = 0;
+ _speedCounter = _speed;
+
+ // Load first note frequency
+ freq = (uint16)_startFreqs[0];
+ _curFreqLo = freq & 0xFF;
+ _curFreqHi = (freq >> 8) & 0xFF;
+ sidWrite(kDarkSIDV1FreqLo, _curFreqLo);
+ sidWrite(kDarkSIDV1FreqHi, _curFreqHi);
+
+ _durCounter = _durCopies[0];
+ return;
+ }
+
+ // Sequence finished: gate off
+ _state = 0;
+ sidWrite(kDarkSIDV1Ctrl, _waveform & 0xFE); // Clear gate bit
+}
+
+} // End of namespace Freescape
diff --git a/engines/freescape/games/dark/c64.sfx.h b/engines/freescape/games/dark/c64.sfx.h
new file mode 100644
index 00000000000..297347931d4
--- /dev/null
+++ b/engines/freescape/games/dark/c64.sfx.h
@@ -0,0 +1,131 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef FREESCAPE_DARK_C64_SFX_H
+#define FREESCAPE_DARK_C64_SFX_H
+
+#include "audio/sid.h"
+
+namespace Freescape {
+
+// SID register offsets (shared with Driller SFX player)
+enum DarkSIDRegs {
+ kDarkSIDV1FreqLo = 0x00,
+ kDarkSIDV1FreqHi = 0x01,
+ kDarkSIDV1PwLo = 0x02,
+ kDarkSIDV1PwHi = 0x03,
+ kDarkSIDV1Ctrl = 0x04,
+ kDarkSIDV1AD = 0x05,
+ kDarkSIDV1SR = 0x06,
+
+ kDarkSIDV2FreqLo = 0x07,
+ kDarkSIDV2FreqHi = 0x08,
+ kDarkSIDV2PwLo = 0x09,
+ kDarkSIDV2PwHi = 0x0A,
+ kDarkSIDV2Ctrl = 0x0B,
+ kDarkSIDV2AD = 0x0C,
+ kDarkSIDV2SR = 0x0D,
+
+ kDarkSIDV3FreqLo = 0x0E,
+ kDarkSIDV3FreqHi = 0x0F,
+ kDarkSIDV3PwLo = 0x10,
+ kDarkSIDV3PwHi = 0x11,
+ kDarkSIDV3Ctrl = 0x12,
+ kDarkSIDV3AD = 0x13,
+ kDarkSIDV3SR = 0x14,
+
+ kDarkSIDFilterLo = 0x15,
+ kDarkSIDFilterHi = 0x16,
+ kDarkSIDFilterCtrl = 0x17,
+ kDarkSIDVolume = 0x18
+};
+
+// 40-byte SFX descriptor from the data table at $C802 in dark2.prg
+struct DarkSideSFXData {
+ uint8 numNotes; // Number of frequency transitions
+ uint8 repeatCount; // Times to replay the full sequence
+ uint8 reserved;
+ uint8 freqWaypoints[20]; // Up to 10 frequency waypoints (lo,hi pairs)
+ uint8 padding; // Offset 23
+ uint8 durations[9]; // Duration for each transition (in speed units)
+ uint8 speed; // Frames per speed unit
+ uint8 padding2; // Offset 34
+ uint8 pwLo; // Pulse Width low byte
+ uint8 pwHi; // Pulse Width high byte
+ uint8 waveform; // SID control register (gate bit managed separately)
+ uint8 attackDecay; // SID Attack/Decay register
+ uint8 sustainRelease; // SID Sustain/Release register
+};
+
+class DarkSideC64SFXPlayer {
+public:
+ DarkSideC64SFXPlayer();
+ ~DarkSideC64SFXPlayer();
+
+ void playSfx(int sfxIndex);
+ void sfxTick();
+ void stopAllSfx();
+
+ bool isSfxActive() const;
+
+private:
+ SID::SID *_sid;
+
+ void sidWrite(int reg, uint8 data);
+ void initSID();
+ void onTimer();
+
+ // State machine ($C801 equivalent)
+ uint8 _state; // 0=off, 1=start, 2=slide
+
+ // Work buffer (copied from SFX data table)
+ uint8 _numNotes;
+ uint8 _repeatCount;
+ uint8 _waveform;
+ uint8 _speed;
+
+ // Runtime state
+ uint8 _repeatLeft; // $CE52: remaining sequence repeats
+ uint8 _notesLeft; // $CE55: remaining notes in this pass
+ uint8 _freqIndex; // $CE54: index into freq/delta arrays (by 2)
+ uint8 _durIndex; // $CE53: index into duration array (by 1)
+ uint8 _durCounter; // $CE4F: remaining ticks for current note
+ uint8 _speedCounter; // $CE56: frames until next freq update
+
+ // Current frequency (16-bit)
+ uint8 _curFreqLo; // $CE50
+ uint8 _curFreqHi; // $CE51
+
+ // Precomputed start frequencies and deltas per note
+ int16 _startFreqs[16]; // $CE97: starting frequency for each note (lo,hi)
+ int16 _deltas[16]; // $CEA6: per-speed-unit frequency delta per note
+ uint8 _durCopies[9]; // $CEB5: copy of durations per note
+
+ void silenceV1();
+ void silenceAll();
+ void setupSfx(int index);
+ void tickStart();
+ void tickSlide();
+};
+
+} // namespace Freescape
+
+#endif // FREESCAPE_DARK_C64_SFX_H
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index f84873213f2..06dcc0c8904 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -34,6 +34,9 @@
namespace Freescape {
DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
+ _playerC64Sfx = nullptr;
+ _playerC64Music = nullptr;
+
// These sounds can be overriden by the class of each platform
_soundIndexShoot = 1;
_soundIndexCollide = -1;
@@ -88,6 +91,11 @@ DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEn
_jetFuelSeconds = _initialEnergy * 6;
}
+DarkEngine::~DarkEngine() {
+ delete _playerC64Sfx;
+ delete _playerC64Music;
+}
+
void DarkEngine::addECDs(Area *area) {
if (!area->entranceWithID(255))
return;
@@ -304,6 +312,9 @@ void DarkEngine::initGameState() {
&_musicHandle, musicStream);
}
}
+
+ if (isC64() && _playerC64Music)
+ _playerC64Music->startMusic();
}
void DarkEngine::loadAssets() {
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index b2af7cd7923..b81d4649c14 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -21,6 +21,8 @@
#include "audio/mixer.h"
#include "common/array.h"
+#include "freescape/games/dark/c64.music.h"
+#include "freescape/games/dark/c64.sfx.h"
namespace Freescape {
@@ -49,6 +51,7 @@ enum DarkFontSize {
class DarkEngine : public FreescapeEngine {
public:
DarkEngine(OSystem *syst, const ADGameDescription *gd);
+ ~DarkEngine();
uint32 _initialEnergy;
uint32 _initialShield;
@@ -105,6 +108,10 @@ public:
int _soundIndexDestroyECD;
Audio::SoundHandle _soundFxHandleJetpack;
+ DarkSideC64SFXPlayer *_playerC64Sfx;
+ DarkSideC64MusicPlayer *_playerC64Music;
+ void playSoundC64(int index) override;
+
Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
void drawString(const DarkFontSize size, const Common::String &str, int x, int y, uint32 primaryColor, uint32 secondaryColor, uint32 backColor, Graphics::Surface *surface);
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 321ad4e2140..b4ba06e75ad 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -17,6 +17,8 @@ MODULE_OBJS := \
games/dark/amiga.o \
games/dark/atari.o \
games/dark/c64.o \
+ games/dark/c64.music.o \
+ games/dark/c64.sfx.o \
games/dark/cpc.o \
games/dark/dark.o \
games/dark/dos.o \
Commit: 1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f
https://github.com/scummvm/scummvm/commit/1dc0f0b1893a0064d7fa9c7f7b95de8388fe5d7f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-03-03T08:17:17+01:00
Commit Message:
FREESCAPE: allow to switch between music/sound in c64
Changed paths:
engines/freescape/games/dark/c64.cpp
engines/freescape/games/dark/c64.music.cpp
engines/freescape/games/dark/c64.music.h
engines/freescape/games/dark/c64.sfx.cpp
engines/freescape/games/dark/c64.sfx.h
engines/freescape/games/dark/dark.cpp
engines/freescape/games/dark/dark.h
engines/freescape/games/driller/c64.cpp
engines/freescape/games/driller/c64.music.cpp
engines/freescape/games/driller/c64.music.h
engines/freescape/games/driller/c64.sfx.cpp
engines/freescape/games/driller/c64.sfx.h
engines/freescape/games/driller/driller.cpp
engines/freescape/games/driller/driller.h
diff --git a/engines/freescape/games/dark/c64.cpp b/engines/freescape/games/dark/c64.cpp
index bd5f973747a..1d4b1d521d9 100644
--- a/engines/freescape/games/dark/c64.cpp
+++ b/engines/freescape/games/dark/c64.cpp
@@ -163,18 +163,38 @@ void DarkEngine::loadAssetsC64FullGame() {
_title = loadAndConvertDoodleImage(&file, &colorFile1, &colorFile2, (byte *)&kC64Palette);
- // TODO: SFX and music both need a SID instance, but only one can be active at a time.
- // Disable SFX for now so the music player can work.
- //_playerC64Sfx = new DarkSideC64SFXPlayer();
+ // Only one SID instance can be active at a time; music is the default.
+ // Create the inactive player first so its SID is destroyed before
+ // the active player's SID is created.
+ _playerC64Sfx = new DarkSideC64SFXPlayer();
+ _playerC64Sfx->destroySID();
_playerC64Music = new DarkSideC64MusicPlayer();
}
void DarkEngine::playSoundC64(int index) {
debugC(1, kFreescapeDebugMedia, "Playing Dark Side C64 SFX %d", index);
- if (_playerC64Sfx)
+ if (_playerC64Sfx && _c64UseSFX)
_playerC64Sfx->playSfx(index);
}
+void DarkEngine::toggleC64Sound() {
+ if (_c64UseSFX) {
+ if (_playerC64Sfx)
+ _playerC64Sfx->destroySID();
+ if (_playerC64Music) {
+ _playerC64Music->initSID();
+ _playerC64Music->startMusic();
+ }
+ _c64UseSFX = false;
+ } else {
+ if (_playerC64Music)
+ _playerC64Music->destroySID();
+ if (_playerC64Sfx)
+ _playerC64Sfx->initSID();
+ _c64UseSFX = true;
+ }
+}
+
void DarkEngine::drawC64UI(Graphics::Surface *surface) {
uint8 r, g, b;
uint32 front = _gfx->_texturePixelFormat.ARGBToColor(0xFF, 0xAA, 0xAA, 0xAA);
diff --git a/engines/freescape/games/dark/c64.music.cpp b/engines/freescape/games/dark/c64.music.cpp
index 9310b2d80a2..bc0a39a2e52 100644
--- a/engines/freescape/games/dark/c64.music.cpp
+++ b/engines/freescape/games/dark/c64.music.cpp
@@ -213,6 +213,7 @@ void DarkSideC64MusicPlayer::ChannelState::reset() {
}
DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
+ _sid = nullptr;
_musicActive = false;
_speedDiv = 1;
_speedCounter = 1;
@@ -222,12 +223,23 @@ DarkSideC64MusicPlayer::DarkSideC64MusicPlayer() {
}
DarkSideC64MusicPlayer::~DarkSideC64MusicPlayer() {
- if (_sid)
+ destroySID();
+}
+
+void DarkSideC64MusicPlayer::destroySID() {
+ if (_sid) {
_sid->stop();
- delete _sid;
+ delete _sid;
+ _sid = nullptr;
+ }
}
void DarkSideC64MusicPlayer::initSID() {
+ if (_sid) {
+ _sid->stop();
+ delete _sid;
+ }
+
_sid = SID::Config::create(SID::Config::kSidPAL);
if (!_sid || !_sid->init()) {
warning("DarkSideC64MusicPlayer: Failed to create SID emulator");
diff --git a/engines/freescape/games/dark/c64.music.h b/engines/freescape/games/dark/c64.music.h
index 78d2d2f7185..ffbd7a8427b 100644
--- a/engines/freescape/games/dark/c64.music.h
+++ b/engines/freescape/games/dark/c64.music.h
@@ -36,12 +36,13 @@ public:
void startMusic();
void stopMusic();
bool isPlaying() const;
+ void initSID();
+ void destroySID();
private:
SID::SID *_sid;
void sidWrite(int reg, uint8 data);
- void initSID();
void onTimer();
// Song setup
diff --git a/engines/freescape/games/dark/c64.sfx.cpp b/engines/freescape/games/dark/c64.sfx.cpp
index ee796b47d54..9ba591e692b 100644
--- a/engines/freescape/games/dark/c64.sfx.cpp
+++ b/engines/freescape/games/dark/c64.sfx.cpp
@@ -221,9 +221,14 @@ DarkSideC64SFXPlayer::DarkSideC64SFXPlayer()
}
DarkSideC64SFXPlayer::~DarkSideC64SFXPlayer() {
+ destroySID();
+}
+
+void DarkSideC64SFXPlayer::destroySID() {
if (_sid) {
_sid->stop();
delete _sid;
+ _sid = nullptr;
}
}
diff --git a/engines/freescape/games/dark/c64.sfx.h b/engines/freescape/games/dark/c64.sfx.h
index 297347931d4..58ec8fe9172 100644
--- a/engines/freescape/games/dark/c64.sfx.h
+++ b/engines/freescape/games/dark/c64.sfx.h
@@ -85,12 +85,13 @@ public:
void stopAllSfx();
bool isSfxActive() const;
+ void initSID();
+ void destroySID();
private:
SID::SID *_sid;
void sidWrite(int reg, uint8 data);
- void initSID();
void onTimer();
// State machine ($C801 equivalent)
diff --git a/engines/freescape/games/dark/dark.cpp b/engines/freescape/games/dark/dark.cpp
index 06dcc0c8904..25198558757 100644
--- a/engines/freescape/games/dark/dark.cpp
+++ b/engines/freescape/games/dark/dark.cpp
@@ -36,6 +36,7 @@ namespace Freescape {
DarkEngine::DarkEngine(OSystem *syst, const ADGameDescription *gd) : FreescapeEngine(syst, gd) {
_playerC64Sfx = nullptr;
_playerC64Music = nullptr;
+ _c64UseSFX = false;
// These sounds can be overriden by the class of each platform
_soundIndexShoot = 1;
@@ -971,12 +972,17 @@ void DarkEngine::drawInfoMenu() {
_eventManager->purgeKeyboardEvents();
saveGameDialog();
_gfx->setViewport(_viewArea);
+ } else if (isC64() && event.customType == kActionToggleSound) {
+ toggleC64Sound();
+ _eventManager->purgeKeyboardEvents();
} else if (isDOS() && event.customType == kActionToggleSound) {
playSound(6, true, _soundFxHandle);
+ _eventManager->purgeKeyboardEvents();
} else if (event.customType == kActionEscape) {
_forceEndGame = true;
cont = false;
- }
+ } else
+ cont = false;
break;
case Common::EVENT_KEYDOWN:
cont = false;
diff --git a/engines/freescape/games/dark/dark.h b/engines/freescape/games/dark/dark.h
index b81d4649c14..ea2f1e733a9 100644
--- a/engines/freescape/games/dark/dark.h
+++ b/engines/freescape/games/dark/dark.h
@@ -110,7 +110,9 @@ public:
DarkSideC64SFXPlayer *_playerC64Sfx;
DarkSideC64MusicPlayer *_playerC64Music;
+ bool _c64UseSFX;
void playSoundC64(int index) override;
+ void toggleC64Sound();
Common::Array<byte> _musicData; // HDSMUSIC.AM TEXT segment (Amiga)
diff --git a/engines/freescape/games/driller/c64.cpp b/engines/freescape/games/driller/c64.cpp
index a12e2733b4c..0af0b4588e3 100644
--- a/engines/freescape/games/driller/c64.cpp
+++ b/engines/freescape/games/driller/c64.cpp
@@ -158,10 +158,12 @@ void DrillerEngine::loadAssetsC64FullGame() {
} else
error("Unknown C64 release");
- // TODO: Re-enable music player once SFX coexistence is resolved.
- // The SID emulator enforces a singleton, so only one instance can exist.
- // _playerSid = new DrillerSIDPlayer();
+ // Only one SID instance can be active at a time; music is the default.
+ // Create the inactive player first so its SID is destroyed before
+ // the active player's SID is created.
_playerC64Sfx = new DrillerC64SFXPlayer();
+ _playerC64Sfx->destroySID();
+ _playerSid = new DrillerSIDPlayer();
// C64 SFX index mapping
// Based on analysis of the C64 binary SFX routines
@@ -185,10 +187,27 @@ void DrillerEngine::loadAssetsC64FullGame() {
void DrillerEngine::playSoundC64(int index) {
debugC(1, kFreescapeDebugMedia, "Playing C64 SFX %d", index);
- if (_playerC64Sfx)
+ if (_playerC64Sfx && _c64UseSFX)
_playerC64Sfx->playSfx(index);
}
+void DrillerEngine::toggleC64Sound() {
+ if (_c64UseSFX) {
+ if (_playerC64Sfx)
+ _playerC64Sfx->destroySID();
+ if (_playerSid) {
+ _playerSid->initSID();
+ _playerSid->startMusic();
+ }
+ _c64UseSFX = false;
+ } else {
+ if (_playerSid)
+ _playerSid->destroySID();
+ if (_playerC64Sfx)
+ _playerC64Sfx->initSID();
+ _c64UseSFX = true;
+ }
+}
void DrillerEngine::drawC64UI(Graphics::Surface *surface) {
diff --git a/engines/freescape/games/driller/c64.music.cpp b/engines/freescape/games/driller/c64.music.cpp
index 68661ca076d..90eac0d1968 100644
--- a/engines/freescape/games/driller/c64.music.cpp
+++ b/engines/freescape/games/driller/c64.music.cpp
@@ -192,12 +192,16 @@ DrillerSIDPlayer::DrillerSIDPlayer() : _sid(nullptr),
}
DrillerSIDPlayer::~DrillerSIDPlayer() {
+ destroySID();
+ debug(DEBUG_LEVEL >= 1, "Driller SID Player Destroyed");
+}
+
+void DrillerSIDPlayer::destroySID() {
if (_sid) {
_sid->stop();
delete _sid;
+ _sid = nullptr;
}
-
- debug(DEBUG_LEVEL >= 1, "Driller SID Player Destroyed");
}
// Tune 0 seems unused, Tune 1 is the main theme
diff --git a/engines/freescape/games/driller/c64.music.h b/engines/freescape/games/driller/c64.music.h
index 57168d0e2d1..4cdcc587f5d 100644
--- a/engines/freescape/games/driller/c64.music.h
+++ b/engines/freescape/games/driller/c64.music.h
@@ -220,10 +220,11 @@ public:
~DrillerSIDPlayer();
void startMusic(int tuneIndex = 1);
void stopMusic();
+ void initSID();
+ void destroySID();
private:
void SID_Write(int reg, uint8_t data);
- void initSID();
void onTimer();
void handleChangeTune(int tuneIndex);
void handleResetVoices();
diff --git a/engines/freescape/games/driller/c64.sfx.cpp b/engines/freescape/games/driller/c64.sfx.cpp
index 2a6cf712a0c..5a494e0f7e1 100644
--- a/engines/freescape/games/driller/c64.sfx.cpp
+++ b/engines/freescape/games/driller/c64.sfx.cpp
@@ -39,9 +39,14 @@ DrillerC64SFXPlayer::DrillerC64SFXPlayer()
}
DrillerC64SFXPlayer::~DrillerC64SFXPlayer() {
+ destroySID();
+}
+
+void DrillerC64SFXPlayer::destroySID() {
if (_sid) {
_sid->stop();
delete _sid;
+ _sid = nullptr;
}
}
diff --git a/engines/freescape/games/driller/c64.sfx.h b/engines/freescape/games/driller/c64.sfx.h
index 325cb4b3a7f..e63b4201323 100644
--- a/engines/freescape/games/driller/c64.sfx.h
+++ b/engines/freescape/games/driller/c64.sfx.h
@@ -68,12 +68,13 @@ public:
void stopAllSfx();
bool isSfxActive() const;
+ void initSID();
+ void destroySID();
private:
SID::SID *_sid;
void sidWrite(int reg, uint8 data);
- void initSID();
// Voice 1 pitch slide state ($CC5B-$CC61)
uint8 _v1Counter; // 0xFF=inactive, 0=expired (marked 0xFF next tick)
diff --git a/engines/freescape/games/driller/driller.cpp b/engines/freescape/games/driller/driller.cpp
index 3a1fa4de6f8..fa4691b37a5 100644
--- a/engines/freescape/games/driller/driller.cpp
+++ b/engines/freescape/games/driller/driller.cpp
@@ -98,6 +98,7 @@ DrillerEngine::DrillerEngine(OSystem *syst, const ADGameDescription *gd) : Frees
_borderExtraTexture = nullptr;
_playerSid = nullptr;
_playerC64Sfx = nullptr;
+ _c64UseSFX = false;
}
DrillerEngine::~DrillerEngine() {
@@ -265,8 +266,8 @@ void DrillerEngine::gotoArea(uint16 areaID, int entranceID) {
if (areaID == _startArea && entranceID == _startEntrance) {
if (isC64()) {
- // TODO: Re-enable music once SFX coexistence is resolved
- // _playerSid->startMusic();
+ if (!_c64UseSFX && _playerSid)
+ _playerSid->startMusic();
playSound(_soundIndexStart, true, _soundFxHandle);
} else {
playSound(_soundIndexStart, true, _soundFxHandle);
@@ -427,8 +428,12 @@ void DrillerEngine::drawInfoMenu() {
} else if (isSpectrum()) {
drawStringInSurface("l-load s-save 1-abort", 76, 97, front, black, surface);
drawStringInSurface("any other key-continue", 76, 105, front, black, surface);
- } else if (isAmiga() || isAtariST())
+ } else if (isAmiga() || isAtariST()) {
drawStringInSurface("press any key to continue", 66, 97, front, black, surface);
+ } else if (isC64()) {
+ drawStringInSurface("l-load s-save run/stop-abort", 76, 97, front, black, surface);
+ drawStringInSurface("t-toggle effect/music", 76, 105, front, black, surface);
+ }
Texture *menuTexture = _gfx->createTexture(surface);
Common::Event event;
@@ -449,6 +454,8 @@ void DrillerEngine::drawInfoMenu() {
_eventManager->purgeKeyboardEvents();
saveGameDialog();
_gfx->setViewport(_viewArea);
+ } else if (isC64() && event.customType == kActionToggleSound) {
+ toggleC64Sound();
} else if (isDOS() && event.customType == kActionToggleSound) {
// TODO
} else if ((isDOS() || isCPC() || isSpectrum()) && event.customType == kActionEscape) {
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index 167ba9d2bcb..52ea8da286d 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -47,8 +47,10 @@ public:
DrillerSIDPlayer *_playerSid;
DrillerC64SFXPlayer *_playerC64Sfx;
+ bool _c64UseSFX;
void playSoundC64(int index) override;
+ void toggleC64Sound();
// Only used for Amiga and Atari ST
Font _fontSmall;
More information about the Scummvm-git-logs
mailing list