[Scummvm-git-logs] scummvm master -> 46c8cab355c5c9bae6216ec544a0a8eb3b3e2a24
neuromancer
noreply at scummvm.org
Fri Feb 6 20:03:18 UTC 2026
This automated email contains information about 20 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
b044ef71b1 FREESCAPE: reeimplemented hardcoded sounds for driller (ZX)
4788c07e3b AUDIO: added initial implementation of ay8912 emulation
7938513f6d AUDIO: added initial implementation of ay8912 emulation
dbf0d70fd5 FREESCAPE: initial implementation of driller audio for cpc
c607200d65 FREESCAPE: improve sounds for driller cpc
c5546d8ea5 FREESCAPE: more cpc sounds
a089f5cf42 FRESCAPE: more precise driller sounds for CPC
75a50512dd FREESCAPE: even more precise driller sounds for CPC
f584c68503 FREESCAPE: unified sound approach for some driller sounds for CPC
799b2e1eae FREESCAPE: unified sound approach for some driller sounds for CPC
88d7eec531 FREESCAPE: better cpc sounds
11228047db FREESCAPE: matched sound 1 for driller (cpc)
f3935288d2 FREESCAPE: matched most of the sounds for driller (cpc)
e774ae2072 FREESCAPE: some additional sounds for driller (cpc)
90d82c4869 FREESCAPE: read driller cpc sound tables from the executable
89bdf28c35 FREESCAPE: fixes in sound handling
d2e61f0f42 FREESCAPE: improve the sound mapping for Driller CPC
91075a7566 FREESCAPE: implement sound for Dark Side CPC
e860d27488 FREESCAPE: implement sound for Total Eclipse CPC (demo)
46c8cab355 FREESCAPE: inherit from EmulatedChip
Commit: b044ef71b1b616bb3b1305d9af5f27bb47c7ec91
https://github.com/scummvm/scummvm/commit/b044ef71b1b616bb3b1305d9af5f27bb47c7ec91
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: reeimplemented hardcoded sounds for driller (ZX)
Changed paths:
engines/freescape/freescape.h
engines/freescape/games/driller/zx.cpp
engines/freescape/sound.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 2e10f5e1f21..36cd94434bd 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -487,6 +487,7 @@ public:
uint16 playSoundDOSSpeaker(uint16 startFrequency, soundSpeakerFx *speakerFxInfo);
void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
+ void playSoundDrillerZX(int index, Audio::SoundHandle &handle);
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/zx.cpp b/engines/freescape/games/driller/zx.cpp
index b42bbba4bcd..c0b7142dac4 100644
--- a/engines/freescape/games/driller/zx.cpp
+++ b/engines/freescape/games/driller/zx.cpp
@@ -28,7 +28,15 @@ namespace Freescape {
void DrillerEngine::initZX() {
_viewArea = Common::Rect(56, 20, 264, 124);
+ _soundIndexShoot = 1;
+ _soundIndexCollide = 2;
+ _soundIndexStepUp = 3;
+ _soundIndexStepDown = 3;
+ _soundIndexMenu = 6;
_soundIndexAreaChange = 10;
+ _soundIndexHit = 7;
+ _soundIndexFallen = 9;
+ _soundIndexMissionComplete = 13;
}
void DrillerEngine::loadAssetsZXFullGame() {
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
index f4eb3107920..eff38dace01 100644
--- a/engines/freescape/sound.cpp
+++ b/engines/freescape/sound.cpp
@@ -324,8 +324,11 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d is not available", index);
return;
- } else if (isSpectrum() && !isDriller()) {
- playSoundZX(_soundsSpeakerFxZX[index], handle);
+ } else if (isSpectrum()) {
+ if (isDriller())
+ playSoundDrillerZX(index, handle);
+ else
+ playSoundZX(_soundsSpeakerFxZX[index], handle);
return;
}
@@ -567,4 +570,102 @@ void FreescapeEngine::loadSoundsFx(Common::SeekableReadStream *file, int offset,
}
}
+void FreescapeEngine::playSoundDrillerZX(int index, Audio::SoundHandle &handle) {
+ debugC(1, kFreescapeDebugMedia, "Playing Driller ZX sound %d", index);
+ Common::Array<soundUnitZX> soundUnits;
+
+ auto addTone = [&](uint16 hl, uint16 de, float multiplier) {
+ soundUnitZX s;
+ s.isRaw = false;
+ s.tStates = hl; // HL determines period
+ s.freqTimesSeconds = de; // DE determines duration (number of cycles)
+ s.multiplier = multiplier;
+ soundUnits.push_back(s);
+ };
+
+ // Linear Sweep: Period increases -> Pitch decreases
+ auto addSweep = [&](uint16 startHl, uint16 endHl, uint16 step, uint16 duration) {
+ for (uint16 hl = startHl; hl < endHl; hl += step) {
+ addTone(hl, duration, 10.0f);
+ }
+ };
+
+ // Zap effect: Decreasing Period (E decrements) -> Pitch increases
+ auto addZap = [&](uint16 startE, uint16 endE, uint16 duration) {
+ for (uint16 e = startE; e > endE; e--) {
+ // Map E (delay loops) to HL (tStates)
+ // Small E -> Short Period -> High Freq
+ uint16 hl = (24 + e) * 4;
+ addTone(hl, duration, 10.0f);
+ }
+ };
+
+ // Sweep Down: Increasing Period (E increments) -> Pitch decreases
+ auto addSweepDown = [&](uint16 startE, uint16 endE, uint16 step, uint16 duration, float multiplier) {
+ for (uint16 e = startE; e < endE; e += step) {
+ uint16 hl = (24 + e) * 4;
+ addTone(hl, duration, multiplier);
+ }
+ };
+
+ switch (index) {
+ case 1: // Shoot (FUN_95A1 -> 95AF)
+ // Laser: High Pitch -> Low Pitch
+ // Adjusted pitch to be even lower (0x200-0x600 is approx 850Hz-280Hz)
+ addSweepDown(0x200, 0x600, 20, 1, 2.0f);
+ break;
+ case 2: // Collide/Bump (FUN_95DE)
+ // Low tone sequence
+ addTone(0x93c, 0x40, 10.0f); // 64 cycles ~340ms
+ addTone(0x7a6, 0x30, 10.0f); // 48 cycles
+ break;
+ case 3: // Step (FUN_95E5)
+ // Short blip
+ // Increased duration significantly again (0xC0 = 192 cycles)
+ addTone(0x7a6, 0xC0, 10.0f);
+ break;
+ case 4: // Silence (FUN_95F7)
+ break;
+ case 5: // Area Change? (FUN_95F8)
+ addTone(0x1f0, 0x60, 10.0f); // High pitch, longer
+ break;
+ case 6: // Menu (Silence?) (FUN_9601)
+ break;
+ case 7: // Hit? (Sweep FUN_9605)
+ // Sweep down (Period increases)
+ addSweep(0x200, 0xC00, 64, 2);
+ break;
+ case 8: // Zap (FUN_961F)
+ // Zap: Low -> High
+ addZap(0xFF, 0x10, 2);
+ break;
+ case 9: // Sweep (FUN_9673)
+ addSweep(0x100, 0x600, 16, 4);
+ break;
+ case 10: // Area Change (FUN_9696)
+ addSweep(0x100, 0x500, 16, 4);
+ break;
+ case 11: // Explosion (FUN_96B9)
+ {
+ soundUnitZX s;
+ s.isRaw = true;
+ s.rawFreq = 0.0f; // Noise
+ s.rawLengthus = 100000; // 100ms noise
+ soundUnits.push_back(s);
+ }
+ break;
+ case 12: // Sweep Down (FUN_96E4)
+ addSweepDown(0x01, 0xFF, 1, 2, 10.0f);
+ break;
+ case 13: // Fall? (FUN_96FD)
+ addSweep(300, 800, 16, 2);
+ break;
+ default:
+ debugC(1, kFreescapeDebugMedia, "Unknown Driller ZX sound %d", index);
+ break;
+ }
+
+ playSoundZX(&soundUnits, handle);
+}
+
} // namespace Freescape
Commit: 4788c07e3b731245807204ce9dcce3be228f9742
https://github.com/scummvm/scummvm/commit/4788c07e3b731245807204ce9dcce3be228f9742
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
AUDIO: added initial implementation of ay8912 emulation
Changed paths:
A audio/softsynth/ay8912.cpp
A audio/softsynth/ay8912.h
diff --git a/audio/softsynth/ay8912.cpp b/audio/softsynth/ay8912.cpp
new file mode 100644
index 00000000000..f55a22bb137
--- /dev/null
+++ b/audio/softsynth/ay8912.cpp
@@ -0,0 +1,291 @@
+/* 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 "audio/softsynth/ay8912.h"
+#include "common/util.h"
+
+namespace Audio {
+
+bool AY8912Stream::_envGenInit = false;
+int AY8912Stream::_envelope[16][128];
+
+/* AY volume table (c) by V_Soft and Lion 17 */
+static int Lion17_AY_table[32] = {
+ 0, 513, 828, 1239, 1923, 3238, 4926, 9110,
+ 10344, 17876, 24682, 30442, 38844, 47270, 56402, 65535,
+ // Duplicate for safety as the C code used 32 size array but init with 16 elements and logic uses /2
+ 0, 513, 828, 1239, 1923, 3238, 4926, 9110,
+ 10344, 17876, 24682, 30442, 38844, 47270, 56402, 65535
+};
+
+/* default equlaizer (layout) settings for AY, ABC stereo */
+static const int default_layout_ay_abc[6] = {
+ 100, 33, 70, 70, 33, 100
+};
+
+AY8912Stream::AY8912Stream(int rate, int chipFreq) : _rate(rate), _chipFreq(chipFreq) {
+ if (!_envGenInit)
+ genEnv();
+
+ // Reset state
+ _bit_a = _bit_b = _bit_c = _bit_n = 0;
+ _cnt_a = _cnt_b = _cnt_c = _cnt_n = _cnt_e = 0;
+ _envPos = 0;
+ _curSeed = 0xffff;
+
+ // Reset registers
+ _regs.tone_a = _regs.tone_b = _regs.tone_c = 0;
+ _regs.noise = 0;
+ _regs.R7_tone_a = _regs.R7_tone_b = _regs.R7_tone_c = 0;
+ _regs.R7_noise_a = _regs.R7_noise_b = _regs.R7_noise_c = 0;
+ _regs.vol_a = _regs.vol_b = _regs.vol_c = 0;
+ _regs.env_a = _regs.env_b = _regs.env_c = 0;
+ _regs.env_freq = _regs.env_style = 0;
+
+ // Initialize table and eq
+ for (int i = 0; i < 32; i++)
+ _table[i] = Lion17_AY_table[i/2]; // AYEMU_AY style
+
+ for (int i = 0; i < 6; i++)
+ _eq[i] = default_layout_ay_abc[i]; // AYEMU_ABC style
+
+ prepareGeneration();
+}
+
+AY8912Stream::~AY8912Stream() {
+}
+
+void AY8912Stream::genEnv() {
+ int env;
+ int pos;
+ int hold;
+ int dir;
+ int vol;
+
+ for (env = 0; env < 16; env++) {
+ hold = 0;
+ dir = (env & 4) ? 1 : -1;
+ vol = (env & 4) ? -1 : 32;
+ for (pos = 0; pos < 128; pos++) {
+ if (!hold) {
+ vol += dir;
+ if (vol < 0 || vol >= 32) {
+ if (env & 8) {
+ if (env & 2)
+ dir = -dir;
+ vol = (dir > 0) ? 0 : 31;
+ if (env & 1) {
+ hold = 1;
+ vol = (dir > 0) ? 31 : 0;
+ }
+ } else {
+ vol = 0;
+ hold = 1;
+ }
+ }
+ }
+ _envelope[env][pos] = vol;
+ }
+ }
+ _envGenInit = true;
+}
+
+void AY8912Stream::prepareGeneration() {
+ int vol, max_l, max_r;
+
+ _chipTactsPerOutcount = _chipFreq / _rate / 8;
+
+ // GenVols
+ for (int n = 0; n < 32; n++) {
+ vol = _table[n];
+ for (int m = 0; m < 6; m++)
+ _vols[m][n] = (int)(((double)vol * _eq[m]) / 100);
+ }
+
+ max_l = _vols[0][31] + _vols[2][31] + _vols[3][31];
+ max_r = _vols[1][31] + _vols[3][31] + _vols[5][31];
+ vol = (max_l > max_r) ? max_l : max_r;
+ _ampGlobal = _chipTactsPerOutcount * vol / 24575; // AYEMU_MAX_AMP
+}
+
+void AY8912Stream::setRegs(const unsigned char *regs) {
+ Common::StackLock lock(_mutex);
+
+ _regs.tone_a = regs[0] + ((regs[1] & 0x0f) << 8);
+ _regs.tone_b = regs[2] + ((regs[3] & 0x0f) << 8);
+ _regs.tone_c = regs[4] + ((regs[5] & 0x0f) << 8);
+
+ _regs.noise = regs[6] & 0x1f;
+
+ _regs.R7_tone_a = !(regs[7] & 0x01);
+ _regs.R7_tone_b = !(regs[7] & 0x02);
+ _regs.R7_tone_c = !(regs[7] & 0x04);
+
+ _regs.R7_noise_a = !(regs[7] & 0x08);
+ _regs.R7_noise_b = !(regs[7] & 0x10);
+ _regs.R7_noise_c = !(regs[7] & 0x20);
+
+ _regs.vol_a = regs[8] & 0x0f;
+ _regs.vol_b = regs[9] & 0x0f;
+ _regs.vol_c = regs[10] & 0x0f;
+ _regs.env_a = regs[8] & 0x10;
+ _regs.env_b = regs[9] & 0x10;
+ _regs.env_c = regs[10] & 0x10;
+ _regs.env_freq = regs[11] + (regs[12] << 8);
+
+ if (regs[13] != 0xff) {
+ _regs.env_style = regs[13] & 0x0f;
+ _envPos = _cnt_e = 0;
+ }
+}
+
+void AY8912Stream::setReg(int reg, unsigned char value) {
+ Common::StackLock lock(_mutex);
+
+ switch (reg) {
+ case 0:
+ _regs.tone_a = (_regs.tone_a & 0x0f00) | value;
+ break;
+ case 1:
+ _regs.tone_a = (_regs.tone_a & 0x00ff) | ((value & 0x0f) << 8);
+ break;
+ case 2:
+ _regs.tone_b = (_regs.tone_b & 0x0f00) | value;
+ break;
+ case 3:
+ _regs.tone_b = (_regs.tone_b & 0x00ff) | ((value & 0x0f) << 8);
+ break;
+ case 4:
+ _regs.tone_c = (_regs.tone_c & 0x0f00) | value;
+ break;
+ case 5:
+ _regs.tone_c = (_regs.tone_c & 0x00ff) | ((value & 0x0f) << 8);
+ break;
+ case 6:
+ _regs.noise = value & 0x1f;
+ break;
+ case 7:
+ _regs.R7_tone_a = !(value & 0x01);
+ _regs.R7_tone_b = !(value & 0x02);
+ _regs.R7_tone_c = !(value & 0x04);
+
+ _regs.R7_noise_a = !(value & 0x08);
+ _regs.R7_noise_b = !(value & 0x10);
+ _regs.R7_noise_c = !(value & 0x20);
+ break;
+ case 8:
+ _regs.vol_a = value & 0x0f;
+ _regs.env_a = value & 0x10;
+ break;
+ case 9:
+ _regs.vol_b = value & 0x0f;
+ _regs.env_b = value & 0x10;
+ break;
+ case 10:
+ _regs.vol_c = value & 0x0f;
+ _regs.env_c = value & 0x10;
+ break;
+ case 11:
+ _regs.env_freq = (_regs.env_freq & 0xff00) | value;
+ break;
+ case 12:
+ _regs.env_freq = (_regs.env_freq & 0x00ff) | (value << 8);
+ break;
+ case 13:
+ _regs.env_style = value & 0x0f;
+ _envPos = _cnt_e = 0;
+ break;
+ }
+}
+
+int AY8912Stream::readBuffer(int16 *buffer, const int numSamples) {
+ Common::StackLock lock(_mutex);
+
+ int mix_l, mix_r;
+ int tmpvol;
+ int m;
+ int frame_count = numSamples / 2; // Stereo samples
+
+ int16 *bufPtr = buffer;
+
+ while (frame_count-- > 0) {
+ mix_l = mix_r = 0;
+
+ for (m = 0; m < _chipTactsPerOutcount; m++) {
+ if (++_cnt_a >= _regs.tone_a) {
+ _cnt_a = 0;
+ _bit_a = !_bit_a;
+ }
+ if (++_cnt_b >= _regs.tone_b) {
+ _cnt_b = 0;
+ _bit_b = !_bit_b;
+ }
+ if (++_cnt_c >= _regs.tone_c) {
+ _cnt_c = 0;
+ _bit_c = !_bit_c;
+ }
+
+ if (++_cnt_n >= (_regs.noise * 2)) {
+ _cnt_n = 0;
+ _curSeed = (_curSeed * 2 + 1) ^ (((_curSeed >> 16) ^ (_curSeed >> 13)) & 1);
+ _bit_n = ((_curSeed >> 16) & 1);
+ }
+
+ if (++_cnt_e >= _regs.env_freq) {
+ _cnt_e = 0;
+ if (++_envPos > 127)
+ _envPos = 64;
+ }
+
+ int envVol = _envelope[_regs.env_style][_envPos];
+
+ if ((_bit_a | !_regs.R7_tone_a) & (_bit_n | !_regs.R7_noise_a)) {
+ tmpvol = (_regs.env_a) ? envVol : _regs.vol_a * 2 + 1;
+ mix_l += _vols[0][tmpvol];
+ mix_r += _vols[1][tmpvol];
+ }
+
+ if ((_bit_b | !_regs.R7_tone_b) & (_bit_n | !_regs.R7_noise_b)) {
+ tmpvol = (_regs.env_b) ? envVol : _regs.vol_b * 2 + 1;
+ mix_l += _vols[2][tmpvol];
+ mix_r += _vols[3][tmpvol];
+ }
+
+ if ((_bit_c | !_regs.R7_tone_c) & (_bit_n | !_regs.R7_noise_c)) {
+ tmpvol = (_regs.env_c) ? envVol : _regs.vol_c * 2 + 1;
+ mix_l += _vols[4][tmpvol];
+ mix_r += _vols[5][tmpvol];
+ }
+ }
+
+ if (_ampGlobal > 0) {
+ mix_l /= _ampGlobal;
+ mix_r /= _ampGlobal;
+ }
+
+ *bufPtr++ = mix_l;
+ *bufPtr++ = mix_r;
+ }
+
+ return numSamples;
+}
+
+} // End of namespace Audio
diff --git a/audio/softsynth/ay8912.h b/audio/softsynth/ay8912.h
new file mode 100644
index 00000000000..f738c2af73a
--- /dev/null
+++ b/audio/softsynth/ay8912.h
@@ -0,0 +1,107 @@
+/* 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 AUDIO_SOFTSYNTH_AY8912_H
+#define AUDIO_SOFTSYNTH_AY8912_H
+
+#include "audio/audiostream.h"
+#include "common/mutex.h"
+
+namespace Audio {
+
+class AY8912Stream : public AudioStream {
+public:
+ enum ChipType {
+ AY_TYPE_AY,
+ AY_TYPE_YM
+ };
+
+ enum StereoType {
+ AY_MONO = 0,
+ AY_ABC,
+ AY_ACB,
+ AY_BAC,
+ AY_BCA,
+ AY_CAB,
+ AY_CBA
+ };
+
+ AY8912Stream(int rate = 44100, int chipFreq = 1773400);
+ ~AY8912Stream();
+
+ // AudioStream interface
+ int readBuffer(int16 *buffer, const int numSamples) override;
+ bool isStereo() const override { return true; }
+ bool endOfData() const override { return false; }
+ bool endOfStream() const override { return false; }
+ int getRate() const override { return _rate; }
+
+ void setReg(int reg, unsigned char value);
+ void setRegs(const unsigned char *regs);
+
+private:
+ struct RegData {
+ int tone_a;
+ int tone_b;
+ int tone_c;
+ int noise;
+ int R7_tone_a;
+ int R7_tone_b;
+ int R7_tone_c;
+ int R7_noise_a;
+ int R7_noise_b;
+ int R7_noise_c;
+ int vol_a;
+ int vol_b;
+ int vol_c;
+ int env_a;
+ int env_b;
+ int env_c;
+ int env_freq;
+ int env_style;
+ };
+
+ Common::Mutex _mutex;
+ int _rate;
+ int _chipFreq;
+
+ // Emulator state
+ int _table[32];
+ int _eq[6];
+ RegData _regs;
+
+ int _bit_a, _bit_b, _bit_c, _bit_n;
+ int _cnt_a, _cnt_b, _cnt_c, _cnt_n, _cnt_e;
+ int _chipTactsPerOutcount;
+ int _ampGlobal;
+ int _vols[6][32];
+ int _envPos;
+ unsigned int _curSeed;
+
+ void prepareGeneration();
+ static void genEnv();
+ static bool _envGenInit;
+ static int _envelope[16][128];
+};
+
+} // End of namespace Audio
+
+#endif // AUDIO_SOFTSYNTH_AY8912_H
Commit: 7938513f6d803032a7b7bbb6885081b2501e0abe
https://github.com/scummvm/scummvm/commit/7938513f6d803032a7b7bbb6885081b2501e0abe
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
AUDIO: added initial implementation of ay8912 emulation
Changed paths:
audio/module.mk
diff --git a/audio/module.mk b/audio/module.mk
index 0a3720755ae..ebafc0b3fb5 100644
--- a/audio/module.mk
+++ b/audio/module.mk
@@ -67,7 +67,8 @@ MODULE_OBJS := \
softsynth/appleiigs.o \
softsynth/fluidsynth.o \
softsynth/eas.o \
- softsynth/pcspk.o
+ softsynth/pcspk.o \
+ softsynth/ay8912.o
ifndef DISABLE_NUKED_OPL
MODULE_OBJS += \
Commit: dbf0d70fd5869c609408cb7ca9180ad5a5a58e20
https://github.com/scummvm/scummvm/commit/dbf0d70fd5869c609408cb7ca9180ad5a5a58e20
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: initial implementation of driller audio for cpc
Changed paths:
engines/freescape/freescape.h
engines/freescape/games/driller/cpc.cpp
engines/freescape/sound.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 36cd94434bd..4a56826babd 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -488,6 +488,7 @@ public:
void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
void playSoundDrillerZX(int index, Audio::SoundHandle &handle);
+ void playSoundDrillerCPC(int index, Audio::SoundHandle &handle);
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/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 138875d3204..b468307e5b8 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -30,6 +30,15 @@ namespace Freescape {
void DrillerEngine::initCPC() {
_viewArea = Common::Rect(36, 16, 284, 117);
+ _soundIndexShoot = 1;
+ _soundIndexCollide = 2;
+ _soundIndexStepUp = 3;
+ _soundIndexStepDown = 3;
+ _soundIndexMenu = 6;
+ _soundIndexAreaChange = 10;
+ _soundIndexHit = 7;
+ _soundIndexFallen = 9;
+ _soundIndexMissionComplete = 13;
}
byte kCPCPaletteTitleData[4][3] = {
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
index eff38dace01..53b03092cd0 100644
--- a/engines/freescape/sound.cpp
+++ b/engines/freescape/sound.cpp
@@ -22,6 +22,7 @@
#include "common/file.h"
#include "audio/audiostream.h"
#include "audio/decoders/raw.h"
+#include "audio/softsynth/ay8912.h"
#include "freescape/freescape.h"
#include "freescape/games/eclipse/eclipse.h"
@@ -330,75 +331,17 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
else
playSoundZX(_soundsSpeakerFxZX[index], handle);
return;
+ } else if (isCPC()) {
+ if (isDriller())
+ playSoundDrillerCPC(index, handle);
+ // else playSoundCPC(...)
+ return;
}
Common::Path filename;
filename = Common::String::format("%s-%d.wav", _targetName.c_str(), index);
debugC(1, kFreescapeDebugMedia, "Playing sound %s", filename.toString().c_str());
playWav(filename);
- /*switch (index) {
- case 1:
- playWav("fsDOS_laserFire.wav");
- break;
- case 2: // Done
- playWav("fsDOS_WallBump.wav");
- break;
- case 3:
- playWav("fsDOS_stairDown.wav");
- break;
- case 4:
- playWav("fsDOS_stairUp.wav");
- break;
- case 5:
- playWav("fsDOS_roomChange.wav");
- break;
- case 6:
- playWav("fsDOS_configMenu.wav");
- break;
- case 7:
- playWav("fsDOS_bigHit.wav");
- break;
- case 8:
- playWav("fsDOS_teleporterActivated.wav");
- break;
- case 9:
- playWav("fsDOS_powerUp.wav");
- break;
- case 10:
- playWav("fsDOS_energyDrain.wav");
- break;
- case 11: // ???
- debugC(1, kFreescapeDebugMedia, "Playing unknown sound");
- break;
- case 12:
- playWav("fsDOS_switchOff.wav");
- break;
- case 13: // Seems to be repeated?
- playWav("fsDOS_laserHit.wav");
- break;
- case 14:
- playWav("fsDOS_tankFall.wav");
- break;
- case 15:
- playWav("fsDOS_successJingle.wav");
- break;
- case 16: // Silence?
- break;
- case 17:
- playWav("fsDOS_badJingle.wav");
- break;
- case 18: // Silence?
- break;
- case 19:
- debugC(1, kFreescapeDebugMedia, "Playing unknown sound");
- break;
- case 20:
- playWav("fsDOS_bigHit.wav");
- break;
- default:
- debugC(1, kFreescapeDebugMedia, "Unexpected sound %d", index);
- break;
- }*/
_syncSound = sync;
}
void FreescapeEngine::playWav(const Common::Path &filename) {
@@ -668,4 +611,164 @@ void FreescapeEngine::playSoundDrillerZX(int index, Audio::SoundHandle &handle)
playSoundZX(&soundUnits, handle);
}
+class DrillerCPCSfxStream : public Audio::AudioStream {
+public:
+ DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) { // 1MHz for CPC AY
+ // Initialize sound chip (silence)
+ initAY();
+
+ _counter = 0;
+ _finished = false;
+ }
+
+ void initAY() {
+ // Silence all channels
+ _ay.setReg(7, 0xFF); // Disable all tones and noise
+ _ay.setReg(8, 0); // Volume A 0
+ _ay.setReg(9, 0); // Volume B 0
+ _ay.setReg(10, 0); // Volume C 0
+ }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ if (_finished)
+ return 0;
+
+ // We need to generate samples from AY
+ // And update the AY state periodically
+
+ // Simulate 50Hz updates
+ int samplesPerTick = _rate / 50;
+ int samplesGenerated = 0;
+
+ while (samplesGenerated < numSamples && !_finished) {
+ int samplesTodo = MIN(numSamples - samplesGenerated, samplesPerTick);
+
+ // Update AY state (simulate FUN_4760 tick)
+ updateState();
+
+ // Generate audio
+ _ay.readBuffer(buffer + samplesGenerated, samplesTodo);
+ samplesGenerated += samplesTodo;
+
+ if (_finished) break;
+ }
+
+ return samplesGenerated;
+ }
+
+ bool isStereo() const override { return true; }
+ bool endOfData() const override { return _finished; }
+ bool endOfStream() const override { return _finished; }
+ int getRate() const override { return _rate; }
+
+private:
+ Audio::AY8912Stream _ay;
+ int _index;
+ int _rate;
+ int _counter;
+ bool _finished;
+
+ void updateState() {
+ // Simulation of Driller CPC sound effects
+ // Based on analysis of FUN_4581 and FUN_4760
+ // CPC AY-3-8912 Clock is 1MHz
+
+ _counter++;
+
+ switch (_index) {
+ case 1: // Shoot
+ // Related to FUN_43E2 (0x43E2) and FUN_5A21 (0x5A21)
+ // The shoot sound logic involves setting up a channel structure and updating it.
+ // FUN_4760 checks 0x3a64 bit 4 and calls FUN_43E2 three times (3 channels?).
+
+ // 0x5A21: LD A,(IX+0x5); ADD A,(IX+0x1); AND 0xf; LD (IX+0x5),A
+ // This suggests a volume or parameter update loop.
+
+ // Implementation: Fast frequency sweep down with decay.
+ // Replicating the "Laser" effect likely produced by the hardware envelope or software sweep.
+
+ if (_counter > 12) {
+ _finished = true;
+ } else {
+ // Sweep logic
+ // Start Frequency High (Period Low) -> End Frequency Low (Period High)
+ int period = 20 + _counter * 30;
+
+ // Apply to Channel A (Reg 0, 1)
+ _ay.setReg(0, period & 0xff);
+ _ay.setReg(1, (period >> 8) & 0xf);
+
+ // Apply to Channel B (Reg 2, 3) - Detuned for thickness
+ _ay.setReg(2, (period + 5) & 0xff);
+ _ay.setReg(3, ((period + 5) >> 8) & 0xf);
+
+ // Volume Decay (Software Envelope)
+ // 0x5A21 suggests updating IX+0x5 (Volume?)
+ int vol = 15 - _counter;
+ if (vol < 0) vol = 0;
+
+ _ay.setReg(8, vol);
+ _ay.setReg(9, vol);
+
+ // Enable Tone A and B (Reg 7)
+ // 0x3C = 0011 1100 (Enable Tone A(0), B(1))
+ // Noise disabled for cleaner "zap"
+ _ay.setReg(7, 0x3C);
+ }
+ break;
+ case 2: // Bump
+ // Low pitch noise/tone
+ if (_counter > 5) {
+ _finished = true;
+ } else {
+ _ay.setReg(0, 0xA0);
+ _ay.setReg(1, 0x05); // Period 0x5A0 (~44Hz)
+ _ay.setReg(8, 15);
+ _ay.setReg(7, 0x3E); // Tone A
+ }
+ break;
+ case 10: // Area Change / Start (Elevator Bell)
+ case 21: // Bell logic from FUN_4581 (0x15)
+ // Related to FUN_4581 case 0x15 which swaps registers 0x39b9/a and 0x39bb/c
+ // This indicates an alternating two-tone effect.
+
+ if (_counter > 50) {
+ _finished = true;
+ } else {
+ // Alternating tones (Ding-Dong / Trill)
+ // Tone 1: High (e.g., C5)
+ // Tone 2: Slightly lower (e.g., A4)
+ int pitch = ((_counter / 3) % 2 == 0) ? 119 : 142;
+
+ _ay.setReg(0, pitch & 0xff);
+ _ay.setReg(1, (pitch >> 8) & 0xf);
+
+ // Volume Decay
+ int vol = 15 - (_counter / 3);
+ if (vol < 0) vol = 0;
+
+ _ay.setReg(8, vol);
+ _ay.setReg(7, 0x3E); // Enable Tone A
+ }
+ break;
+ case 13: // Mission Complete
+ // Fanfare?
+ if (_counter > 100) _finished = true;
+ // TODO: Implement fanfare
+ break;
+ default:
+ // For unknown sounds, play nothing for now
+ _finished = true;
+ break;
+ }
+ }
+};
+
+void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
+ debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
+ // Create a new stream for the sound
+ DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
+}
+
} // namespace Freescape
Commit: c607200d65e049113343a5975d033f83f8390c3c
https://github.com/scummvm/scummvm/commit/c607200d65e049113343a5975d033f83f8390c3c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: improve sounds for driller cpc
Changed paths:
engines/freescape/games/driller/cpc.cpp
engines/freescape/sound.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index b468307e5b8..711c0aa5f08 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -26,6 +26,9 @@
#include "freescape/games/driller/driller.h"
#include "freescape/language/8bitDetokeniser.h"
+#include "audio/audiostream.h"
+#include "audio/softsynth/ay8912.h"
+
namespace Freescape {
void DrillerEngine::initCPC() {
@@ -249,4 +252,197 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
drawCompass(surface, 230, 156, _pitch - 30, 10, 60, front);
}
+class DrillerCPCSfxStream : public Audio::AudioStream {
+public:
+ DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) { // 1MHz for CPC AY
+ // Initialize sound chip (silence)
+ initAY();
+
+ _counter = 0;
+ _finished = false;
+
+ // Initialize state based on index
+ _var_3a64 = 0;
+
+ if (index == 1) { // Shoot
+ _var_3a64 = 0xfffc;
+
+ // Channel data from binary extraction
+ uint8_t shoot_data[3][24] = {
+ {0x20, 0xa, 0x11, 0xc1, 0x42, 0x5, 0x2, 0x2, 0x1, 0x1, 0x21, 0x1b, 0x77, 0xee, 0x40, 0x85, 0x21, 0x85, 0x24, 0x84, 0x1f, 0x86, 0x9, 0xd},
+ {0x0, 0x0, 0x0, 0x1a, 0xf, 0xa, 0x19, 0x1, 0x1b, 0x5, 0x19, 0x0, 0xc, 0x10, 0x1, 0x64, 0xf, 0x0, 0xf, 0x6, 0x16, 0x10, 0x10, 0x1},
+ {0x82, 0x20, 0x9c, 0x9, 0x7, 0x20, 0x1, 0x18, 0x8, 0x8, 0x8, 0xc, 0x16, 0x35, 0x35, 0x7, 0x4, 0x4, 0x4, 0x4, 0x85, 0xc, 0x82, 0x20}
+ };
+
+ for (int i=0; i<3; i++) {
+ memcpy(_channels[i].data, shoot_data[i], 24);
+ }
+ } else if (index == 10) { // Area Change (Bell)
+ _var_3a64 = 0x9408;
+ }
+ }
+
+ void initAY() {
+ // Silence all channels
+ writeReg(7, 0xFF); // Disable all tones and noise
+ writeReg(8, 0); // Volume A 0
+ writeReg(9, 0); // Volume B 0
+ writeReg(10, 0); // Volume C 0
+ }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ if (_finished)
+ return 0;
+
+ int samplesPerTick = _rate / 50;
+ int samplesGenerated = 0;
+
+ while (samplesGenerated < numSamples && !_finished) {
+ int samplesTodo = MIN(numSamples - samplesGenerated, samplesPerTick);
+
+ updateState();
+
+ _ay.readBuffer(buffer + samplesGenerated, samplesTodo);
+ samplesGenerated += samplesTodo;
+
+ if (_finished) break;
+ }
+
+ return samplesGenerated;
+ }
+
+ bool isStereo() const override { return true; }
+ bool endOfData() const override { return _finished; }
+ bool endOfStream() const override { return _finished; }
+ int getRate() const override { return _rate; }
+
+private:
+ Audio::AY8912Stream _ay;
+ int _index;
+ int _rate;
+ int _counter;
+ bool _finished;
+ uint8_t _regs[16];
+
+ void writeReg(int reg, uint8_t val) {
+ if (reg >= 0 && reg < 16) {
+ _regs[reg] = val;
+ _ay.setReg(reg, val);
+ }
+ }
+
+ uint16_t _var_3a64;
+
+ struct ChannelState {
+ uint8_t data[24];
+ };
+
+ ChannelState _channels[3];
+
+ void processChannel(int ch_idx) {
+ uint8_t *d = _channels[ch_idx].data;
+
+ // Emulate FUN_5a21 logic
+ if (d[0x16] == 0) {
+ d[4]--;
+ if (d[4] == 0) {
+ d[4] = d[2]; // Reset counter
+ d[5] = (d[5] + d[1]) & 0xf; // Accumulate
+
+ // Write to Env Period (Approx logic based on FUN_5a21)
+ // Assuming scaling for audible effect
+ int val = d[5] * 20;
+ writeReg(11, val & 0xff);
+ writeReg(12, (val >> 8) & 0xff);
+
+ // Trigger?
+ writeReg(13, 0); // Shape
+
+ // Set Mixer/Volume for this channel
+ if (ch_idx == 0) {
+ writeReg(8, 0x10); // Vol A = Env
+ writeReg(7, _regs[7] & ~1); // Enable Tone A
+ }
+ }
+ }
+ }
+
+ void updateState() {
+ _counter++;
+
+ // Safety timeout
+ if (_counter > 200) {
+ _finished = true;
+ return;
+ }
+
+ if (_var_3a64 & 0x10) { // Update channels
+ // Process channels using extracted data
+ // for (int i=0; i<3; i++) {
+ // processChannel(i);
+ // }
+
+ // Manual implementation for Shoot (Index 1) since extracted data seems to be idle/delay state
+ if (_index == 1) {
+ // Sweep Tone A
+ // Period: 10 -> ~130 (Slower sweep)
+ // Was: 10 + (_counter * 5)
+ int period = 10 + (_counter * 2);
+ writeReg(0, period & 0xff);
+ writeReg(1, (period >> 8) & 0xf);
+
+ // Enable Tone A, Disable Noise
+ writeReg(7, 0x3E); // 0011 1110. Tone A (bit 0) = 0 (On).
+
+ // Volume A: Envelope or Decay
+ // Use software volume decay for reliability
+ // Was: 15 - (_counter / 2) -> 30 ticks (~0.6s)
+ // Now: 15 - (_counter / 4) -> 60 ticks (~1.2s)
+ int vol = 15 - (_counter / 4);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+
+ if (vol == 0) _finished = true;
+ }
+ }
+
+ // Handle Bell (Index 10) - Using data derived from binary (0x1802, 0xF602)
+ if (_index == 10) {
+ if (_counter > 200) {
+ _finished = true;
+ } else {
+ // Alternating pitch based on binary values found near 0x4219 (0x0218 and 0x02F6)
+ // 0x0218 = 536 (~116Hz)
+ // 0x02F6 = 758 (~82Hz)
+ // Even slower alternation (was /6 -> 120ms, now /12 -> 240ms)
+ int pitch = ((_counter / 12) % 2 == 0) ? 0x218 : 0x2F6;
+ writeReg(0, pitch & 0xff);
+ writeReg(1, (pitch >> 8) & 0xf);
+
+ // Even slower decay
+ int vol = 15 - (_counter / 12);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ writeReg(7, 0x3E); // Tone A only
+ }
+ }
+
+ // Handle unhandled sounds
+ if (_index != 1 && _index != 10) {
+ _finished = true;
+ }
+
+ if (_finished) {
+ initAY(); // Silence
+ }
+ }
+};
+
+void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
+ debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
+ // Create a new stream for the sound
+ DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
+}
+
} // End of namespace Freescape
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
index 53b03092cd0..5c191b52e1f 100644
--- a/engines/freescape/sound.cpp
+++ b/engines/freescape/sound.cpp
@@ -395,7 +395,7 @@ void FreescapeEngine::stopAllSounds(Audio::SoundHandle &handle) {
}
void FreescapeEngine::waitForSounds() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST())
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || (isCPC() && isDriller()))
while (_mixer->isSoundHandleActive(_soundFxHandle))
waitInLoop(10);
else {
@@ -405,7 +405,7 @@ void FreescapeEngine::waitForSounds() {
}
bool FreescapeEngine::isPlayingSound() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST())
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || (isCPC() && isDriller()))
return _mixer->isSoundHandleActive(_soundFxHandle);
return (!_speaker->endOfStream());
@@ -611,164 +611,5 @@ void FreescapeEngine::playSoundDrillerZX(int index, Audio::SoundHandle &handle)
playSoundZX(&soundUnits, handle);
}
-class DrillerCPCSfxStream : public Audio::AudioStream {
-public:
- DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) { // 1MHz for CPC AY
- // Initialize sound chip (silence)
- initAY();
-
- _counter = 0;
- _finished = false;
- }
-
- void initAY() {
- // Silence all channels
- _ay.setReg(7, 0xFF); // Disable all tones and noise
- _ay.setReg(8, 0); // Volume A 0
- _ay.setReg(9, 0); // Volume B 0
- _ay.setReg(10, 0); // Volume C 0
- }
-
- int readBuffer(int16 *buffer, const int numSamples) override {
- if (_finished)
- return 0;
-
- // We need to generate samples from AY
- // And update the AY state periodically
-
- // Simulate 50Hz updates
- int samplesPerTick = _rate / 50;
- int samplesGenerated = 0;
-
- while (samplesGenerated < numSamples && !_finished) {
- int samplesTodo = MIN(numSamples - samplesGenerated, samplesPerTick);
-
- // Update AY state (simulate FUN_4760 tick)
- updateState();
-
- // Generate audio
- _ay.readBuffer(buffer + samplesGenerated, samplesTodo);
- samplesGenerated += samplesTodo;
-
- if (_finished) break;
- }
-
- return samplesGenerated;
- }
-
- bool isStereo() const override { return true; }
- bool endOfData() const override { return _finished; }
- bool endOfStream() const override { return _finished; }
- int getRate() const override { return _rate; }
-
-private:
- Audio::AY8912Stream _ay;
- int _index;
- int _rate;
- int _counter;
- bool _finished;
-
- void updateState() {
- // Simulation of Driller CPC sound effects
- // Based on analysis of FUN_4581 and FUN_4760
- // CPC AY-3-8912 Clock is 1MHz
-
- _counter++;
-
- switch (_index) {
- case 1: // Shoot
- // Related to FUN_43E2 (0x43E2) and FUN_5A21 (0x5A21)
- // The shoot sound logic involves setting up a channel structure and updating it.
- // FUN_4760 checks 0x3a64 bit 4 and calls FUN_43E2 three times (3 channels?).
-
- // 0x5A21: LD A,(IX+0x5); ADD A,(IX+0x1); AND 0xf; LD (IX+0x5),A
- // This suggests a volume or parameter update loop.
-
- // Implementation: Fast frequency sweep down with decay.
- // Replicating the "Laser" effect likely produced by the hardware envelope or software sweep.
-
- if (_counter > 12) {
- _finished = true;
- } else {
- // Sweep logic
- // Start Frequency High (Period Low) -> End Frequency Low (Period High)
- int period = 20 + _counter * 30;
-
- // Apply to Channel A (Reg 0, 1)
- _ay.setReg(0, period & 0xff);
- _ay.setReg(1, (period >> 8) & 0xf);
-
- // Apply to Channel B (Reg 2, 3) - Detuned for thickness
- _ay.setReg(2, (period + 5) & 0xff);
- _ay.setReg(3, ((period + 5) >> 8) & 0xf);
-
- // Volume Decay (Software Envelope)
- // 0x5A21 suggests updating IX+0x5 (Volume?)
- int vol = 15 - _counter;
- if (vol < 0) vol = 0;
-
- _ay.setReg(8, vol);
- _ay.setReg(9, vol);
-
- // Enable Tone A and B (Reg 7)
- // 0x3C = 0011 1100 (Enable Tone A(0), B(1))
- // Noise disabled for cleaner "zap"
- _ay.setReg(7, 0x3C);
- }
- break;
- case 2: // Bump
- // Low pitch noise/tone
- if (_counter > 5) {
- _finished = true;
- } else {
- _ay.setReg(0, 0xA0);
- _ay.setReg(1, 0x05); // Period 0x5A0 (~44Hz)
- _ay.setReg(8, 15);
- _ay.setReg(7, 0x3E); // Tone A
- }
- break;
- case 10: // Area Change / Start (Elevator Bell)
- case 21: // Bell logic from FUN_4581 (0x15)
- // Related to FUN_4581 case 0x15 which swaps registers 0x39b9/a and 0x39bb/c
- // This indicates an alternating two-tone effect.
-
- if (_counter > 50) {
- _finished = true;
- } else {
- // Alternating tones (Ding-Dong / Trill)
- // Tone 1: High (e.g., C5)
- // Tone 2: Slightly lower (e.g., A4)
- int pitch = ((_counter / 3) % 2 == 0) ? 119 : 142;
-
- _ay.setReg(0, pitch & 0xff);
- _ay.setReg(1, (pitch >> 8) & 0xf);
-
- // Volume Decay
- int vol = 15 - (_counter / 3);
- if (vol < 0) vol = 0;
-
- _ay.setReg(8, vol);
- _ay.setReg(7, 0x3E); // Enable Tone A
- }
- break;
- case 13: // Mission Complete
- // Fanfare?
- if (_counter > 100) _finished = true;
- // TODO: Implement fanfare
- break;
- default:
- // For unknown sounds, play nothing for now
- _finished = true;
- break;
- }
- }
-};
-
-void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
- debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
- // Create a new stream for the sound
- DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
-}
} // namespace Freescape
Commit: c5546d8ea5ec625e0dfacca0dd5ae46c3d50ba4f
https://github.com/scummvm/scummvm/commit/c5546d8ea5ec625e0dfacca0dd5ae46c3d50ba4f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: more cpc sounds
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 711c0aa5f08..25d298e9b48 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -36,7 +36,7 @@ void DrillerEngine::initCPC() {
_soundIndexShoot = 1;
_soundIndexCollide = 2;
_soundIndexStepUp = 3;
- _soundIndexStepDown = 3;
+ _soundIndexStepDown = 4;
_soundIndexMenu = 6;
_soundIndexAreaChange = 10;
_soundIndexHit = 7;
@@ -406,6 +406,13 @@ private:
}
}
+ // Handle Collide (Index 2) - Found in FUN_26E2
+ // Disassembly at 0x26E2 appears to be a stub (INC BC; RET P).
+ // User feedback suggests it might be silence. Skipping implementation.
+ if (_index == 2) {
+ _finished = true;
+ }
+
// Handle Bell (Index 10) - Using data derived from binary (0x1802, 0xF602)
if (_index == 10) {
if (_counter > 200) {
@@ -427,8 +434,173 @@ private:
}
}
+ // Handle Step Up (Index 3) - Found in FUN_2607
+ // Decompilation shows uVar3 = 600 (0x258), suggesting a lower base pitch.
+ // User feedback indicates longer duration and lower pitch.
+ if (_index == 3) {
+ if (_counter > 50) { // Increased duration
+ _finished = true;
+ } else {
+ // Sweep Pitch Up (Period Down)
+ // Start around 600 (Low pitch) and sweep up
+ int period = 600 - (_counter * 6);
+ if (period < 10) period = 10;
+
+ writeReg(0, period & 0xff);
+ writeReg(1, (period >> 8) & 0xf);
+
+ // Slower volume decay
+ int vol = 15 - (_counter / 4);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ writeReg(7, 0x3E); // Tone A
+ }
+ }
+
+ // Handle Step Down (Index 4) - Found in FUN_2207
+ // Using similar low pitch base but sweeping down (Period Up)
+ if (_index == 4) {
+ if (_counter > 50) { // Increased duration
+ _finished = true;
+ } else {
+ // Sweep Pitch Down (Period Up)
+ // Start around 600 and sweep down
+ int period = 600 + (_counter * 6);
+ writeReg(0, period & 0xff);
+ writeReg(1, (period >> 8) & 0xf);
+
+ // Slower volume decay
+ int vol = 15 - (_counter / 4);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ writeReg(7, 0x3E); // Tone A
+ }
+ }
+
+ // Handle Menu (Index 6) - Handled by FUN_2207
+ // FUN_2207 is a generic handler for indices 4-9, likely reading parameters from a table.
+ // Implementing as a short high blip (standard menu sound).
+ if (_index == 6) {
+ if (_counter > 5) {
+ _finished = true;
+ } else {
+ writeReg(0, 50); // High pitch
+ writeReg(1, 0);
+ writeReg(8, 15);
+ writeReg(7, 0x3E); // Tone A
+ }
+ }
+
+ // Handle Hit (Index 7) - Handled by FUN_2207
+ // Implementing as a noise+tone crunch (Collision/Hit effect).
+ if (_index == 7) {
+ if (_counter > 15) {
+ _finished = true;
+ } else {
+ // Fast sweep down with noise (Zap/Crunch)
+ int period = 200 + (_counter * 20);
+ writeReg(0, period & 0xff);
+ writeReg(1, (period >> 8) & 0xf);
+
+ writeReg(6, 10 + _counter); // Sweep noise too
+ writeReg(7, 0x36); // Tone A + Noise A
+
+ int vol = 15 - _counter;
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ }
+ }
+
+ // Handle Fallen (Index 9) - Also handled by FUN_2207
+ // Likely a longer falling pitch sound
+ if (_index == 9) {
+ if (_counter > 100) { // 2 seconds
+ _finished = true;
+ } else {
+ // Sweep Pitch Down (Period Up) from high pitch (low period) to low pitch (high period)
+ // Start 100, End ~1000
+ int period = 100 + (_counter * 9);
+ writeReg(0, period & 0xff);
+ writeReg(1, (period >> 8) & 0xf);
+
+ // Volume decay over duration
+ int vol = 15 - (_counter / 7);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ writeReg(7, 0x3E); // Tone A
+ }
+ }
+
+ // Handle Sound 13 (Mission Complete) - Handled by 0x1D8F (>= 10)
+ // Uses Register Dump -> Hardware Envelope
+ if (_index == 13) {
+ if (_counter == 0) {
+ // Success/Jingle
+ writeReg(0, 30); // High Tone
+ writeReg(1, 0);
+ writeReg(6, 0);
+ writeReg(7, 0x3E); // Tone A
+ writeReg(8, 0x10); // Envelope
+ writeReg(11, 0xFF); // Env Period Low
+ writeReg(12, 0x30); // Env Period High (Slow)
+ writeReg(13, 14); // Shape 14 (Triangle inverted? 1110) - or 4?
+ // 14: /\/\/\ (Attack then alternate)
+ }
+ if (_counter > 100) _finished = true;
+ }
+
+ // Handle Sound 14 (Timeout?) - Handled by 0x1D8F (>= 10)
+ if (_index == 14) {
+ if (_counter == 0) {
+ // Alarm
+ writeReg(0, 100);
+ writeReg(1, 0);
+ writeReg(6, 0);
+ writeReg(7, 0x3E);
+ writeReg(8, 0x10);
+ writeReg(11, 0x00);
+ writeReg(12, 0x10); // Fast
+ writeReg(13, 8); // Sawtooth
+ }
+ if (_counter > 50) _finished = true;
+ }
+
+ // Handle Sound 15 (Scripted Sound) - Teleport/Success
+ // Uses 0x1D8F logic (Register Dump) -> Hardware Envelope
+ if (_index == 15) {
+ if (_counter == 0) {
+ // Setup Hardware Envelope Sound
+ writeReg(0, 50); // Tone Period
+ writeReg(1, 0);
+ writeReg(6, 0); // Noise Period
+ writeReg(7, 0x3E); // Enable Tone A only
+ writeReg(8, 0x10); // Vol A = Envelope
+ writeReg(11, 0x00); // Env Period Low
+ writeReg(12, 0x10); // Env Period High (4096)
+ writeReg(13, 10); // Env Shape 10 (Triangle/Warble)
+ }
+ if (_counter > 50) _finished = true;
+ }
+
+ // Handle Sound 16 (Scripted Sound) - Failure/Heavy
+ // Uses 0x1D8F logic (Register Dump) -> Hardware Envelope
+ if (_index == 16) {
+ if (_counter == 0) {
+ // Setup Hardware Envelope Sound (Noise + Tone)
+ writeReg(0, 200); // Tone Period
+ writeReg(1, 0);
+ writeReg(6, 20); // Noise Period
+ writeReg(7, 0x36); // Enable Tone A + Noise A (0011 0110)
+ writeReg(8, 0x10); // Vol A = Envelope
+ writeReg(11, 0x00); // Env Period Low
+ writeReg(12, 0x20); // Env Period High (8192)
+ writeReg(13, 0); // Env Shape 0 (Decay)
+ }
+ if (_counter > 50) _finished = true;
+ }
+
// Handle unhandled sounds
- if (_index != 1 && _index != 10) {
+ if (_index != 1 && _index != 10 && _index != 3 && _index != 4 && _index != 9 && _index != 13 && _index != 14 && _index != 15 && _index != 16) {
_finished = true;
}
@@ -439,7 +611,8 @@ private:
};
void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
- debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
+ // DO NOT CHANGE: This debug line is used to track sound usage in Driller CPC
+ debug("Playing Driller CPC sound %d", index);
// Create a new stream for the sound
DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index);
_mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
Commit: a089f5cf423399ff0e349005363eb9b54b22ee8f
https://github.com/scummvm/scummvm/commit/a089f5cf423399ff0e349005363eb9b54b22ee8f
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FRESCAPE: more precise driller sounds for CPC
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 25d298e9b48..dd8fa0a394b 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -252,55 +252,126 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
drawCompass(surface, 230, 156, _pitch - 30, 10, 60, front);
}
+/**
+ * Driller CPC Sound Implementation
+ *
+ * Based on reverse engineering of DRILL.BIN (loads at 0x1c62)
+ *
+ * Sound Dispatch (0x6305):
+ * Index 1 -> sub_2112h (Shoot)
+ * Index 2 -> sub_26e2h (Collide)
+ * Index 3 -> sub_2607h (Step Up)
+ * Index 4-9 -> sub_2207h (Generic handler)
+ * Index >= 10 -> l1d8fh (High index handler with HW envelope)
+ *
+ * Data Tables (from DRILL.BIN):
+ * Tone Table at 0x4034: 4 bytes per entry (count, period_lo, period_hi, delta)
+ * Envelope Table at 0x4078: 4 bytes per entry (step, count, delta_lo, delta_hi)
+ * Sound Definition at 0x40e0: 7 bytes per entry
+ *
+ * AY Register Write at 0x4872:
+ * Port 0xF4 = register select, Port 0xF6 = data
+ */
+
+// Original data tables extracted from DRILL.BIN for reference
+// These are preserved for documentation - the actual implementation uses
+// simplified parameters that approximate the original sound behavior.
+// TODO: Use these tables for more accurate sound reproduction
+
+// Tone table entries from DRILL.BIN at 0x4034 (file offset 0x23D2)
+// Format: { iterations, period_low, period_high, delta }
+// Period values are 12-bit (0-4095), frequency = 1MHz / (16 * period)
+#if 0
+static const uint8 kDrillerCPCToneTable[][4] = {
+ {0x01, 0x01, 0x00, 0x01}, // Entry 0: period=0x0001, delta=0x01
+ {0x02, 0x0f, 0x01, 0x03}, // Entry 1: period=0x010F (271), delta=0x03
+ {0x01, 0xf1, 0x01, 0x00}, // Entry 2: period=0x01F1 (497), delta=0x00
+ {0x01, 0x0f, 0xff, 0x18}, // Entry 3
+ {0x01, 0x06, 0xfe, 0x3f}, // Entry 4
+ {0x01, 0x0f, 0xff, 0x18}, // Entry 5
+ {0x02, 0x01, 0x00, 0x06}, // Entry 6: period=0x0001, delta=0x06
+ {0x0f, 0xff, 0x0f, 0x00}, // Entry 7
+ {0x04, 0x05, 0xff, 0x0f}, // Entry 8
+ {0x01, 0x05, 0x01, 0x01}, // Entry 9: period=0x0105 (261), delta=0x01
+ {0x00, 0x7b, 0x0f, 0xff}, // Entry 10
+};
+
+// Envelope table entries from DRILL.BIN at 0x4078 (file offset 0x2416)
+// Format: { step, count, delta_low, delta_high }
+static const uint8 kDrillerCPCEnvelopeTable[][4] = {
+ {0x01, 0x02, 0x00, 0xff}, // Entry 0: fast decay
+ {0x01, 0x10, 0x01, 0x01}, // Entry 1
+ {0x01, 0x02, 0x30, 0x10}, // Entry 2
+ {0x01, 0x02, 0xd0, 0x10}, // Entry 3
+ {0x03, 0x01, 0xe0, 0x06}, // Entry 4
+ {0x01, 0x20, 0x06, 0x01}, // Entry 5
+ {0xe0, 0x06, 0x00, 0x00}, // Entry 6
+ {0x01, 0x02, 0xfb, 0x03}, // Entry 7
+ {0x01, 0x02, 0xfd, 0x0c}, // Entry 8
+ {0x02, 0x01, 0x04, 0x03}, // Entry 9
+ {0x08, 0xf5, 0x03, 0x00}, // Entry 10
+ {0x01, 0x10, 0x02, 0x06}, // Entry 11
+ {0x01, 0x80, 0x01, 0x03}, // Entry 12
+ {0x01, 0x64, 0x01, 0x01}, // Entry 13
+};
+
+// Sound definition table from DRILL.BIN at 0x40e0 (file offset 0x247E)
+// Format: { flags, tone_idx, env_idx, period_lo, period_hi, volume, duration }
+static const uint8 kDrillerCPCSoundDefs[][7] = {
+ {0x02, 0x00, 0x0d, 0x00, 0x00, 0x0f, 0x01}, // Sound def 0
+ {0x03, 0x00, 0x01, 0x00, 0x00, 0x0f, 0x01}, // Sound def 1
+ {0x09, 0x00, 0x02, 0x20, 0x01, 0x0f, 0x01}, // Sound def 2
+ {0x09, 0x00, 0x03, 0x20, 0x01, 0x0f, 0x01}, // Sound def 3
+ {0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01}, // Sound def 4
+ {0x09, 0x03, 0x00, 0x27, 0x00, 0x0f, 0x01}, // Sound def 5
+ {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // Sound def 6
+ {0x09, 0x00, 0x04, 0x20, 0x01, 0x0f, 0x08}, // Sound def 7
+ {0x09, 0x00, 0x07, 0x00, 0x01, 0x0f, 0x18}, // Sound def 8
+ {0x01, 0x00, 0x08, 0x00, 0x01, 0x0f, 0x02}, // Sound def 9
+};
+#endif
+
class DrillerCPCSfxStream : public Audio::AudioStream {
public:
- DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) { // 1MHz for CPC AY
- // Initialize sound chip (silence)
+ DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) {
+ // CPC uses 1MHz clock for AY-3-8912
initAY();
-
_counter = 0;
_finished = false;
+ _phase = 0;
+
+ // Initialize channel state
+ for (int i = 0; i < 3; i++) {
+ _channelPeriod[i] = 0;
+ _channelVolume[i] = 0;
+ _channelDelta[i] = 0;
+ _channelDuration[i] = 0;
+ }
- // Initialize state based on index
- _var_3a64 = 0;
-
- if (index == 1) { // Shoot
- _var_3a64 = 0xfffc;
-
- // Channel data from binary extraction
- uint8_t shoot_data[3][24] = {
- {0x20, 0xa, 0x11, 0xc1, 0x42, 0x5, 0x2, 0x2, 0x1, 0x1, 0x21, 0x1b, 0x77, 0xee, 0x40, 0x85, 0x21, 0x85, 0x24, 0x84, 0x1f, 0x86, 0x9, 0xd},
- {0x0, 0x0, 0x0, 0x1a, 0xf, 0xa, 0x19, 0x1, 0x1b, 0x5, 0x19, 0x0, 0xc, 0x10, 0x1, 0x64, 0xf, 0x0, 0xf, 0x6, 0x16, 0x10, 0x10, 0x1},
- {0x82, 0x20, 0x9c, 0x9, 0x7, 0x20, 0x1, 0x18, 0x8, 0x8, 0x8, 0xc, 0x16, 0x35, 0x35, 0x7, 0x4, 0x4, 0x4, 0x4, 0x85, 0xc, 0x82, 0x20}
- };
-
- for (int i=0; i<3; i++) {
- memcpy(_channels[i].data, shoot_data[i], 24);
- }
- } else if (index == 10) { // Area Change (Bell)
- _var_3a64 = 0x9408;
- }
+ // Setup based on sound index using dispatch table logic from 0x6305
+ setupSound(index);
}
void initAY() {
- // Silence all channels
- writeReg(7, 0xFF); // Disable all tones and noise
- writeReg(8, 0); // Volume A 0
- writeReg(9, 0); // Volume B 0
- writeReg(10, 0); // Volume C 0
+ // Silence all channels (AY register 7 = mixer, FF = all disabled)
+ writeReg(7, 0xFF);
+ writeReg(8, 0); // Volume A = 0
+ writeReg(9, 0); // Volume B = 0
+ writeReg(10, 0); // Volume C = 0
}
int readBuffer(int16 *buffer, const int numSamples) override {
if (_finished)
return 0;
+ // Process at 50Hz (CPC interrupt rate)
int samplesPerTick = _rate / 50;
int samplesGenerated = 0;
while (samplesGenerated < numSamples && !_finished) {
int samplesTodo = MIN(numSamples - samplesGenerated, samplesPerTick);
- updateState();
+ updateSound();
_ay.readBuffer(buffer + samplesGenerated, samplesTodo);
samplesGenerated += samplesTodo;
@@ -321,292 +392,349 @@ private:
int _index;
int _rate;
int _counter;
+ int _phase;
bool _finished;
- uint8_t _regs[16];
-
- void writeReg(int reg, uint8_t val) {
- if (reg >= 0 && reg < 16) {
- _regs[reg] = val;
- _ay.setReg(reg, val);
- }
- }
-
- uint16_t _var_3a64;
-
- struct ChannelState {
- uint8_t data[24];
- };
+ uint8 _regs[16];
+
+ // Channel state for multi-channel processing (sub_2e96h)
+ uint16 _channelPeriod[3];
+ int16 _channelDelta[3];
+ uint8 _channelVolume[3];
+ uint16 _channelDuration[3];
+
+ // Sound-specific state
+ uint16 _tonePeriod;
+ int16 _toneDelta;
+ uint8 _loopCount;
+ uint8 _maxLoops;
+
+ void writeReg(int reg, uint8 val) {
+ if (reg >= 0 && reg < 16) {
+ _regs[reg] = val;
+ _ay.setReg(reg, val);
+ }
+ }
- ChannelState _channels[3];
+ void setupSound(int index) {
+ // Dispatch based on index (mirrors 0x6305 logic)
+ switch (index) {
+ case 1: // Shoot (sub_2112h)
+ setupShoot();
+ break;
+ case 2: // Collide (sub_26e2h)
+ setupCollide();
+ break;
+ case 3: // Step Up (sub_2607h)
+ setupStepUp();
+ break;
+ case 4: // Step Down (sub_2207h, index 4)
+ case 5: // (sub_2207h, index 5)
+ case 6: // Menu (sub_2207h, index 6)
+ case 7: // Hit (sub_2207h, index 7)
+ case 8: // (sub_2207h, index 8)
+ case 9: // Fallen (sub_2207h, index 9)
+ setupGeneric(index);
+ break;
+ default:
+ if (index >= 10) {
+ // High index handler (l1d8fh) - uses HW envelope
+ setupHighIndex(index);
+ } else {
+ _finished = true;
+ }
+ break;
+ }
+ }
- void processChannel(int ch_idx) {
- uint8_t *d = _channels[ch_idx].data;
+ // Sound 1: Shoot - Based on sub_2112h analysis
+ // Laser-like descending pitch sweep from high to low
+ // Original uses 8 channels with complex envelope, simplified here
+ void setupShoot() {
+ // Period 100 (~625Hz) sweeping to 800 (~78Hz) over ~1.2 seconds
+ _tonePeriod = 100; // Starting period (higher pitch ~625Hz)
+ _toneDelta = 12; // Increase period each tick (descend pitch)
+ _channelVolume[0] = 15;
+ _maxLoops = 60; // 60 ticks at 50Hz = 1.2 seconds
+ _loopCount = 0;
+
+ // Enable tone on channel A
+ writeReg(7, 0x3E); // Tone A only (bit 0 = 0)
+ }
- // Emulate FUN_5a21 logic
- if (d[0x16] == 0) {
- d[4]--;
- if (d[4] == 0) {
- d[4] = d[2]; // Reset counter
- d[5] = (d[5] + d[1]) & 0xf; // Accumulate
+ // Sound 2: Collide - Based on sub_26e2h analysis
+ // Bump/thud sound with noise + low tone
+ void setupCollide() {
+ _tonePeriod = 600; // Low frequency tone (~104Hz)
+ _channelVolume[0] = 15;
+ _maxLoops = 15; // ~300ms
+ _loopCount = 0;
+
+ // Enable noise + tone on channel A for thud effect
+ writeReg(6, 0x18); // Noise period (lower = rougher)
+ writeReg(7, 0x36); // Tone A + Noise A
+ }
- // Write to Env Period (Approx logic based on FUN_5a21)
- // Assuming scaling for audible effect
- int val = d[5] * 20;
- writeReg(11, val & 0xff);
- writeReg(12, (val >> 8) & 0xff);
+ // Sound 3: Step Up - Based on sub_2607h analysis
+ // Short ascending blip for footstep going up
+ void setupStepUp() {
+ // Period 400 (~156Hz) ascending to 150 (~417Hz)
+ _tonePeriod = 400; // Start lower pitch
+ _toneDelta = -10; // Decrease period (ascend pitch)
+ _channelVolume[0] = 12;
+ _maxLoops = 20; // ~400ms
+ _loopCount = 0;
+
+ writeReg(7, 0x3E); // Tone A only
+ }
- // Trigger?
- writeReg(13, 0); // Shape
+ // Sounds 4-9: Generic handler based on sub_2207h
+ // Each sound has specific characteristics
+ void setupGeneric(int index) {
+ _loopCount = 0;
+
+ switch (index) {
+ case 4: // Step Down - descending blip for footstep going down
+ _tonePeriod = 200; // Start higher pitch (~312Hz)
+ _toneDelta = 15; // Increase period (descend pitch)
+ _channelVolume[0] = 12;
+ _maxLoops = 20; // ~400ms
+ writeReg(7, 0x3E); // Tone A only
+ break;
+
+ case 5: // Area transition sound
+ _tonePeriod = 300; // Medium pitch (~208Hz)
+ _toneDelta = 0;
+ _channelVolume[0] = 15;
+ _maxLoops = 25; // ~500ms
+ writeReg(7, 0x3E);
+ break;
+
+ case 6: // Menu click - short high blip
+ _tonePeriod = 150; // Higher pitch (~417Hz)
+ _toneDelta = 0;
+ _channelVolume[0] = 15;
+ _maxLoops = 8; // ~160ms - short click
+ writeReg(7, 0x3E);
+ break;
+
+ case 7: // Hit - impact sound with noise
+ _tonePeriod = 400; // Low-mid pitch
+ _toneDelta = 20; // Descending
+ _channelVolume[0] = 15;
+ _maxLoops = 20;
+ writeReg(6, 0x10); // Add some noise
+ writeReg(7, 0x36); // Tone + Noise
+ break;
+
+ case 8: // Generic sound
+ _tonePeriod = 250;
+ _toneDelta = 5;
+ _channelVolume[0] = 12;
+ _maxLoops = 30;
+ writeReg(7, 0x3E);
+ break;
+
+ case 9: // Fallen - long descending sweep
+ _tonePeriod = 150; // Start high (~417Hz)
+ _toneDelta = 8; // Slow descent
+ _channelVolume[0] = 15;
+ _maxLoops = 80; // ~1.6 seconds - long fall
+ writeReg(7, 0x3E);
+ break;
+
+ default:
+ _finished = true;
+ return;
+ }
+ }
- // Set Mixer/Volume for this channel
- if (ch_idx == 0) {
- writeReg(8, 0x10); // Vol A = Env
- writeReg(7, _regs[7] & ~1); // Enable Tone A
- }
- }
- }
- }
+ // Sounds >= 10: High index handler (l1d8fh)
+ // Uses hardware envelope for sustained sounds
+ void setupHighIndex(int index) {
+ _loopCount = 0;
+
+ // Setup based on specific high index sounds
+ switch (index) {
+ case 10: // Area Change - Based on flags byte 0x87 at 0x38e9
+ // Bit 2=1 (NOISE enabled), Bit 3=0 (TONE disabled)
+ _tonePeriod = 0;
+ _toneDelta = 0;
+ _maxLoops = 100; // ~2 seconds with envelope decay
+ // Set noise period from data byte 9 (0x7f & 0x1f = 0x1f)
+ writeReg(6, 0x1F); // Noise period
+ // Mixer: disable tone, enable noise on channel A
+ // 0x37 = 0b00110111: bits 0-2=1 (tones off), bit 3=0 (noise A on)
+ writeReg(7, 0x37);
+ // Use hardware envelope for natural decay
+ writeReg(11, 0x00); // Envelope period low
+ writeReg(12, 0x20); // Envelope period high (slow decay)
+ writeReg(13, 0x00); // Envelope shape: single decay (\)
+ writeReg(8, 0x10); // Volume A = envelope
+ break;
+
+ case 11: // Explosion/rumble
+ _tonePeriod = 800; // Low rumble
+ _maxLoops = 60;
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ writeReg(6, 0x1F); // Noise period (rough)
+ writeReg(7, 0x36); // Tone A + Noise A
+ writeReg(11, 0x00);
+ writeReg(12, 0x30); // Medium-slow decay
+ writeReg(13, 0x00); // Decay shape
+ writeReg(8, 0x10);
+ break;
+
+ case 12: // Warning tone
+ _tonePeriod = 180; // ~347Hz
+ _maxLoops = 80;
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ writeReg(7, 0x3E);
+ writeReg(11, 0x00);
+ writeReg(12, 0x08); // Faster cycle
+ writeReg(13, 0x0E); // Continue, alternate (warble)
+ writeReg(8, 0x10);
+ break;
+
+ case 13: // Mission Complete - triumphant jingle
+ _tonePeriod = 150; // Start high (~417Hz)
+ _toneDelta = -1; // Slowly rise in pitch
+ _maxLoops = 120; // ~2.4 seconds
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ writeReg(7, 0x3E);
+ writeReg(11, 0x00);
+ writeReg(12, 0x40); // Very slow envelope
+ writeReg(13, 0x0E); // Attack/decay cycle
+ writeReg(8, 0x10);
+ break;
+
+ default:
+ // Generic high index sound
+ _tonePeriod = 250;
+ _maxLoops = 50;
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ writeReg(7, 0x3E);
+ writeReg(8, 0x10);
+ writeReg(11, 0x00);
+ writeReg(12, 0x10);
+ writeReg(13, 0x00);
+ break;
+ }
+ }
- void updateState() {
+ void updateSound() {
_counter++;
+ _loopCount++;
- // Safety timeout
- if (_counter > 200) {
+ if (_loopCount >= _maxLoops) {
_finished = true;
+ initAY();
return;
}
- if (_var_3a64 & 0x10) { // Update channels
- // Process channels using extracted data
- // for (int i=0; i<3; i++) {
- // processChannel(i);
- // }
-
- // Manual implementation for Shoot (Index 1) since extracted data seems to be idle/delay state
- if (_index == 1) {
- // Sweep Tone A
- // Period: 10 -> ~130 (Slower sweep)
- // Was: 10 + (_counter * 5)
- int period = 10 + (_counter * 2);
- writeReg(0, period & 0xff);
- writeReg(1, (period >> 8) & 0xf);
-
- // Enable Tone A, Disable Noise
- writeReg(7, 0x3E); // 0011 1110. Tone A (bit 0) = 0 (On).
-
- // Volume A: Envelope or Decay
- // Use software volume decay for reliability
- // Was: 15 - (_counter / 2) -> 30 ticks (~0.6s)
- // Now: 15 - (_counter / 4) -> 60 ticks (~1.2s)
- int vol = 15 - (_counter / 4);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
-
- if (vol == 0) _finished = true;
- }
- }
-
- // Handle Collide (Index 2) - Found in FUN_26E2
- // Disassembly at 0x26E2 appears to be a stub (INC BC; RET P).
- // User feedback suggests it might be silence. Skipping implementation.
- if (_index == 2) {
- _finished = true;
- }
-
- // Handle Bell (Index 10) - Using data derived from binary (0x1802, 0xF602)
- if (_index == 10) {
- if (_counter > 200) {
- _finished = true;
- } else {
- // Alternating pitch based on binary values found near 0x4219 (0x0218 and 0x02F6)
- // 0x0218 = 536 (~116Hz)
- // 0x02F6 = 758 (~82Hz)
- // Even slower alternation (was /6 -> 120ms, now /12 -> 240ms)
- int pitch = ((_counter / 12) % 2 == 0) ? 0x218 : 0x2F6;
- writeReg(0, pitch & 0xff);
- writeReg(1, (pitch >> 8) & 0xf);
-
- // Even slower decay
- int vol = 15 - (_counter / 12);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- writeReg(7, 0x3E); // Tone A only
- }
- }
-
- // Handle Step Up (Index 3) - Found in FUN_2607
- // Decompilation shows uVar3 = 600 (0x258), suggesting a lower base pitch.
- // User feedback indicates longer duration and lower pitch.
- if (_index == 3) {
- if (_counter > 50) { // Increased duration
- _finished = true;
- } else {
- // Sweep Pitch Up (Period Down)
- // Start around 600 (Low pitch) and sweep up
- int period = 600 - (_counter * 6);
- if (period < 10) period = 10;
-
- writeReg(0, period & 0xff);
- writeReg(1, (period >> 8) & 0xf);
-
- // Slower volume decay
- int vol = 15 - (_counter / 4);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- writeReg(7, 0x3E); // Tone A
- }
- }
-
- // Handle Step Down (Index 4) - Found in FUN_2207
- // Using similar low pitch base but sweeping down (Period Up)
- if (_index == 4) {
- if (_counter > 50) { // Increased duration
- _finished = true;
- } else {
- // Sweep Pitch Down (Period Up)
- // Start around 600 and sweep down
- int period = 600 + (_counter * 6);
- writeReg(0, period & 0xff);
- writeReg(1, (period >> 8) & 0xf);
-
- // Slower volume decay
- int vol = 15 - (_counter / 4);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- writeReg(7, 0x3E); // Tone A
- }
- }
-
- // Handle Menu (Index 6) - Handled by FUN_2207
- // FUN_2207 is a generic handler for indices 4-9, likely reading parameters from a table.
- // Implementing as a short high blip (standard menu sound).
- if (_index == 6) {
- if (_counter > 5) {
- _finished = true;
- } else {
- writeReg(0, 50); // High pitch
- writeReg(1, 0);
- writeReg(8, 15);
- writeReg(7, 0x3E); // Tone A
- }
- }
-
- // Handle Hit (Index 7) - Handled by FUN_2207
- // Implementing as a noise+tone crunch (Collision/Hit effect).
- if (_index == 7) {
- if (_counter > 15) {
- _finished = true;
- } else {
- // Fast sweep down with noise (Zap/Crunch)
- int period = 200 + (_counter * 20);
- writeReg(0, period & 0xff);
- writeReg(1, (period >> 8) & 0xf);
-
- writeReg(6, 10 + _counter); // Sweep noise too
- writeReg(7, 0x36); // Tone A + Noise A
-
- int vol = 15 - _counter;
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- }
- }
-
- // Handle Fallen (Index 9) - Also handled by FUN_2207
- // Likely a longer falling pitch sound
- if (_index == 9) {
- if (_counter > 100) { // 2 seconds
- _finished = true;
- } else {
- // Sweep Pitch Down (Period Up) from high pitch (low period) to low pitch (high period)
- // Start 100, End ~1000
- int period = 100 + (_counter * 9);
- writeReg(0, period & 0xff);
- writeReg(1, (period >> 8) & 0xf);
-
- // Volume decay over duration
- int vol = 15 - (_counter / 7);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- writeReg(7, 0x3E); // Tone A
- }
- }
-
- // Handle Sound 13 (Mission Complete) - Handled by 0x1D8F (>= 10)
- // Uses Register Dump -> Hardware Envelope
- if (_index == 13) {
- if (_counter == 0) {
- // Success/Jingle
- writeReg(0, 30); // High Tone
- writeReg(1, 0);
- writeReg(6, 0);
- writeReg(7, 0x3E); // Tone A
- writeReg(8, 0x10); // Envelope
- writeReg(11, 0xFF); // Env Period Low
- writeReg(12, 0x30); // Env Period High (Slow)
- writeReg(13, 14); // Shape 14 (Triangle inverted? 1110) - or 4?
- // 14: /\/\/\ (Attack then alternate)
- }
- if (_counter > 100) _finished = true;
- }
-
- // Handle Sound 14 (Timeout?) - Handled by 0x1D8F (>= 10)
- if (_index == 14) {
- if (_counter == 0) {
- // Alarm
- writeReg(0, 100);
- writeReg(1, 0);
- writeReg(6, 0);
- writeReg(7, 0x3E);
- writeReg(8, 0x10);
- writeReg(11, 0x00);
- writeReg(12, 0x10); // Fast
- writeReg(13, 8); // Sawtooth
- }
- if (_counter > 50) _finished = true;
- }
-
- // Handle Sound 15 (Scripted Sound) - Teleport/Success
- // Uses 0x1D8F logic (Register Dump) -> Hardware Envelope
- if (_index == 15) {
- if (_counter == 0) {
- // Setup Hardware Envelope Sound
- writeReg(0, 50); // Tone Period
- writeReg(1, 0);
- writeReg(6, 0); // Noise Period
- writeReg(7, 0x3E); // Enable Tone A only
- writeReg(8, 0x10); // Vol A = Envelope
- writeReg(11, 0x00); // Env Period Low
- writeReg(12, 0x10); // Env Period High (4096)
- writeReg(13, 10); // Env Shape 10 (Triangle/Warble)
- }
- if (_counter > 50) _finished = true;
- }
-
- // Handle Sound 16 (Scripted Sound) - Failure/Heavy
- // Uses 0x1D8F logic (Register Dump) -> Hardware Envelope
- if (_index == 16) {
- if (_counter == 0) {
- // Setup Hardware Envelope Sound (Noise + Tone)
- writeReg(0, 200); // Tone Period
- writeReg(1, 0);
- writeReg(6, 20); // Noise Period
- writeReg(7, 0x36); // Enable Tone A + Noise A (0011 0110)
- writeReg(8, 0x10); // Vol A = Envelope
- writeReg(11, 0x00); // Env Period Low
- writeReg(12, 0x20); // Env Period High (8192)
- writeReg(13, 0); // Env Shape 0 (Decay)
- }
- if (_counter > 50) _finished = true;
- }
-
- // Handle unhandled sounds
- if (_index != 1 && _index != 10 && _index != 3 && _index != 4 && _index != 9 && _index != 13 && _index != 14 && _index != 15 && _index != 16) {
- _finished = true;
- }
-
- if (_finished) {
- initAY(); // Silence
- }
+ // Update based on sound type
+ switch (_index) {
+ case 1: // Shoot
+ updateShoot();
+ break;
+ case 2: // Collide
+ updateCollide();
+ break;
+ case 3: // Step Up
+ updateStepUp();
+ break;
+ case 4:
+ case 5:
+ case 6:
+ case 7:
+ case 8:
+ case 9:
+ updateGeneric();
+ break;
+ default:
+ if (_index >= 10) {
+ updateHighIndex();
+ }
+ break;
+ }
+ }
+
+ void updateShoot() {
+ // Descending pitch sweep (period increases = lower pitch)
+ _tonePeriod += _toneDelta;
+ if (_tonePeriod > 1000) _tonePeriod = 1000; // Cap at ~62Hz
+
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+
+ // Volume decay over duration
+ int vol = 15 - (_loopCount / 4);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ }
+
+ void updateCollide() {
+ // Bump sound - quick decay with slight pitch drop
+ _tonePeriod += 10; // Slight pitch drop
+ if (_tonePeriod > 900) _tonePeriod = 900;
+
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+
+ // Quick decay
+ int vol = 15 - _loopCount;
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ }
+
+ void updateStepUp() {
+ // Ascending pitch sweep (period decreases = higher pitch)
+ int period = (int)_tonePeriod + _toneDelta;
+ if (period < 100) period = 100; // Cap at ~625Hz
+ _tonePeriod = period;
+
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+
+ // Volume decay
+ int vol = _channelVolume[0] - (_loopCount / 3);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ }
+
+ void updateGeneric() {
+ // Apply tone delta
+ int period = (int)_tonePeriod + _toneDelta;
+ if (period < 50) period = 50;
+ if (period > 2000) period = 2000;
+ _tonePeriod = period;
+
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+
+ // Volume decay based on sound type
+ int decayRate = (_index == 6) ? 2 : 5; // Menu click decays faster
+ int vol = _channelVolume[0] - (_loopCount / decayRate);
+ if (vol < 0) vol = 0;
+ writeReg(8, vol);
+ }
+
+ void updateHighIndex() {
+ // High index sounds mostly use hardware envelope
+ // Update tone if delta is set (for pitch sweeps)
+ if (_toneDelta != 0) {
+ int period = (int)_tonePeriod + _toneDelta;
+ if (period < 50) period = 50;
+ if (period > 1000) period = 1000;
+ _tonePeriod = period;
+
+ writeReg(0, _tonePeriod & 0xFF);
+ writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ }
+ // Hardware envelope handles volume automatically
}
};
Commit: 75a50512dd258d3b57f61542ff70970c714f31ac
https://github.com/scummvm/scummvm/commit/75a50512dd258d3b57f61542ff70970c714f31ac
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: even more precise driller sounds for CPC
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index dd8fa0a394b..08d4f358ad2 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -331,6 +331,60 @@ static const uint8 kDrillerCPCSoundDefs[][7] = {
};
#endif
+/**
+ * Sound entries extracted from DRILL.BIN at their respective addresses.
+ *
+ * Sound entry structure (from assembly at l38e9h):
+ * Byte 0: Flags
+ * - Bits 0-1: Channel number (1-3 â A/B/C)
+ * - Bit 2: If set, DISABLE tone (assembly at 4825: bit 2,(ix+000h))
+ * - Bit 3: If set, DISABLE noise (assembly at 482c: bit 3,(ix+000h))
+ * - Bit 6: Disable flag (checked at 1cdc: bit 6,(ix+000h))
+ * - Bit 7: Additional flag
+ * Bytes 1-3: Parameters for calibration processing (IX[1], IX[2], IX[3])
+ * Bytes 4-6: Secondary parameters (IX[4], IX[5], IX[6])
+ * Byte 7: Delta sign/additional param (stored at 03a5ah)
+ * Byte 8: Entry size
+ * Bytes 9+: Additional parameters (volume nibbles, etc.)
+ */
+// Sound entry raw bytes - preserved for reference and future use
+// These entries will be used for full calibration-based implementation
+#if 0 // Currently using tuned approximations instead
+static const uint8 kDrillerCPCSoundEntry1[] = {
+ // Sound 1 (Shoot) at l3904h (file offset 0x1CA2)
+ // Flags 0x81: channel A, bits 2,3=0 â tone+noise enabled
+ 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe, 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a
+};
+
+static const uint8 kDrillerCPCSoundEntry6[] = {
+ // Sound 6 at l3912h (file offset 0x1CB0)
+ // Flags 0x86: channel B/C, needs verification
+ 0x86, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0xfd, 0x10, 0x7f, 0x7f, 0x00, 0x03, 0x03
+};
+#endif
+
+/**
+ * Initial calibration values from assembly at 0x4e78-0x4e87.
+ *
+ * Original code:
+ * ld a,07dh ; 4e76 - Default A = 125
+ * ld (l39f9h),a ; 4e78 - Store
+ * ld h,a ; 4e7b - H = 125
+ * ld l,000h ; 4e7c - HL = 125 * 256 = 32000
+ * srl h ; rr l ; 4e7e-4e84 - HL = 32000 / 4 = 8000
+ * add hl,de ; 4e86 - HL += DE (DE = 0x20 = 32)
+ * ld (l38a6h),hl ; 4e87 - Store calibration = 8032
+ *
+ * Note: The calibration value 0x1F60 (8032) causes processed params to be
+ * large negative numbers, which wrap to values > 4095 (AY max period).
+ * Setting to 0 for now - needs investigation of actual in-game values.
+ * The calibration may be initialized differently at game start or may
+ * represent position-based attenuation that doesn't apply to shoot sound.
+ */
+#if 0 // Calibration system not yet fully implemented
+static const uint16 kInitialCalibration[3] = {0x0000, 0x0000, 0x0000};
+#endif
+
class DrillerCPCSfxStream : public Audio::AudioStream {
public:
DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) {
@@ -339,13 +393,25 @@ public:
_counter = 0;
_finished = false;
_phase = 0;
+ _channelCount = 0;
+ _outerLoops = 0;
+ _loopCount = 0;
+ _maxLoops = 0;
+ _tonePeriod = 0;
+ _toneDelta = 0;
+ _processedFlags = 0;
- // Initialize channel state
- for (int i = 0; i < 3; i++) {
+ // Initialize channel state for 8 channels (sub_2e96h uses up to 8)
+ for (int i = 0; i < 8; i++) {
_channelPeriod[i] = 0;
_channelVolume[i] = 0;
_channelDelta[i] = 0;
- _channelDuration[i] = 0;
+ _channelDone[i] = false;
+ }
+
+ // Initialize processed parameters
+ for (int i = 0; i < 6; i++) {
+ _processedParams[i] = 0;
}
// Setup based on sound index using dispatch table logic from 0x6305
@@ -396,17 +462,30 @@ private:
bool _finished;
uint8 _regs[16];
- // Channel state for multi-channel processing (sub_2e96h)
- uint16 _channelPeriod[3];
- int16 _channelDelta[3];
- uint8 _channelVolume[3];
- uint16 _channelDuration[3];
+ /**
+ * Channel state for multi-channel processing (sub_2e96h at 0x2e96)
+ * Sound 1 uses 8 channels, distributed from processed parameters.
+ * Channel data base at 0x0223, 6 bytes per channel in original.
+ */
+ uint16 _channelPeriod[8]; // Tone period per channel
+ int16 _channelDelta[8]; // Delta per channel (can be negative)
+ uint8 _channelVolume[8]; // Volume per channel
+ bool _channelDone[8]; // Completion status per channel
// Sound-specific state
- uint16 _tonePeriod;
- int16 _toneDelta;
- uint8 _loopCount;
- uint8 _maxLoops;
+ uint16 _tonePeriod; // Current tone period (for simple sounds)
+ int16 _toneDelta; // Tone delta (for simple sounds)
+ uint8 _channelCount; // Number of active channels (l3bc3h)
+ uint8 _outerLoops; // Outer loop count (l3bc4h = 5)
+ uint16 _loopCount; // Current iteration
+ uint16 _maxLoops; // Total iteration limit
+
+ /**
+ * Processed parameters from calibration (output of l1cffh)
+ * 6 x 16-bit values stored at working buffer 0x02D1
+ */
+ int16 _processedParams[6];
+ uint8 _processedFlags; // Accumulated flag bits (stored at 0x01e6)
void writeReg(int reg, uint8 val) {
if (reg >= 0 && reg < 16) {
@@ -415,6 +494,82 @@ private:
}
}
+ /**
+ * Processes sound entry parameters through calibration system.
+ * NOTE: Currently disabled - using tuned approximations instead.
+ * This function implements the calibration at l1cffh but requires
+ * correct runtime calibration values to produce valid results.
+ */
+#if 0 // Calibration-based processing - needs emulator capture for accuracy
+ /**
+ * Original assembly at l1cffh (0x1cff-0x1d54):
+ * ld de,(l3d16h) ; 1cff - DE = working buffer (0x02D1)
+ * ld ix,(l3d1ah) ; 1d04 - IX = sound entry pointer
+ * ld hl,l38a6h ; 1d08 - HL = calibration table
+ * ld b,003h ; 1d0b - Loop 3 times
+ *
+ * Algorithm per iteration:
+ * 1. BC = calibration[i] from l38a6h
+ * 2. HL = entry[1+i] * 64 (via srl h ; rr l twice)
+ * 3. result1 = HL - BC
+ * 4. Store result1 (2 bytes)
+ * 5. BC = entry[4+i] * 64
+ * 6. result2 = result1 + BC
+ * 7. Store result2 (2 bytes)
+ * 8. Advance entry pointer (inc ix)
+ *
+ * @param soundEntry Raw sound entry data (14 bytes from l3904h etc.)
+ */
+ void processParameters(const uint8 *soundEntry) {
+ _processedFlags = 0; // Assembly: xor a ; 1d03
+
+ for (int i = 0; i < 3; ++i) {
+ // Assembly at l1d0dh: ld c,(hl) ; inc hl ; ld b,(hl) ; inc hl
+ // BC = calibration[i] from l38a6h
+ const int16 calibration = static_cast<int16>(kInitialCalibration[i]);
+
+ // Assembly at 1d13-1d1e:
+ // ld l,000h ; 1d13
+ // ld h,(ix+001h) ; 1d15 - H = entry[1+i] (IX advances each iteration)
+ // srl h ; rr l ; 1d18-1d1e - HL = entry[1+i] * 64
+ int16 hl = static_cast<int16>(soundEntry[1 + i]) * 64;
+
+ // Assembly at 1d20-1d21: or a ; sbc hl,bc
+ hl -= calibration;
+
+ // Assembly at 1d23-1d28: Track positive result
+ // jr z,l1d2ah ; jp m,l1d2ah ; set 6,a
+ if (hl > 0) {
+ _processedFlags |= (1 << (5 - i * 2));
+ }
+
+ // Assembly at 1d2a-1d31: srl a ; store result
+ _processedParams[i * 2] = hl;
+
+ // Assembly at 1d32-1d3d:
+ // ld c,000h ; 1d32
+ // ld b,(ix+004h) ; 1d34 - B = entry[4+i]
+ // srl b ; rr c ; 1d37-1d3d - BC = entry[4+i] * 64
+ const int16 bc = static_cast<int16>(soundEntry[4 + i]) * 64;
+
+ // Assembly at 1d3f-1d40: or a ; adc hl,bc
+ hl += bc;
+
+ // Assembly at 1d42-1d45: Track overflow
+ // jp p,l1d47h ; set 6,a
+ if (hl < 0) {
+ _processedFlags |= (1 << (4 - i * 2));
+ }
+
+ // Assembly at 1d47-1d4e: srl a ; store result
+ _processedParams[i * 2 + 1] = hl;
+
+ // Assembly at 1d4f: inc ix (pointer advances for next iteration)
+ }
+ // Assembly at 1d55: ld (001e6h),a - store flags
+ }
+#endif
+
void setupSound(int index) {
// Dispatch based on index (mirrors 0x6305 logic)
switch (index) {
@@ -446,181 +601,406 @@ private:
}
}
- // Sound 1: Shoot - Based on sub_2112h analysis
- // Laser-like descending pitch sweep from high to low
- // Original uses 8 channels with complex envelope, simplified here
+ /**
+ * Sound 1: Shoot - implements sub_2112h at 0x2112-0x2206
+ *
+ * Original assembly flow:
+ * sub_2112h:
+ * ld iy,(l3d16h) ; 2118 - IY = processed params buffer (0x02D1)
+ * ; Lines 211c-2185: Distribute 6 params to 8 channel working areas
+ * ld a,008h ; 2188 - Channel count = 8
+ * ld (l3bc3h),a ; 218a - Store channel count
+ * call sub_2e96h ; 218d - Call sound update loop
+ *
+ * Sound entry at l3904h: 81 00 00 00 0c 01 0c fe 0e 66 22 66 1c 0a
+ * Byte 0: 0x81 = flags (channel A, tone+noise enabled)
+ * Bytes 1-3: 0x00, 0x00, 0x00 (base params)
+ * Bytes 4-6: 0x0c, 0x01, 0x0c (periods: 12*64=768, 1*64=64, 12*64=768)
+ * Byte 7: 0xfe = -2 signed (delta sign/multiplier)
+ * Byte 8: 0x0e = 14 (entry size)
+ * Bytes 9-13: 0x66, 0x22, 0x66, 0x1c, 0x0a (volume nibbles per channel pair)
+ *
+ * Calibration at l1cffh:
+ * For each i (0-2): result1 = entry[1+i]*64 - calibration
+ * result2 = result1 + entry[4+i]*64
+ *
+ * With calibration=0 (initial game state):
+ * processed[0] = 0*64 - 0 = 0 (invalid period)
+ * processed[1] = 0 + 0x0c*64 = 768 (~81 Hz bass)
+ * processed[2] = 0*64 - 0 = 0 (invalid)
+ * processed[3] = 0 + 0x01*64 = 64 (~977 Hz high)
+ * processed[4] = 0*64 - 0 = 0 (invalid)
+ * processed[5] = 0 + 0x0c*64 = 768 (~81 Hz)
+ *
+ * sub_2112h distribution (lines 631-666):
+ * IY[0-1] â ch 0,3,4,7 at offset 0-1 (period base)
+ * IY[2-3] â ch 1,2,5,6 at offset 0-1 (period base)
+ * IY[4-5] â ch 0,1,4,5 at offset 2-3
+ * IY[6-7] â ch 2,3,6,7 at offset 2-3
+ * IY[8-9] â ch 0,1,2,3 at offset 4-5 (delta)
+ * IY[10-11] â ch 4,5,6,7 at offset 4-5 (delta)
+ *
+ * sub_2e96h runs outer loop 5 times (l3bc4h=5), each with different op_type
+ * sub_2d87h processes each channel with the current op_type
+ *
+ * CPC AY 1MHz: freq = 1000000 / (16 * period)
+ */
void setupShoot() {
- // Period 100 (~625Hz) sweeping to 800 (~78Hz) over ~1.2 seconds
- _tonePeriod = 100; // Starting period (higher pitch ~625Hz)
- _toneDelta = 12; // Increase period each tick (descend pitch)
- _channelVolume[0] = 15;
- _maxLoops = 60; // 60 ticks at 50Hz = 1.2 seconds
+ // Sound entry at l3904h
+ const uint8 soundEntry[] = {
+ 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe,
+ 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a
+ };
+
+ // Calibration processing at l1cffh
+ // The calibration value adjusts the base period. With the assembly's
+ // initial value (~8000), periods wrap in 16-bit arithmetic.
+ // Using calibration=700 to get periods in the laser frequency range.
+ // This gives processed[1] = 0 - 700 + 768 = 68 â 919 Hz
+ const int16 calibration = 700;
+
+ int16 processed[6];
+ for (int i = 0; i < 3; i++) {
+ int16 val1 = static_cast<int16>(soundEntry[1 + i]) * 64;
+ int16 val2 = static_cast<int16>(soundEntry[4 + i]) * 64;
+ processed[i * 2] = val1 - calibration;
+ processed[i * 2 + 1] = val1 - calibration + val2;
+ }
+ // With calib=700: processed = [-700, 68, -700, -636, -700, 68]
+ // Only processed[1] and processed[5] are valid (68 â 919 Hz)
+
+ _channelCount = 8;
+ _outerLoops = 5; // l3bc4h = 5
+ _maxLoops = 40; // 5 outer à 8 channels
+
+ // Channel setup following sub_2112h distribution
+ // Each channel has: [base_period, param2, delta]
+ for (int ch = 0; ch < 8; ch++) {
+ int16 basePeriod;
+ int16 param2;
+ int16 delta;
+
+ // offset 0-1: IY[0-1] â ch 0,3,4,7; IY[2-3] â ch 1,2,5,6
+ if (ch == 0 || ch == 3 || ch == 4 || ch == 7) {
+ basePeriod = processed[0]; // 0
+ } else {
+ basePeriod = processed[1]; // 768
+ }
+
+ // offset 2-3: IY[4-5] â ch 0,1,4,5; IY[6-7] â ch 2,3,6,7
+ if (ch == 0 || ch == 1 || ch == 4 || ch == 5) {
+ param2 = processed[2]; // 0
+ } else {
+ param2 = processed[3]; // 64
+ }
+
+ // offset 4-5: IY[8-9] â ch 0-3; IY[10-11] â ch 4-7
+ if (ch < 4) {
+ delta = processed[4]; // 0
+ } else {
+ delta = processed[5]; // 768
+ }
+
+ // sub_2d87h op_type=1: HL = basePeriod + delta
+ // For channels to be active, we need valid period
+ int16 effectivePeriod = basePeriod + delta;
+
+ if (effectivePeriod <= 0 || effectivePeriod > 4095) {
+ // Invalid period - check if base or param2 gives valid period
+ if (basePeriod > 0 && basePeriod <= 4095) {
+ effectivePeriod = basePeriod;
+ } else if (param2 > 0 && param2 <= 4095) {
+ effectivePeriod = param2;
+ } else if (delta > 0 && delta <= 4095) {
+ effectivePeriod = delta;
+ } else {
+ _channelPeriod[ch] = 0;
+ _channelDelta[ch] = 0;
+ _channelDone[ch] = true;
+ continue;
+ }
+ }
+
+ _channelPeriod[ch] = static_cast<uint16>(effectivePeriod);
+ // Delta sign from byte 7: 0xfe = -2 (signed)
+ // This indicates descending period (rising frequency)
+ // Using small delta for smooth sweep
+ _channelDelta[ch] = -2;
+ _channelDone[ch] = false;
+
+ debug("setupShoot: ch%d base=%d param2=%d delta=%d -> period=%d",
+ ch, basePeriod, param2, delta, _channelPeriod[ch]);
+ }
+
+ // Volume from sound entry bytes 9-13 (nibbles)
+ // 0x66 â 6,6; 0x22 â 2,2; 0x66 â 6,6; 0x1c â 12,1; 0x0a â 10,0
+ _channelVolume[0] = soundEntry[9] & 0x0F; // 6
+ _channelVolume[1] = (soundEntry[9] >> 4) & 0x0F; // 6
+ _channelVolume[2] = soundEntry[10] & 0x0F; // 2
+ _channelVolume[3] = (soundEntry[10] >> 4) & 0x0F; // 2
+ _channelVolume[4] = soundEntry[11] & 0x0F; // 6
+ _channelVolume[5] = (soundEntry[11] >> 4) & 0x0F; // 6
+ _channelVolume[6] = soundEntry[12] & 0x0F; // 12
+ _channelVolume[7] = (soundEntry[12] >> 4) & 0x0F; // 1
+
+ // Mixer: flags 0x81 = channel A (bits 0-1=01), tone+noise enabled (bits 2,3=0)
+ // Mixer register 7: bit 0 = tone A off, bit 3 = noise A off
+ // So 0x36 = 0b00110110 = tone A on, noise A on
+ writeReg(7, 0x36);
+
+ // Noise period from assembly - mid-range grit
+ writeReg(6, 0x10);
+
+ // Find first active channel for initial output
+ uint16 initPeriod = 768; // Default to entry value
+ for (int ch = 0; ch < 8; ch++) {
+ if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
+ initPeriod = _channelPeriod[ch];
+ break;
+ }
+ }
+
+ writeReg(0, initPeriod & 0xFF);
+ writeReg(1, (initPeriod >> 8) & 0x0F);
+ writeReg(8, 15); // Start at max volume
+
_loopCount = 0;
- // Enable tone on channel A
- writeReg(7, 0x3E); // Tone A only (bit 0 = 0)
+ debug("setupShoot: calib=%d initPeriod=%d", calibration, initPeriod);
}
- // Sound 2: Collide - Based on sub_26e2h analysis
- // Bump/thud sound with noise + low tone
+ /**
+ * Sound 2: Collide - implements sub_26e2h at 0x26e2
+ *
+ * From assembly: Sound 2 skips range check but uses same calibration.
+ * Uses 2 channels (copies to 2 pairs of working areas).
+ * Creates a "bump/thud" sound when hitting walls.
+ *
+ * Characteristics: Low frequency impact with noise, quick decay
+ */
void setupCollide() {
- _tonePeriod = 600; // Low frequency tone (~104Hz)
- _channelVolume[0] = 15;
- _maxLoops = 15; // ~300ms
+ _channelCount = 2; // Assembly: 2 channel pairs
+ _maxLoops = 12; // ~240ms - quick impact
_loopCount = 0;
- // Enable noise + tone on channel A for thud effect
- writeReg(6, 0x18); // Noise period (lower = rougher)
+ // Low frequency for "thump" character
+ // Period 500 â ~125 Hz (bass thud)
+ _channelPeriod[0] = 500;
+ _channelPeriod[1] = 600;
+ _channelDelta[0] = 30; // Descending pitch
+ _channelDelta[1] = 40;
+ _channelVolume[0] = 15;
+ _channelVolume[1] = 12;
+ _channelDone[0] = false;
+ _channelDone[1] = false;
+
+ // Set initial tone
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+
+ // Enable noise + tone on channel A for impact texture
+ // Noise adds the "crunch" to the collision
+ writeReg(6, 0x10); // Noise period (mid-range)
writeReg(7, 0x36); // Tone A + Noise A
+ writeReg(8, 15); // Max volume for impact
}
- // Sound 3: Step Up - Based on sub_2607h analysis
- // Short ascending blip for footstep going up
+ /**
+ * Sound 3: Step Up - implements sub_2607h at 0x2607
+ *
+ * Short ascending blip for footstep going up terrain.
+ * Quick pitch rise then decay - characteristic "bip" sound.
+ */
void setupStepUp() {
- // Period 400 (~156Hz) ascending to 150 (~417Hz)
- _tonePeriod = 400; // Start lower pitch
- _toneDelta = -10; // Decrease period (ascend pitch)
- _channelVolume[0] = 12;
- _maxLoops = 20; // ~400ms
+ _channelCount = 1;
+ _maxLoops = 8; // ~160ms - quick step
_loopCount = 0;
- writeReg(7, 0x3E); // Tone A only
+ // Start at medium pitch, ascend (period decreases)
+ // Period 300 (~208Hz) â Period 150 (~417Hz)
+ _channelPeriod[0] = 300;
+ _channelDelta[0] = -20; // Ascending pitch
+ _channelVolume[0] = 10;
+ _channelDone[0] = false;
+
+ // Set initial tone
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+
+ // Tone only - clean step sound
+ writeReg(7, 0x3E); // Tone A only, no noise
+ writeReg(8, 10); // Medium volume
}
- // Sounds 4-9: Generic handler based on sub_2207h
- // Each sound has specific characteristics
+ /**
+ * Sounds 4-9: Generic handler based on sub_2207h at 0x2207
+ *
+ * These sounds share a common handler in the original but have
+ * different parameter entries. Each has distinct characteristics.
+ */
void setupGeneric(int index) {
+ _channelCount = 1;
_loopCount = 0;
+ _channelDone[0] = false;
switch (index) {
case 4: // Step Down - descending blip for footstep going down
- _tonePeriod = 200; // Start higher pitch (~312Hz)
- _toneDelta = 15; // Increase period (descend pitch)
- _channelVolume[0] = 12;
- _maxLoops = 20; // ~400ms
- writeReg(7, 0x3E); // Tone A only
+ // Opposite of step up: starts higher, descends
+ _channelPeriod[0] = 180; // Start higher (~347Hz)
+ _channelDelta[0] = 15; // Descending pitch
+ _channelVolume[0] = 10;
+ _maxLoops = 8; // ~160ms - quick step
+ writeReg(7, 0x3E); // Tone A only
break;
- case 5: // Area transition sound
- _tonePeriod = 300; // Medium pitch (~208Hz)
- _toneDelta = 0;
- _channelVolume[0] = 15;
- _maxLoops = 25; // ~500ms
+ case 5: // Reserved/unused in original
+ _channelPeriod[0] = 250;
+ _channelDelta[0] = 0;
+ _channelVolume[0] = 8;
+ _maxLoops = 10;
writeReg(7, 0x3E);
break;
case 6: // Menu click - short high blip
- _tonePeriod = 150; // Higher pitch (~417Hz)
- _toneDelta = 0;
- _channelVolume[0] = 15;
- _maxLoops = 8; // ~160ms - short click
- writeReg(7, 0x3E);
+ // Quick "bip" for menu selection
+ _channelPeriod[0] = 120; // High pitch (~521Hz)
+ _channelDelta[0] = 0; // Steady pitch
+ _channelVolume[0] = 12;
+ _maxLoops = 4; // ~80ms - very short click
+ writeReg(7, 0x3E); // Tone A only
break;
- case 7: // Hit - impact sound with noise
- _tonePeriod = 400; // Low-mid pitch
- _toneDelta = 20; // Descending
+ case 7: // Hit - impact when player takes damage
+ // Sharp impact with noise texture
+ _channelPeriod[0] = 350; // Low-mid pitch (~179Hz)
+ _channelDelta[0] = 25; // Quick descend
_channelVolume[0] = 15;
- _maxLoops = 20;
- writeReg(6, 0x10); // Add some noise
- writeReg(7, 0x36); // Tone + Noise
+ _maxLoops = 15; // ~300ms
+ writeReg(6, 0x0c); // Noise adds "crunch"
+ writeReg(7, 0x36); // Tone + Noise
break;
- case 8: // Generic sound
- _tonePeriod = 250;
- _toneDelta = 5;
- _channelVolume[0] = 12;
- _maxLoops = 30;
+ case 8: // Reserved/generic
+ _channelPeriod[0] = 200;
+ _channelDelta[0] = 8;
+ _channelVolume[0] = 10;
+ _maxLoops = 15;
writeReg(7, 0x3E);
break;
- case 9: // Fallen - long descending sweep
- _tonePeriod = 150; // Start high (~417Hz)
- _toneDelta = 8; // Slow descent
+ case 9: // Fallen - long descending sweep when falling
+ // Dramatic fall sound - starts high, slowly descends
+ _channelPeriod[0] = 100; // Start high (~625Hz)
+ _channelDelta[0] = 6; // Slow descent
_channelVolume[0] = 15;
- _maxLoops = 80; // ~1.6 seconds - long fall
- writeReg(7, 0x3E);
+ _maxLoops = 60; // ~1.2 seconds - long fall
+ writeReg(7, 0x3E); // Tone only - "wheee" character
break;
default:
_finished = true;
return;
}
+
+ // Set initial tone period
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ writeReg(8, _channelVolume[0]);
}
- // Sounds >= 10: High index handler (l1d8fh)
- // Uses hardware envelope for sustained sounds
+ /**
+ * Sounds >= 10: High index handler (l1d8fh / sub_257bh)
+ *
+ * Uses hardware envelope for sustained atmospheric sounds.
+ * From assembly: calculates channel count as (index - 8),
+ * reads parameters from IX+9 onwards.
+ *
+ * Sound 7 entry at l38e9h has flags 0x87 (noise only, no tone).
+ */
void setupHighIndex(int index) {
+ _channelCount = 1;
_loopCount = 0;
+ _channelDone[0] = false;
- // Setup based on specific high index sounds
switch (index) {
- case 10: // Area Change - Based on flags byte 0x87 at 0x38e9
- // Bit 2=1 (NOISE enabled), Bit 3=0 (TONE disabled)
- _tonePeriod = 0;
- _toneDelta = 0;
- _maxLoops = 100; // ~2 seconds with envelope decay
- // Set noise period from data byte 9 (0x7f & 0x1f = 0x1f)
- writeReg(6, 0x1F); // Noise period
- // Mixer: disable tone, enable noise on channel A
- // 0x37 = 0b00110111: bits 0-2=1 (tones off), bit 3=0 (noise A on)
- writeReg(7, 0x37);
- // Use hardware envelope for natural decay
+ case 10: // Area Change - atmospheric transition
+ // From sound 7 entry flags 0x87: bit 2=1 (disable tone), bit 3=0 (noise on)
+ // Creates a "whoosh" effect for area transitions
+ _channelPeriod[0] = 0;
+ _channelDelta[0] = 0;
+ _channelVolume[0] = 15;
+ _maxLoops = 50; // ~1 second
+
+ writeReg(6, 0x18); // Noise period - medium texture
+ writeReg(7, 0x37); // 0b00110111: noise A only, no tone
+ // Hardware envelope for smooth decay
writeReg(11, 0x00); // Envelope period low
- writeReg(12, 0x20); // Envelope period high (slow decay)
- writeReg(13, 0x00); // Envelope shape: single decay (\)
- writeReg(8, 0x10); // Volume A = envelope
+ writeReg(12, 0x18); // Envelope period high
+ writeReg(13, 0x00); // Shape: single decay (\)
+ writeReg(8, 0x10); // Volume = envelope mode
break;
- case 11: // Explosion/rumble
- _tonePeriod = 800; // Low rumble
- _maxLoops = 60;
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
- writeReg(6, 0x1F); // Noise period (rough)
- writeReg(7, 0x36); // Tone A + Noise A
+ case 11: // Explosion/rumble effect
+ _channelPeriod[0] = 700;
+ _channelDelta[0] = 20; // Descending rumble
+ _channelVolume[0] = 15;
+ _maxLoops = 40;
+
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ writeReg(6, 0x1C); // Rough noise
+ writeReg(7, 0x36); // Tone + Noise
writeReg(11, 0x00);
- writeReg(12, 0x30); // Medium-slow decay
- writeReg(13, 0x00); // Decay shape
+ writeReg(12, 0x20);
+ writeReg(13, 0x00); // Decay
writeReg(8, 0x10);
break;
- case 12: // Warning tone
- _tonePeriod = 180; // ~347Hz
- _maxLoops = 80;
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
- writeReg(7, 0x3E);
+ case 12: // Warning/alert tone
+ _channelPeriod[0] = 150;
+ _channelDelta[0] = 0;
+ _channelVolume[0] = 15;
+ _maxLoops = 60;
+
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ writeReg(7, 0x3E); // Tone only
writeReg(11, 0x00);
- writeReg(12, 0x08); // Faster cycle
+ writeReg(12, 0x06); // Fast cycle for alarm effect
writeReg(13, 0x0E); // Continue, alternate (warble)
writeReg(8, 0x10);
break;
- case 13: // Mission Complete - triumphant jingle
- _tonePeriod = 150; // Start high (~417Hz)
- _toneDelta = -1; // Slowly rise in pitch
- _maxLoops = 120; // ~2.4 seconds
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
- writeReg(7, 0x3E);
+ case 13: // Mission Complete - triumphant fanfare
+ // Rising pitch with envelope swell
+ _channelPeriod[0] = 200;
+ _channelDelta[0] = -2; // Ascending pitch
+ _channelVolume[0] = 15;
+ _maxLoops = 80; // ~1.6 seconds
+
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ writeReg(7, 0x3E); // Tone only - clean victory sound
writeReg(11, 0x00);
- writeReg(12, 0x40); // Very slow envelope
- writeReg(13, 0x0E); // Attack/decay cycle
+ writeReg(12, 0x30); // Slow envelope
+ writeReg(13, 0x0A); // Attack/decay (/ then hold)
writeReg(8, 0x10);
break;
default:
- // Generic high index sound
- _tonePeriod = 250;
- _maxLoops = 50;
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ // Generic high index - simple tone with decay
+ _channelPeriod[0] = 200;
+ _channelDelta[0] = 0;
+ _channelVolume[0] = 12;
+ _maxLoops = 30;
+
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
writeReg(7, 0x3E);
- writeReg(8, 0x10);
writeReg(11, 0x00);
writeReg(12, 0x10);
writeReg(13, 0x00);
+ writeReg(8, 0x10);
break;
}
}
@@ -662,62 +1042,165 @@ private:
}
}
+ /**
+ * Sound update loop for Shoot - implements sub_2e96h at 0x2e96-0x2edb
+ *
+ * Original assembly structure:
+ * sub_2e96h:
+ * call sub_2a65h ; 2e9c - Initialize
+ * ld de,00006h ; 2e9f - 6 bytes per channel
+ * ld a,(l3bc4h) ; 2ea6 - A = outer loop count (5)
+ * ld l,a ; 2ea9 - L = 5
+ * l2eaah:
+ * ld (0025fh),a ; 2eaa - Store op_type = outer counter
+ * ld a,(l3bc3h) ; 2eaf - A = channel count (8)
+ * ld b,a ; 2eb2 - B = 8
+ * ld ix,00223h ; 2eb3 - IX = channel data base
+ * l2eb7h:
+ * call sub_2d87h ; 2eb7 - Process one channel step
+ * add ix,de ; 2ec5 - Next channel (+6 bytes)
+ * djnz l2eb7h ; 2ec7 - Loop 8 channels
+ * dec l ; 2ecd - Decrement outer counter
+ * jr nz,l2eaah ; 2ecf - Continue if L > 0
+ *
+ * The 8 channels are mixed by cycling through them rapidly.
+ * Each tick we output one channel's period to the AY.
+ */
void updateShoot() {
- // Descending pitch sweep (period increases = lower pitch)
- _tonePeriod += _toneDelta;
- if (_tonePeriod > 1000) _tonePeriod = 1000; // Cap at ~62Hz
+ bool anyActive = false;
+ int activeChannel = -1;
+ uint16 outputPeriod = 0;
+
+ // Cycle through channels - each tick advances to next active channel
+ int startCh = _loopCount % _channelCount;
+ for (int i = 0; i < _channelCount; i++) {
+ int ch = (startCh + i) % _channelCount;
+ if (_channelDone[ch]) continue;
+
+ // Apply delta to period (sub_2d87h simplified)
+ int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
+
+ // Check bounds (AY period is 12-bit: 0-4095)
+ // Use lower bound of 20 to avoid very high frequencies that sound harsh
+ if (newPeriod <= 20 || newPeriod > 4095) {
+ _channelDone[ch] = true;
+ continue;
+ }
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ anyActive = true;
- // Volume decay over duration
- int vol = 15 - (_loopCount / 4);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
+ // Use this channel for output
+ if (activeChannel < 0) {
+ activeChannel = ch;
+ outputPeriod = _channelPeriod[ch];
+ }
+ }
+
+ // Output to AY
+ if (activeChannel >= 0 && outputPeriod > 0) {
+ writeReg(0, outputPeriod & 0xFF);
+ writeReg(1, (outputPeriod >> 8) & 0x0F);
+
+ // Volume decay: starts at max, decays over ~40 ticks
+ // Use slower decay to maintain presence throughout the sound
+ int vol = 15 - (_loopCount / 4);
+ if (vol < 2) vol = 2;
+ writeReg(8, static_cast<uint8>(vol));
+
+ debug("updateShoot: loop=%d ch=%d period=%d freq=%dHz vol=%d",
+ _loopCount, activeChannel, outputPeriod,
+ 1000000 / (16 * outputPeriod), vol);
+ }
+
+ // Check completion
+ if (!anyActive || _loopCount >= _maxLoops) {
+ debug("updateShoot: FINISHED at loop=%d", _loopCount);
+ _finished = true;
+ initAY();
+ }
}
void updateCollide() {
- // Bump sound - quick decay with slight pitch drop
- _tonePeriod += 10; // Slight pitch drop
- if (_tonePeriod > 900) _tonePeriod = 900;
+ // Process both channels
+ for (int ch = 0; ch < _channelCount; ++ch) {
+ if (!_channelDone[ch]) {
+ int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
+ if (newPeriod > 1200) {
+ _channelDone[ch] = true;
+ } else {
+ _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ }
+ }
+ }
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ // Output primary channel
+ if (!_channelDone[0]) {
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ }
- // Quick decay
- int vol = 15 - _loopCount;
+ // Quick decay - collision is sharp impact
+ int vol = 15 - (_loopCount * 2);
if (vol < 0) vol = 0;
writeReg(8, vol);
+
+ // Fade noise out quickly too
+ int noise = 0x10 - _loopCount;
+ if (noise < 0x04) noise = 0x04;
+ writeReg(6, noise);
}
void updateStepUp() {
- // Ascending pitch sweep (period decreases = higher pitch)
- int period = (int)_tonePeriod + _toneDelta;
- if (period < 100) period = 100; // Cap at ~625Hz
- _tonePeriod = period;
+ if (!_channelDone[0]) {
+ // Ascending pitch sweep (period decreases = higher pitch)
+ int32 newPeriod = static_cast<int32>(_channelPeriod[0]) + _channelDelta[0];
+ if (newPeriod < 80) { // Cap at ~781Hz
+ _channelDone[0] = true;
+ } else {
+ _channelPeriod[0] = static_cast<uint16>(newPeriod);
+ }
+ }
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- // Volume decay
- int vol = _channelVolume[0] - (_loopCount / 3);
+ // Quick volume decay for short "bip"
+ int vol = _channelVolume[0] - _loopCount;
if (vol < 0) vol = 0;
writeReg(8, vol);
}
void updateGeneric() {
- // Apply tone delta
- int period = (int)_tonePeriod + _toneDelta;
- if (period < 50) period = 50;
- if (period > 2000) period = 2000;
- _tonePeriod = period;
-
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
-
- // Volume decay based on sound type
- int decayRate = (_index == 6) ? 2 : 5; // Menu click decays faster
- int vol = _channelVolume[0] - (_loopCount / decayRate);
+ if (!_channelDone[0]) {
+ // Apply tone delta
+ int32 newPeriod = static_cast<int32>(_channelPeriod[0]) + _channelDelta[0];
+ if (newPeriod < 50 || newPeriod > 2000) {
+ _channelDone[0] = true;
+ } else {
+ _channelPeriod[0] = static_cast<uint16>(newPeriod);
+ }
+ }
+
+ writeReg(0, _channelPeriod[0] & 0xFF);
+ writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+
+ // Volume decay varies by sound type
+ int vol;
+ switch (_index) {
+ case 6: // Menu click - very fast decay
+ vol = _channelVolume[0] - (_loopCount * 3);
+ break;
+ case 9: // Fallen - slow decay
+ vol = _channelVolume[0] - (_loopCount / 8);
+ break;
+ case 7: // Hit - medium-fast decay
+ vol = _channelVolume[0] - _loopCount;
+ break;
+ default:
+ vol = _channelVolume[0] - (_loopCount / 2);
+ break;
+ }
if (vol < 0) vol = 0;
writeReg(8, vol);
}
Commit: f584c68503b1d17f4d4b22c6f69dda3486e14ad1
https://github.com/scummvm/scummvm/commit/f584c68503b1d17f4d4b22c6f69dda3486e14ad1
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: unified sound approach for some driller sounds for CPC
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 08d4f358ad2..1ab8e27a7d6 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -341,49 +341,63 @@ static const uint8 kDrillerCPCSoundDefs[][7] = {
* - Bit 3: If set, DISABLE noise (assembly at 482c: bit 3,(ix+000h))
* - Bit 6: Disable flag (checked at 1cdc: bit 6,(ix+000h))
* - Bit 7: Additional flag
- * Bytes 1-3: Parameters for calibration processing (IX[1], IX[2], IX[3])
- * Bytes 4-6: Secondary parameters (IX[4], IX[5], IX[6])
- * Byte 7: Delta sign/additional param (stored at 03a5ah)
- * Byte 8: Entry size
- * Bytes 9+: Additional parameters (volume nibbles, etc.)
+ * Bytes 1-3: Base params for calibration (multiply by 64, subtract calibration)
+ * Bytes 4-6: Add params (multiply by 64, add to result)
+ * Byte 7: Delta sign/multiplier (signed: 0xfe=-2, 0xfd=-3, 0xff=-1)
+ * Byte 8: Entry size (total bytes in entry)
+ * Bytes 9+: Volume nibbles (low=even ch, high=odd ch)
+ *
+ * Calibration formula at l1cffh:
+ * For each i (0-2):
+ * result1 = entry[1+i] * 64 - calibration[i]
+ * result2 = result1 + entry[4+i] * 64
+ *
+ * The calibration value is position-dependent in the original game.
+ * We use a fixed calibration that produces reasonable frequencies.
*/
-// Sound entry raw bytes - preserved for reference and future use
-// These entries will be used for full calibration-based implementation
-#if 0 // Currently using tuned approximations instead
-static const uint8 kDrillerCPCSoundEntry1[] = {
- // Sound 1 (Shoot) at l3904h (file offset 0x1CA2)
- // Flags 0x81: channel A, bits 2,3=0 â tone+noise enabled
- 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe, 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a
+
+// Sound entry data from DRILL.BIN - used by unified processing system
+// Entry structure: flags(1), base[3], add[3], delta(1), size(1), vol/extra[...]
+// Channel count for sub_2207h: compare bytes 0x0c-0x0f
+
+static const uint8 kSoundEntry7[] = {
+ // Sound 7 (Hit) at l38e9h - 27 bytes (0x1b)
+ // Type 7 â sub_2207h, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
+ // Flags 0x87: bit 2=1 (no tone), bit 3=0 (noise on)
+ 0x87, 0x00, 0x00, 0x00, 0x08, 0x0a, 0x08, 0xff, // bytes 0-7
+ 0x1b, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x05, 0x05 // bytes 8-15 (0c-0f for channel count)
};
-static const uint8 kDrillerCPCSoundEntry6[] = {
- // Sound 6 at l3912h (file offset 0x1CB0)
- // Flags 0x86: channel B/C, needs verification
- 0x86, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0xfd, 0x10, 0x7f, 0x7f, 0x00, 0x03, 0x03
+static const uint8 kSoundEntry1[] = {
+ // Sound 1 (Shoot) at l3904h - 14 bytes (0x0e)
+ // Type 1 â sub_2112h â 8 channels (hardcoded in handler)
+ // Flags 0x81: bits 2,3=0 â tone+noise enabled
+ 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe, // bytes 0-7
+ 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a // bytes 8-13
+};
+
+static const uint8 kSoundEntry6[] = {
+ // Sound 6 (Menu) at l3912h - 16 bytes (0x10)
+ // Type 6 â sub_2207h, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
+ // Flags 0x86: bit 2=1 (no tone), bit 3=0 (noise on)
+ 0x86, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0xfd, // bytes 0-7
+ 0x10, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x03, 0x03 // bytes 8-15 (0c-0f for channel count)
};
-#endif
/**
- * Initial calibration values from assembly at 0x4e78-0x4e87.
+ * Calibration value for sound processing.
+ *
+ * The original assembly uses position-dependent calibration (~8000 initially).
+ * This creates wrapped 16-bit values. We use a smaller calibration that
+ * produces valid AY periods in the audible frequency range.
*
- * Original code:
- * ld a,07dh ; 4e76 - Default A = 125
- * ld (l39f9h),a ; 4e78 - Store
- * ld h,a ; 4e7b - H = 125
- * ld l,000h ; 4e7c - HL = 125 * 256 = 32000
- * srl h ; rr l ; 4e7e-4e84 - HL = 32000 / 4 = 8000
- * add hl,de ; 4e86 - HL += DE (DE = 0x20 = 32)
- * ld (l38a6h),hl ; 4e87 - Store calibration = 8032
+ * Calibration affects the starting period:
+ * period = entry[4+i] * 64 - calibration
*
- * Note: The calibration value 0x1F60 (8032) causes processed params to be
- * large negative numbers, which wrap to values > 4095 (AY max period).
- * Setting to 0 for now - needs investigation of actual in-game values.
- * The calibration may be initialized differently at game start or may
- * represent position-based attenuation that doesn't apply to shoot sound.
+ * With calibration=700 and entry[4]=0x0c (12):
+ * period = 12 * 64 - 700 = 768 - 700 = 68 â 919 Hz
*/
-#if 0 // Calibration system not yet fully implemented
-static const uint16 kInitialCalibration[3] = {0x0000, 0x0000, 0x0000};
-#endif
+static const int16 kSoundCalibration = 700;
class DrillerCPCSfxStream : public Audio::AudioStream {
public:
@@ -494,6 +508,184 @@ private:
}
}
+ /**
+ * Unified sound entry processing - implements l1cffh calibration
+ *
+ * Processes a sound entry through the calibration system and sets up
+ * all channel parameters. This is the core function that makes all
+ * sounds work consistently.
+ *
+ * @param entry Raw sound entry bytes (14+ bytes from l38e9h, l3904h, etc.)
+ * @param numChannels Number of channels to use (8 for shoot, fewer for others)
+ * @param outerLoops Number of outer loop iterations (l3bc4h, typically 5)
+ */
+ void setupFromEntry(const uint8 *entry, int numChannels, int outerLoops) {
+ // Extract flags from byte 0
+ uint8 flags = entry[0];
+ int8 deltaSign = static_cast<int8>(entry[7]); // Signed delta from byte 7
+
+ // Process parameters through calibration (l1cffh)
+ // Formula: result1 = entry[1+i]*64 - calibration
+ // result2 = result1 + entry[4+i]*64
+ int16 processed[6];
+ for (int i = 0; i < 3; i++) {
+ int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
+ int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
+ processed[i * 2] = val1 - kSoundCalibration;
+ processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
+ }
+
+ _channelCount = numChannels;
+ _outerLoops = outerLoops;
+ _maxLoops = outerLoops * numChannels;
+
+ // Channel setup following sub_2112h distribution pattern
+ for (int ch = 0; ch < numChannels; ch++) {
+ int16 basePeriod, param2, delta;
+
+ // Distribution pattern from assembly (lines 631-666 of sub_2112h)
+ // IY[0-1] â ch 0,3,4,7; IY[2-3] â ch 1,2,5,6
+ if (ch == 0 || ch == 3 || ch == 4 || ch == 7) {
+ basePeriod = processed[0];
+ } else {
+ basePeriod = processed[1];
+ }
+
+ // IY[4-5] â ch 0,1,4,5; IY[6-7] â ch 2,3,6,7
+ if (ch == 0 || ch == 1 || ch == 4 || ch == 5) {
+ param2 = processed[2];
+ } else {
+ param2 = processed[3];
+ }
+
+ // IY[8-9] â ch 0-3; IY[10-11] â ch 4-7
+ if (ch < 4) {
+ delta = processed[4];
+ } else {
+ delta = processed[5];
+ }
+
+ // Calculate effective period (sub_2d87h op_type=1: HL = base + delta)
+ int16 effectivePeriod = basePeriod + delta;
+
+ // Handle invalid periods - try fallbacks
+ if (effectivePeriod <= 0 || effectivePeriod > 4095) {
+ if (basePeriod > 0 && basePeriod <= 4095) {
+ effectivePeriod = basePeriod;
+ } else if (param2 > 0 && param2 <= 4095) {
+ effectivePeriod = param2;
+ } else if (delta > 0 && delta <= 4095) {
+ effectivePeriod = delta;
+ } else {
+ _channelPeriod[ch] = 0;
+ _channelDelta[ch] = 0;
+ _channelDone[ch] = true;
+ continue;
+ }
+ }
+
+ _channelPeriod[ch] = static_cast<uint16>(effectivePeriod);
+ _channelDelta[ch] = deltaSign; // Use delta sign from entry byte 7
+ _channelDone[ch] = false;
+
+ debug("setupFromEntry: ch%d base=%d param2=%d delta=%d -> period=%d deltaSgn=%d",
+ ch, basePeriod, param2, delta, _channelPeriod[ch], deltaSign);
+ }
+
+ // Extract volume nibbles from bytes 9+ (low nibble = even ch, high = odd ch)
+ for (int i = 0; i < (numChannels + 1) / 2 && i < 5; i++) {
+ int ch0 = i * 2;
+ int ch1 = i * 2 + 1;
+ if (ch0 < numChannels) _channelVolume[ch0] = entry[9 + i] & 0x0F;
+ if (ch1 < numChannels) _channelVolume[ch1] = (entry[9 + i] >> 4) & 0x0F;
+ }
+
+ // Configure mixer based on flags
+ // Bit 2: disable tone, Bit 3: disable noise
+ uint8 mixer = 0x3F; // Start with all disabled
+ int channel = (flags & 0x03); // Channel A=1, B=2, C=3
+ if (channel >= 1 && channel <= 3) {
+ int chIdx = channel - 1;
+ if (!(flags & 0x04)) mixer &= ~(1 << chIdx); // Enable tone
+ if (!(flags & 0x08)) mixer &= ~(1 << (chIdx + 3)); // Enable noise
+ }
+ writeReg(7, mixer);
+
+ // Noise period (reasonable default)
+ writeReg(6, 0x10);
+
+ // Find first active channel for initial output
+ uint16 initPeriod = 100;
+ for (int ch = 0; ch < numChannels; ch++) {
+ if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
+ initPeriod = _channelPeriod[ch];
+ break;
+ }
+ }
+
+ writeReg(0, initPeriod & 0xFF);
+ writeReg(1, (initPeriod >> 8) & 0x0F);
+ writeReg(8, 15); // Start at max volume
+
+ _loopCount = 0;
+
+ debug("setupFromEntry: flags=0x%02x mixer=0x%02x initPeriod=%d deltaSign=%d",
+ flags, mixer, initPeriod, deltaSign);
+ }
+
+ /**
+ * Unified update function for entry-based sounds
+ * Applies delta per outer loop, cycles through channels
+ */
+ void updateFromEntry() {
+ int outerLoop = _loopCount / _channelCount;
+ int innerIdx = _loopCount % _channelCount;
+
+ // Apply delta at start of each outer loop
+ if (innerIdx == 0 && _loopCount > 0) {
+ for (int ch = 0; ch < _channelCount; ch++) {
+ if (_channelDone[ch]) continue;
+
+ int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
+ if (newPeriod <= 20 || newPeriod > 4095) {
+ _channelDone[ch] = true;
+ continue;
+ }
+ _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ }
+ }
+
+ // Find active channel for output
+ bool anyActive = false;
+ int activeChannel = -1;
+ uint16 outputPeriod = 0;
+
+ for (int i = 0; i < _channelCount; i++) {
+ int ch = (innerIdx + i) % _channelCount;
+ if (_channelDone[ch]) continue;
+
+ anyActive = true;
+ if (activeChannel < 0) {
+ activeChannel = ch;
+ outputPeriod = _channelPeriod[ch];
+ }
+ }
+
+ if (activeChannel >= 0 && outputPeriod > 0) {
+ writeReg(0, outputPeriod & 0xFF);
+ writeReg(1, (outputPeriod >> 8) & 0x0F);
+
+ int vol = 15 - (outerLoop * 2);
+ if (vol < 4) vol = 4;
+ writeReg(8, static_cast<uint8>(vol));
+ }
+
+ if (!anyActive || _loopCount >= _maxLoops) {
+ _finished = true;
+ initAY();
+ }
+ }
+
/**
* Processes sound entry parameters through calibration system.
* NOTE: Currently disabled - using tuned approximations instead.
@@ -604,169 +796,14 @@ private:
/**
* Sound 1: Shoot - implements sub_2112h at 0x2112-0x2206
*
- * Original assembly flow:
- * sub_2112h:
- * ld iy,(l3d16h) ; 2118 - IY = processed params buffer (0x02D1)
- * ; Lines 211c-2185: Distribute 6 params to 8 channel working areas
- * ld a,008h ; 2188 - Channel count = 8
- * ld (l3bc3h),a ; 218a - Store channel count
- * call sub_2e96h ; 218d - Call sound update loop
- *
- * Sound entry at l3904h: 81 00 00 00 0c 01 0c fe 0e 66 22 66 1c 0a
- * Byte 0: 0x81 = flags (channel A, tone+noise enabled)
- * Bytes 1-3: 0x00, 0x00, 0x00 (base params)
- * Bytes 4-6: 0x0c, 0x01, 0x0c (periods: 12*64=768, 1*64=64, 12*64=768)
- * Byte 7: 0xfe = -2 signed (delta sign/multiplier)
- * Byte 8: 0x0e = 14 (entry size)
- * Bytes 9-13: 0x66, 0x22, 0x66, 0x1c, 0x0a (volume nibbles per channel pair)
- *
- * Calibration at l1cffh:
- * For each i (0-2): result1 = entry[1+i]*64 - calibration
- * result2 = result1 + entry[4+i]*64
- *
- * With calibration=0 (initial game state):
- * processed[0] = 0*64 - 0 = 0 (invalid period)
- * processed[1] = 0 + 0x0c*64 = 768 (~81 Hz bass)
- * processed[2] = 0*64 - 0 = 0 (invalid)
- * processed[3] = 0 + 0x01*64 = 64 (~977 Hz high)
- * processed[4] = 0*64 - 0 = 0 (invalid)
- * processed[5] = 0 + 0x0c*64 = 768 (~81 Hz)
+ * Uses unified setupFromEntry() with assembly data from l3904h.
+ * Assembly: 8 channels (ld a,008h at 2188h), 5 outer loops (l3bc4h).
*
- * sub_2112h distribution (lines 631-666):
- * IY[0-1] â ch 0,3,4,7 at offset 0-1 (period base)
- * IY[2-3] â ch 1,2,5,6 at offset 0-1 (period base)
- * IY[4-5] â ch 0,1,4,5 at offset 2-3
- * IY[6-7] â ch 2,3,6,7 at offset 2-3
- * IY[8-9] â ch 0,1,2,3 at offset 4-5 (delta)
- * IY[10-11] â ch 4,5,6,7 at offset 4-5 (delta)
- *
- * sub_2e96h runs outer loop 5 times (l3bc4h=5), each with different op_type
- * sub_2d87h processes each channel with the current op_type
- *
- * CPC AY 1MHz: freq = 1000000 / (16 * period)
+ * Entry at l3904h: 81 00 00 00 0c 01 0c fe 0e 66 22 66 1c 0a
*/
void setupShoot() {
- // Sound entry at l3904h
- const uint8 soundEntry[] = {
- 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe,
- 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a
- };
-
- // Calibration processing at l1cffh
- // The calibration value adjusts the base period. With the assembly's
- // initial value (~8000), periods wrap in 16-bit arithmetic.
- // Using calibration=700 to get periods in the laser frequency range.
- // This gives processed[1] = 0 - 700 + 768 = 68 â 919 Hz
- const int16 calibration = 700;
-
- int16 processed[6];
- for (int i = 0; i < 3; i++) {
- int16 val1 = static_cast<int16>(soundEntry[1 + i]) * 64;
- int16 val2 = static_cast<int16>(soundEntry[4 + i]) * 64;
- processed[i * 2] = val1 - calibration;
- processed[i * 2 + 1] = val1 - calibration + val2;
- }
- // With calib=700: processed = [-700, 68, -700, -636, -700, 68]
- // Only processed[1] and processed[5] are valid (68 â 919 Hz)
-
- _channelCount = 8;
- _outerLoops = 5; // l3bc4h = 5
- _maxLoops = 40; // 5 outer à 8 channels
-
- // Channel setup following sub_2112h distribution
- // Each channel has: [base_period, param2, delta]
- for (int ch = 0; ch < 8; ch++) {
- int16 basePeriod;
- int16 param2;
- int16 delta;
-
- // offset 0-1: IY[0-1] â ch 0,3,4,7; IY[2-3] â ch 1,2,5,6
- if (ch == 0 || ch == 3 || ch == 4 || ch == 7) {
- basePeriod = processed[0]; // 0
- } else {
- basePeriod = processed[1]; // 768
- }
-
- // offset 2-3: IY[4-5] â ch 0,1,4,5; IY[6-7] â ch 2,3,6,7
- if (ch == 0 || ch == 1 || ch == 4 || ch == 5) {
- param2 = processed[2]; // 0
- } else {
- param2 = processed[3]; // 64
- }
-
- // offset 4-5: IY[8-9] â ch 0-3; IY[10-11] â ch 4-7
- if (ch < 4) {
- delta = processed[4]; // 0
- } else {
- delta = processed[5]; // 768
- }
-
- // sub_2d87h op_type=1: HL = basePeriod + delta
- // For channels to be active, we need valid period
- int16 effectivePeriod = basePeriod + delta;
-
- if (effectivePeriod <= 0 || effectivePeriod > 4095) {
- // Invalid period - check if base or param2 gives valid period
- if (basePeriod > 0 && basePeriod <= 4095) {
- effectivePeriod = basePeriod;
- } else if (param2 > 0 && param2 <= 4095) {
- effectivePeriod = param2;
- } else if (delta > 0 && delta <= 4095) {
- effectivePeriod = delta;
- } else {
- _channelPeriod[ch] = 0;
- _channelDelta[ch] = 0;
- _channelDone[ch] = true;
- continue;
- }
- }
-
- _channelPeriod[ch] = static_cast<uint16>(effectivePeriod);
- // Delta sign from byte 7: 0xfe = -2 (signed)
- // This indicates descending period (rising frequency)
- // Using small delta for smooth sweep
- _channelDelta[ch] = -2;
- _channelDone[ch] = false;
-
- debug("setupShoot: ch%d base=%d param2=%d delta=%d -> period=%d",
- ch, basePeriod, param2, delta, _channelPeriod[ch]);
- }
-
- // Volume from sound entry bytes 9-13 (nibbles)
- // 0x66 â 6,6; 0x22 â 2,2; 0x66 â 6,6; 0x1c â 12,1; 0x0a â 10,0
- _channelVolume[0] = soundEntry[9] & 0x0F; // 6
- _channelVolume[1] = (soundEntry[9] >> 4) & 0x0F; // 6
- _channelVolume[2] = soundEntry[10] & 0x0F; // 2
- _channelVolume[3] = (soundEntry[10] >> 4) & 0x0F; // 2
- _channelVolume[4] = soundEntry[11] & 0x0F; // 6
- _channelVolume[5] = (soundEntry[11] >> 4) & 0x0F; // 6
- _channelVolume[6] = soundEntry[12] & 0x0F; // 12
- _channelVolume[7] = (soundEntry[12] >> 4) & 0x0F; // 1
-
- // Mixer: flags 0x81 = channel A (bits 0-1=01), tone+noise enabled (bits 2,3=0)
- // Mixer register 7: bit 0 = tone A off, bit 3 = noise A off
- // So 0x36 = 0b00110110 = tone A on, noise A on
- writeReg(7, 0x36);
-
- // Noise period from assembly - mid-range grit
- writeReg(6, 0x10);
-
- // Find first active channel for initial output
- uint16 initPeriod = 768; // Default to entry value
- for (int ch = 0; ch < 8; ch++) {
- if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
- initPeriod = _channelPeriod[ch];
- break;
- }
- }
-
- writeReg(0, initPeriod & 0xFF);
- writeReg(1, (initPeriod >> 8) & 0x0F);
- writeReg(8, 15); // Start at max volume
-
- _loopCount = 0;
-
- debug("setupShoot: calib=%d initPeriod=%d", calibration, initPeriod);
+ // Assembly: sub_2112h uses 8 channels and calls sub_2e96h with l3bc4h=5
+ setupFromEntry(kSoundEntry1, 8, 5);
}
/**
@@ -835,25 +872,46 @@ private:
/**
* Sounds 4-9: Generic handler based on sub_2207h at 0x2207
*
- * These sounds share a common handler in the original but have
- * different parameter entries. Each has distinct characteristics.
+ * Assembly uses calibrated params from l1cffh. Channel count determined by
+ * comparing entry bytes 0x0c-0x0f:
+ * - All equal â 5 channels
+ * - One pair differs â 6 channels
+ * - Both pairs differ â 8 channels
+ *
+ * Sound 6 (l3912h): bytes 0x0c-0x0f = 03 03 03 03 â 5 channels
+ * Sound 7 (l38e9h): bytes 0x0c-0x0f = 03 03 05 05 â 8 channels
*/
void setupGeneric(int index) {
- _channelCount = 1;
_loopCount = 0;
- _channelDone[0] = false;
switch (index) {
- case 4: // Step Down - descending blip for footstep going down
- // Opposite of step up: starts higher, descends
- _channelPeriod[0] = 180; // Start higher (~347Hz)
- _channelDelta[0] = 15; // Descending pitch
+ case 6: // Menu - uses l3912h entry data (type 6)
+ // Assembly: bytes 0c-0f all equal (03 03 03 03) â 5 channels
+ // l3bc4h outer loops not explicitly specified, using 5
+ setupFromEntry(kSoundEntry6, 5, 5);
+ return;
+
+ case 7: // Hit - uses l38e9h entry data (type 7)
+ // Assembly: bytes 0c != 0e (03 != 05) â 8 channels
+ setupFromEntry(kSoundEntry7, 8, 5);
+ return;
+
+ // Sounds 4, 5, 8, 9: No explicit entry data found in assembly.
+ // Using approximations until entries are located.
+ // TODO: Find assembly entry data for these sounds
+ case 4: // Step Down
+ _channelCount = 1;
+ _channelDone[0] = false;
+ _channelPeriod[0] = 180;
+ _channelDelta[0] = 15;
_channelVolume[0] = 10;
- _maxLoops = 8; // ~160ms - quick step
- writeReg(7, 0x3E); // Tone A only
+ _maxLoops = 8;
+ writeReg(7, 0x3E);
break;
- case 5: // Reserved/unused in original
+ case 5: // Reserved
+ _channelCount = 1;
+ _channelDone[0] = false;
_channelPeriod[0] = 250;
_channelDelta[0] = 0;
_channelVolume[0] = 8;
@@ -861,26 +919,9 @@ private:
writeReg(7, 0x3E);
break;
- case 6: // Menu click - short high blip
- // Quick "bip" for menu selection
- _channelPeriod[0] = 120; // High pitch (~521Hz)
- _channelDelta[0] = 0; // Steady pitch
- _channelVolume[0] = 12;
- _maxLoops = 4; // ~80ms - very short click
- writeReg(7, 0x3E); // Tone A only
- break;
-
- case 7: // Hit - impact when player takes damage
- // Sharp impact with noise texture
- _channelPeriod[0] = 350; // Low-mid pitch (~179Hz)
- _channelDelta[0] = 25; // Quick descend
- _channelVolume[0] = 15;
- _maxLoops = 15; // ~300ms
- writeReg(6, 0x0c); // Noise adds "crunch"
- writeReg(7, 0x36); // Tone + Noise
- break;
-
- case 8: // Reserved/generic
+ case 8: // Reserved
+ _channelCount = 1;
+ _channelDone[0] = false;
_channelPeriod[0] = 200;
_channelDelta[0] = 8;
_channelVolume[0] = 10;
@@ -888,13 +929,14 @@ private:
writeReg(7, 0x3E);
break;
- case 9: // Fallen - long descending sweep when falling
- // Dramatic fall sound - starts high, slowly descends
- _channelPeriod[0] = 100; // Start high (~625Hz)
- _channelDelta[0] = 6; // Slow descent
+ case 9: // Fallen
+ _channelCount = 1;
+ _channelDone[0] = false;
+ _channelPeriod[0] = 100;
+ _channelDelta[0] = 6;
_channelVolume[0] = 15;
- _maxLoops = 60; // ~1.2 seconds - long fall
- writeReg(7, 0x3E); // Tone only - "wheee" character
+ _maxLoops = 60;
+ writeReg(7, 0x3E);
break;
default:
@@ -902,7 +944,7 @@ private:
return;
}
- // Set initial tone period
+ // Set initial tone period for non-entry-based sounds
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
writeReg(8, _channelVolume[0]);
@@ -911,52 +953,59 @@ private:
/**
* Sounds >= 10: High index handler (l1d8fh / sub_257bh)
*
- * Uses hardware envelope for sustained atmospheric sounds.
- * From assembly: calculates channel count as (index - 8),
- * reads parameters from IX+9 onwards.
+ * From assembly at sub_257bh (0x257b):
+ * ld a,(001e5h) ; Get type
+ * sub 008h ; Channel count = type - 8
+ * ld (l3bc3h),a ; Store channel count
+ *
+ * So type 10 â 2 channels, type 11 â 3 channels, etc.
+ * Uses calibration table and hardware envelope.
*
- * Sound 7 entry at l38e9h has flags 0x87 (noise only, no tone).
+ * TODO: Find specific entry data for high index sounds.
+ * Current implementation uses approximations with HW envelope.
*/
void setupHighIndex(int index) {
- _channelCount = 1;
+ // Assembly: channel count = type - 8
+ _channelCount = index - 8;
+ if (_channelCount < 1) _channelCount = 1;
+ if (_channelCount > 8) _channelCount = 8;
_loopCount = 0;
_channelDone[0] = false;
switch (index) {
- case 10: // Area Change - atmospheric transition
- // From sound 7 entry flags 0x87: bit 2=1 (disable tone), bit 3=0 (noise on)
- // Creates a "whoosh" effect for area transitions
+ case 10: // Area Change - 2 channels (10 - 8 = 2)
+ // Assembly: uses calibration + HW envelope
+ // Approximation until entry data found
_channelPeriod[0] = 0;
_channelDelta[0] = 0;
_channelVolume[0] = 15;
- _maxLoops = 50; // ~1 second
+ _maxLoops = 100; // ~2 seconds
- writeReg(6, 0x18); // Noise period - medium texture
- writeReg(7, 0x37); // 0b00110111: noise A only, no tone
- // Hardware envelope for smooth decay
+ writeReg(6, 0x18); // Noise period
+ writeReg(7, 0x37); // Noise A only, no tone
writeReg(11, 0x00); // Envelope period low
- writeReg(12, 0x18); // Envelope period high
- writeReg(13, 0x00); // Shape: single decay (\)
+ writeReg(12, 0x40); // Envelope period high
+ writeReg(13, 0x00); // Shape: single decay
writeReg(8, 0x10); // Volume = envelope mode
break;
- case 11: // Explosion/rumble effect
+ case 11: // 3 channels (11 - 8 = 3)
_channelPeriod[0] = 700;
- _channelDelta[0] = 20; // Descending rumble
+ _channelDelta[0] = 20;
_channelVolume[0] = 15;
_maxLoops = 40;
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(6, 0x1C); // Rough noise
- writeReg(7, 0x36); // Tone + Noise
+ writeReg(6, 0x1C);
+ writeReg(7, 0x36);
writeReg(11, 0x00);
writeReg(12, 0x20);
- writeReg(13, 0x00); // Decay
+ writeReg(13, 0x00);
writeReg(8, 0x10);
break;
- case 12: // Warning/alert tone
+ case 12: // 4 channels (12 - 8 = 4)
_channelPeriod[0] = 150;
_channelDelta[0] = 0;
_channelVolume[0] = 15;
@@ -964,31 +1013,29 @@ private:
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E); // Tone only
+ writeReg(7, 0x3E);
writeReg(11, 0x00);
- writeReg(12, 0x06); // Fast cycle for alarm effect
- writeReg(13, 0x0E); // Continue, alternate (warble)
+ writeReg(12, 0x06);
+ writeReg(13, 0x0E);
writeReg(8, 0x10);
break;
- case 13: // Mission Complete - triumphant fanfare
- // Rising pitch with envelope swell
+ case 13: // Mission Complete - 5 channels (13 - 8 = 5)
_channelPeriod[0] = 200;
- _channelDelta[0] = -2; // Ascending pitch
+ _channelDelta[0] = -2;
_channelVolume[0] = 15;
- _maxLoops = 80; // ~1.6 seconds
+ _maxLoops = 80;
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E); // Tone only - clean victory sound
+ writeReg(7, 0x3E);
writeReg(11, 0x00);
- writeReg(12, 0x30); // Slow envelope
- writeReg(13, 0x0A); // Attack/decay (/ then hold)
+ writeReg(12, 0x30);
+ writeReg(13, 0x0A);
writeReg(8, 0x10);
break;
default:
- // Generic high index - simple tone with decay
_channelPeriod[0] = 200;
_channelDelta[0] = 0;
_channelVolume[0] = 12;
@@ -1043,82 +1090,13 @@ private:
}
/**
- * Sound update loop for Shoot - implements sub_2e96h at 0x2e96-0x2edb
- *
- * Original assembly structure:
- * sub_2e96h:
- * call sub_2a65h ; 2e9c - Initialize
- * ld de,00006h ; 2e9f - 6 bytes per channel
- * ld a,(l3bc4h) ; 2ea6 - A = outer loop count (5)
- * ld l,a ; 2ea9 - L = 5
- * l2eaah:
- * ld (0025fh),a ; 2eaa - Store op_type = outer counter
- * ld a,(l3bc3h) ; 2eaf - A = channel count (8)
- * ld b,a ; 2eb2 - B = 8
- * ld ix,00223h ; 2eb3 - IX = channel data base
- * l2eb7h:
- * call sub_2d87h ; 2eb7 - Process one channel step
- * add ix,de ; 2ec5 - Next channel (+6 bytes)
- * djnz l2eb7h ; 2ec7 - Loop 8 channels
- * dec l ; 2ecd - Decrement outer counter
- * jr nz,l2eaah ; 2ecf - Continue if L > 0
+ * Sound update loop for Shoot - uses unified updateFromEntry()
*
- * The 8 channels are mixed by cycling through them rapidly.
- * Each tick we output one channel's period to the AY.
+ * Assembly: sub_2e96h runs 5 outer loops à 8 channels.
+ * Delta from entry byte 7 (0xfe = -2) applied per outer loop.
*/
void updateShoot() {
- bool anyActive = false;
- int activeChannel = -1;
- uint16 outputPeriod = 0;
-
- // Cycle through channels - each tick advances to next active channel
- int startCh = _loopCount % _channelCount;
- for (int i = 0; i < _channelCount; i++) {
- int ch = (startCh + i) % _channelCount;
- if (_channelDone[ch]) continue;
-
- // Apply delta to period (sub_2d87h simplified)
- int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
-
- // Check bounds (AY period is 12-bit: 0-4095)
- // Use lower bound of 20 to avoid very high frequencies that sound harsh
- if (newPeriod <= 20 || newPeriod > 4095) {
- _channelDone[ch] = true;
- continue;
- }
-
- _channelPeriod[ch] = static_cast<uint16>(newPeriod);
- anyActive = true;
-
- // Use this channel for output
- if (activeChannel < 0) {
- activeChannel = ch;
- outputPeriod = _channelPeriod[ch];
- }
- }
-
- // Output to AY
- if (activeChannel >= 0 && outputPeriod > 0) {
- writeReg(0, outputPeriod & 0xFF);
- writeReg(1, (outputPeriod >> 8) & 0x0F);
-
- // Volume decay: starts at max, decays over ~40 ticks
- // Use slower decay to maintain presence throughout the sound
- int vol = 15 - (_loopCount / 4);
- if (vol < 2) vol = 2;
- writeReg(8, static_cast<uint8>(vol));
-
- debug("updateShoot: loop=%d ch=%d period=%d freq=%dHz vol=%d",
- _loopCount, activeChannel, outputPeriod,
- 1000000 / (16 * outputPeriod), vol);
- }
-
- // Check completion
- if (!anyActive || _loopCount >= _maxLoops) {
- debug("updateShoot: FINISHED at loop=%d", _loopCount);
- _finished = true;
- initAY();
- }
+ updateFromEntry();
}
void updateCollide() {
@@ -1172,8 +1150,14 @@ private:
}
void updateGeneric() {
+ // Sounds 6 and 7 use unified entry-based system
+ if (_index == 6 || _index == 7) {
+ updateFromEntry();
+ return;
+ }
+
+ // Other generic sounds use simple single-channel update
if (!_channelDone[0]) {
- // Apply tone delta
int32 newPeriod = static_cast<int32>(_channelPeriod[0]) + _channelDelta[0];
if (newPeriod < 50 || newPeriod > 2000) {
_channelDone[0] = true;
@@ -1188,15 +1172,9 @@ private:
// Volume decay varies by sound type
int vol;
switch (_index) {
- case 6: // Menu click - very fast decay
- vol = _channelVolume[0] - (_loopCount * 3);
- break;
case 9: // Fallen - slow decay
vol = _channelVolume[0] - (_loopCount / 8);
break;
- case 7: // Hit - medium-fast decay
- vol = _channelVolume[0] - _loopCount;
- break;
default:
vol = _channelVolume[0] - (_loopCount / 2);
break;
Commit: 799b2e1eaec9cfc9b62400bb5247951c9e3ae19e
https://github.com/scummvm/scummvm/commit/799b2e1eaec9cfc9b62400bb5247951c9e3ae19e
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: unified sound approach for some driller sounds for CPC
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 1ab8e27a7d6..699fdc5bac9 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -356,34 +356,74 @@ static const uint8 kDrillerCPCSoundDefs[][7] = {
* We use a fixed calibration that produces reasonable frequencies.
*/
-// Sound entry data from DRILL.BIN - used by unified processing system
-// Entry structure: flags(1), base[3], add[3], delta(1), size(1), vol/extra[...]
-// Channel count for sub_2207h: compare bytes 0x0c-0x0f
+/**
+ * Sound entries extracted from DRILL.BIN at their respective addresses.
+ *
+ * IMPLEMENTATION STATUS:
+ * - Types 1, 6, 7: HAVE ASSEMBLY ENTRY DATA (l3904h, l3912h, l38e9h, l3922h, l3932h, l3942h)
+ * - Types 2, 3: HANDLERS EXIST but NO ENTRY DATA in table (sub_26e2h, sub_2607h)
+ * - Types 4, 5, 8, 9: HANDLERS EXIST (sub_2207h) but NO ENTRY DATA found
+ * - Types >= 10: Use sub_257bh with hardware envelope
+ *
+ * Entry structure: flags(1), base[3], add[3], delta(1), size(1), vol/extra[...]
+ * Channel count for sub_2207h: compare bytes 0x0c-0x0f
+ * - All equal â 5 channels
+ * - One pair differs â 6 channels
+ * - Both pairs differ â 8 channels
+ */
+// Sound 7 (Hit) at l38e9h - 27 bytes (0x1b)
+// Type 7 â sub_2207h, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
+// Flags 0x87: bit 2=1 (no tone), bit 3=0 (noise on)
static const uint8 kSoundEntry7[] = {
- // Sound 7 (Hit) at l38e9h - 27 bytes (0x1b)
- // Type 7 â sub_2207h, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
- // Flags 0x87: bit 2=1 (no tone), bit 3=0 (noise on)
0x87, 0x00, 0x00, 0x00, 0x08, 0x0a, 0x08, 0xff, // bytes 0-7
0x1b, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x05, 0x05 // bytes 8-15 (0c-0f for channel count)
};
+// Sound 1 (Shoot) at l3904h - 14 bytes (0x0e)
+// Type 1 â sub_2112h â 8 channels (hardcoded in handler)
+// Flags 0x81: bits 2,3=0 â tone+noise enabled
static const uint8 kSoundEntry1[] = {
- // Sound 1 (Shoot) at l3904h - 14 bytes (0x0e)
- // Type 1 â sub_2112h â 8 channels (hardcoded in handler)
- // Flags 0x81: bits 2,3=0 â tone+noise enabled
0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe, // bytes 0-7
0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a // bytes 8-13
};
+// Sound 6 (Menu) at l3912h - 16 bytes (0x10)
+// Type 6 â sub_2207h, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
+// Flags 0x86: bit 2=1 (no tone), bit 3=0 (noise on)
static const uint8 kSoundEntry6[] = {
- // Sound 6 (Menu) at l3912h - 16 bytes (0x10)
- // Type 6 â sub_2207h, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
- // Flags 0x86: bit 2=1 (no tone), bit 3=0 (noise on)
0x86, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0xfd, // bytes 0-7
0x10, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x03, 0x03 // bytes 8-15 (0c-0f for channel count)
};
+/**
+ * Additional entry variants - documented for reference and future area-specific sound selection.
+ * The original game modifies entries based on area parameters (see lines 13703-13765 in assembly).
+ * These variants are preserved but currently unused.
+ */
+#if 0 // Area-specific entry variants - for future use
+// Sound 7 variant at l3922h - 16 bytes (0x10)
+// Type 7 â sub_2207h, bytes 0c-0f = 01 01 01 01 (all equal) â 5 channels
+static const uint8 kSoundEntry7b[] = {
+ 0x87, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02, 0xfc, // bytes 0-7
+ 0x10, 0xcc, 0xcc, 0x00, 0x01, 0x01, 0x01, 0x01 // bytes 8-15
+};
+
+// Sound 6 variant at l3932h - 16 bytes (0x10)
+// Type 6 â sub_2207h, bytes 0c-0f = 01 01 01 01 (all equal) â 5 channels
+static const uint8 kSoundEntry6b[] = {
+ 0x86, 0x00, 0x00, 0x00, 0x02, 0x03, 0x02, 0xfb, // bytes 0-7
+ 0x10, 0xcc, 0xcc, 0x00, 0x01, 0x01, 0x01, 0x01 // bytes 8-15
+};
+
+// Sound 6 variant at l3942h - 16 bytes (0x10) - with non-zero base params!
+// Type 6 â sub_2207h, bytes 0c-0f = 01 03 01 03 (differ) â 6 channels
+static const uint8 kSoundEntry6c[] = {
+ 0x86, 0x3f, 0x30, 0x3d, 0x02, 0x02, 0x06, 0xfa, // bytes 0-7
+ 0x10, 0x7f, 0x7f, 0x22, 0x01, 0x03, 0x01, 0x03 // bytes 8-15
+};
+#endif
+
/**
* Calibration value for sound processing.
*
@@ -809,14 +849,20 @@ private:
/**
* Sound 2: Collide - implements sub_26e2h at 0x26e2
*
- * From assembly: Sound 2 skips range check but uses same calibration.
- * Uses 2 channels (copies to 2 pairs of working areas).
- * Creates a "bump/thud" sound when hitting walls.
+ * APPROXIMATION: No entry with type 2 exists in the assembly sound table.
+ * Handler sub_26e2h exists and is dispatched for type 2, but no entries found.
+ *
+ * From assembly analysis of sub_26e2h (lines 1281-1341):
+ * - Increments counter at l3b68h
+ * - Loads parameters from processed buffer (l3d16h)
+ * - Copies to 2 pairs of working areas (001e7h-001fdh)
+ * - Would use 2 channels if entry existed
*
- * Characteristics: Low frequency impact with noise, quick decay
+ * Approximation based on handler behavior: 2 channels, impact sound
*/
void setupCollide() {
- _channelCount = 2; // Assembly: 2 channel pairs
+ // Assembly: sub_26e2h uses 2 channel pairs
+ _channelCount = 2;
_maxLoops = 12; // ~240ms - quick impact
_loopCount = 0;
@@ -836,7 +882,6 @@ private:
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
// Enable noise + tone on channel A for impact texture
- // Noise adds the "crunch" to the collision
writeReg(6, 0x10); // Noise period (mid-range)
writeReg(7, 0x36); // Tone A + Noise A
writeReg(8, 15); // Max volume for impact
@@ -845,20 +890,29 @@ private:
/**
* Sound 3: Step Up - implements sub_2607h at 0x2607
*
- * Short ascending blip for footstep going up terrain.
- * Quick pitch rise then decay - characteristic "bip" sound.
+ * APPROXIMATION: No entry with type 3 exists in the assembly sound table.
+ * Handler sub_2607h exists and is dispatched for type 3, but no entries found.
+ *
+ * From assembly analysis of sub_2607h (lines 1177-1237):
+ * - ld a,004h ; ld (l3bc3h),a â Uses 4 channels (line 1227-1228)
+ * - Copies processed params to 4 pairs of working areas
+ * - Calls sub_2e96h for the main update loop
+ *
+ * Approximation: 4 channels with ascending pitch for step up sound
*/
void setupStepUp() {
- _channelCount = 1;
- _maxLoops = 8; // ~160ms - quick step
+ // Assembly: sub_2607h uses 4 channels (line 1227: ld a,004h)
+ _channelCount = 4;
+ _maxLoops = 20; // 4 ch * 5 outer loops
_loopCount = 0;
- // Start at medium pitch, ascend (period decreases)
- // Period 300 (~208Hz) â Period 150 (~417Hz)
- _channelPeriod[0] = 300;
- _channelDelta[0] = -20; // Ascending pitch
- _channelVolume[0] = 10;
- _channelDone[0] = false;
+ // Setup 4 channels with staggered periods (ascending effect)
+ for (int ch = 0; ch < 4; ch++) {
+ _channelPeriod[ch] = 300 - ch * 30; // 300, 270, 240, 210
+ _channelDelta[ch] = -15; // Ascending pitch
+ _channelVolume[ch] = 10 - ch; // Decreasing volume
+ _channelDone[ch] = false;
+ }
// Set initial tone
writeReg(0, _channelPeriod[0] & 0xFF);
@@ -872,70 +926,82 @@ private:
/**
* Sounds 4-9: Generic handler based on sub_2207h at 0x2207
*
- * Assembly uses calibrated params from l1cffh. Channel count determined by
- * comparing entry bytes 0x0c-0x0f:
+ * IMPLEMENTATION STATUS:
+ * - Sound 6 (Menu): ASSEMBLY DATA from l3912h (type 6, 5 channels)
+ * - Sound 7 (Hit): ASSEMBLY DATA from l38e9h (type 7, 8 channels)
+ * - Sound 4, 5, 8, 9: APPROXIMATION - no entries with these types exist
+ *
+ * Channel count for sub_2207h determined by comparing entry bytes 0x0c-0x0f:
* - All equal â 5 channels
* - One pair differs â 6 channels
* - Both pairs differ â 8 channels
- *
- * Sound 6 (l3912h): bytes 0x0c-0x0f = 03 03 03 03 â 5 channels
- * Sound 7 (l38e9h): bytes 0x0c-0x0f = 03 03 05 05 â 8 channels
*/
void setupGeneric(int index) {
_loopCount = 0;
switch (index) {
- case 6: // Menu - uses l3912h entry data (type 6)
- // Assembly: bytes 0c-0f all equal (03 03 03 03) â 5 channels
- // l3bc4h outer loops not explicitly specified, using 5
+ case 6: // Menu - ASSEMBLY DATA from l3912h
+ // Type 6, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
setupFromEntry(kSoundEntry6, 5, 5);
return;
- case 7: // Hit - uses l38e9h entry data (type 7)
- // Assembly: bytes 0c != 0e (03 != 05) â 8 channels
+ case 7: // Hit - ASSEMBLY DATA from l38e9h
+ // Type 7, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
setupFromEntry(kSoundEntry7, 8, 5);
return;
- // Sounds 4, 5, 8, 9: No explicit entry data found in assembly.
- // Using approximations until entries are located.
- // TODO: Find assembly entry data for these sounds
- case 4: // Step Down
- _channelCount = 1;
- _channelDone[0] = false;
- _channelPeriod[0] = 180;
- _channelDelta[0] = 15;
- _channelVolume[0] = 10;
- _maxLoops = 8;
- writeReg(7, 0x3E);
+ // APPROXIMATIONS: Sounds 4, 5, 8, 9 have no entries in the assembly table.
+ // These would use sub_2207h if entries existed.
+ // Implementing reasonable approximations based on handler behavior.
+
+ case 4: // Step Down - APPROXIMATION (no type 4 entry exists)
+ // Would use 5 channels (sub_2207h default) if entry existed
+ // Descending pitch (opposite of step up)
+ _channelCount = 4; // Match step handler channel count
+ for (int ch = 0; ch < 4; ch++) {
+ _channelPeriod[ch] = 180 + ch * 20; // 180, 200, 220, 240
+ _channelDelta[ch] = 15; // Descending pitch
+ _channelVolume[ch] = 10 - ch;
+ _channelDone[ch] = false;
+ }
+ _maxLoops = 20;
+ writeReg(7, 0x3E); // Tone only
break;
- case 5: // Reserved
- _channelCount = 1;
- _channelDone[0] = false;
- _channelPeriod[0] = 250;
- _channelDelta[0] = 0;
- _channelVolume[0] = 8;
- _maxLoops = 10;
+ case 5: // Reserved - APPROXIMATION (no type 5 entry exists)
+ _channelCount = 5; // sub_2207h minimum
+ for (int ch = 0; ch < 5; ch++) {
+ _channelPeriod[ch] = 250;
+ _channelDelta[ch] = 0;
+ _channelVolume[ch] = 8;
+ _channelDone[ch] = false;
+ }
+ _maxLoops = 25;
writeReg(7, 0x3E);
break;
- case 8: // Reserved
- _channelCount = 1;
- _channelDone[0] = false;
- _channelPeriod[0] = 200;
- _channelDelta[0] = 8;
- _channelVolume[0] = 10;
- _maxLoops = 15;
+ case 8: // Reserved - APPROXIMATION (no type 8 entry exists)
+ _channelCount = 5; // sub_2207h default
+ for (int ch = 0; ch < 5; ch++) {
+ _channelPeriod[ch] = 200 + ch * 10;
+ _channelDelta[ch] = 8;
+ _channelVolume[ch] = 10;
+ _channelDone[ch] = false;
+ }
+ _maxLoops = 25;
writeReg(7, 0x3E);
break;
- case 9: // Fallen
- _channelCount = 1;
- _channelDone[0] = false;
- _channelPeriod[0] = 100;
- _channelDelta[0] = 6;
- _channelVolume[0] = 15;
- _maxLoops = 60;
+ case 9: // Fallen - APPROXIMATION (no type 9 entry exists)
+ // Long descending sound for falling
+ _channelCount = 5;
+ for (int ch = 0; ch < 5; ch++) {
+ _channelPeriod[ch] = 100 + ch * 20;
+ _channelDelta[ch] = 6;
+ _channelVolume[ch] = 15 - ch;
+ _channelDone[ch] = false;
+ }
+ _maxLoops = 60; // Long sound
writeReg(7, 0x3E);
break;
@@ -944,7 +1010,7 @@ private:
return;
}
- // Set initial tone period for non-entry-based sounds
+ // Set initial tone period for approximation-based sounds
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
writeReg(8, _channelVolume[0]);
@@ -953,32 +1019,41 @@ private:
/**
* Sounds >= 10: High index handler (l1d8fh / sub_257bh)
*
- * From assembly at sub_257bh (0x257b):
+ * From assembly at sub_257bh (0x257b, line 1109 in disassembly):
* ld a,(001e5h) ; Get type
* sub 008h ; Channel count = type - 8
* ld (l3bc3h),a ; Store channel count
*
- * So type 10 â 2 channels, type 11 â 3 channels, etc.
- * Uses calibration table and hardware envelope.
+ * Channel count formula: index - 8
+ * Type 10 â 2 channels
+ * Type 11 â 3 channels
+ * Type 12 â 4 channels
+ * Type 13 â 5 channels
*
- * TODO: Find specific entry data for high index sounds.
- * Current implementation uses approximations with HW envelope.
+ * APPROXIMATION: No specific entry data found for high index sounds.
+ * These use hardware envelope (AY registers 11-13) based on sub_257bh behavior.
+ * Uses l38a6h calibration table and processes through l1cffh.
*/
void setupHighIndex(int index) {
- // Assembly: channel count = type - 8
+ // Assembly (line 1109): channel count = type - 8
_channelCount = index - 8;
if (_channelCount < 1) _channelCount = 1;
if (_channelCount > 8) _channelCount = 8;
_loopCount = 0;
- _channelDone[0] = false;
+
+ for (int ch = 0; ch < _channelCount; ch++) {
+ _channelDone[ch] = false;
+ }
switch (index) {
case 10: // Area Change - 2 channels (10 - 8 = 2)
- // Assembly: uses calibration + HW envelope
- // Approximation until entry data found
+ // APPROXIMATION: Uses HW envelope for transition effect
_channelPeriod[0] = 0;
+ _channelPeriod[1] = 0;
_channelDelta[0] = 0;
+ _channelDelta[1] = 0;
_channelVolume[0] = 15;
+ _channelVolume[1] = 15;
_maxLoops = 100; // ~2 seconds
writeReg(6, 0x18); // Noise period
@@ -989,56 +1064,89 @@ private:
writeReg(8, 0x10); // Volume = envelope mode
break;
- case 11: // 3 channels (11 - 8 = 3)
+ case 11: // Explosion - 3 channels (11 - 8 = 3)
+ // APPROXIMATION: Low rumble with envelope
_channelPeriod[0] = 700;
+ _channelPeriod[1] = 750;
+ _channelPeriod[2] = 800;
_channelDelta[0] = 20;
+ _channelDelta[1] = 25;
+ _channelDelta[2] = 30;
_channelVolume[0] = 15;
+ _channelVolume[1] = 14;
+ _channelVolume[2] = 13;
_maxLoops = 40;
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
writeReg(6, 0x1C);
- writeReg(7, 0x36);
+ writeReg(7, 0x36); // Tone + Noise
writeReg(11, 0x00);
writeReg(12, 0x20);
- writeReg(13, 0x00);
- writeReg(8, 0x10);
+ writeReg(13, 0x00); // Single decay
+ writeReg(8, 0x10); // Envelope mode
break;
- case 12: // 4 channels (12 - 8 = 4)
+ case 12: // Warning - 4 channels (12 - 8 = 4)
+ // APPROXIMATION: Alert tone with envelope
_channelPeriod[0] = 150;
+ _channelPeriod[1] = 160;
+ _channelPeriod[2] = 170;
+ _channelPeriod[3] = 180;
_channelDelta[0] = 0;
+ _channelDelta[1] = 0;
+ _channelDelta[2] = 0;
+ _channelDelta[3] = 0;
_channelVolume[0] = 15;
+ _channelVolume[1] = 14;
+ _channelVolume[2] = 13;
+ _channelVolume[3] = 12;
_maxLoops = 60;
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E);
+ writeReg(7, 0x3E); // Tone only
writeReg(11, 0x00);
writeReg(12, 0x06);
- writeReg(13, 0x0E);
- writeReg(8, 0x10);
+ writeReg(13, 0x0E); // Continue, attack-decay
+ writeReg(8, 0x10); // Envelope mode
break;
case 13: // Mission Complete - 5 channels (13 - 8 = 5)
+ // APPROXIMATION: Triumphant ascending tone
_channelPeriod[0] = 200;
+ _channelPeriod[1] = 190;
+ _channelPeriod[2] = 180;
+ _channelPeriod[3] = 170;
+ _channelPeriod[4] = 160;
_channelDelta[0] = -2;
+ _channelDelta[1] = -2;
+ _channelDelta[2] = -2;
+ _channelDelta[3] = -2;
+ _channelDelta[4] = -2;
_channelVolume[0] = 15;
+ _channelVolume[1] = 14;
+ _channelVolume[2] = 13;
+ _channelVolume[3] = 12;
+ _channelVolume[4] = 11;
_maxLoops = 80;
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E);
+ writeReg(7, 0x3E); // Tone only
writeReg(11, 0x00);
writeReg(12, 0x30);
- writeReg(13, 0x0A);
- writeReg(8, 0x10);
+ writeReg(13, 0x0A); // Continue, hold
+ writeReg(8, 0x10); // Envelope mode
break;
default:
- _channelPeriod[0] = 200;
- _channelDelta[0] = 0;
- _channelVolume[0] = 12;
+ // Generic high index sound
+ for (int ch = 0; ch < _channelCount; ch++) {
+ _channelPeriod[ch] = 200 + ch * 20;
+ _channelDelta[ch] = 0;
+ _channelVolume[ch] = 12 - ch;
+ }
_maxLoops = 30;
writeReg(0, _channelPeriod[0] & 0xFF);
@@ -1047,7 +1155,7 @@ private:
writeReg(11, 0x00);
writeReg(12, 0x10);
writeReg(13, 0x00);
- writeReg(8, 0x10);
+ writeReg(8, 0x10); // Envelope mode
break;
}
}
@@ -1130,53 +1238,89 @@ private:
}
void updateStepUp() {
- if (!_channelDone[0]) {
- // Ascending pitch sweep (period decreases = higher pitch)
- int32 newPeriod = static_cast<int32>(_channelPeriod[0]) + _channelDelta[0];
- if (newPeriod < 80) { // Cap at ~781Hz
- _channelDone[0] = true;
- } else {
- _channelPeriod[0] = static_cast<uint16>(newPeriod);
+ // Update all 4 channels (assembly: sub_2607h uses 4 channels)
+ int outerLoop = _loopCount / _channelCount;
+ int innerIdx = _loopCount % _channelCount;
+
+ // Apply delta at start of each outer loop
+ if (innerIdx == 0 && _loopCount > 0) {
+ for (int ch = 0; ch < _channelCount; ch++) {
+ if (_channelDone[ch]) continue;
+
+ int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
+ if (newPeriod < 80 || newPeriod > 4095) {
+ _channelDone[ch] = true;
+ } else {
+ _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ }
}
}
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ // Find active channel for output
+ int activeChannel = innerIdx % _channelCount;
+ if (!_channelDone[activeChannel] && _channelPeriod[activeChannel] > 0) {
+ writeReg(0, _channelPeriod[activeChannel] & 0xFF);
+ writeReg(1, (_channelPeriod[activeChannel] >> 8) & 0x0F);
+ }
- // Quick volume decay for short "bip"
- int vol = _channelVolume[0] - _loopCount;
+ // Quick volume decay
+ int vol = 10 - outerLoop * 2;
if (vol < 0) vol = 0;
writeReg(8, vol);
}
void updateGeneric() {
- // Sounds 6 and 7 use unified entry-based system
+ // Sounds 6 and 7 use unified entry-based system (ASSEMBLY DATA)
if (_index == 6 || _index == 7) {
updateFromEntry();
return;
}
- // Other generic sounds use simple single-channel update
- if (!_channelDone[0]) {
- int32 newPeriod = static_cast<int32>(_channelPeriod[0]) + _channelDelta[0];
- if (newPeriod < 50 || newPeriod > 2000) {
- _channelDone[0] = true;
- } else {
- _channelPeriod[0] = static_cast<uint16>(newPeriod);
+ // Sounds 4, 5, 8, 9: APPROXIMATIONS using multi-channel update
+ // (mimics sub_2207h behavior with 4-5 channels)
+ int outerLoop = _loopCount / _channelCount;
+ int innerIdx = _loopCount % _channelCount;
+
+ // Apply delta at start of each outer loop
+ if (innerIdx == 0 && _loopCount > 0) {
+ for (int ch = 0; ch < _channelCount; ch++) {
+ if (_channelDone[ch]) continue;
+
+ int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
+ if (newPeriod < 50 || newPeriod > 2000) {
+ _channelDone[ch] = true;
+ } else {
+ _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ }
}
}
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ // Find active channel for output
+ bool anyActive = false;
+ for (int i = 0; i < _channelCount; i++) {
+ int ch = (innerIdx + i) % _channelCount;
+ if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
+ writeReg(0, _channelPeriod[ch] & 0xFF);
+ writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
+ anyActive = true;
+ break;
+ }
+ }
+
+ if (!anyActive) {
+ _finished = true;
+ initAY();
+ return;
+ }
// Volume decay varies by sound type
int vol;
switch (_index) {
case 9: // Fallen - slow decay
- vol = _channelVolume[0] - (_loopCount / 8);
+ vol = 15 - (outerLoop / 2);
break;
default:
- vol = _channelVolume[0] - (_loopCount / 2);
+ vol = 15 - outerLoop * 2;
break;
}
if (vol < 0) vol = 0;
Commit: 88d7eec53170516571c7ef2827ccaf485fa70fcf
https://github.com/scummvm/scummvm/commit/88d7eec53170516571c7ef2827ccaf485fa70fcf
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: better cpc sounds
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 699fdc5bac9..42a1078fb3e 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -397,33 +397,85 @@ static const uint8 kSoundEntry6[] = {
};
/**
- * Additional entry variants - documented for reference and future area-specific sound selection.
- * The original game modifies entries based on area parameters (see lines 13703-13765 in assembly).
- * These variants are preserved but currently unused.
+ * Sound 2 (Collide) - Synthetic entry for sub_26e2h
+ *
+ * sub_26e2h at 0x26e2 (lines 1281-1380 in assembly):
+ * - Uses 2 channels (line 1349: ld a,002h)
+ * - Reads entry from l3d1ah
+ * - Reads processed params from l3d16h (after calibration at l1cffh)
+ * - Duplicates params to 2 working areas (lines 1303-1314)
+ * - Calls sub_2e96h for the main update loop
+ *
+ * Entry structure for type 2:
+ * byte 0: flags (0x82 = type 2, tone+noise enabled)
+ * bytes 1-3: base params (multiplied by 64, subtract calibration)
+ * bytes 4-6: add params (multiplied by 64, added)
+ * byte 7: delta sign
+ * byte 8: entry size
+ * byte 9: volume nibbles
+ * bytes 0xa: extra byte (copied to iy+001h at line 1295-1296)
+ * bytes 0xb-0xc: threshold for comparison (line 1344-1345)
+ * byte 0xd: flag mask for AND with 01e6h (line 1300)
+ *
+ * Collide uses result1 values (IX[0,4,8]) which are entry[1-3]*64 - calibration.
+ * With base=0 and calibration=700, this gives period 700 (~89 Hz bass thud).
+ * Using base values to shift the frequency range.
*/
-#if 0 // Area-specific entry variants - for future use
-// Sound 7 variant at l3922h - 16 bytes (0x10)
-// Type 7 â sub_2207h, bytes 0c-0f = 01 01 01 01 (all equal) â 5 channels
-static const uint8 kSoundEntry7b[] = {
- 0x87, 0x00, 0x00, 0x00, 0x02, 0x01, 0x02, 0xfc, // bytes 0-7
- 0x10, 0xcc, 0xcc, 0x00, 0x01, 0x01, 0x01, 0x01 // bytes 8-15
+// Sound 2 (Collide) entry from DRILL_CODE.bin offset 0x1d00
+static const uint8 kSoundEntry2[] = {
+ 0x82, // Flags: type 2, bits 2,3=0 (tone+noise)
+ 0x40, 0x2e, 0x40, // Base params: 64, 46, 64
+ 0x00, 0x00, 0x00, // Add params: 0, 0, 0
+ 0xf8, // Byte 7 (stored to l3a5ah, NOT a delta sign)
+ 0x0e, // Entry size (14 bytes)
+ 0x11, // Volume nibbles (low=1, high=1)
+ 0x50, 0x00, // Threshold (0x0050 = 80)
+ 0x20, // Flag mask (0x20 = 32)
+ 0x04 // Extra byte
};
-// Sound 6 variant at l3932h - 16 bytes (0x10)
-// Type 6 â sub_2207h, bytes 0c-0f = 01 01 01 01 (all equal) â 5 channels
-static const uint8 kSoundEntry6b[] = {
- 0x86, 0x00, 0x00, 0x00, 0x02, 0x03, 0x02, 0xfb, // bytes 0-7
- 0x10, 0xcc, 0xcc, 0x00, 0x01, 0x01, 0x01, 0x01 // bytes 8-15
+/**
+ * Sound 3 (Step) - Synthetic entry for sub_2607h
+ *
+ * sub_2607h at 0x2607 (lines 1177-1280 in assembly):
+ * - Uses 4 channels (line 1227: ld a,004h)
+ * - Reads processed params from l3d16h (lines 1184-1209)
+ * - Reads entry[4] and entry[5] to determine distribution (lines 1203, 1219)
+ * - Distributes to 4 pairs of working areas
+ * - Calls sub_2e96h for the main update loop
+ *
+ * Distribution logic (lines 1210-1225):
+ * if entry[4] == 0: B=1, use one pattern
+ * else if entry[5] == 0: B=2, use another pattern
+ * else: B=3, use third pattern
+ *
+ * Step uses BOTH result1 (IX[0,2]) AND result2 (IX[4,6,8,10]) values.
+ * For an ascending chirp, we want:
+ * - result1 values: mid-range starting pitch (~300-400 Hz)
+ * - result2 values: higher pitch offset (~500-800 Hz)
+ * - Negative delta to decrease period (ascending pitch)
+ *
+ * With calibration=700:
+ * base=0x09: result1 = 9*64 - 700 = -124 â 124 period (~504 Hz)
+ * add=0x0a: result2 = -124 + 10*64 = 516 period - wait that's wrong
+ * Actually: result2 = result1 + add*64 = -124 + 640 = 516 â too low
+ *
+ * Let's use base=0x0b for result1 = 11*64-700 = 4 â ~15kHz (too high)
+ * base=0x0a: result1 = 10*64-700 = -60 â 60 period (~1042 Hz) - good chirp start
+ * add=0x02: result2 = -60 + 128 = 68 period (~919 Hz)
+ */
+static const uint8 kSoundEntry3[] = {
+ 0x83, // Flags: type 3, bits 2,3=0 (tone+noise)
+ 0x00, 0x00, 0x00, // Base params (to be determined from assembly)
+ 0x06, 0x04, 0x06, // Add params
+ 0xfe, // Delta sign (-2)
+ 0x10, // Entry size (16 bytes)
+ 0x66, // Volume nibbles (low=6, high=6)
+ 0x01, // entry[4] - pattern selector (non-zero)
+ 0x01, // entry[5] - pattern selector (non-zero)
+ 0x00, 0x00, 0x00, 0x00 // Padding to 16 bytes
};
-// Sound 6 variant at l3942h - 16 bytes (0x10) - with non-zero base params!
-// Type 6 â sub_2207h, bytes 0c-0f = 01 03 01 03 (differ) â 6 channels
-static const uint8 kSoundEntry6c[] = {
- 0x86, 0x3f, 0x30, 0x3d, 0x02, 0x02, 0x06, 0xfa, // bytes 0-7
- 0x10, 0x7f, 0x7f, 0x22, 0x01, 0x03, 0x01, 0x03 // bytes 8-15
-};
-#endif
-
/**
* Calibration value for sound processing.
*
@@ -849,78 +901,154 @@ private:
/**
* Sound 2: Collide - implements sub_26e2h at 0x26e2
*
- * APPROXIMATION: No entry with type 2 exists in the assembly sound table.
- * Handler sub_26e2h exists and is dispatched for type 2, but no entries found.
+ * Assembly at sub_26e2h (lines 1281-1380):
+ * Line 1302: ld ix,(l3d16h) â Reads processed params
+ * Lines 1303-1314: Copies params to 2 working areas (DUPLICATES each):
+ * IX[0-1] â 01e7h AND 01edh (period1)
+ * IX[4-5] â 01e9h AND 01efh (period2)
+ * IX[8-9] â 01ebh AND 01f1h (delta)
+ * Line 1349: ld a,002h â Sets 2 CHANNELS
+ * Line 1351: call sub_2e96h â Main update loop
*
- * From assembly analysis of sub_26e2h (lines 1281-1341):
- * - Increments counter at l3b68h
- * - Loads parameters from processed buffer (l3d16h)
- * - Copies to 2 pairs of working areas (001e7h-001fdh)
- * - Would use 2 channels if entry existed
+ * Uses result1 values (IX byte offsets 0-1, 4-5, 8-9):
+ * IX[0-1] = processed[0] = entry[1]*64 - calibration (period1)
+ * IX[4-5] = processed[2] = entry[2]*64 - calibration (period2)
+ * IX[8-9] = processed[4] = entry[3]*64 - calibration (delta base)
*
- * Approximation based on handler behavior: 2 channels, impact sound
+ * NOTE: Calibration and delta accumulation via sub_2a65h needs investigation.
*/
void setupCollide() {
- // Assembly: sub_26e2h uses 2 channel pairs
+ const uint8 *entry = kSoundEntry2;
+
+ // Process parameters through calibration (l1cffh)
+ // Formula: result1 = entry[1+i]*64 - calibration
+ int16 processed[6];
+ for (int i = 0; i < 3; i++) {
+ int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
+ int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
+ processed[i * 2] = val1 - kSoundCalibration;
+ processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
+ }
+
+ // Assembly line 1349: sub_26e2h uses exactly 2 channels
_channelCount = 2;
- _maxLoops = 12; // ~240ms - quick impact
+ _outerLoops = 5;
+ _maxLoops = _channelCount * _outerLoops; // 2 * 5 = 10
_loopCount = 0;
- // Low frequency for "thump" character
- // Period 500 â ~125 Hz (bass thud)
- _channelPeriod[0] = 500;
- _channelPeriod[1] = 600;
- _channelDelta[0] = 30; // Descending pitch
- _channelDelta[1] = 40;
+ // sub_26e2h reads result1 values at byte offsets 0-1, 4-5, 8-9
+ // With base=0 and calibration=700:
+ // period1 = processed[0] = 0 - 700 = -700
+ // period2 = processed[2] = 0 - 700 = -700
+ // delta = processed[4] = 0 - 700 = -700
+
+ // Handle negative values (lines 1315-1323): negate if negative
+ int16 period1 = processed[0];
+ if (period1 < 0) period1 = -period1;
+ int16 period2 = processed[2];
+ if (period2 < 0) period2 = -period2;
+
+ // Both channels use same period (duplicated in assembly lines 1305-1314)
+ _channelPeriod[0] = static_cast<uint16>(period1);
+ _channelPeriod[1] = static_cast<uint16>(period1);
+
+ // Delta from entry[7] = 0xff = -1 (signed)
+ int8 deltaSign = static_cast<int8>(entry[7]);
+ _channelDelta[0] = deltaSign;
+ _channelDelta[1] = deltaSign;
+
_channelVolume[0] = 15;
- _channelVolume[1] = 12;
+ _channelVolume[1] = 15;
_channelDone[0] = false;
_channelDone[1] = false;
- // Set initial tone
+ // Set initial tone period
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- // Enable noise + tone on channel A for impact texture
- writeReg(6, 0x10); // Noise period (mid-range)
- writeReg(7, 0x36); // Tone A + Noise A
- writeReg(8, 15); // Max volume for impact
+ // Configure mixer based on flags byte 0
+ // Flags 0x82: bit 2=0 (tone enabled), bit 3=0 (noise enabled)
+ writeReg(6, 0x10); // Noise period
+ writeReg(7, 0x36); // Enable tone A + noise A
+ writeReg(8, 15); // Start at MAX volume
+
+ debug("setupCollide: period=%d delta=%d (~%dHz)",
+ _channelPeriod[0], _channelDelta[0],
+ _channelPeriod[0] > 0 ? 1000000 / (16 * _channelPeriod[0]) : 0);
}
/**
* Sound 3: Step Up - implements sub_2607h at 0x2607
*
- * APPROXIMATION: No entry with type 3 exists in the assembly sound table.
- * Handler sub_2607h exists and is dispatched for type 3, but no entries found.
+ * Assembly at sub_2607h (lines 1177-1280):
+ * Line 1184: ld ix,(l3d16h) â Reads processed params
+ * Lines 1185-1209: Distributes params to 4 pairs of working areas:
+ * IX[0-1] â 01e7h, 01f9h (period1 for ch 0,2)
+ * IX[2-3] â 01edh, 01f3h (period1 for ch 1,3)
+ * IX[4-5] â 01e9h, 01efh (period2)
+ * IX[6-7] â 01f5h, 01fbh (period2)
+ * IX[8-9], IX[10-11] â delta distribution based on entry[4-5]
+ * Lines 1210-1225: Pattern selection based on entry[4] and entry[5]
+ * Line 1227: ld a,004h â Sets 4 CHANNELS
+ * Line 1229: call sub_2e96h â Main update loop
*
- * From assembly analysis of sub_2607h (lines 1177-1237):
- * - ld a,004h ; ld (l3bc3h),a â Uses 4 channels (line 1227-1228)
- * - Copies processed params to 4 pairs of working areas
- * - Calls sub_2e96h for the main update loop
- *
- * Approximation: 4 channels with ascending pitch for step up sound
+ * NOTE: Delta accumulation via sub_2a65h needs investigation.
*/
void setupStepUp() {
- // Assembly: sub_2607h uses 4 channels (line 1227: ld a,004h)
+ const uint8 *entry = kSoundEntry3;
+ int8 deltaSign = static_cast<int8>(entry[7]); // -2 for ascending pitch
+
+ // Process parameters through calibration (l1cffh)
+ int16 processed[6];
+ for (int i = 0; i < 3; i++) {
+ int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
+ int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
+ processed[i * 2] = val1 - kSoundCalibration;
+ processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
+ }
+
+ // Assembly line 1227: sub_2607h uses exactly 4 channels
_channelCount = 4;
- _maxLoops = 20; // 4 ch * 5 outer loops
+ _outerLoops = 5;
+ _maxLoops = _channelCount * _outerLoops; // 4 * 5 = 20
_loopCount = 0;
- // Setup 4 channels with staggered periods (ascending effect)
+ // Distribution to 4 channels following assembly (lines 1185-1209)
+ // sub_2607h reads from processed buffer:
+ // IX[0-1] = processed[0] = result1[0] â channels 0,2
+ // IX[2-3] = processed[1] = result2[0] â channels 1,3
for (int ch = 0; ch < 4; ch++) {
- _channelPeriod[ch] = 300 - ch * 30; // 300, 270, 240, 210
- _channelDelta[ch] = -15; // Ascending pitch
- _channelVolume[ch] = 10 - ch; // Decreasing volume
+ int16 periodBase;
+
+ // Assembly distributes: channels 0,2 use IX[0-1], channels 1,3 use IX[2-3]
+ if (ch == 0 || ch == 2) {
+ periodBase = processed[0]; // result1
+ } else {
+ periodBase = processed[1]; // result2
+ }
+
+ // Handle negative values (negate if negative)
+ if (periodBase < 0) periodBase = -periodBase;
+
+ _channelPeriod[ch] = static_cast<uint16>(periodBase);
+ _channelDelta[ch] = deltaSign; // -2 from entry[7]
+ _channelVolume[ch] = 15;
_channelDone[ch] = false;
}
- // Set initial tone
+ // Set initial tone period
writeReg(0, _channelPeriod[0] & 0xFF);
writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- // Tone only - clean step sound
- writeReg(7, 0x3E); // Tone A only, no noise
- writeReg(8, 10); // Medium volume
+ // Configure mixer based on flags
+ // Flags 0x83: bit 2=0 (tone enabled), bit 3=0 (noise enabled)
+ writeReg(6, 0x10); // Noise period
+ writeReg(7, 0x36); // Enable tone A + noise A
+ writeReg(8, 15); // Start at MAX volume
+
+ debug("setupStepUp: period[0]=%d period[1]=%d delta=%d (~%dHz)",
+ _channelPeriod[0], _channelPeriod[1], _channelDelta[0],
+ _channelPeriod[0] > 0 ? 1000000 / (16 * _channelPeriod[0]) : 0);
}
/**
@@ -1207,12 +1335,33 @@ private:
updateFromEntry();
}
+ /**
+ * Update for Sound 2 (Collide) - follows sub_2e96h loop
+ *
+ * Assembly sub_2e96h (lines 2292-2335):
+ * - Outer loop count from l3bc4h (=5)
+ * - Inner loop for each channel
+ * - Calls sub_2d87h per channel (applies delta based on op_type)
+ *
+ * For type 2, both channels have duplicated parameters,
+ * so they evolve similarly but may finish at different times.
+ */
void updateCollide() {
- // Process both channels
- for (int ch = 0; ch < _channelCount; ++ch) {
- if (!_channelDone[ch]) {
+ int outerLoop = _loopCount / _channelCount;
+ int innerIdx = _loopCount % _channelCount;
+
+ // sub_2d87h applies delta based on outer loop iteration (op_type at 0x025f)
+ // op_type 1,2 = add delta, op_type 3,4 = subtract delta
+ // For simplicity, we apply delta uniformly at start of each outer loop
+ if (innerIdx == 0 && _loopCount > 0) {
+ for (int ch = 0; ch < _channelCount; ch++) {
+ if (_channelDone[ch]) continue;
+
+ // Apply delta (can be negative from entry[7])
int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
- if (newPeriod > 1200) {
+
+ // Check bounds (AY period range is 1-4095)
+ if (newPeriod < 20 || newPeriod > 4095) {
_channelDone[ch] = true;
} else {
_channelPeriod[ch] = static_cast<uint16>(newPeriod);
@@ -1220,35 +1369,55 @@ private:
}
}
- // Output primary channel
- if (!_channelDone[0]) {
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
+ // Output current channel's period
+ int ch = innerIdx % _channelCount;
+ if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
+ writeReg(0, _channelPeriod[ch] & 0xFF);
+ writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
}
- // Quick decay - collision is sharp impact
- int vol = 15 - (_loopCount * 2);
- if (vol < 0) vol = 0;
- writeReg(8, vol);
+ // Volume decay matching other working sounds (start at 15, min 4)
+ int vol = 15 - (outerLoop * 2);
+ if (vol < 4) vol = 4;
+ writeReg(8, static_cast<uint8>(vol));
- // Fade noise out quickly too
- int noise = 0x10 - _loopCount;
- if (noise < 0x04) noise = 0x04;
- writeReg(6, noise);
+ // Check if all channels are done
+ bool anyActive = false;
+ for (int i = 0; i < _channelCount; i++) {
+ if (!_channelDone[i]) {
+ anyActive = true;
+ break;
+ }
+ }
+ if (!anyActive) {
+ _finished = true;
+ initAY();
+ }
}
+ /**
+ * Update for Sound 3 (Step) - follows sub_2e96h loop
+ *
+ * Assembly sub_2e96h (lines 2292-2335):
+ * - Outer loop count from l3bc4h (=5)
+ * - Inner loop for each of 4 channels
+ * - sub_2d87h processes based on op_type (1-5)
+ *
+ * For type 3, 4 channels with different parameter distributions.
+ */
void updateStepUp() {
- // Update all 4 channels (assembly: sub_2607h uses 4 channels)
int outerLoop = _loopCount / _channelCount;
int innerIdx = _loopCount % _channelCount;
- // Apply delta at start of each outer loop
+ // Apply delta at start of each outer loop (sub_2d87h behavior)
if (innerIdx == 0 && _loopCount > 0) {
for (int ch = 0; ch < _channelCount; ch++) {
if (_channelDone[ch]) continue;
int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
- if (newPeriod < 80 || newPeriod > 4095) {
+
+ // Check bounds
+ if (newPeriod < 20 || newPeriod > 4095) {
_channelDone[ch] = true;
} else {
_channelPeriod[ch] = static_cast<uint16>(newPeriod);
@@ -1256,17 +1425,30 @@ private:
}
}
- // Find active channel for output
- int activeChannel = innerIdx % _channelCount;
- if (!_channelDone[activeChannel] && _channelPeriod[activeChannel] > 0) {
- writeReg(0, _channelPeriod[activeChannel] & 0xFF);
- writeReg(1, (_channelPeriod[activeChannel] >> 8) & 0x0F);
+ // Output current channel's period
+ int ch = innerIdx % _channelCount;
+ if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
+ writeReg(0, _channelPeriod[ch] & 0xFF);
+ writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
}
- // Quick volume decay
- int vol = 10 - outerLoop * 2;
- if (vol < 0) vol = 0;
- writeReg(8, vol);
+ // Volume decay matching other working sounds (start at 15, min 4)
+ int vol = 15 - (outerLoop * 2);
+ if (vol < 4) vol = 4;
+ writeReg(8, static_cast<uint8>(vol));
+
+ // Check if all channels are done
+ bool anyActive = false;
+ for (int i = 0; i < _channelCount; i++) {
+ if (!_channelDone[i]) {
+ anyActive = true;
+ break;
+ }
+ }
+ if (!anyActive) {
+ _finished = true;
+ initAY();
+ }
}
void updateGeneric() {
Commit: 11228047db9a856b609b2bffbc69a4f45c11e989
https://github.com/scummvm/scummvm/commit/11228047db9a856b609b2bffbc69a4f45c11e989
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: matched sound 1 for driller (cpc)
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 42a1078fb3e..3ef7632987c 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -255,300 +255,176 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
/**
* Driller CPC Sound Implementation
*
- * Based on reverse engineering of DRILL.BIN (loads at 0x1c62)
+ * Based on reverse engineering of DRILL.BIN (loads at 0x1c62).
*
- * Sound Dispatch (0x6305):
- * Index 1 -> sub_2112h (Shoot)
- * Index 2 -> sub_26e2h (Collide)
- * Index 3 -> sub_2607h (Step Up)
- * Index 4-9 -> sub_2207h (Generic handler)
- * Index >= 10 -> l1d8fh (High index handler with HW envelope)
+ * All sounds use the sub_4760h system (0x4760-0x4871):
+ * - Sound initialization loads 7-byte entry from l40e0h
+ * - Volume envelope from "Tone" Table at l4034h
+ * - Pitch sweep from "Envelope" Table at l4078h
+ * - 50Hz interrupt-driven update via sub_7571h (0x7571-0x76A9)
*
- * Data Tables (from DRILL.BIN):
- * Tone Table at 0x4034: 4 bytes per entry (count, period_lo, period_hi, delta)
- * Envelope Table at 0x4078: 4 bytes per entry (step, count, delta_lo, delta_hi)
- * Sound Definition at 0x40e0: 7 bytes per entry
- *
- * AY Register Write at 0x4872:
+ * AY-3-8912 PSG with 1MHz clock, register write at 0x4872:
* Port 0xF4 = register select, Port 0xF6 = data
*/
-// Original data tables extracted from DRILL.BIN for reference
-// These are preserved for documentation - the actual implementation uses
-// simplified parameters that approximate the original sound behavior.
-// TODO: Use these tables for more accurate sound reproduction
-
-// Tone table entries from DRILL.BIN at 0x4034 (file offset 0x23D2)
-// Format: { iterations, period_low, period_high, delta }
-// Period values are 12-bit (0-4095), frequency = 1MHz / (16 * period)
-#if 0
-static const uint8 kDrillerCPCToneTable[][4] = {
- {0x01, 0x01, 0x00, 0x01}, // Entry 0: period=0x0001, delta=0x01
- {0x02, 0x0f, 0x01, 0x03}, // Entry 1: period=0x010F (271), delta=0x03
- {0x01, 0xf1, 0x01, 0x00}, // Entry 2: period=0x01F1 (497), delta=0x00
- {0x01, 0x0f, 0xff, 0x18}, // Entry 3
- {0x01, 0x06, 0xfe, 0x3f}, // Entry 4
- {0x01, 0x0f, 0xff, 0x18}, // Entry 5
- {0x02, 0x01, 0x00, 0x06}, // Entry 6: period=0x0001, delta=0x06
- {0x0f, 0xff, 0x0f, 0x00}, // Entry 7
- {0x04, 0x05, 0xff, 0x0f}, // Entry 8
- {0x01, 0x05, 0x01, 0x01}, // Entry 9: period=0x0105 (261), delta=0x01
- {0x00, 0x7b, 0x0f, 0xff}, // Entry 10
-};
-
-// Envelope table entries from DRILL.BIN at 0x4078 (file offset 0x2416)
-// Format: { step, count, delta_low, delta_high }
-static const uint8 kDrillerCPCEnvelopeTable[][4] = {
- {0x01, 0x02, 0x00, 0xff}, // Entry 0: fast decay
- {0x01, 0x10, 0x01, 0x01}, // Entry 1
- {0x01, 0x02, 0x30, 0x10}, // Entry 2
- {0x01, 0x02, 0xd0, 0x10}, // Entry 3
- {0x03, 0x01, 0xe0, 0x06}, // Entry 4
- {0x01, 0x20, 0x06, 0x01}, // Entry 5
- {0xe0, 0x06, 0x00, 0x00}, // Entry 6
- {0x01, 0x02, 0xfb, 0x03}, // Entry 7
- {0x01, 0x02, 0xfd, 0x0c}, // Entry 8
- {0x02, 0x01, 0x04, 0x03}, // Entry 9
- {0x08, 0xf5, 0x03, 0x00}, // Entry 10
- {0x01, 0x10, 0x02, 0x06}, // Entry 11
- {0x01, 0x80, 0x01, 0x03}, // Entry 12
- {0x01, 0x64, 0x01, 0x01}, // Entry 13
-};
-
-// Sound definition table from DRILL.BIN at 0x40e0 (file offset 0x247E)
-// Format: { flags, tone_idx, env_idx, period_lo, period_hi, volume, duration }
-static const uint8 kDrillerCPCSoundDefs[][7] = {
- {0x02, 0x00, 0x0d, 0x00, 0x00, 0x0f, 0x01}, // Sound def 0
- {0x03, 0x00, 0x01, 0x00, 0x00, 0x0f, 0x01}, // Sound def 1
- {0x09, 0x00, 0x02, 0x20, 0x01, 0x0f, 0x01}, // Sound def 2
- {0x09, 0x00, 0x03, 0x20, 0x01, 0x0f, 0x01}, // Sound def 3
- {0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01}, // Sound def 4
- {0x09, 0x03, 0x00, 0x27, 0x00, 0x0f, 0x01}, // Sound def 5
- {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // Sound def 6
- {0x09, 0x00, 0x04, 0x20, 0x01, 0x0f, 0x08}, // Sound def 7
- {0x09, 0x00, 0x07, 0x00, 0x01, 0x0f, 0x18}, // Sound def 8
- {0x01, 0x00, 0x08, 0x00, 0x01, 0x0f, 0x02}, // Sound def 9
-};
-#endif
-
/**
- * Sound entries extracted from DRILL.BIN at their respective addresses.
+ * "Tone" Table at l4034h (file offset 0x23D2) - actually controls VOLUME ENVELOPE
*
- * Sound entry structure (from assembly at l38e9h):
- * Byte 0: Flags
- * - Bits 0-1: Channel number (1-3 â A/B/C)
- * - Bit 2: If set, DISABLE tone (assembly at 4825: bit 2,(ix+000h))
- * - Bit 3: If set, DISABLE noise (assembly at 482c: bit 3,(ix+000h))
- * - Bit 6: Disable flag (checked at 1cdc: bit 6,(ix+000h))
- * - Bit 7: Additional flag
- * Bytes 1-3: Base params for calibration (multiply by 64, subtract calibration)
- * Bytes 4-6: Add params (multiply by 64, add to result)
- * Byte 7: Delta sign/multiplier (signed: 0xfe=-2, 0xfd=-3, 0xff=-1)
- * Byte 8: Entry size (total bytes in entry)
- * Bytes 9+: Volume nibbles (low=even ch, high=odd ch)
+ * 17 entries, variable-length: byte[0] = triplet count, then (count * 3) bytes of data.
+ * Stored as flat 4-byte entries because all verified entries have count <= 4.
*
- * Calibration formula at l1cffh:
- * For each i (0-2):
- * result1 = entry[1+i] * 64 - calibration[i]
- * result2 = result1 + entry[4+i] * 64
+ * Format: {triplet_count, counter, delta, limit}
+ * triplet_count: number of {counter, delta, limit} triplets (first one inline)
+ * counter: ticks between volume changes (reload value)
+ * delta: signed value added to volume each time (masked to 4 bits)
+ * limit: how many times counter expires before advancing to next triplet
*
- * The calibration value is position-dependent in the original game.
- * We use a fixed calibration that produces reasonable frequencies.
+ * Volume update per tick (sub_7571h at l763ah):
+ * 1. dec limit_countdown; if != 0, skip
+ * 2. reload limit, apply: volume = (volume + delta) & 0x0F
+ * 3. dec counter; if != 0, skip
+ * 4. advance to next triplet (or set finished flag if all done)
*/
-
-/**
- * Sound entries extracted from DRILL.BIN at their respective addresses.
- *
- * IMPLEMENTATION STATUS:
- * - Types 1, 6, 7: HAVE ASSEMBLY ENTRY DATA (l3904h, l3912h, l38e9h, l3922h, l3932h, l3942h)
- * - Types 2, 3: HANDLERS EXIST but NO ENTRY DATA in table (sub_26e2h, sub_2607h)
- * - Types 4, 5, 8, 9: HANDLERS EXIST (sub_2207h) but NO ENTRY DATA found
- * - Types >= 10: Use sub_257bh with hardware envelope
- *
- * Entry structure: flags(1), base[3], add[3], delta(1), size(1), vol/extra[...]
- * Channel count for sub_2207h: compare bytes 0x0c-0x0f
- * - All equal â 5 channels
- * - One pair differs â 6 channels
- * - Both pairs differ â 8 channels
- */
-
-// Sound 7 (Hit) at l38e9h - 27 bytes (0x1b)
-// Type 7 â sub_2207h, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
-// Flags 0x87: bit 2=1 (no tone), bit 3=0 (noise on)
-static const uint8 kSoundEntry7[] = {
- 0x87, 0x00, 0x00, 0x00, 0x08, 0x0a, 0x08, 0xff, // bytes 0-7
- 0x1b, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x05, 0x05 // bytes 8-15 (0c-0f for channel count)
-};
-
-// Sound 1 (Shoot) at l3904h - 14 bytes (0x0e)
-// Type 1 â sub_2112h â 8 channels (hardcoded in handler)
-// Flags 0x81: bits 2,3=0 â tone+noise enabled
-static const uint8 kSoundEntry1[] = {
- 0x81, 0x00, 0x00, 0x00, 0x0c, 0x01, 0x0c, 0xfe, // bytes 0-7
- 0x0e, 0x66, 0x22, 0x66, 0x1c, 0x0a // bytes 8-13
-};
-
-// Sound 6 (Menu) at l3912h - 16 bytes (0x10)
-// Type 6 â sub_2207h, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
-// Flags 0x86: bit 2=1 (no tone), bit 3=0 (noise on)
-static const uint8 kSoundEntry6[] = {
- 0x86, 0x00, 0x00, 0x00, 0x06, 0x08, 0x06, 0xfd, // bytes 0-7
- 0x10, 0x7f, 0x7f, 0x00, 0x03, 0x03, 0x03, 0x03 // bytes 8-15 (0c-0f for channel count)
+static const uint8 kToneTable[][4] = {
+ {0x01, 0x01, 0x00, 0x01}, // 0
+ {0x02, 0x0f, 0x01, 0x03}, // 1
+ {0x01, 0xf1, 0x01, 0x00}, // 2
+ {0x01, 0x0f, 0xff, 0x18}, // 3
+ {0x01, 0x06, 0xfe, 0x3f}, // 4
+ {0x01, 0x0f, 0xff, 0x18}, // 5
+ {0x02, 0x01, 0x00, 0x06}, // 6
+ {0x0f, 0xff, 0x0f, 0x00}, // 7
+ {0x04, 0x05, 0xff, 0x0f}, // 8
+ {0x01, 0x05, 0x01, 0x01}, // 9
+ {0x00, 0x7b, 0x0f, 0xff}, // 10
+ {0x04, 0x00, 0x00, 0x00}, // 11
+ {0x03, 0x01, 0x0f, 0x01}, // 12
+ {0x01, 0xf1, 0x2a, 0x01}, // 13
+ {0x0f, 0x18, 0x00, 0x00}, // 14
+ {0x02, 0x01, 0x0f, 0x01}, // 15
+ {0x01, 0xf1, 0x01, 0x00}, // 16
};
/**
- * Sound 2 (Collide) - Synthetic entry for sub_26e2h
+ * "Envelope" Table at l4078h (file offset 0x2416) - actually controls PITCH SWEEP
*
- * sub_26e2h at 0x26e2 (lines 1281-1380 in assembly):
- * - Uses 2 channels (line 1349: ld a,002h)
- * - Reads entry from l3d1ah
- * - Reads processed params from l3d16h (after calibration at l1cffh)
- * - Duplicates params to 2 working areas (lines 1303-1314)
- * - Calls sub_2e96h for the main update loop
+ * 26 entries, variable-length: byte[0] = triplet count, then (count * 3) bytes of data.
+ * Stored as flat 4-byte entries because all verified entries have count <= 5.
*
- * Entry structure for type 2:
- * byte 0: flags (0x82 = type 2, tone+noise enabled)
- * bytes 1-3: base params (multiplied by 64, subtract calibration)
- * bytes 4-6: add params (multiplied by 64, added)
- * byte 7: delta sign
- * byte 8: entry size
- * byte 9: volume nibbles
- * bytes 0xa: extra byte (copied to iy+001h at line 1295-1296)
- * bytes 0xb-0xc: threshold for comparison (line 1344-1345)
- * byte 0xd: flag mask for AND with 01e6h (line 1300)
+ * Format: {triplet_count, counter, delta, limit}
+ * triplet_count: number of {counter, delta, limit} triplets (first one inline)
+ * counter: how many delta applications before advancing to next triplet
+ * delta: signed byte added to 16-bit period each time limit expires
+ * limit: ticks between pitch changes (reload value)
*
- * Collide uses result1 values (IX[0,4,8]) which are entry[1-3]*64 - calibration.
- * With base=0 and calibration=700, this gives period 700 (~89 Hz bass thud).
- * Using base values to shift the frequency range.
+ * Pitch update per tick (sub_7571h at l758bh):
+ * 1. dec limit_countdown; if != 0, skip
+ * 2. reload limit, apply: period += sign_extend(delta)
+ * 3. write period to AY tone registers
+ * 4. dec counter; if != 0, skip
+ * 5. advance to next triplet (or check duration if all done)
*/
-// Sound 2 (Collide) entry from DRILL_CODE.bin offset 0x1d00
-static const uint8 kSoundEntry2[] = {
- 0x82, // Flags: type 2, bits 2,3=0 (tone+noise)
- 0x40, 0x2e, 0x40, // Base params: 64, 46, 64
- 0x00, 0x00, 0x00, // Add params: 0, 0, 0
- 0xf8, // Byte 7 (stored to l3a5ah, NOT a delta sign)
- 0x0e, // Entry size (14 bytes)
- 0x11, // Volume nibbles (low=1, high=1)
- 0x50, 0x00, // Threshold (0x0050 = 80)
- 0x20, // Flag mask (0x20 = 32)
- 0x04 // Extra byte
+static const uint8 kEnvelopeTable[][4] = {
+ {0x01, 0x02, 0x00, 0xff}, // 0
+ {0x01, 0x10, 0x01, 0x01}, // 1
+ {0x01, 0x02, 0x30, 0x10}, // 2
+ {0x01, 0x02, 0xd0, 0x10}, // 3
+ {0x03, 0x01, 0xe0, 0x06}, // 4
+ {0x01, 0x20, 0x06, 0x01}, // 5
+ {0xe0, 0x06, 0x00, 0x00}, // 6
+ {0x01, 0x02, 0xfb, 0x03}, // 7
+ {0x01, 0x02, 0xfd, 0x0c}, // 8
+ {0x02, 0x01, 0x04, 0x03}, // 9
+ {0x08, 0xf5, 0x03, 0x00}, // 10
+ {0x01, 0x10, 0x02, 0x06}, // 11
+ {0x01, 0x80, 0x01, 0x03}, // 12
+ {0x01, 0x64, 0x01, 0x01}, // 13
+ {0x03, 0x01, 0x00, 0x7b}, // 14
+ {0x01, 0xcf, 0x01, 0x01}, // 15
+ {0x00, 0x96, 0x00, 0x00}, // 16
+ {0x05, 0x01, 0x00, 0x4b}, // 17
+ {0x01, 0xe9, 0x01, 0x01}, // 18
+ {0x00, 0x30, 0x01, 0xe1}, // 19
+ {0x01, 0x01, 0x00, 0x96}, // 20
+ {0x03, 0x02, 0xf2, 0x15}, // 21
+ {0x05, 0x60, 0x01, 0x02}, // 22
+ {0x00, 0x40, 0x00, 0x00}, // 23
+ {0x01, 0x01, 0x00, 0x10}, // 24
+ {0x01, 0x01, 0x00, 0x0f}, // 25
};
/**
- * Sound 3 (Step) - Synthetic entry for sub_2607h
- *
- * sub_2607h at 0x2607 (lines 1177-1280 in assembly):
- * - Uses 4 channels (line 1227: ld a,004h)
- * - Reads processed params from l3d16h (lines 1184-1209)
- * - Reads entry[4] and entry[5] to determine distribution (lines 1203, 1219)
- * - Distributes to 4 pairs of working areas
- * - Calls sub_2e96h for the main update loop
- *
- * Distribution logic (lines 1210-1225):
- * if entry[4] == 0: B=1, use one pattern
- * else if entry[5] == 0: B=2, use another pattern
- * else: B=3, use third pattern
- *
- * Step uses BOTH result1 (IX[0,2]) AND result2 (IX[4,6,8,10]) values.
- * For an ascending chirp, we want:
- * - result1 values: mid-range starting pitch (~300-400 Hz)
- * - result2 values: higher pitch offset (~500-800 Hz)
- * - Negative delta to decrease period (ascending pitch)
- *
- * With calibration=700:
- * base=0x09: result1 = 9*64 - 700 = -124 â 124 period (~504 Hz)
- * add=0x0a: result2 = -124 + 10*64 = 516 period - wait that's wrong
- * Actually: result2 = result1 + add*64 = -124 + 640 = 516 â too low
+ * Sound Definition Table at l40e0h (file offset 0x247E)
+ * 7 bytes per entry: {flags, tone_idx, env_idx, period_lo, period_hi, volume, duration}
*
- * Let's use base=0x0b for result1 = 11*64-700 = 4 â ~15kHz (too high)
- * base=0x0a: result1 = 10*64-700 = -60 â 60 period (~1042 Hz) - good chirp start
- * add=0x02: result2 = -60 + 128 = 68 period (~919 Hz)
+ * Flags:
+ * Bits 0-1: Channel (1=A, 2=B, 3=C)
+ * Bit 2: If set, DISABLE tone
+ * Bit 3: If set, DISABLE noise
*/
-static const uint8 kSoundEntry3[] = {
- 0x83, // Flags: type 3, bits 2,3=0 (tone+noise)
- 0x00, 0x00, 0x00, // Base params (to be determined from assembly)
- 0x06, 0x04, 0x06, // Add params
- 0xfe, // Delta sign (-2)
- 0x10, // Entry size (16 bytes)
- 0x66, // Volume nibbles (low=6, high=6)
- 0x01, // entry[4] - pattern selector (non-zero)
- 0x01, // entry[5] - pattern selector (non-zero)
- 0x00, 0x00, 0x00, 0x00 // Padding to 16 bytes
+static const uint8 kSoundDefTable[][7] = {
+ {0x02, 0x00, 0x0d, 0x00, 0x00, 0x0f, 0x01}, // 1: ch=2 T+N env=13 per=0 vol=15 dur=1
+ {0x03, 0x00, 0x01, 0x00, 0x00, 0x0f, 0x01}, // 2: ch=3 T+N env=1 per=0 vol=15 dur=1
+ {0x09, 0x00, 0x02, 0x20, 0x01, 0x0f, 0x01}, // 3: ch=1 T env=2 per=288 vol=15 dur=1
+ {0x09, 0x00, 0x03, 0x20, 0x01, 0x0f, 0x01}, // 4: Step Down - ch=1 T env=3 per=288
+ {0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01}, // 5: ch=1 N tone=1 env=0 per=256 vol=0
+ {0x09, 0x03, 0x00, 0x27, 0x00, 0x0f, 0x01}, // 6: ch=1 T tone=3 env=0 per=39 vol=15
+ {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // 7: ch=1 N tone=4 env=0 per=0 vol=1
+ {0x09, 0x00, 0x04, 0x20, 0x01, 0x0f, 0x08}, // 8: ch=1 T env=4 per=288 vol=15 dur=8
+ {0x09, 0x00, 0x07, 0x00, 0x01, 0x0f, 0x18}, // 9: Fallen - ch=1 T env=7 per=256 dur=24
+ {0x01, 0x00, 0x08, 0x00, 0x01, 0x0f, 0x02}, // 10: Area Change - ch=1 T+N env=8 per=256 dur=2
+ {0x01, 0x05, 0x09, 0x10, 0x00, 0x0f, 0x0d}, // 11: ch=1 T+N tone=5 env=9 per=16 dur=13
+ {0x09, 0x00, 0x0b, 0x70, 0x00, 0x0f, 0x01}, // 12: ch=1 T env=11 per=112 vol=15
+ {0x05, 0x06, 0x00, 0x00, 0x00, 0x0f, 0x01}, // 13: Mission Complete - ch=1 N tone=6 env=0
+ {0x09, 0x00, 0x0c, 0x50, 0x00, 0x0f, 0x01}, // 14: ch=1 T env=12 per=80 vol=15
+ {0x09, 0x08, 0x0e, 0x77, 0x00, 0x0f, 0x01}, // 15: ch=1 T tone=8 env=14 per=119
+ {0x0a, 0x08, 0x11, 0x6a, 0x00, 0x0f, 0x01}, // 16: ch=2 T tone=8 env=17 per=106
+ {0x09, 0x0c, 0x15, 0xbc, 0x03, 0x00, 0x01}, // 17: ch=1 T tone=12 env=21 per=956
+ {0x06, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x01}, // 18: ch=2 N tone=15 env=24
+ {0x09, 0x00, 0x19, 0xf6, 0x02, 0x0f, 0x01}, // 19: ch=1 T env=25 per=758
+ {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // 20: ch=1 N tone=4 env=0
};
-/**
- * Calibration value for sound processing.
- *
- * The original assembly uses position-dependent calibration (~8000 initially).
- * This creates wrapped 16-bit values. We use a smaller calibration that
- * produces valid AY periods in the audible frequency range.
- *
- * Calibration affects the starting period:
- * period = entry[4+i] * 64 - calibration
- *
- * With calibration=700 and entry[4]=0x0c (12):
- * period = 12 * 64 - 700 = 768 - 700 = 68 â 919 Hz
- */
-static const int16 kSoundCalibration = 700;
-
class DrillerCPCSfxStream : public Audio::AudioStream {
public:
- DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _index(index), _rate(rate) {
- // CPC uses 1MHz clock for AY-3-8912
- initAY();
- _counter = 0;
+ DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _rate(rate) {
_finished = false;
- _phase = 0;
- _channelCount = 0;
- _outerLoops = 0;
- _loopCount = 0;
- _maxLoops = 0;
- _tonePeriod = 0;
- _toneDelta = 0;
- _processedFlags = 0;
-
- // Initialize channel state for 8 channels (sub_2e96h uses up to 8)
- for (int i = 0; i < 8; i++) {
- _channelPeriod[i] = 0;
- _channelVolume[i] = 0;
- _channelDelta[i] = 0;
- _channelDone[i] = false;
- }
+ _tickSampleCount = 0;
- // Initialize processed parameters
- for (int i = 0; i < 6; i++) {
- _processedParams[i] = 0;
- }
+ // Silence all channels
+ writeReg(7, 0xFF);
+ for (int r = 0; r < 11; r++)
+ writeReg(r, 0);
- // Setup based on sound index using dispatch table logic from 0x6305
- setupSound(index);
- }
+ // Initialize channel state
+ memset(&_ch, 0, sizeof(_ch));
- void initAY() {
- // Silence all channels (AY register 7 = mixer, FF = all disabled)
- writeReg(7, 0xFF);
- writeReg(8, 0); // Volume A = 0
- writeReg(9, 0); // Volume B = 0
- writeReg(10, 0); // Volume C = 0
+ setupSound(index);
}
int readBuffer(int16 *buffer, const int numSamples) override {
if (_finished)
return 0;
- // Process at 50Hz (CPC interrupt rate)
- int samplesPerTick = _rate / 50;
int samplesGenerated = 0;
+ int samplesPerTick = _rate / 50; // 50Hz interrupt rate
while (samplesGenerated < numSamples && !_finished) {
- int samplesTodo = MIN(numSamples - samplesGenerated, samplesPerTick);
-
- updateSound();
-
- _ay.readBuffer(buffer + samplesGenerated, samplesTodo);
- samplesGenerated += samplesTodo;
+ // Generate samples until next tick
+ int remaining = samplesPerTick - _tickSampleCount;
+ int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+ if (toGenerate > 0) {
+ _ay.readBuffer(buffer + samplesGenerated, toGenerate);
+ samplesGenerated += toGenerate;
+ _tickSampleCount += toGenerate;
+ }
- if (_finished) break;
+ // Run interrupt handler at 50Hz tick boundary
+ if (_tickSampleCount >= samplesPerTick) {
+ _tickSampleCount -= samplesPerTick;
+ tickUpdate();
+ }
}
return samplesGenerated;
@@ -561,37 +437,51 @@ public:
private:
Audio::AY8912Stream _ay;
- int _index;
int _rate;
- int _counter;
- int _phase;
bool _finished;
- uint8 _regs[16];
-
- /**
- * Channel state for multi-channel processing (sub_2e96h at 0x2e96)
- * Sound 1 uses 8 channels, distributed from processed parameters.
- * Channel data base at 0x0223, 6 bytes per channel in original.
- */
- uint16 _channelPeriod[8]; // Tone period per channel
- int16 _channelDelta[8]; // Delta per channel (can be negative)
- uint8 _channelVolume[8]; // Volume per channel
- bool _channelDone[8]; // Completion status per channel
-
- // Sound-specific state
- uint16 _tonePeriod; // Current tone period (for simple sounds)
- int16 _toneDelta; // Tone delta (for simple sounds)
- uint8 _channelCount; // Number of active channels (l3bc3h)
- uint8 _outerLoops; // Outer loop count (l3bc4h = 5)
- uint16 _loopCount; // Current iteration
- uint16 _maxLoops; // Total iteration limit
+ int _tickSampleCount; // Samples generated in current tick
+ uint8 _regs[16]; // Shadow copy of AY registers
/**
- * Processed parameters from calibration (output of l1cffh)
- * 6 x 16-bit values stored at working buffer 0x02D1
+ * Channel state - mirrors the 23-byte per-channel structure at l416dh
+ * as populated by sub_4760h and updated by sub_7571h.
+ *
+ * "vol" fields come from the "tone" table (l4034h) - controls volume envelope
+ * "pitch" fields come from the "envelope" table (l4078h) - controls pitch sweep
*/
- int16 _processedParams[6];
- uint8 _processedFlags; // Accumulated flag bits (stored at 0x01e6)
+ struct ChannelState {
+ // Volume modulation (from "tone" table)
+ uint8 volCounter; // ix+000h: initial counter value
+ int8 volDelta; // ix+001h: signed delta added to volume
+ uint8 volLimit; // ix+002h: initial limit value
+ uint8 volCounterCur; // ix+003h: current counter (decremented)
+ uint8 volLimitCur; // ix+004h: current limit countdown
+ uint8 volume; // ix+005h: current AY volume (0-15)
+ uint8 volTripletTotal; // ix+006h: total number of volume triplets
+ uint8 volCurrentStep; // ix+007h: current triplet index
+ uint8 duration; // ix+008h: repeat count
+ uint8 volToneIdx; // tone table index (to recompute data pointer)
+
+ // Pitch modulation (from "envelope" table)
+ uint8 pitchCounter; // ix+00Bh: initial counter value
+ int8 pitchDelta; // ix+00Ch: signed delta added to period
+ uint8 pitchLimit; // ix+00Dh: initial limit value
+ uint8 pitchCounterCur; // ix+00Eh: current counter (decremented)
+ uint8 pitchLimitCur; // ix+00Fh: current limit countdown
+ uint16 period; // ix+010h-011h: current 16-bit AY tone period
+ uint8 pitchTripletTotal; // ix+012h: total number of pitch triplets
+ uint8 pitchCurrentStep; // ix+013h: current triplet index
+ uint8 pitchEnvIdx; // envelope table index (to recompute data pointer)
+
+ uint8 finishedFlag; // ix+016h: set when volume envelope exhausted
+
+ // AY register mapping for this channel
+ uint8 channelNum; // 1=A, 2=B, 3=C
+ uint8 toneRegLo; // AY register for tone fine
+ uint8 toneRegHi; // AY register for tone coarse
+ uint8 volReg; // AY register for volume
+ bool active; // Channel is producing sound
+ } _ch;
void writeReg(int reg, uint8 val) {
if (reg >= 0 && reg < 16) {
@@ -601,927 +491,241 @@ private:
}
/**
- * Unified sound entry processing - implements l1cffh calibration
- *
- * Processes a sound entry through the calibration system and sets up
- * all channel parameters. This is the core function that makes all
- * sounds work consistently.
- *
- * @param entry Raw sound entry bytes (14+ bytes from l38e9h, l3904h, etc.)
- * @param numChannels Number of channels to use (8 for shoot, fewer for others)
- * @param outerLoops Number of outer loop iterations (l3bc4h, typically 5)
- */
- void setupFromEntry(const uint8 *entry, int numChannels, int outerLoops) {
- // Extract flags from byte 0
- uint8 flags = entry[0];
- int8 deltaSign = static_cast<int8>(entry[7]); // Signed delta from byte 7
-
- // Process parameters through calibration (l1cffh)
- // Formula: result1 = entry[1+i]*64 - calibration
- // result2 = result1 + entry[4+i]*64
- int16 processed[6];
- for (int i = 0; i < 3; i++) {
- int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
- int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
- processed[i * 2] = val1 - kSoundCalibration;
- processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
- }
-
- _channelCount = numChannels;
- _outerLoops = outerLoops;
- _maxLoops = outerLoops * numChannels;
-
- // Channel setup following sub_2112h distribution pattern
- for (int ch = 0; ch < numChannels; ch++) {
- int16 basePeriod, param2, delta;
-
- // Distribution pattern from assembly (lines 631-666 of sub_2112h)
- // IY[0-1] â ch 0,3,4,7; IY[2-3] â ch 1,2,5,6
- if (ch == 0 || ch == 3 || ch == 4 || ch == 7) {
- basePeriod = processed[0];
- } else {
- basePeriod = processed[1];
- }
-
- // IY[4-5] â ch 0,1,4,5; IY[6-7] â ch 2,3,6,7
- if (ch == 0 || ch == 1 || ch == 4 || ch == 5) {
- param2 = processed[2];
- } else {
- param2 = processed[3];
- }
-
- // IY[8-9] â ch 0-3; IY[10-11] â ch 4-7
- if (ch < 4) {
- delta = processed[4];
- } else {
- delta = processed[5];
- }
-
- // Calculate effective period (sub_2d87h op_type=1: HL = base + delta)
- int16 effectivePeriod = basePeriod + delta;
-
- // Handle invalid periods - try fallbacks
- if (effectivePeriod <= 0 || effectivePeriod > 4095) {
- if (basePeriod > 0 && basePeriod <= 4095) {
- effectivePeriod = basePeriod;
- } else if (param2 > 0 && param2 <= 4095) {
- effectivePeriod = param2;
- } else if (delta > 0 && delta <= 4095) {
- effectivePeriod = delta;
- } else {
- _channelPeriod[ch] = 0;
- _channelDelta[ch] = 0;
- _channelDone[ch] = true;
- continue;
- }
- }
-
- _channelPeriod[ch] = static_cast<uint16>(effectivePeriod);
- _channelDelta[ch] = deltaSign; // Use delta sign from entry byte 7
- _channelDone[ch] = false;
-
- debug("setupFromEntry: ch%d base=%d param2=%d delta=%d -> period=%d deltaSgn=%d",
- ch, basePeriod, param2, delta, _channelPeriod[ch], deltaSign);
- }
-
- // Extract volume nibbles from bytes 9+ (low nibble = even ch, high = odd ch)
- for (int i = 0; i < (numChannels + 1) / 2 && i < 5; i++) {
- int ch0 = i * 2;
- int ch1 = i * 2 + 1;
- if (ch0 < numChannels) _channelVolume[ch0] = entry[9 + i] & 0x0F;
- if (ch1 < numChannels) _channelVolume[ch1] = (entry[9 + i] >> 4) & 0x0F;
- }
-
- // Configure mixer based on flags
- // Bit 2: disable tone, Bit 3: disable noise
- uint8 mixer = 0x3F; // Start with all disabled
- int channel = (flags & 0x03); // Channel A=1, B=2, C=3
- if (channel >= 1 && channel <= 3) {
- int chIdx = channel - 1;
- if (!(flags & 0x04)) mixer &= ~(1 << chIdx); // Enable tone
- if (!(flags & 0x08)) mixer &= ~(1 << (chIdx + 3)); // Enable noise
- }
- writeReg(7, mixer);
-
- // Noise period (reasonable default)
- writeReg(6, 0x10);
-
- // Find first active channel for initial output
- uint16 initPeriod = 100;
- for (int ch = 0; ch < numChannels; ch++) {
- if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
- initPeriod = _channelPeriod[ch];
- break;
- }
- }
-
- writeReg(0, initPeriod & 0xFF);
- writeReg(1, (initPeriod >> 8) & 0x0F);
- writeReg(8, 15); // Start at max volume
-
- _loopCount = 0;
-
- debug("setupFromEntry: flags=0x%02x mixer=0x%02x initPeriod=%d deltaSign=%d",
- flags, mixer, initPeriod, deltaSign);
- }
-
- /**
- * Unified update function for entry-based sounds
- * Applies delta per outer loop, cycles through channels
- */
- void updateFromEntry() {
- int outerLoop = _loopCount / _channelCount;
- int innerIdx = _loopCount % _channelCount;
-
- // Apply delta at start of each outer loop
- if (innerIdx == 0 && _loopCount > 0) {
- for (int ch = 0; ch < _channelCount; ch++) {
- if (_channelDone[ch]) continue;
-
- int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
- if (newPeriod <= 20 || newPeriod > 4095) {
- _channelDone[ch] = true;
- continue;
- }
- _channelPeriod[ch] = static_cast<uint16>(newPeriod);
- }
- }
-
- // Find active channel for output
- bool anyActive = false;
- int activeChannel = -1;
- uint16 outputPeriod = 0;
-
- for (int i = 0; i < _channelCount; i++) {
- int ch = (innerIdx + i) % _channelCount;
- if (_channelDone[ch]) continue;
-
- anyActive = true;
- if (activeChannel < 0) {
- activeChannel = ch;
- outputPeriod = _channelPeriod[ch];
- }
- }
-
- if (activeChannel >= 0 && outputPeriod > 0) {
- writeReg(0, outputPeriod & 0xFF);
- writeReg(1, (outputPeriod >> 8) & 0x0F);
-
- int vol = 15 - (outerLoop * 2);
- if (vol < 4) vol = 4;
- writeReg(8, static_cast<uint8>(vol));
- }
-
- if (!anyActive || _loopCount >= _maxLoops) {
- _finished = true;
- initAY();
- }
- }
-
- /**
- * Processes sound entry parameters through calibration system.
- * NOTE: Currently disabled - using tuned approximations instead.
- * This function implements the calibration at l1cffh but requires
- * correct runtime calibration values to produce valid results.
- */
-#if 0 // Calibration-based processing - needs emulator capture for accuracy
- /**
- * Original assembly at l1cffh (0x1cff-0x1d54):
- * ld de,(l3d16h) ; 1cff - DE = working buffer (0x02D1)
- * ld ix,(l3d1ah) ; 1d04 - IX = sound entry pointer
- * ld hl,l38a6h ; 1d08 - HL = calibration table
- * ld b,003h ; 1d0b - Loop 3 times
+ * Route all sounds through the sub_4760h system.
*
- * Algorithm per iteration:
- * 1. BC = calibration[i] from l38a6h
- * 2. HL = entry[1+i] * 64 (via srl h ; rr l twice)
- * 3. result1 = HL - BC
- * 4. Store result1 (2 bytes)
- * 5. BC = entry[4+i] * 64
- * 6. result2 = result1 + BC
- * 7. Store result2 (2 bytes)
- * 8. Advance entry pointer (inc ix)
- *
- * @param soundEntry Raw sound entry data (14 bytes from l3904h etc.)
+ * In the original game, sub_4760h is called with a 1-based sound number.
+ * It loads a 7-byte entry from l40e0h and configures AY registers.
*/
- void processParameters(const uint8 *soundEntry) {
- _processedFlags = 0; // Assembly: xor a ; 1d03
-
- for (int i = 0; i < 3; ++i) {
- // Assembly at l1d0dh: ld c,(hl) ; inc hl ; ld b,(hl) ; inc hl
- // BC = calibration[i] from l38a6h
- const int16 calibration = static_cast<int16>(kInitialCalibration[i]);
-
- // Assembly at 1d13-1d1e:
- // ld l,000h ; 1d13
- // ld h,(ix+001h) ; 1d15 - H = entry[1+i] (IX advances each iteration)
- // srl h ; rr l ; 1d18-1d1e - HL = entry[1+i] * 64
- int16 hl = static_cast<int16>(soundEntry[1 + i]) * 64;
-
- // Assembly at 1d20-1d21: or a ; sbc hl,bc
- hl -= calibration;
-
- // Assembly at 1d23-1d28: Track positive result
- // jr z,l1d2ah ; jp m,l1d2ah ; set 6,a
- if (hl > 0) {
- _processedFlags |= (1 << (5 - i * 2));
- }
-
- // Assembly at 1d2a-1d31: srl a ; store result
- _processedParams[i * 2] = hl;
-
- // Assembly at 1d32-1d3d:
- // ld c,000h ; 1d32
- // ld b,(ix+004h) ; 1d34 - B = entry[4+i]
- // srl b ; rr c ; 1d37-1d3d - BC = entry[4+i] * 64
- const int16 bc = static_cast<int16>(soundEntry[4 + i]) * 64;
-
- // Assembly at 1d3f-1d40: or a ; adc hl,bc
- hl += bc;
-
- // Assembly at 1d42-1d45: Track overflow
- // jp p,l1d47h ; set 6,a
- if (hl < 0) {
- _processedFlags |= (1 << (4 - i * 2));
- }
-
- // Assembly at 1d47-1d4e: srl a ; store result
- _processedParams[i * 2 + 1] = hl;
-
- // Assembly at 1d4f: inc ix (pointer advances for next iteration)
- }
- // Assembly at 1d55: ld (001e6h),a - store flags
- }
-#endif
-
void setupSound(int index) {
- // Dispatch based on index (mirrors 0x6305 logic)
- switch (index) {
- case 1: // Shoot (sub_2112h)
- setupShoot();
- break;
- case 2: // Collide (sub_26e2h)
- setupCollide();
- break;
- case 3: // Step Up (sub_2607h)
- setupStepUp();
- break;
- case 4: // Step Down (sub_2207h, index 4)
- case 5: // (sub_2207h, index 5)
- case 6: // Menu (sub_2207h, index 6)
- case 7: // Hit (sub_2207h, index 7)
- case 8: // (sub_2207h, index 8)
- case 9: // Fallen (sub_2207h, index 9)
- setupGeneric(index);
- break;
- default:
- if (index >= 10) {
- // High index handler (l1d8fh) - uses HW envelope
- setupHighIndex(index);
- } else {
- _finished = true;
- }
- break;
- }
- }
-
- /**
- * Sound 1: Shoot - implements sub_2112h at 0x2112-0x2206
- *
- * Uses unified setupFromEntry() with assembly data from l3904h.
- * Assembly: 8 channels (ld a,008h at 2188h), 5 outer loops (l3bc4h).
- *
- * Entry at l3904h: 81 00 00 00 0c 01 0c fe 0e 66 22 66 1c 0a
- */
- void setupShoot() {
- // Assembly: sub_2112h uses 8 channels and calls sub_2e96h with l3bc4h=5
- setupFromEntry(kSoundEntry1, 8, 5);
- }
-
- /**
- * Sound 2: Collide - implements sub_26e2h at 0x26e2
- *
- * Assembly at sub_26e2h (lines 1281-1380):
- * Line 1302: ld ix,(l3d16h) â Reads processed params
- * Lines 1303-1314: Copies params to 2 working areas (DUPLICATES each):
- * IX[0-1] â 01e7h AND 01edh (period1)
- * IX[4-5] â 01e9h AND 01efh (period2)
- * IX[8-9] â 01ebh AND 01f1h (delta)
- * Line 1349: ld a,002h â Sets 2 CHANNELS
- * Line 1351: call sub_2e96h â Main update loop
- *
- * Uses result1 values (IX byte offsets 0-1, 4-5, 8-9):
- * IX[0-1] = processed[0] = entry[1]*64 - calibration (period1)
- * IX[4-5] = processed[2] = entry[2]*64 - calibration (period2)
- * IX[8-9] = processed[4] = entry[3]*64 - calibration (delta base)
- *
- * NOTE: Calibration and delta accumulation via sub_2a65h needs investigation.
- */
- void setupCollide() {
- const uint8 *entry = kSoundEntry2;
-
- // Process parameters through calibration (l1cffh)
- // Formula: result1 = entry[1+i]*64 - calibration
- int16 processed[6];
- for (int i = 0; i < 3; i++) {
- int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
- int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
- processed[i * 2] = val1 - kSoundCalibration;
- processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
- }
-
- // Assembly line 1349: sub_26e2h uses exactly 2 channels
- _channelCount = 2;
- _outerLoops = 5;
- _maxLoops = _channelCount * _outerLoops; // 2 * 5 = 10
- _loopCount = 0;
-
- // sub_26e2h reads result1 values at byte offsets 0-1, 4-5, 8-9
- // With base=0 and calibration=700:
- // period1 = processed[0] = 0 - 700 = -700
- // period2 = processed[2] = 0 - 700 = -700
- // delta = processed[4] = 0 - 700 = -700
-
- // Handle negative values (lines 1315-1323): negate if negative
- int16 period1 = processed[0];
- if (period1 < 0) period1 = -period1;
- int16 period2 = processed[2];
- if (period2 < 0) period2 = -period2;
-
- // Both channels use same period (duplicated in assembly lines 1305-1314)
- _channelPeriod[0] = static_cast<uint16>(period1);
- _channelPeriod[1] = static_cast<uint16>(period1);
-
- // Delta from entry[7] = 0xff = -1 (signed)
- int8 deltaSign = static_cast<int8>(entry[7]);
- _channelDelta[0] = deltaSign;
- _channelDelta[1] = deltaSign;
-
- _channelVolume[0] = 15;
- _channelVolume[1] = 15;
- _channelDone[0] = false;
- _channelDone[1] = false;
-
- // Set initial tone period
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
-
- // Configure mixer based on flags byte 0
- // Flags 0x82: bit 2=0 (tone enabled), bit 3=0 (noise enabled)
- writeReg(6, 0x10); // Noise period
- writeReg(7, 0x36); // Enable tone A + noise A
- writeReg(8, 15); // Start at MAX volume
-
- debug("setupCollide: period=%d delta=%d (~%dHz)",
- _channelPeriod[0], _channelDelta[0],
- _channelPeriod[0] > 0 ? 1000000 / (16 * _channelPeriod[0]) : 0);
- }
-
- /**
- * Sound 3: Step Up - implements sub_2607h at 0x2607
- *
- * Assembly at sub_2607h (lines 1177-1280):
- * Line 1184: ld ix,(l3d16h) â Reads processed params
- * Lines 1185-1209: Distributes params to 4 pairs of working areas:
- * IX[0-1] â 01e7h, 01f9h (period1 for ch 0,2)
- * IX[2-3] â 01edh, 01f3h (period1 for ch 1,3)
- * IX[4-5] â 01e9h, 01efh (period2)
- * IX[6-7] â 01f5h, 01fbh (period2)
- * IX[8-9], IX[10-11] â delta distribution based on entry[4-5]
- * Lines 1210-1225: Pattern selection based on entry[4] and entry[5]
- * Line 1227: ld a,004h â Sets 4 CHANNELS
- * Line 1229: call sub_2e96h â Main update loop
- *
- * NOTE: Delta accumulation via sub_2a65h needs investigation.
- */
- void setupStepUp() {
- const uint8 *entry = kSoundEntry3;
- int8 deltaSign = static_cast<int8>(entry[7]); // -2 for ascending pitch
-
- // Process parameters through calibration (l1cffh)
- int16 processed[6];
- for (int i = 0; i < 3; i++) {
- int16 val1 = static_cast<int16>(entry[1 + i]) * 64;
- int16 val2 = static_cast<int16>(entry[4 + i]) * 64;
- processed[i * 2] = val1 - kSoundCalibration;
- processed[i * 2 + 1] = val1 - kSoundCalibration + val2;
- }
-
- // Assembly line 1227: sub_2607h uses exactly 4 channels
- _channelCount = 4;
- _outerLoops = 5;
- _maxLoops = _channelCount * _outerLoops; // 4 * 5 = 20
- _loopCount = 0;
-
- // Distribution to 4 channels following assembly (lines 1185-1209)
- // sub_2607h reads from processed buffer:
- // IX[0-1] = processed[0] = result1[0] â channels 0,2
- // IX[2-3] = processed[1] = result2[0] â channels 1,3
- for (int ch = 0; ch < 4; ch++) {
- int16 periodBase;
-
- // Assembly distributes: channels 0,2 use IX[0-1], channels 1,3 use IX[2-3]
- if (ch == 0 || ch == 2) {
- periodBase = processed[0]; // result1
- } else {
- periodBase = processed[1]; // result2
- }
-
- // Handle negative values (negate if negative)
- if (periodBase < 0) periodBase = -periodBase;
-
- _channelPeriod[ch] = static_cast<uint16>(periodBase);
- _channelDelta[ch] = deltaSign; // -2 from entry[7]
- _channelVolume[ch] = 15;
- _channelDone[ch] = false;
+ if (index >= 1 && index <= 20) {
+ setupSub4760h(index);
+ } else {
+ _finished = true;
}
-
- // Set initial tone period
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
-
- // Configure mixer based on flags
- // Flags 0x83: bit 2=0 (tone enabled), bit 3=0 (noise enabled)
- writeReg(6, 0x10); // Noise period
- writeReg(7, 0x36); // Enable tone A + noise A
- writeReg(8, 15); // Start at MAX volume
-
- debug("setupStepUp: period[0]=%d period[1]=%d delta=%d (~%dHz)",
- _channelPeriod[0], _channelPeriod[1], _channelDelta[0],
- _channelPeriod[0] > 0 ? 1000000 / (16 * _channelPeriod[0]) : 0);
}
/**
- * Sounds 4-9: Generic handler based on sub_2207h at 0x2207
- *
- * IMPLEMENTATION STATUS:
- * - Sound 6 (Menu): ASSEMBLY DATA from l3912h (type 6, 5 channels)
- * - Sound 7 (Hit): ASSEMBLY DATA from l38e9h (type 7, 8 channels)
- * - Sound 4, 5, 8, 9: APPROXIMATION - no entries with these types exist
+ * Implements sub_4760h (0x4760-0x4871) - sound initialization.
*
- * Channel count for sub_2207h determined by comparing entry bytes 0x0c-0x0f:
- * - All equal â 5 channels
- * - One pair differs â 6 channels
- * - Both pairs differ â 8 channels
+ * Assembly flow:
+ * 1. entry = l40e0h[(soundNum-1) * 7]
+ * 2. channel = flags & 0x03 (1=A, 2=B, 3=C)
+ * 3. Configure mixer from flags bits 2-3
+ * 4. Set AY tone period from entry[3-4]
+ * 5. Set AY volume from entry[5]
+ * 6. Load "tone" table (l4034h) -> volume envelope fields
+ * 7. Load "envelope" table (l4078h) -> pitch sweep fields
+ * 8. Set duration from entry[6]
*/
- void setupGeneric(int index) {
- _loopCount = 0;
-
- switch (index) {
- case 6: // Menu - ASSEMBLY DATA from l3912h
- // Type 6, bytes 0c-0f = 03 03 03 03 (all equal) â 5 channels
- setupFromEntry(kSoundEntry6, 5, 5);
- return;
-
- case 7: // Hit - ASSEMBLY DATA from l38e9h
- // Type 7, bytes 0c-0f = 03 03 05 05 (differ) â 8 channels
- setupFromEntry(kSoundEntry7, 8, 5);
- return;
-
- // APPROXIMATIONS: Sounds 4, 5, 8, 9 have no entries in the assembly table.
- // These would use sub_2207h if entries existed.
- // Implementing reasonable approximations based on handler behavior.
-
- case 4: // Step Down - APPROXIMATION (no type 4 entry exists)
- // Would use 5 channels (sub_2207h default) if entry existed
- // Descending pitch (opposite of step up)
- _channelCount = 4; // Match step handler channel count
- for (int ch = 0; ch < 4; ch++) {
- _channelPeriod[ch] = 180 + ch * 20; // 180, 200, 220, 240
- _channelDelta[ch] = 15; // Descending pitch
- _channelVolume[ch] = 10 - ch;
- _channelDone[ch] = false;
- }
- _maxLoops = 20;
- writeReg(7, 0x3E); // Tone only
- break;
-
- case 5: // Reserved - APPROXIMATION (no type 5 entry exists)
- _channelCount = 5; // sub_2207h minimum
- for (int ch = 0; ch < 5; ch++) {
- _channelPeriod[ch] = 250;
- _channelDelta[ch] = 0;
- _channelVolume[ch] = 8;
- _channelDone[ch] = false;
- }
- _maxLoops = 25;
- writeReg(7, 0x3E);
- break;
-
- case 8: // Reserved - APPROXIMATION (no type 8 entry exists)
- _channelCount = 5; // sub_2207h default
- for (int ch = 0; ch < 5; ch++) {
- _channelPeriod[ch] = 200 + ch * 10;
- _channelDelta[ch] = 8;
- _channelVolume[ch] = 10;
- _channelDone[ch] = false;
- }
- _maxLoops = 25;
- writeReg(7, 0x3E);
- break;
-
- case 9: // Fallen - APPROXIMATION (no type 9 entry exists)
- // Long descending sound for falling
- _channelCount = 5;
- for (int ch = 0; ch < 5; ch++) {
- _channelPeriod[ch] = 100 + ch * 20;
- _channelDelta[ch] = 6;
- _channelVolume[ch] = 15 - ch;
- _channelDone[ch] = false;
- }
- _maxLoops = 60; // Long sound
- writeReg(7, 0x3E);
- break;
-
- default:
+ void setupSub4760h(int soundNum) {
+ if (soundNum < 1 || soundNum > 20) {
_finished = true;
return;
}
- // Set initial tone period for approximation-based sounds
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(8, _channelVolume[0]);
- }
-
- /**
- * Sounds >= 10: High index handler (l1d8fh / sub_257bh)
- *
- * From assembly at sub_257bh (0x257b, line 1109 in disassembly):
- * ld a,(001e5h) ; Get type
- * sub 008h ; Channel count = type - 8
- * ld (l3bc3h),a ; Store channel count
- *
- * Channel count formula: index - 8
- * Type 10 â 2 channels
- * Type 11 â 3 channels
- * Type 12 â 4 channels
- * Type 13 â 5 channels
- *
- * APPROXIMATION: No specific entry data found for high index sounds.
- * These use hardware envelope (AY registers 11-13) based on sub_257bh behavior.
- * Uses l38a6h calibration table and processes through l1cffh.
- */
- void setupHighIndex(int index) {
- // Assembly (line 1109): channel count = type - 8
- _channelCount = index - 8;
- if (_channelCount < 1) _channelCount = 1;
- if (_channelCount > 8) _channelCount = 8;
- _loopCount = 0;
-
- for (int ch = 0; ch < _channelCount; ch++) {
- _channelDone[ch] = false;
- }
-
- switch (index) {
- case 10: // Area Change - 2 channels (10 - 8 = 2)
- // APPROXIMATION: Uses HW envelope for transition effect
- _channelPeriod[0] = 0;
- _channelPeriod[1] = 0;
- _channelDelta[0] = 0;
- _channelDelta[1] = 0;
- _channelVolume[0] = 15;
- _channelVolume[1] = 15;
- _maxLoops = 100; // ~2 seconds
-
- writeReg(6, 0x18); // Noise period
- writeReg(7, 0x37); // Noise A only, no tone
- writeReg(11, 0x00); // Envelope period low
- writeReg(12, 0x40); // Envelope period high
- writeReg(13, 0x00); // Shape: single decay
- writeReg(8, 0x10); // Volume = envelope mode
- break;
-
- case 11: // Explosion - 3 channels (11 - 8 = 3)
- // APPROXIMATION: Low rumble with envelope
- _channelPeriod[0] = 700;
- _channelPeriod[1] = 750;
- _channelPeriod[2] = 800;
- _channelDelta[0] = 20;
- _channelDelta[1] = 25;
- _channelDelta[2] = 30;
- _channelVolume[0] = 15;
- _channelVolume[1] = 14;
- _channelVolume[2] = 13;
- _maxLoops = 40;
-
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(6, 0x1C);
- writeReg(7, 0x36); // Tone + Noise
- writeReg(11, 0x00);
- writeReg(12, 0x20);
- writeReg(13, 0x00); // Single decay
- writeReg(8, 0x10); // Envelope mode
- break;
-
- case 12: // Warning - 4 channels (12 - 8 = 4)
- // APPROXIMATION: Alert tone with envelope
- _channelPeriod[0] = 150;
- _channelPeriod[1] = 160;
- _channelPeriod[2] = 170;
- _channelPeriod[3] = 180;
- _channelDelta[0] = 0;
- _channelDelta[1] = 0;
- _channelDelta[2] = 0;
- _channelDelta[3] = 0;
- _channelVolume[0] = 15;
- _channelVolume[1] = 14;
- _channelVolume[2] = 13;
- _channelVolume[3] = 12;
- _maxLoops = 60;
-
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E); // Tone only
- writeReg(11, 0x00);
- writeReg(12, 0x06);
- writeReg(13, 0x0E); // Continue, attack-decay
- writeReg(8, 0x10); // Envelope mode
- break;
-
- case 13: // Mission Complete - 5 channels (13 - 8 = 5)
- // APPROXIMATION: Triumphant ascending tone
- _channelPeriod[0] = 200;
- _channelPeriod[1] = 190;
- _channelPeriod[2] = 180;
- _channelPeriod[3] = 170;
- _channelPeriod[4] = 160;
- _channelDelta[0] = -2;
- _channelDelta[1] = -2;
- _channelDelta[2] = -2;
- _channelDelta[3] = -2;
- _channelDelta[4] = -2;
- _channelVolume[0] = 15;
- _channelVolume[1] = 14;
- _channelVolume[2] = 13;
- _channelVolume[3] = 12;
- _channelVolume[4] = 11;
- _maxLoops = 80;
-
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E); // Tone only
- writeReg(11, 0x00);
- writeReg(12, 0x30);
- writeReg(13, 0x0A); // Continue, hold
- writeReg(8, 0x10); // Envelope mode
- break;
-
- default:
- // Generic high index sound
- for (int ch = 0; ch < _channelCount; ch++) {
- _channelPeriod[ch] = 200 + ch * 20;
- _channelDelta[ch] = 0;
- _channelVolume[ch] = 12 - ch;
- }
- _maxLoops = 30;
-
- writeReg(0, _channelPeriod[0] & 0xFF);
- writeReg(1, (_channelPeriod[0] >> 8) & 0x0F);
- writeReg(7, 0x3E);
- writeReg(11, 0x00);
- writeReg(12, 0x10);
- writeReg(13, 0x00);
- writeReg(8, 0x10); // Envelope mode
- break;
- }
- }
-
- void updateSound() {
- _counter++;
- _loopCount++;
-
- if (_loopCount >= _maxLoops) {
+ const uint8 *entry = kSoundDefTable[soundNum - 1];
+ uint8 flags = entry[0];
+ uint8 toneIdx = entry[1];
+ uint8 envIdx = entry[2];
+ uint16 period = entry[3] | (entry[4] << 8);
+ uint8 volume = entry[5];
+ uint8 duration = entry[6];
+
+ // Channel number (1-based): 1=A, 2=B, 3=C
+ uint8 channelNum = flags & 0x03;
+ if (channelNum < 1 || channelNum > 3) {
_finished = true;
- initAY();
return;
}
- // Update based on sound type
- switch (_index) {
- case 1: // Shoot
- updateShoot();
- break;
- case 2: // Collide
- updateCollide();
- break;
- case 3: // Step Up
- updateStepUp();
- break;
- case 4:
- case 5:
- case 6:
- case 7:
- case 8:
- case 9:
- updateGeneric();
- break;
- default:
- if (_index >= 10) {
- updateHighIndex();
- }
- break;
- }
- }
+ // AY register mapping
+ _ch.channelNum = channelNum;
+ _ch.toneRegLo = (channelNum - 1) * 2; // A=0, B=2, C=4
+ _ch.toneRegHi = (channelNum - 1) * 2 + 1; // A=1, B=3, C=5
+ _ch.volReg = channelNum + 7; // A=8, B=9, C=10
+
+ // Configure mixer (register 7)
+ // Start with all disabled (0xFF), selectively enable per flags
+ // Bit 2 set in flags = DISABLE tone, Bit 3 set = DISABLE noise
+ uint8 mixer = 0xFF;
+ if (!(flags & 0x04))
+ mixer &= ~(1 << (channelNum - 1)); // Enable tone
+ if (!(flags & 0x08))
+ mixer &= ~(1 << (channelNum - 1 + 3)); // Enable noise
+ writeReg(7, mixer);
- /**
- * Sound update loop for Shoot - uses unified updateFromEntry()
- *
- * Assembly: sub_2e96h runs 5 outer loops à 8 channels.
- * Delta from entry byte 7 (0xfe = -2) applied per outer loop.
- */
- void updateShoot() {
- updateFromEntry();
+ // Set AY tone period from entry[3-4]
+ _ch.period = period;
+ writeReg(_ch.toneRegLo, period & 0xFF);
+ writeReg(_ch.toneRegHi, (period >> 8) & 0x0F);
+
+ // Set AY volume from entry[5]
+ _ch.volume = volume;
+ writeReg(_ch.volReg, volume);
+
+ // Duration from entry[6]
+ _ch.duration = duration;
+
+ // Load volume envelope from "tone" table (l4034h)
+ // Table format: byte[0]=triplet_count, then triplets of {counter, delta, limit}
+ const uint8 *toneRaw = &kToneTable[0][0];
+ int toneBase = toneIdx * 4;
+ _ch.volTripletTotal = toneRaw[toneBase];
+ _ch.volCurrentStep = 0;
+ _ch.volToneIdx = toneIdx;
+
+ // Load first volume triplet
+ int volOff = toneBase + 1;
+ _ch.volCounter = toneRaw[volOff];
+ _ch.volDelta = static_cast<int8>(toneRaw[volOff + 1]);
+ _ch.volLimit = toneRaw[volOff + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+
+ // Load pitch sweep from "envelope" table (l4078h)
+ // Table format: byte[0]=triplet_count, then triplets of {counter, delta, limit}
+ const uint8 *envRaw = &kEnvelopeTable[0][0];
+ int envBase = envIdx * 4;
+ _ch.pitchTripletTotal = envRaw[envBase];
+ _ch.pitchCurrentStep = 0;
+ _ch.pitchEnvIdx = envIdx;
+
+ // Load first pitch triplet
+ int pitchOff = envBase + 1;
+ _ch.pitchCounter = envRaw[pitchOff];
+ _ch.pitchDelta = static_cast<int8>(envRaw[pitchOff + 1]);
+ _ch.pitchLimit = envRaw[pitchOff + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
+
+ _ch.finishedFlag = 0;
+ _ch.active = true;
+
+ debug("sub_4760h: sound %d ch=%d mixer=0x%02x period=%d vol=%d dur=%d tone[%d] env[%d]",
+ soundNum, channelNum, mixer, period, volume, duration, toneIdx, envIdx);
+ debug(" vol envelope: triplets=%d counter=%d delta=%d limit=%d",
+ _ch.volTripletTotal, _ch.volCounter, _ch.volDelta, _ch.volLimit);
+ debug(" pitch sweep: triplets=%d counter=%d delta=%d limit=%d",
+ _ch.pitchTripletTotal, _ch.pitchCounter, _ch.pitchDelta, _ch.pitchLimit);
}
/**
- * Update for Sound 2 (Collide) - follows sub_2e96h loop
+ * Implements sub_7571h (0x7571-0x76A9) - 50Hz interrupt-driven update.
*
- * Assembly sub_2e96h (lines 2292-2335):
- * - Outer loop count from l3bc4h (=5)
- * - Inner loop for each channel
- * - Calls sub_2d87h per channel (applies delta based on op_type)
+ * Called at 50Hz (CPC vsync rate). Updates pitch first, then volume.
*
- * For type 2, both channels have duplicated parameters,
- * so they evolve similarly but may finish at different times.
- */
- void updateCollide() {
- int outerLoop = _loopCount / _channelCount;
- int innerIdx = _loopCount % _channelCount;
-
- // sub_2d87h applies delta based on outer loop iteration (op_type at 0x025f)
- // op_type 1,2 = add delta, op_type 3,4 = subtract delta
- // For simplicity, we apply delta uniformly at start of each outer loop
- if (innerIdx == 0 && _loopCount > 0) {
- for (int ch = 0; ch < _channelCount; ch++) {
- if (_channelDone[ch]) continue;
-
- // Apply delta (can be negative from entry[7])
- int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
-
- // Check bounds (AY period range is 1-4095)
- if (newPeriod < 20 || newPeriod > 4095) {
- _channelDone[ch] = true;
- } else {
- _channelPeriod[ch] = static_cast<uint16>(newPeriod);
- }
- }
- }
-
- // Output current channel's period
- int ch = innerIdx % _channelCount;
- if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
- writeReg(0, _channelPeriod[ch] & 0xFF);
- writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
- }
-
- // Volume decay matching other working sounds (start at 15, min 4)
- int vol = 15 - (outerLoop * 2);
- if (vol < 4) vol = 4;
- writeReg(8, static_cast<uint8>(vol));
-
- // Check if all channels are done
- bool anyActive = false;
- for (int i = 0; i < _channelCount; i++) {
- if (!_channelDone[i]) {
- anyActive = true;
- break;
- }
- }
- if (!anyActive) {
- _finished = true;
- initAY();
- }
- }
-
- /**
- * Update for Sound 3 (Step) - follows sub_2e96h loop
+ * PITCH UPDATE (l758bh):
+ * 1. dec pitchLimitCur; if != 0, skip to volume
+ * 2. reload pitchLimitCur from pitchLimit
+ * 3. period += sign_extend(pitchDelta); write to AY tone regs
+ * 4. dec pitchCounterCur; if != 0, skip to volume
+ * 5. advance pitch triplet; if all done -> dec duration; if 0 -> shutdown
*
- * Assembly sub_2e96h (lines 2292-2335):
- * - Outer loop count from l3bc4h (=5)
- * - Inner loop for each of 4 channels
- * - sub_2d87h processes based on op_type (1-5)
+ * VOLUME UPDATE (l763ah):
+ * 1. if finishedFlag set, skip entirely
+ * 2. dec volLimitCur; if != 0, skip
+ * 3. reload volLimitCur from volLimit
+ * 4. volume = (volume + volDelta) & 0x0F; write to AY vol reg
+ * 5. dec volCounterCur; if != 0, skip
+ * 6. advance vol triplet; if all done -> set finishedFlag
*
- * For type 3, 4 channels with different parameter distributions.
+ * SHUTDOWN (l761eh):
+ * - Write volume 0 to AY
+ * - Mark channel inactive
*/
- void updateStepUp() {
- int outerLoop = _loopCount / _channelCount;
- int innerIdx = _loopCount % _channelCount;
-
- // Apply delta at start of each outer loop (sub_2d87h behavior)
- if (innerIdx == 0 && _loopCount > 0) {
- for (int ch = 0; ch < _channelCount; ch++) {
- if (_channelDone[ch]) continue;
-
- int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
-
- // Check bounds
- if (newPeriod < 20 || newPeriod > 4095) {
- _channelDone[ch] = true;
- } else {
- _channelPeriod[ch] = static_cast<uint16>(newPeriod);
- }
- }
- }
-
- // Output current channel's period
- int ch = innerIdx % _channelCount;
- if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
- writeReg(0, _channelPeriod[ch] & 0xFF);
- writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
- }
-
- // Volume decay matching other working sounds (start at 15, min 4)
- int vol = 15 - (outerLoop * 2);
- if (vol < 4) vol = 4;
- writeReg(8, static_cast<uint8>(vol));
-
- // Check if all channels are done
- bool anyActive = false;
- for (int i = 0; i < _channelCount; i++) {
- if (!_channelDone[i]) {
- anyActive = true;
- break;
- }
- }
- if (!anyActive) {
+ void tickUpdate() {
+ if (!_ch.active) {
_finished = true;
- initAY();
- }
- }
-
- void updateGeneric() {
- // Sounds 6 and 7 use unified entry-based system (ASSEMBLY DATA)
- if (_index == 6 || _index == 7) {
- updateFromEntry();
return;
}
- // Sounds 4, 5, 8, 9: APPROXIMATIONS using multi-channel update
- // (mimics sub_2207h behavior with 4-5 channels)
- int outerLoop = _loopCount / _channelCount;
- int innerIdx = _loopCount % _channelCount;
-
- // Apply delta at start of each outer loop
- if (innerIdx == 0 && _loopCount > 0) {
- for (int ch = 0; ch < _channelCount; ch++) {
- if (_channelDone[ch]) continue;
-
- int32 newPeriod = static_cast<int32>(_channelPeriod[ch]) + _channelDelta[ch];
- if (newPeriod < 50 || newPeriod > 2000) {
- _channelDone[ch] = true;
+ const uint8 *toneRaw = &kToneTable[0][0];
+ const uint8 *envRaw = &kEnvelopeTable[0][0];
+
+ // === PITCH UPDATE (l758bh) ===
+ _ch.pitchLimitCur--;
+ if (_ch.pitchLimitCur == 0) {
+ // Reload limit countdown
+ _ch.pitchLimitCur = _ch.pitchLimit;
+
+ // period += sign_extend(pitchDelta)
+ int32 newPeriod = static_cast<int32>(_ch.period) +
+ static_cast<int16>(static_cast<int8>(_ch.pitchDelta));
+ if (newPeriod < 0) newPeriod = 0;
+ if (newPeriod > 4095) newPeriod = 4095;
+ _ch.period = static_cast<uint16>(newPeriod);
+
+ // Write period to AY tone registers
+ writeReg(_ch.toneRegLo, _ch.period & 0xFF);
+ writeReg(_ch.toneRegHi, (_ch.period >> 8) & 0x0F);
+
+ // Decrement pitch counter
+ _ch.pitchCounterCur--;
+ if (_ch.pitchCounterCur == 0) {
+ // Advance to next pitch triplet
+ _ch.pitchCurrentStep++;
+ if (_ch.pitchCurrentStep >= _ch.pitchTripletTotal) {
+ // All pitch triplets exhausted -> check duration
+ _ch.duration--;
+ if (_ch.duration == 0) {
+ // SHUTDOWN (l761eh): silence and deactivate
+ writeReg(_ch.volReg, 0);
+ _ch.active = false;
+ _finished = true;
+ return;
+ }
+ // Duration > 0: restart pitch pattern from beginning
+ _ch.pitchCurrentStep = 0;
+ int off = _ch.pitchEnvIdx * 4 + 1;
+ _ch.pitchCounter = envRaw[off];
+ _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
+ _ch.pitchLimit = envRaw[off + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
} else {
- _channelPeriod[ch] = static_cast<uint16>(newPeriod);
+ // Load next pitch triplet
+ int off = _ch.pitchEnvIdx * 4 + 1 + _ch.pitchCurrentStep * 3;
+ _ch.pitchCounter = envRaw[off];
+ _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
+ _ch.pitchLimit = envRaw[off + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
}
}
}
- // Find active channel for output
- bool anyActive = false;
- for (int i = 0; i < _channelCount; i++) {
- int ch = (innerIdx + i) % _channelCount;
- if (!_channelDone[ch] && _channelPeriod[ch] > 0) {
- writeReg(0, _channelPeriod[ch] & 0xFF);
- writeReg(1, (_channelPeriod[ch] >> 8) & 0x0F);
- anyActive = true;
- break;
+ // === VOLUME UPDATE (l763ah) ===
+ if (!_ch.finishedFlag) {
+ _ch.volLimitCur--;
+ if (_ch.volLimitCur == 0) {
+ // Reload limit countdown
+ _ch.volLimitCur = _ch.volLimit;
+
+ // volume = (volume + volDelta) & 0x0F
+ _ch.volume = (_ch.volume + _ch.volDelta) & 0x0F;
+ writeReg(_ch.volReg, _ch.volume);
+
+ // Decrement volume counter
+ _ch.volCounterCur--;
+ if (_ch.volCounterCur == 0) {
+ // Advance to next volume triplet
+ _ch.volCurrentStep++;
+ if (_ch.volCurrentStep >= _ch.volTripletTotal) {
+ // All volume triplets exhausted -> set finished flag
+ // NOTE: Does NOT shutdown channel - pitch continues
+ _ch.finishedFlag = 1;
+ } else {
+ // Load next volume triplet
+ int off = _ch.volToneIdx * 4 + 1 + _ch.volCurrentStep * 3;
+ _ch.volCounter = toneRaw[off];
+ _ch.volDelta = static_cast<int8>(toneRaw[off + 1]);
+ _ch.volLimit = toneRaw[off + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+ }
+ }
}
}
-
- if (!anyActive) {
- _finished = true;
- initAY();
- return;
- }
-
- // Volume decay varies by sound type
- int vol;
- switch (_index) {
- case 9: // Fallen - slow decay
- vol = 15 - (outerLoop / 2);
- break;
- default:
- vol = 15 - outerLoop * 2;
- break;
- }
- if (vol < 0) vol = 0;
- writeReg(8, vol);
- }
-
- void updateHighIndex() {
- // High index sounds mostly use hardware envelope
- // Update tone if delta is set (for pitch sweeps)
- if (_toneDelta != 0) {
- int period = (int)_tonePeriod + _toneDelta;
- if (period < 50) period = 50;
- if (period > 1000) period = 1000;
- _tonePeriod = period;
-
- writeReg(0, _tonePeriod & 0xFF);
- writeReg(1, (_tonePeriod >> 8) & 0x0F);
- }
- // Hardware envelope handles volume automatically
}
};
Commit: f3935288d23c2ca0266081c5f7772e3beb9c9222
https://github.com/scummvm/scummvm/commit/f3935288d23c2ca0266081c5f7772e3beb9c9222
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: matched most of the sounds for driller (cpc)
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 3ef7632987c..be36625e131 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -261,7 +261,7 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
* - Sound initialization loads 7-byte entry from l40e0h
* - Volume envelope from "Tone" Table at l4034h
* - Pitch sweep from "Envelope" Table at l4078h
- * - 50Hz interrupt-driven update via sub_7571h (0x7571-0x76A9)
+ * - 300Hz interrupt-driven update via sub_7571h (0x7571-0x76A9)
*
* AY-3-8912 PSG with 1MHz clock, register write at 0x4872:
* Port 0xF4 = register select, Port 0xF6 = data
@@ -396,6 +396,10 @@ public:
for (int r = 0; r < 11; r++)
writeReg(r, 0);
+ // Initialize noise period from game init at 0x66D5 (table at 0x66A4h)
+ // Register 6 is set ONCE during init and never changed by sub_4760h or sub_7571h
+ writeReg(6, 0x07);
+
// Initialize channel state
memset(&_ch, 0, sizeof(_ch));
@@ -407,7 +411,10 @@ public:
return 0;
int samplesGenerated = 0;
- int samplesPerTick = _rate / 50; // 50Hz interrupt rate
+ // AY8912Stream is stereo: readBuffer counts int16 values (2 per frame).
+ // CPC interrupts fire at 300Hz (6 per frame). sub_7571h is called
+ // unconditionally at every interrupt (0x68DD), NOT inside the 50Hz divider.
+ int samplesPerTick = (_rate / 300) * 2;
while (samplesGenerated < numSamples && !_finished) {
// Generate samples until next tick
@@ -420,7 +427,7 @@ public:
_tickSampleCount += toGenerate;
}
- // Run interrupt handler at 50Hz tick boundary
+ // Run interrupt handler at 300Hz tick boundary
if (_tickSampleCount >= samplesPerTick) {
_tickSampleCount -= samplesPerTick;
tickUpdate();
@@ -610,9 +617,9 @@ private:
}
/**
- * Implements sub_7571h (0x7571-0x76A9) - 50Hz interrupt-driven update.
+ * Implements sub_7571h (0x7571-0x76A9) - 300Hz interrupt-driven update.
*
- * Called at 50Hz (CPC vsync rate). Updates pitch first, then volume.
+ * Called at 300Hz (every CPC interrupt). Updates pitch first, then volume.
*
* PITCH UPDATE (l758bh):
* 1. dec pitchLimitCur; if != 0, skip to volume
@@ -674,8 +681,25 @@ private:
_finished = true;
return;
}
- // Duration > 0: restart pitch pattern from beginning
+ // Duration > 0: restart BOTH volume and pitch from beginning
+ // Assembly at 0x75D0: reloads tone (volume) table first triplet,
+ // then resets both position indices and done flag,
+ // then loads first envelope (pitch) triplet.
+
+ // Reload first volume triplet (from tone table)
+ int volOff = _ch.volToneIdx * 4 + 1;
+ _ch.volCounter = toneRaw[volOff];
+ _ch.volDelta = static_cast<int8>(toneRaw[volOff + 1]);
+ _ch.volLimit = toneRaw[volOff + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+
+ // Reset both position indices and done flag
+ _ch.volCurrentStep = 0;
_ch.pitchCurrentStep = 0;
+ _ch.finishedFlag = 0;
+
+ // Reload first pitch triplet (from envelope table)
int off = _ch.pitchEnvIdx * 4 + 1;
_ch.pitchCounter = envRaw[off];
_ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
Commit: e774ae207288c14a5b7e524dbadec7eb2c3bc150
https://github.com/scummvm/scummvm/commit/e774ae207288c14a5b7e524dbadec7eb2c3bc150
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: some additional sounds for driller (cpc)
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index be36625e131..8c358322ca2 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -391,18 +391,13 @@ public:
_finished = false;
_tickSampleCount = 0;
- // Silence all channels
- writeReg(7, 0xFF);
- for (int r = 0; r < 11; r++)
- writeReg(r, 0);
+ // Reset all AY registers to match CPC init state
+ for (int r = 0; r < 14; r++)
+ _ay.setReg(r, 0);
+ // Noise period from CPC init table at 0x66A4h (verified in binary)
+ _ay.setReg(6, 0x07);
- // Initialize noise period from game init at 0x66D5 (table at 0x66A4h)
- // Register 6 is set ONCE during init and never changed by sub_4760h or sub_7571h
- writeReg(6, 0x07);
-
- // Initialize channel state
memset(&_ch, 0, sizeof(_ch));
-
setupSound(index);
}
@@ -447,7 +442,6 @@ private:
int _rate;
bool _finished;
int _tickSampleCount; // Samples generated in current tick
- uint8 _regs[16]; // Shadow copy of AY registers
/**
* Channel state - mirrors the 23-byte per-channel structure at l416dh
@@ -491,10 +485,7 @@ private:
} _ch;
void writeReg(int reg, uint8 val) {
- if (reg >= 0 && reg < 16) {
- _regs[reg] = val;
- _ay.setReg(reg, val);
- }
+ _ay.setReg(reg, val);
}
/**
@@ -564,7 +555,7 @@ private:
// Set AY tone period from entry[3-4]
_ch.period = period;
writeReg(_ch.toneRegLo, period & 0xFF);
- writeReg(_ch.toneRegHi, (period >> 8) & 0x0F);
+ writeReg(_ch.toneRegHi, period >> 8);
// Set AY volume from entry[5]
_ch.volume = volume;
@@ -655,16 +646,13 @@ private:
// Reload limit countdown
_ch.pitchLimitCur = _ch.pitchLimit;
- // period += sign_extend(pitchDelta)
- int32 newPeriod = static_cast<int32>(_ch.period) +
- static_cast<int16>(static_cast<int8>(_ch.pitchDelta));
- if (newPeriod < 0) newPeriod = 0;
- if (newPeriod > 4095) newPeriod = 4095;
- _ch.period = static_cast<uint16>(newPeriod);
+ // period += sign_extend(pitchDelta) with natural 16-bit wrapping
+ // Assembly: add hl,de where de = sign-extended 8-bit delta
+ _ch.period += static_cast<int8>(_ch.pitchDelta);
- // Write period to AY tone registers
+ // Write period to AY tone registers (AY masks coarse to 4 bits)
writeReg(_ch.toneRegLo, _ch.period & 0xFF);
- writeReg(_ch.toneRegHi, (_ch.period >> 8) & 0x0F);
+ writeReg(_ch.toneRegHi, _ch.period >> 8);
// Decrement pitch counter
_ch.pitchCounterCur--;
Commit: 90d82c48692582819d5545a68e94271b8b68c701
https://github.com/scummvm/scummvm/commit/90d82c48692582819d5545a68e94271b8b68c701
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: read driller cpc sound tables from the executable
Changed paths:
engines/freescape/freescape.h
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 4a56826babd..b1e51903c5a 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -498,6 +498,12 @@ public:
void playSoundZX(Common::Array<soundUnitZX> *data, Audio::SoundHandle &handle);
Common::HashMap<uint16, Common::Array<soundUnitZX>*> _soundsSpeakerFxZX;
+
+ void loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int offsetEnvelope, int offsetSoundDef);
+ Common::Array<byte> _soundsCPCToneTable;
+ Common::Array<byte> _soundsCPCEnvelopeTable;
+ Common::Array<byte> _soundsCPCSoundDefTable;
+
int _soundIndexShoot;
int _soundIndexCollide;
int _soundIndexStepDown;
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 8c358322ca2..7c7ac9153e9 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -171,12 +171,31 @@ void DrillerEngine::loadAssetsCPCFullGame() {
if (!file.isOpen())
error("Failed to open DRILL.BIN");
+ loadSoundsCPC(&file, 0x23D2 + 0x80, 0x2416 + 0x80, 0x247E + 0x80);
loadMessagesFixedSize(&file, 0x214c, 14, 20);
loadFonts(&file, 0x5b69);
loadGlobalObjects(&file, 0x1d07, 8);
load8bitBinary(&file, 0x5ccb, 16);
}
+void FreescapeEngine::loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int offsetEnvelope, int offsetSoundDef) {
+ // DRILL.BIN has a 128-byte AMSDOS header, so file offsets = (memory_addr - 0x1C62) + 0x80
+ // Tone table at l4034h: volume envelope data (68 bytes = gap to envelope table)
+ _soundsCPCToneTable.resize(68);
+ file->seek(offsetTone);
+ file->read(_soundsCPCToneTable.data(), 68);
+
+ // Envelope table at l4078h: pitch sweep data (104 bytes = gap to sound def table)
+ _soundsCPCEnvelopeTable.resize(104);
+ file->seek(offsetEnvelope);
+ file->read(_soundsCPCEnvelopeTable.data(), 104);
+
+ // Sound definition table at l40e0h: 20 entries à 7 bytes = 140 bytes
+ _soundsCPCSoundDefTable.resize(140);
+ file->seek(offsetSoundDef);
+ file->read(_soundsCPCSoundDefTable.data(), 140);
+}
+
void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
uint32 color = _currentArea->_underFireBackgroundColor;
uint8 r, g, b;
@@ -265,129 +284,59 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
*
* AY-3-8912 PSG with 1MHz clock, register write at 0x4872:
* Port 0xF4 = register select, Port 0xF6 = data
- */
-
-/**
- * "Tone" Table at l4034h (file offset 0x23D2) - actually controls VOLUME ENVELOPE
*
- * 17 entries, variable-length: byte[0] = triplet count, then (count * 3) bytes of data.
- * Stored as flat 4-byte entries because all verified entries have count <= 4.
+ * ---- Sound Definition Table (l40e0h) ----
+ * 20 entries, 7 bytes each. Loaded by sub_4760h with 1-based sound number.
+ * Byte 0: flags
+ * - Bits 0-1: channel number (1=A, 2=B, 3=C)
+ * - Bit 2: tone disable (0 = enable tone, 1 = disable)
+ * - Bit 3: noise disable (0 = enable noise, 1 = disable)
+ * Byte 1: "tone" table index (volume envelope)
+ * Byte 2: "envelope" table index (pitch sweep)
+ * Bytes 3-4: initial AY tone period (little-endian, 12-bit)
+ * Byte 5: initial AY volume (0-15)
+ * Byte 6: duration (repeat count; 0 = single play)
*
- * Format: {triplet_count, counter, delta, limit}
- * triplet_count: number of {counter, delta, limit} triplets (first one inline)
- * counter: ticks between volume changes (reload value)
- * delta: signed value added to volume each time (masked to 4 bits)
- * limit: how many times counter expires before advancing to next triplet
+ * ---- "Tone" Table (l4034h) - Volume Envelope ----
+ * Despite the name, this table controls VOLUME modulation, not pitch.
+ * Indexed by 4-byte stride: base = index * 4.
+ * Byte 0: number of triplets (N)
+ * Then N triplets of 3 bytes each:
+ * Byte 0: counter - how many times to apply the delta
+ * Byte 1: delta (signed) - added to volume each step
+ * Byte 2: limit - ticks between each application
*
- * Volume update per tick (sub_7571h at l763ah):
- * 1. dec limit_countdown; if != 0, skip
- * 2. reload limit, apply: volume = (volume + delta) & 0x0F
- * 3. dec counter; if != 0, skip
- * 4. advance to next triplet (or set finished flag if all done)
- */
-static const uint8 kToneTable[][4] = {
- {0x01, 0x01, 0x00, 0x01}, // 0
- {0x02, 0x0f, 0x01, 0x03}, // 1
- {0x01, 0xf1, 0x01, 0x00}, // 2
- {0x01, 0x0f, 0xff, 0x18}, // 3
- {0x01, 0x06, 0xfe, 0x3f}, // 4
- {0x01, 0x0f, 0xff, 0x18}, // 5
- {0x02, 0x01, 0x00, 0x06}, // 6
- {0x0f, 0xff, 0x0f, 0x00}, // 7
- {0x04, 0x05, 0xff, 0x0f}, // 8
- {0x01, 0x05, 0x01, 0x01}, // 9
- {0x00, 0x7b, 0x0f, 0xff}, // 10
- {0x04, 0x00, 0x00, 0x00}, // 11
- {0x03, 0x01, 0x0f, 0x01}, // 12
- {0x01, 0xf1, 0x2a, 0x01}, // 13
- {0x0f, 0x18, 0x00, 0x00}, // 14
- {0x02, 0x01, 0x0f, 0x01}, // 15
- {0x01, 0xf1, 0x01, 0x00}, // 16
-};
-
-/**
- * "Envelope" Table at l4078h (file offset 0x2416) - actually controls PITCH SWEEP
+ * Volume update algorithm (sub_7571h, l763ah):
+ * Every tick: decrement limit countdown. When it reaches 0:
+ * 1. Reload limit countdown from current triplet's limit
+ * 2. volume = (volume + delta) & 0x0F
+ * 3. Decrement counter. When it reaches 0:
+ * Advance to next triplet, or set finishedFlag if all done.
*
- * 26 entries, variable-length: byte[0] = triplet count, then (count * 3) bytes of data.
- * Stored as flat 4-byte entries because all verified entries have count <= 5.
+ * ---- "Envelope" Table (l4078h) - Pitch Sweep ----
+ * Despite the name, this table controls PITCH modulation, not envelope.
+ * Indexed by 4-byte stride: base = index * 4.
+ * Byte 0: number of triplets (N)
+ * Then N triplets of 3 bytes each:
+ * Byte 0: counter - how many times to apply the delta
+ * Byte 1: delta (signed) - added to period each step
+ * Byte 2: limit - ticks between each application
*
- * Format: {triplet_count, counter, delta, limit}
- * triplet_count: number of {counter, delta, limit} triplets (first one inline)
- * counter: how many delta applications before advancing to next triplet
- * delta: signed byte added to 16-bit period each time limit expires
- * limit: ticks between pitch changes (reload value)
- *
- * Pitch update per tick (sub_7571h at l758bh):
- * 1. dec limit_countdown; if != 0, skip
- * 2. reload limit, apply: period += sign_extend(delta)
- * 3. write period to AY tone registers
- * 4. dec counter; if != 0, skip
- * 5. advance to next triplet (or check duration if all done)
- */
-static const uint8 kEnvelopeTable[][4] = {
- {0x01, 0x02, 0x00, 0xff}, // 0
- {0x01, 0x10, 0x01, 0x01}, // 1
- {0x01, 0x02, 0x30, 0x10}, // 2
- {0x01, 0x02, 0xd0, 0x10}, // 3
- {0x03, 0x01, 0xe0, 0x06}, // 4
- {0x01, 0x20, 0x06, 0x01}, // 5
- {0xe0, 0x06, 0x00, 0x00}, // 6
- {0x01, 0x02, 0xfb, 0x03}, // 7
- {0x01, 0x02, 0xfd, 0x0c}, // 8
- {0x02, 0x01, 0x04, 0x03}, // 9
- {0x08, 0xf5, 0x03, 0x00}, // 10
- {0x01, 0x10, 0x02, 0x06}, // 11
- {0x01, 0x80, 0x01, 0x03}, // 12
- {0x01, 0x64, 0x01, 0x01}, // 13
- {0x03, 0x01, 0x00, 0x7b}, // 14
- {0x01, 0xcf, 0x01, 0x01}, // 15
- {0x00, 0x96, 0x00, 0x00}, // 16
- {0x05, 0x01, 0x00, 0x4b}, // 17
- {0x01, 0xe9, 0x01, 0x01}, // 18
- {0x00, 0x30, 0x01, 0xe1}, // 19
- {0x01, 0x01, 0x00, 0x96}, // 20
- {0x03, 0x02, 0xf2, 0x15}, // 21
- {0x05, 0x60, 0x01, 0x02}, // 22
- {0x00, 0x40, 0x00, 0x00}, // 23
- {0x01, 0x01, 0x00, 0x10}, // 24
- {0x01, 0x01, 0x00, 0x0f}, // 25
-};
-
-/**
- * Sound Definition Table at l40e0h (file offset 0x247E)
- * 7 bytes per entry: {flags, tone_idx, env_idx, period_lo, period_hi, volume, duration}
- *
- * Flags:
- * Bits 0-1: Channel (1=A, 2=B, 3=C)
- * Bit 2: If set, DISABLE tone
- * Bit 3: If set, DISABLE noise
+ * Pitch update algorithm (sub_7571h, l758bh):
+ * Every tick: decrement limit countdown. When it reaches 0:
+ * 1. Reload limit countdown from current triplet's limit
+ * 2. period += sign_extend(delta) (natural 16-bit wrapping)
+ * 3. Decrement counter. When it reaches 0:
+ * Advance to next triplet, or if all done:
+ * - Decrement duration. If 0 -> shutdown (silence + deactivate).
+ * - If duration > 0 -> restart BOTH volume and pitch from beginning.
*/
-static const uint8 kSoundDefTable[][7] = {
- {0x02, 0x00, 0x0d, 0x00, 0x00, 0x0f, 0x01}, // 1: ch=2 T+N env=13 per=0 vol=15 dur=1
- {0x03, 0x00, 0x01, 0x00, 0x00, 0x0f, 0x01}, // 2: ch=3 T+N env=1 per=0 vol=15 dur=1
- {0x09, 0x00, 0x02, 0x20, 0x01, 0x0f, 0x01}, // 3: ch=1 T env=2 per=288 vol=15 dur=1
- {0x09, 0x00, 0x03, 0x20, 0x01, 0x0f, 0x01}, // 4: Step Down - ch=1 T env=3 per=288
- {0x05, 0x01, 0x00, 0x00, 0x01, 0x00, 0x01}, // 5: ch=1 N tone=1 env=0 per=256 vol=0
- {0x09, 0x03, 0x00, 0x27, 0x00, 0x0f, 0x01}, // 6: ch=1 T tone=3 env=0 per=39 vol=15
- {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // 7: ch=1 N tone=4 env=0 per=0 vol=1
- {0x09, 0x00, 0x04, 0x20, 0x01, 0x0f, 0x08}, // 8: ch=1 T env=4 per=288 vol=15 dur=8
- {0x09, 0x00, 0x07, 0x00, 0x01, 0x0f, 0x18}, // 9: Fallen - ch=1 T env=7 per=256 dur=24
- {0x01, 0x00, 0x08, 0x00, 0x01, 0x0f, 0x02}, // 10: Area Change - ch=1 T+N env=8 per=256 dur=2
- {0x01, 0x05, 0x09, 0x10, 0x00, 0x0f, 0x0d}, // 11: ch=1 T+N tone=5 env=9 per=16 dur=13
- {0x09, 0x00, 0x0b, 0x70, 0x00, 0x0f, 0x01}, // 12: ch=1 T env=11 per=112 vol=15
- {0x05, 0x06, 0x00, 0x00, 0x00, 0x0f, 0x01}, // 13: Mission Complete - ch=1 N tone=6 env=0
- {0x09, 0x00, 0x0c, 0x50, 0x00, 0x0f, 0x01}, // 14: ch=1 T env=12 per=80 vol=15
- {0x09, 0x08, 0x0e, 0x77, 0x00, 0x0f, 0x01}, // 15: ch=1 T tone=8 env=14 per=119
- {0x0a, 0x08, 0x11, 0x6a, 0x00, 0x0f, 0x01}, // 16: ch=2 T tone=8 env=17 per=106
- {0x09, 0x0c, 0x15, 0xbc, 0x03, 0x00, 0x01}, // 17: ch=1 T tone=12 env=21 per=956
- {0x06, 0x0f, 0x18, 0x00, 0x00, 0x00, 0x01}, // 18: ch=2 N tone=15 env=24
- {0x09, 0x00, 0x19, 0xf6, 0x02, 0x0f, 0x01}, // 19: ch=1 T env=25 per=758
- {0x05, 0x04, 0x00, 0x00, 0x00, 0x01, 0x01}, // 20: ch=1 N tone=4 env=0
-};
class DrillerCPCSfxStream : public Audio::AudioStream {
public:
- DrillerCPCSfxStream(int index, int rate = 44100) : _ay(rate, 1000000), _rate(rate) {
+ DrillerCPCSfxStream(int index, const byte *soundDefTable, const byte *toneTable, const byte *envelopeTable, int rate = 44100)
+ : _ay(rate, 1000000), _rate(rate),
+ _soundDefTable(soundDefTable), _toneTable(toneTable), _envelopeTable(envelopeTable) {
_finished = false;
_tickSampleCount = 0;
@@ -443,6 +392,11 @@ private:
bool _finished;
int _tickSampleCount; // Samples generated in current tick
+ // Pointers to table data loaded from DRILL.BIN (owned by FreescapeEngine)
+ const byte *_soundDefTable; // 20 entries à 7 bytes at l40e0h
+ const byte *_toneTable; // Volume envelope data at l4034h
+ const byte *_envelopeTable; // Pitch sweep data at l4078h
+
/**
* Channel state - mirrors the 23-byte per-channel structure at l416dh
* as populated by sub_4760h and updated by sub_7571h.
@@ -521,7 +475,7 @@ private:
return;
}
- const uint8 *entry = kSoundDefTable[soundNum - 1];
+ const byte *entry = &_soundDefTable[(soundNum - 1) * 7];
uint8 flags = entry[0];
uint8 toneIdx = entry[1];
uint8 envIdx = entry[2];
@@ -565,34 +519,32 @@ private:
_ch.duration = duration;
// Load volume envelope from "tone" table (l4034h)
- // Table format: byte[0]=triplet_count, then triplets of {counter, delta, limit}
- const uint8 *toneRaw = &kToneTable[0][0];
+ // Assembly: index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
int toneBase = toneIdx * 4;
- _ch.volTripletTotal = toneRaw[toneBase];
+ _ch.volTripletTotal = _toneTable[toneBase];
_ch.volCurrentStep = 0;
_ch.volToneIdx = toneIdx;
// Load first volume triplet
int volOff = toneBase + 1;
- _ch.volCounter = toneRaw[volOff];
- _ch.volDelta = static_cast<int8>(toneRaw[volOff + 1]);
- _ch.volLimit = toneRaw[volOff + 2];
+ _ch.volCounter = _toneTable[volOff];
+ _ch.volDelta = static_cast<int8>(_toneTable[volOff + 1]);
+ _ch.volLimit = _toneTable[volOff + 2];
_ch.volCounterCur = _ch.volCounter;
_ch.volLimitCur = _ch.volLimit;
// Load pitch sweep from "envelope" table (l4078h)
- // Table format: byte[0]=triplet_count, then triplets of {counter, delta, limit}
- const uint8 *envRaw = &kEnvelopeTable[0][0];
+ // Assembly: index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
int envBase = envIdx * 4;
- _ch.pitchTripletTotal = envRaw[envBase];
+ _ch.pitchTripletTotal = _envelopeTable[envBase];
_ch.pitchCurrentStep = 0;
_ch.pitchEnvIdx = envIdx;
// Load first pitch triplet
int pitchOff = envBase + 1;
- _ch.pitchCounter = envRaw[pitchOff];
- _ch.pitchDelta = static_cast<int8>(envRaw[pitchOff + 1]);
- _ch.pitchLimit = envRaw[pitchOff + 2];
+ _ch.pitchCounter = _envelopeTable[pitchOff];
+ _ch.pitchDelta = static_cast<int8>(_envelopeTable[pitchOff + 1]);
+ _ch.pitchLimit = _envelopeTable[pitchOff + 2];
_ch.pitchCounterCur = _ch.pitchCounter;
_ch.pitchLimitCur = _ch.pitchLimit;
@@ -637,8 +589,8 @@ private:
return;
}
- const uint8 *toneRaw = &kToneTable[0][0];
- const uint8 *envRaw = &kEnvelopeTable[0][0];
+ const byte *toneRaw = _toneTable;
+ const byte *envRaw = _envelopeTable;
// === PITCH UPDATE (l758bh) ===
_ch.pitchLimitCur--;
@@ -745,7 +697,8 @@ void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle)
// DO NOT CHANGE: This debug line is used to track sound usage in Driller CPC
debug("Playing Driller CPC sound %d", index);
// Create a new stream for the sound
- DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index);
+ DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index,
+ _soundsCPCSoundDefTable.data(), _soundsCPCToneTable.data(), _soundsCPCEnvelopeTable.data());
_mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
}
Commit: 89bdf28c3543afed984815503cbe676fe9a16665
https://github.com/scummvm/scummvm/commit/89bdf28c3543afed984815503cbe676fe9a16665
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: fixes in sound handling
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 7c7ac9153e9..4cf8dba2dea 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -406,39 +406,39 @@ private:
*/
struct ChannelState {
// Volume modulation (from "tone" table)
- uint8 volCounter; // ix+000h: initial counter value
- int8 volDelta; // ix+001h: signed delta added to volume
- uint8 volLimit; // ix+002h: initial limit value
- uint8 volCounterCur; // ix+003h: current counter (decremented)
- uint8 volLimitCur; // ix+004h: current limit countdown
- uint8 volume; // ix+005h: current AY volume (0-15)
- uint8 volTripletTotal; // ix+006h: total number of volume triplets
- uint8 volCurrentStep; // ix+007h: current triplet index
- uint8 duration; // ix+008h: repeat count
- uint8 volToneIdx; // tone table index (to recompute data pointer)
+ byte volCounter; // ix+000h: initial counter value
+ int8 volDelta; // ix+001h: signed delta added to volume
+ byte volLimit; // ix+002h: initial limit value
+ byte volCounterCur; // ix+003h: current counter (decremented)
+ byte volLimitCur; // ix+004h: current limit countdown
+ byte volume; // ix+005h: current AY volume (0-15)
+ byte volTripletTotal; // ix+006h: total number of volume triplets
+ byte volCurrentStep; // ix+007h: current triplet index
+ byte duration; // ix+008h: repeat count
+ byte volToneIdx; // tone table index (to recompute data pointer)
// Pitch modulation (from "envelope" table)
- uint8 pitchCounter; // ix+00Bh: initial counter value
- int8 pitchDelta; // ix+00Ch: signed delta added to period
- uint8 pitchLimit; // ix+00Dh: initial limit value
- uint8 pitchCounterCur; // ix+00Eh: current counter (decremented)
- uint8 pitchLimitCur; // ix+00Fh: current limit countdown
- uint16 period; // ix+010h-011h: current 16-bit AY tone period
- uint8 pitchTripletTotal; // ix+012h: total number of pitch triplets
- uint8 pitchCurrentStep; // ix+013h: current triplet index
- uint8 pitchEnvIdx; // envelope table index (to recompute data pointer)
-
- uint8 finishedFlag; // ix+016h: set when volume envelope exhausted
+ byte pitchCounter; // ix+00Bh: initial counter value
+ int8 pitchDelta; // ix+00Ch: signed delta added to period
+ byte pitchLimit; // ix+00Dh: initial limit value
+ byte pitchCounterCur; // ix+00Eh: current counter (decremented)
+ byte pitchLimitCur; // ix+00Fh: current limit countdown
+ uint16 period; // ix+010h-011h: current 16-bit AY tone period
+ byte pitchTripletTotal; // ix+012h: total number of pitch triplets
+ byte pitchCurrentStep; // ix+013h: current triplet index
+ byte pitchEnvIdx; // envelope table index (to recompute data pointer)
+
+ byte finishedFlag; // ix+016h: set when volume envelope exhausted
// AY register mapping for this channel
- uint8 channelNum; // 1=A, 2=B, 3=C
- uint8 toneRegLo; // AY register for tone fine
- uint8 toneRegHi; // AY register for tone coarse
- uint8 volReg; // AY register for volume
+ byte channelNum; // 1=A, 2=B, 3=C
+ byte toneRegLo; // AY register for tone fine
+ byte toneRegHi; // AY register for tone coarse
+ byte volReg; // AY register for volume
bool active; // Channel is producing sound
} _ch;
- void writeReg(int reg, uint8 val) {
+ void writeReg(int reg, byte val) {
_ay.setReg(reg, val);
}
@@ -476,15 +476,15 @@ private:
}
const byte *entry = &_soundDefTable[(soundNum - 1) * 7];
- uint8 flags = entry[0];
- uint8 toneIdx = entry[1];
- uint8 envIdx = entry[2];
+ byte flags = entry[0];
+ byte toneIdx = entry[1];
+ byte envIdx = entry[2];
uint16 period = entry[3] | (entry[4] << 8);
- uint8 volume = entry[5];
- uint8 duration = entry[6];
+ byte volume = entry[5];
+ byte duration = entry[6];
// Channel number (1-based): 1=A, 2=B, 3=C
- uint8 channelNum = flags & 0x03;
+ byte channelNum = flags & 0x03;
if (channelNum < 1 || channelNum > 3) {
_finished = true;
return;
@@ -499,7 +499,7 @@ private:
// Configure mixer (register 7)
// Start with all disabled (0xFF), selectively enable per flags
// Bit 2 set in flags = DISABLE tone, Bit 3 set = DISABLE noise
- uint8 mixer = 0xFF;
+ byte mixer = 0xFF;
if (!(flags & 0x04))
mixer &= ~(1 << (channelNum - 1)); // Enable tone
if (!(flags & 0x08))
@@ -551,11 +551,11 @@ private:
_ch.finishedFlag = 0;
_ch.active = true;
- debug("sub_4760h: sound %d ch=%d mixer=0x%02x period=%d vol=%d dur=%d tone[%d] env[%d]",
+ debugC(1, kFreescapeDebugMedia, "sub_4760h: sound %d ch=%d mixer=0x%02x period=%d vol=%d dur=%d tone[%d] env[%d]",
soundNum, channelNum, mixer, period, volume, duration, toneIdx, envIdx);
- debug(" vol envelope: triplets=%d counter=%d delta=%d limit=%d",
+ debugC(1, kFreescapeDebugMedia, " vol envelope: triplets=%d counter=%d delta=%d limit=%d",
_ch.volTripletTotal, _ch.volCounter, _ch.volDelta, _ch.volLimit);
- debug(" pitch sweep: triplets=%d counter=%d delta=%d limit=%d",
+ debugC(1, kFreescapeDebugMedia, " pitch sweep: triplets=%d counter=%d delta=%d limit=%d",
_ch.pitchTripletTotal, _ch.pitchCounter, _ch.pitchDelta, _ch.pitchLimit);
}
@@ -694,8 +694,7 @@ private:
};
void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
- // DO NOT CHANGE: This debug line is used to track sound usage in Driller CPC
- debug("Playing Driller CPC sound %d", index);
+ debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
// Create a new stream for the sound
DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index,
_soundsCPCSoundDefTable.data(), _soundsCPCToneTable.data(), _soundsCPCEnvelopeTable.data());
Commit: d2e61f0f4268f454925c2b4aad15b44c66748289
https://github.com/scummvm/scummvm/commit/d2e61f0f4268f454925c2b4aad15b44c66748289
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: improve the sound mapping for Driller CPC
Changed paths:
engines/freescape/games/driller/cpc.cpp
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index 4cf8dba2dea..cbc41426745 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -33,15 +33,24 @@ namespace Freescape {
void DrillerEngine::initCPC() {
_viewArea = Common::Rect(36, 16, 284, 117);
- _soundIndexShoot = 1;
- _soundIndexCollide = 2;
- _soundIndexStepUp = 3;
- _soundIndexStepDown = 4;
- _soundIndexMenu = 6;
- _soundIndexAreaChange = 10;
- _soundIndexHit = 7;
- _soundIndexFallen = 9;
- _soundIndexMissionComplete = 13;
+ // Sound mappings from DRILL.BIN disassembly (sub_4760h call sites)
+ _soundIndexShoot = 1; // 0x5BBC: LD A,01h; CALL 4760h
+ _soundIndexCollide = 19; // 0x4CE6/0x5AE4: LD A,13h; CALL 4760h
+ _soundIndexStepUp = 3; // 0x5025: deferred via (3B63h)
+ _soundIndexStepDown = 4; // 0x4FB2: deferred via (3B63h)
+ _soundIndexFall = 9; // long dur=24 falling sound
+ _soundIndexStart = 6; // 0x4906/0x4C84: game start transition
+ _soundIndexMenu = 6; // reuse start/transition sound
+ _soundIndexAreaChange = 10; // TODO: verify this
+ _soundIndexHit = 2; // 0x6801: LD A,02h; drill/hit destruction
+ _soundIndexFallen = 9; // long dur=24 tone sweep
+ _soundIndexNoShield = 9; // game-over conditions reuse fallen sound
+ _soundIndexNoEnergy = 9;
+ _soundIndexTimeout = 9;
+ _soundIndexForceEndGame = 9;
+ _soundIndexCrushed = 9;
+ _soundIndexMissionComplete = 13; // via object handler at 0x61E4
+ // Sound 5 is used when deploying or recalling the drill, but these are not currently implemented yet
}
byte kCPCPaletteTitleData[4][3] = {
Commit: 91075a7566ff4f2ae4c24ab923241b8156c99aaa
https://github.com/scummvm/scummvm/commit/91075a7566ff4f2ae4c24ab923241b8156c99aaa
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: implement sound for Dark Side CPC
Changed paths:
engines/freescape/freescape.h
engines/freescape/games/dark/cpc.cpp
engines/freescape/games/driller/cpc.cpp
engines/freescape/sound.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index b1e51903c5a..42b89a947a8 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -488,7 +488,7 @@ public:
void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
void playSoundDrillerZX(int index, Audio::SoundHandle &handle);
- void playSoundDrillerCPC(int index, Audio::SoundHandle &handle);
+ void playSoundCPC(int index, Audio::SoundHandle &handle);
virtual void playSoundFx(int index, bool sync);
virtual void loadSoundsFx(Common::SeekableReadStream *file, int offset, int number);
Common::HashMap<uint16, soundFx *> _soundsFx;
@@ -499,7 +499,7 @@ public:
void playSoundZX(Common::Array<soundUnitZX> *data, Audio::SoundHandle &handle);
Common::HashMap<uint16, Common::Array<soundUnitZX>*> _soundsSpeakerFxZX;
- void loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int offsetEnvelope, int offsetSoundDef);
+ void loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int sizeTone, int offsetEnvelope, int sizeEnvelope, int offsetSoundDef, int sizeSoundDef);
Common::Array<byte> _soundsCPCToneTable;
Common::Array<byte> _soundsCPCEnvelopeTable;
Common::Array<byte> _soundsCPCSoundDefTable;
diff --git a/engines/freescape/games/dark/cpc.cpp b/engines/freescape/games/dark/cpc.cpp
index 8cae0c940c0..5df3839a54e 100644
--- a/engines/freescape/games/dark/cpc.cpp
+++ b/engines/freescape/games/dark/cpc.cpp
@@ -30,11 +30,12 @@ namespace Freescape {
void DarkEngine::initCPC() {
_viewArea = Common::Rect(36, 24, 284, 125);
- _soundIndexShoot = 0xa;
- _soundIndexStart = 0x17;
- _soundIndexAreaChange = 0x1c;
- _soundIndexDestroyECD = 0x1b;
- _soundIndexRestoreECD = 8;
+ // Sound mappings from DARKCODE.BIN disassembly (sub_7409h call sites)
+ // _soundIndexShoot = 1 inherited from constructor (0x61BF: LD A,01h; CALL 7409h)
+ _soundIndexStart = 23; // 0x2F68: game start transition
+ _soundIndexAreaChange = 28; // 0x6802: deferred via (1FF2h), area transition portal
+ _soundIndexDestroyECD = 27; // ECD destruction
+ _soundIndexRestoreECD = 8; // 0x7A77: deferred via (1FF2h), encounter/objective
}
extern byte kCPCPaletteTitleData[4][3];
@@ -89,6 +90,7 @@ void DarkEngine::loadAssetsCPCFullGame() {
loadFonts(&file, 0x60f3);
loadGlobalObjects(&file, 0x9a, 23);
load8bitBinary(&file, 0x6255, 16);
+ loadSoundsCPC(&file, 0x09B7, 160, 0x0A57, 284, 0x0B73, 203);
_indicators.push_back(loadBundledImage("dark_fallen_indicator"));
_indicators.push_back(loadBundledImage("dark_crouch_indicator"));
_indicators.push_back(loadBundledImage("dark_walk_indicator"));
diff --git a/engines/freescape/games/driller/cpc.cpp b/engines/freescape/games/driller/cpc.cpp
index cbc41426745..db9b895c1a3 100644
--- a/engines/freescape/games/driller/cpc.cpp
+++ b/engines/freescape/games/driller/cpc.cpp
@@ -26,9 +26,6 @@
#include "freescape/games/driller/driller.h"
#include "freescape/language/8bitDetokeniser.h"
-#include "audio/audiostream.h"
-#include "audio/softsynth/ay8912.h"
-
namespace Freescape {
void DrillerEngine::initCPC() {
@@ -180,29 +177,25 @@ void DrillerEngine::loadAssetsCPCFullGame() {
if (!file.isOpen())
error("Failed to open DRILL.BIN");
- loadSoundsCPC(&file, 0x23D2 + 0x80, 0x2416 + 0x80, 0x247E + 0x80);
+ loadSoundsCPC(&file, 0x23D2 + 0x80, 68, 0x2416 + 0x80, 104, 0x247E + 0x80, 140);
loadMessagesFixedSize(&file, 0x214c, 14, 20);
loadFonts(&file, 0x5b69);
loadGlobalObjects(&file, 0x1d07, 8);
load8bitBinary(&file, 0x5ccb, 16);
}
-void FreescapeEngine::loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int offsetEnvelope, int offsetSoundDef) {
- // DRILL.BIN has a 128-byte AMSDOS header, so file offsets = (memory_addr - 0x1C62) + 0x80
- // Tone table at l4034h: volume envelope data (68 bytes = gap to envelope table)
- _soundsCPCToneTable.resize(68);
+void FreescapeEngine::loadSoundsCPC(Common::SeekableReadStream *file, int offsetTone, int sizeTone, int offsetEnvelope, int sizeEnvelope, int offsetSoundDef, int sizeSoundDef) {
+ _soundsCPCToneTable.resize(sizeTone);
file->seek(offsetTone);
- file->read(_soundsCPCToneTable.data(), 68);
+ file->read(_soundsCPCToneTable.data(), sizeTone);
- // Envelope table at l4078h: pitch sweep data (104 bytes = gap to sound def table)
- _soundsCPCEnvelopeTable.resize(104);
+ _soundsCPCEnvelopeTable.resize(sizeEnvelope);
file->seek(offsetEnvelope);
- file->read(_soundsCPCEnvelopeTable.data(), 104);
+ file->read(_soundsCPCEnvelopeTable.data(), sizeEnvelope);
- // Sound definition table at l40e0h: 20 entries à 7 bytes = 140 bytes
- _soundsCPCSoundDefTable.resize(140);
+ _soundsCPCSoundDefTable.resize(sizeSoundDef);
file->seek(offsetSoundDef);
- file->read(_soundsCPCSoundDefTable.data(), 140);
+ file->read(_soundsCPCSoundDefTable.data(), sizeSoundDef);
}
void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
@@ -280,434 +273,4 @@ void DrillerEngine::drawCPCUI(Graphics::Surface *surface) {
drawCompass(surface, 230, 156, _pitch - 30, 10, 60, front);
}
-/**
- * Driller CPC Sound Implementation
- *
- * Based on reverse engineering of DRILL.BIN (loads at 0x1c62).
- *
- * All sounds use the sub_4760h system (0x4760-0x4871):
- * - Sound initialization loads 7-byte entry from l40e0h
- * - Volume envelope from "Tone" Table at l4034h
- * - Pitch sweep from "Envelope" Table at l4078h
- * - 300Hz interrupt-driven update via sub_7571h (0x7571-0x76A9)
- *
- * AY-3-8912 PSG with 1MHz clock, register write at 0x4872:
- * Port 0xF4 = register select, Port 0xF6 = data
- *
- * ---- Sound Definition Table (l40e0h) ----
- * 20 entries, 7 bytes each. Loaded by sub_4760h with 1-based sound number.
- * Byte 0: flags
- * - Bits 0-1: channel number (1=A, 2=B, 3=C)
- * - Bit 2: tone disable (0 = enable tone, 1 = disable)
- * - Bit 3: noise disable (0 = enable noise, 1 = disable)
- * Byte 1: "tone" table index (volume envelope)
- * Byte 2: "envelope" table index (pitch sweep)
- * Bytes 3-4: initial AY tone period (little-endian, 12-bit)
- * Byte 5: initial AY volume (0-15)
- * Byte 6: duration (repeat count; 0 = single play)
- *
- * ---- "Tone" Table (l4034h) - Volume Envelope ----
- * Despite the name, this table controls VOLUME modulation, not pitch.
- * Indexed by 4-byte stride: base = index * 4.
- * Byte 0: number of triplets (N)
- * Then N triplets of 3 bytes each:
- * Byte 0: counter - how many times to apply the delta
- * Byte 1: delta (signed) - added to volume each step
- * Byte 2: limit - ticks between each application
- *
- * Volume update algorithm (sub_7571h, l763ah):
- * Every tick: decrement limit countdown. When it reaches 0:
- * 1. Reload limit countdown from current triplet's limit
- * 2. volume = (volume + delta) & 0x0F
- * 3. Decrement counter. When it reaches 0:
- * Advance to next triplet, or set finishedFlag if all done.
- *
- * ---- "Envelope" Table (l4078h) - Pitch Sweep ----
- * Despite the name, this table controls PITCH modulation, not envelope.
- * Indexed by 4-byte stride: base = index * 4.
- * Byte 0: number of triplets (N)
- * Then N triplets of 3 bytes each:
- * Byte 0: counter - how many times to apply the delta
- * Byte 1: delta (signed) - added to period each step
- * Byte 2: limit - ticks between each application
- *
- * Pitch update algorithm (sub_7571h, l758bh):
- * Every tick: decrement limit countdown. When it reaches 0:
- * 1. Reload limit countdown from current triplet's limit
- * 2. period += sign_extend(delta) (natural 16-bit wrapping)
- * 3. Decrement counter. When it reaches 0:
- * Advance to next triplet, or if all done:
- * - Decrement duration. If 0 -> shutdown (silence + deactivate).
- * - If duration > 0 -> restart BOTH volume and pitch from beginning.
- */
-
-class DrillerCPCSfxStream : public Audio::AudioStream {
-public:
- DrillerCPCSfxStream(int index, const byte *soundDefTable, const byte *toneTable, const byte *envelopeTable, int rate = 44100)
- : _ay(rate, 1000000), _rate(rate),
- _soundDefTable(soundDefTable), _toneTable(toneTable), _envelopeTable(envelopeTable) {
- _finished = false;
- _tickSampleCount = 0;
-
- // Reset all AY registers to match CPC init state
- for (int r = 0; r < 14; r++)
- _ay.setReg(r, 0);
- // Noise period from CPC init table at 0x66A4h (verified in binary)
- _ay.setReg(6, 0x07);
-
- memset(&_ch, 0, sizeof(_ch));
- setupSound(index);
- }
-
- int readBuffer(int16 *buffer, const int numSamples) override {
- if (_finished)
- return 0;
-
- int samplesGenerated = 0;
- // AY8912Stream is stereo: readBuffer counts int16 values (2 per frame).
- // CPC interrupts fire at 300Hz (6 per frame). sub_7571h is called
- // unconditionally at every interrupt (0x68DD), NOT inside the 50Hz divider.
- int samplesPerTick = (_rate / 300) * 2;
-
- while (samplesGenerated < numSamples && !_finished) {
- // Generate samples until next tick
- int remaining = samplesPerTick - _tickSampleCount;
- int toGenerate = MIN(numSamples - samplesGenerated, remaining);
-
- if (toGenerate > 0) {
- _ay.readBuffer(buffer + samplesGenerated, toGenerate);
- samplesGenerated += toGenerate;
- _tickSampleCount += toGenerate;
- }
-
- // Run interrupt handler at 300Hz tick boundary
- if (_tickSampleCount >= samplesPerTick) {
- _tickSampleCount -= samplesPerTick;
- tickUpdate();
- }
- }
-
- return samplesGenerated;
- }
-
- bool isStereo() const override { return true; }
- bool endOfData() const override { return _finished; }
- bool endOfStream() const override { return _finished; }
- int getRate() const override { return _rate; }
-
-private:
- Audio::AY8912Stream _ay;
- int _rate;
- bool _finished;
- int _tickSampleCount; // Samples generated in current tick
-
- // Pointers to table data loaded from DRILL.BIN (owned by FreescapeEngine)
- const byte *_soundDefTable; // 20 entries à 7 bytes at l40e0h
- const byte *_toneTable; // Volume envelope data at l4034h
- const byte *_envelopeTable; // Pitch sweep data at l4078h
-
- /**
- * Channel state - mirrors the 23-byte per-channel structure at l416dh
- * as populated by sub_4760h and updated by sub_7571h.
- *
- * "vol" fields come from the "tone" table (l4034h) - controls volume envelope
- * "pitch" fields come from the "envelope" table (l4078h) - controls pitch sweep
- */
- struct ChannelState {
- // Volume modulation (from "tone" table)
- byte volCounter; // ix+000h: initial counter value
- int8 volDelta; // ix+001h: signed delta added to volume
- byte volLimit; // ix+002h: initial limit value
- byte volCounterCur; // ix+003h: current counter (decremented)
- byte volLimitCur; // ix+004h: current limit countdown
- byte volume; // ix+005h: current AY volume (0-15)
- byte volTripletTotal; // ix+006h: total number of volume triplets
- byte volCurrentStep; // ix+007h: current triplet index
- byte duration; // ix+008h: repeat count
- byte volToneIdx; // tone table index (to recompute data pointer)
-
- // Pitch modulation (from "envelope" table)
- byte pitchCounter; // ix+00Bh: initial counter value
- int8 pitchDelta; // ix+00Ch: signed delta added to period
- byte pitchLimit; // ix+00Dh: initial limit value
- byte pitchCounterCur; // ix+00Eh: current counter (decremented)
- byte pitchLimitCur; // ix+00Fh: current limit countdown
- uint16 period; // ix+010h-011h: current 16-bit AY tone period
- byte pitchTripletTotal; // ix+012h: total number of pitch triplets
- byte pitchCurrentStep; // ix+013h: current triplet index
- byte pitchEnvIdx; // envelope table index (to recompute data pointer)
-
- byte finishedFlag; // ix+016h: set when volume envelope exhausted
-
- // AY register mapping for this channel
- byte channelNum; // 1=A, 2=B, 3=C
- byte toneRegLo; // AY register for tone fine
- byte toneRegHi; // AY register for tone coarse
- byte volReg; // AY register for volume
- bool active; // Channel is producing sound
- } _ch;
-
- void writeReg(int reg, byte val) {
- _ay.setReg(reg, val);
- }
-
- /**
- * Route all sounds through the sub_4760h system.
- *
- * In the original game, sub_4760h is called with a 1-based sound number.
- * It loads a 7-byte entry from l40e0h and configures AY registers.
- */
- void setupSound(int index) {
- if (index >= 1 && index <= 20) {
- setupSub4760h(index);
- } else {
- _finished = true;
- }
- }
-
- /**
- * Implements sub_4760h (0x4760-0x4871) - sound initialization.
- *
- * Assembly flow:
- * 1. entry = l40e0h[(soundNum-1) * 7]
- * 2. channel = flags & 0x03 (1=A, 2=B, 3=C)
- * 3. Configure mixer from flags bits 2-3
- * 4. Set AY tone period from entry[3-4]
- * 5. Set AY volume from entry[5]
- * 6. Load "tone" table (l4034h) -> volume envelope fields
- * 7. Load "envelope" table (l4078h) -> pitch sweep fields
- * 8. Set duration from entry[6]
- */
- void setupSub4760h(int soundNum) {
- if (soundNum < 1 || soundNum > 20) {
- _finished = true;
- return;
- }
-
- const byte *entry = &_soundDefTable[(soundNum - 1) * 7];
- byte flags = entry[0];
- byte toneIdx = entry[1];
- byte envIdx = entry[2];
- uint16 period = entry[3] | (entry[4] << 8);
- byte volume = entry[5];
- byte duration = entry[6];
-
- // Channel number (1-based): 1=A, 2=B, 3=C
- byte channelNum = flags & 0x03;
- if (channelNum < 1 || channelNum > 3) {
- _finished = true;
- return;
- }
-
- // AY register mapping
- _ch.channelNum = channelNum;
- _ch.toneRegLo = (channelNum - 1) * 2; // A=0, B=2, C=4
- _ch.toneRegHi = (channelNum - 1) * 2 + 1; // A=1, B=3, C=5
- _ch.volReg = channelNum + 7; // A=8, B=9, C=10
-
- // Configure mixer (register 7)
- // Start with all disabled (0xFF), selectively enable per flags
- // Bit 2 set in flags = DISABLE tone, Bit 3 set = DISABLE noise
- byte mixer = 0xFF;
- if (!(flags & 0x04))
- mixer &= ~(1 << (channelNum - 1)); // Enable tone
- if (!(flags & 0x08))
- mixer &= ~(1 << (channelNum - 1 + 3)); // Enable noise
- writeReg(7, mixer);
-
- // Set AY tone period from entry[3-4]
- _ch.period = period;
- writeReg(_ch.toneRegLo, period & 0xFF);
- writeReg(_ch.toneRegHi, period >> 8);
-
- // Set AY volume from entry[5]
- _ch.volume = volume;
- writeReg(_ch.volReg, volume);
-
- // Duration from entry[6]
- _ch.duration = duration;
-
- // Load volume envelope from "tone" table (l4034h)
- // Assembly: index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
- int toneBase = toneIdx * 4;
- _ch.volTripletTotal = _toneTable[toneBase];
- _ch.volCurrentStep = 0;
- _ch.volToneIdx = toneIdx;
-
- // Load first volume triplet
- int volOff = toneBase + 1;
- _ch.volCounter = _toneTable[volOff];
- _ch.volDelta = static_cast<int8>(_toneTable[volOff + 1]);
- _ch.volLimit = _toneTable[volOff + 2];
- _ch.volCounterCur = _ch.volCounter;
- _ch.volLimitCur = _ch.volLimit;
-
- // Load pitch sweep from "envelope" table (l4078h)
- // Assembly: index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
- int envBase = envIdx * 4;
- _ch.pitchTripletTotal = _envelopeTable[envBase];
- _ch.pitchCurrentStep = 0;
- _ch.pitchEnvIdx = envIdx;
-
- // Load first pitch triplet
- int pitchOff = envBase + 1;
- _ch.pitchCounter = _envelopeTable[pitchOff];
- _ch.pitchDelta = static_cast<int8>(_envelopeTable[pitchOff + 1]);
- _ch.pitchLimit = _envelopeTable[pitchOff + 2];
- _ch.pitchCounterCur = _ch.pitchCounter;
- _ch.pitchLimitCur = _ch.pitchLimit;
-
- _ch.finishedFlag = 0;
- _ch.active = true;
-
- debugC(1, kFreescapeDebugMedia, "sub_4760h: sound %d ch=%d mixer=0x%02x period=%d vol=%d dur=%d tone[%d] env[%d]",
- soundNum, channelNum, mixer, period, volume, duration, toneIdx, envIdx);
- debugC(1, kFreescapeDebugMedia, " vol envelope: triplets=%d counter=%d delta=%d limit=%d",
- _ch.volTripletTotal, _ch.volCounter, _ch.volDelta, _ch.volLimit);
- debugC(1, kFreescapeDebugMedia, " pitch sweep: triplets=%d counter=%d delta=%d limit=%d",
- _ch.pitchTripletTotal, _ch.pitchCounter, _ch.pitchDelta, _ch.pitchLimit);
- }
-
- /**
- * Implements sub_7571h (0x7571-0x76A9) - 300Hz interrupt-driven update.
- *
- * Called at 300Hz (every CPC interrupt). Updates pitch first, then volume.
- *
- * PITCH UPDATE (l758bh):
- * 1. dec pitchLimitCur; if != 0, skip to volume
- * 2. reload pitchLimitCur from pitchLimit
- * 3. period += sign_extend(pitchDelta); write to AY tone regs
- * 4. dec pitchCounterCur; if != 0, skip to volume
- * 5. advance pitch triplet; if all done -> dec duration; if 0 -> shutdown
- *
- * VOLUME UPDATE (l763ah):
- * 1. if finishedFlag set, skip entirely
- * 2. dec volLimitCur; if != 0, skip
- * 3. reload volLimitCur from volLimit
- * 4. volume = (volume + volDelta) & 0x0F; write to AY vol reg
- * 5. dec volCounterCur; if != 0, skip
- * 6. advance vol triplet; if all done -> set finishedFlag
- *
- * SHUTDOWN (l761eh):
- * - Write volume 0 to AY
- * - Mark channel inactive
- */
- void tickUpdate() {
- if (!_ch.active) {
- _finished = true;
- return;
- }
-
- const byte *toneRaw = _toneTable;
- const byte *envRaw = _envelopeTable;
-
- // === PITCH UPDATE (l758bh) ===
- _ch.pitchLimitCur--;
- if (_ch.pitchLimitCur == 0) {
- // Reload limit countdown
- _ch.pitchLimitCur = _ch.pitchLimit;
-
- // period += sign_extend(pitchDelta) with natural 16-bit wrapping
- // Assembly: add hl,de where de = sign-extended 8-bit delta
- _ch.period += static_cast<int8>(_ch.pitchDelta);
-
- // Write period to AY tone registers (AY masks coarse to 4 bits)
- writeReg(_ch.toneRegLo, _ch.period & 0xFF);
- writeReg(_ch.toneRegHi, _ch.period >> 8);
-
- // Decrement pitch counter
- _ch.pitchCounterCur--;
- if (_ch.pitchCounterCur == 0) {
- // Advance to next pitch triplet
- _ch.pitchCurrentStep++;
- if (_ch.pitchCurrentStep >= _ch.pitchTripletTotal) {
- // All pitch triplets exhausted -> check duration
- _ch.duration--;
- if (_ch.duration == 0) {
- // SHUTDOWN (l761eh): silence and deactivate
- writeReg(_ch.volReg, 0);
- _ch.active = false;
- _finished = true;
- return;
- }
- // Duration > 0: restart BOTH volume and pitch from beginning
- // Assembly at 0x75D0: reloads tone (volume) table first triplet,
- // then resets both position indices and done flag,
- // then loads first envelope (pitch) triplet.
-
- // Reload first volume triplet (from tone table)
- int volOff = _ch.volToneIdx * 4 + 1;
- _ch.volCounter = toneRaw[volOff];
- _ch.volDelta = static_cast<int8>(toneRaw[volOff + 1]);
- _ch.volLimit = toneRaw[volOff + 2];
- _ch.volCounterCur = _ch.volCounter;
- _ch.volLimitCur = _ch.volLimit;
-
- // Reset both position indices and done flag
- _ch.volCurrentStep = 0;
- _ch.pitchCurrentStep = 0;
- _ch.finishedFlag = 0;
-
- // Reload first pitch triplet (from envelope table)
- int off = _ch.pitchEnvIdx * 4 + 1;
- _ch.pitchCounter = envRaw[off];
- _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
- _ch.pitchLimit = envRaw[off + 2];
- _ch.pitchCounterCur = _ch.pitchCounter;
- _ch.pitchLimitCur = _ch.pitchLimit;
- } else {
- // Load next pitch triplet
- int off = _ch.pitchEnvIdx * 4 + 1 + _ch.pitchCurrentStep * 3;
- _ch.pitchCounter = envRaw[off];
- _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
- _ch.pitchLimit = envRaw[off + 2];
- _ch.pitchCounterCur = _ch.pitchCounter;
- _ch.pitchLimitCur = _ch.pitchLimit;
- }
- }
- }
-
- // === VOLUME UPDATE (l763ah) ===
- if (!_ch.finishedFlag) {
- _ch.volLimitCur--;
- if (_ch.volLimitCur == 0) {
- // Reload limit countdown
- _ch.volLimitCur = _ch.volLimit;
-
- // volume = (volume + volDelta) & 0x0F
- _ch.volume = (_ch.volume + _ch.volDelta) & 0x0F;
- writeReg(_ch.volReg, _ch.volume);
-
- // Decrement volume counter
- _ch.volCounterCur--;
- if (_ch.volCounterCur == 0) {
- // Advance to next volume triplet
- _ch.volCurrentStep++;
- if (_ch.volCurrentStep >= _ch.volTripletTotal) {
- // All volume triplets exhausted -> set finished flag
- // NOTE: Does NOT shutdown channel - pitch continues
- _ch.finishedFlag = 1;
- } else {
- // Load next volume triplet
- int off = _ch.volToneIdx * 4 + 1 + _ch.volCurrentStep * 3;
- _ch.volCounter = toneRaw[off];
- _ch.volDelta = static_cast<int8>(toneRaw[off + 1]);
- _ch.volLimit = toneRaw[off + 2];
- _ch.volCounterCur = _ch.volCounter;
- _ch.volLimitCur = _ch.volLimit;
- }
- }
- }
- }
- }
-};
-
-void FreescapeEngine::playSoundDrillerCPC(int index, Audio::SoundHandle &handle) {
- debugC(1, kFreescapeDebugMedia, "Playing Driller CPC sound %d", index);
- // Create a new stream for the sound
- DrillerCPCSfxStream *stream = new DrillerCPCSfxStream(index,
- _soundsCPCSoundDefTable.data(), _soundsCPCToneTable.data(), _soundsCPCEnvelopeTable.data());
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
-}
-
} // End of namespace Freescape
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
index 5c191b52e1f..060522acac0 100644
--- a/engines/freescape/sound.cpp
+++ b/engines/freescape/sound.cpp
@@ -332,9 +332,7 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
playSoundZX(_soundsSpeakerFxZX[index], handle);
return;
} else if (isCPC()) {
- if (isDriller())
- playSoundDrillerCPC(index, handle);
- // else playSoundCPC(...)
+ playSoundCPC(index, handle);
return;
}
@@ -395,7 +393,7 @@ void FreescapeEngine::stopAllSounds(Audio::SoundHandle &handle) {
}
void FreescapeEngine::waitForSounds() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST() || (isCPC() && isDriller()))
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
while (_mixer->isSoundHandleActive(_soundFxHandle))
waitInLoop(10);
else {
@@ -405,7 +403,7 @@ void FreescapeEngine::waitForSounds() {
}
bool FreescapeEngine::isPlayingSound() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST() || (isCPC() && isDriller()))
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
return _mixer->isSoundHandleActive(_soundFxHandle);
return (!_speaker->endOfStream());
@@ -513,6 +511,389 @@ void FreescapeEngine::loadSoundsFx(Common::SeekableReadStream *file, int offset,
}
}
+/**
+ * CPC Sound Implementation (shared by Driller, Dark Side, and other Freescape CPC games)
+ *
+ * Based on reverse engineering of DRILL.BIN and DARKCODE.BIN (both load at 0x1C62).
+ * The sound engine is identical across games; only table contents and sizes differ.
+ *
+ * All sounds use the sub_4760h system:
+ * - Sound initialization loads 7-byte entry from the sound definition table
+ * - Volume envelope from "Tone" Table
+ * - Pitch sweep from "Envelope" Table
+ * - 300Hz interrupt-driven update
+ *
+ * AY-3-8912 PSG with 1MHz clock:
+ * Port 0xF4 = register select, Port 0xF6 = data
+ *
+ * ---- Sound Definition Table ----
+ * N entries, 7 bytes each. Loaded with 1-based sound number.
+ * Byte 0: flags
+ * - Bits 0-1: channel number (1=A, 2=B, 3=C)
+ * - Bit 2: tone disable (0 = enable tone, 1 = disable)
+ * - Bit 3: noise disable (0 = enable noise, 1 = disable)
+ * Byte 1: "tone" table index (volume envelope)
+ * Byte 2: "envelope" table index (pitch sweep)
+ * Bytes 3-4: initial AY tone period (little-endian, 12-bit)
+ * Byte 5: initial AY volume (0-15)
+ * Byte 6: duration (repeat count; 0 = single play)
+ *
+ * ---- "Tone" Table - Volume Envelope ----
+ * Despite the name, this table controls VOLUME modulation, not pitch.
+ * Indexed by 4-byte stride: base = index * 4.
+ * Byte 0: number of triplets (N)
+ * Then N triplets of 3 bytes each:
+ * Byte 0: counter - how many times to apply the delta
+ * Byte 1: delta (signed) - added to volume each step
+ * Byte 2: limit - ticks between each application
+ *
+ * ---- "Envelope" Table - Pitch Sweep ----
+ * Despite the name, this table controls PITCH modulation, not envelope.
+ * Indexed by 4-byte stride: base = index * 4.
+ * Byte 0: number of triplets (N)
+ * Then N triplets of 3 bytes each:
+ * Byte 0: counter - how many times to apply the delta
+ * Byte 1: delta (signed) - added to period each step
+ * Byte 2: limit - ticks between each application
+ */
+
+class CPCSfxStream : public Audio::AudioStream {
+public:
+ CPCSfxStream(int index, const byte *soundDefTable, int soundDefTableSize,
+ const byte *toneTable, const byte *envelopeTable, int rate = 44100)
+ : _ay(rate, 1000000), _rate(rate),
+ _soundDefTable(soundDefTable), _soundDefTableSize(soundDefTableSize),
+ _toneTable(toneTable), _envelopeTable(envelopeTable) {
+ _finished = false;
+ _tickSampleCount = 0;
+
+ // Reset all AY registers to match CPC init state
+ for (int r = 0; r < 14; r++)
+ _ay.setReg(r, 0);
+ // Noise period from CPC init table (verified in binary)
+ _ay.setReg(6, 0x07);
+
+ memset(&_ch, 0, sizeof(_ch));
+ setupSound(index);
+ }
+
+ int readBuffer(int16 *buffer, const int numSamples) override {
+ if (_finished)
+ return 0;
+
+ int samplesGenerated = 0;
+ // AY8912Stream is stereo: readBuffer counts int16 values (2 per frame).
+ // CPC interrupts fire at 300Hz (6 per frame). The update routine is called
+ // unconditionally at every interrupt, NOT inside the 50Hz divider.
+ int samplesPerTick = (_rate / 300) * 2;
+
+ while (samplesGenerated < numSamples && !_finished) {
+ // Generate samples until next tick
+ int remaining = samplesPerTick - _tickSampleCount;
+ int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+ if (toGenerate > 0) {
+ _ay.readBuffer(buffer + samplesGenerated, toGenerate);
+ samplesGenerated += toGenerate;
+ _tickSampleCount += toGenerate;
+ }
+
+ // Run interrupt handler at 300Hz tick boundary
+ if (_tickSampleCount >= samplesPerTick) {
+ _tickSampleCount -= samplesPerTick;
+ tickUpdate();
+ }
+ }
+
+ return samplesGenerated;
+ }
+
+ bool isStereo() const override { return true; }
+ bool endOfData() const override { return _finished; }
+ bool endOfStream() const override { return _finished; }
+ int getRate() const override { return _rate; }
+
+private:
+ Audio::AY8912Stream _ay;
+ int _rate;
+ bool _finished;
+ int _tickSampleCount; // Samples generated in current tick
+
+ // Pointers to table data loaded from game binary (owned by FreescapeEngine)
+ const byte *_soundDefTable;
+ int _soundDefTableSize; // Size in bytes (numSounds * 7)
+ const byte *_toneTable; // Volume envelope data
+ const byte *_envelopeTable; // Pitch sweep data
+
+ /**
+ * Channel state - mirrors the 23-byte per-channel structure
+ * as populated by the init routine and updated at 300Hz.
+ *
+ * "vol" fields come from the "tone" table - controls volume envelope
+ * "pitch" fields come from the "envelope" table - controls pitch sweep
+ */
+ struct ChannelState {
+ // Volume modulation (from "tone" table)
+ byte volCounter; // ix+000h: initial counter value
+ int8 volDelta; // ix+001h: signed delta added to volume
+ byte volLimit; // ix+002h: initial limit value
+ byte volCounterCur; // ix+003h: current counter (decremented)
+ byte volLimitCur; // ix+004h: current limit countdown
+ byte volume; // ix+005h: current AY volume (0-15)
+ byte volTripletTotal; // ix+006h: total number of volume triplets
+ byte volCurrentStep; // ix+007h: current triplet index
+ byte duration; // ix+008h: repeat count
+ byte volToneIdx; // tone table index (to recompute data pointer)
+
+ // Pitch modulation (from "envelope" table)
+ byte pitchCounter; // ix+00Bh: initial counter value
+ int8 pitchDelta; // ix+00Ch: signed delta added to period
+ byte pitchLimit; // ix+00Dh: initial limit value
+ byte pitchCounterCur; // ix+00Eh: current counter (decremented)
+ byte pitchLimitCur; // ix+00Fh: current limit countdown
+ uint16 period; // ix+010h-011h: current 16-bit AY tone period
+ byte pitchTripletTotal; // ix+012h: total number of pitch triplets
+ byte pitchCurrentStep; // ix+013h: current triplet index
+ byte pitchEnvIdx; // envelope table index (to recompute data pointer)
+
+ byte finishedFlag; // ix+016h: set when volume envelope exhausted
+
+ // AY register mapping for this channel
+ byte channelNum; // 1=A, 2=B, 3=C
+ byte toneRegLo; // AY register for tone fine
+ byte toneRegHi; // AY register for tone coarse
+ byte volReg; // AY register for volume
+ bool active; // Channel is producing sound
+ } _ch;
+
+ void writeReg(int reg, byte val) {
+ _ay.setReg(reg, val);
+ }
+
+ void setupSound(int index) {
+ int maxSounds = _soundDefTableSize / 7;
+ if (index >= 1 && index <= maxSounds) {
+ setupSub4760h(index);
+ } else {
+ _finished = true;
+ }
+ }
+
+ /**
+ * Sound initialization - loads 7-byte entry and configures AY registers.
+ */
+ void setupSub4760h(int soundNum) {
+ int maxSounds = _soundDefTableSize / 7;
+ if (soundNum < 1 || soundNum > maxSounds) {
+ _finished = true;
+ return;
+ }
+
+ const byte *entry = &_soundDefTable[(soundNum - 1) * 7];
+ byte flags = entry[0];
+ byte toneIdx = entry[1];
+ byte envIdx = entry[2];
+ uint16 period = entry[3] | (entry[4] << 8);
+ byte volume = entry[5];
+ byte duration = entry[6];
+
+ // Channel number (1-based): 1=A, 2=B, 3=C
+ byte channelNum = flags & 0x03;
+ if (channelNum < 1 || channelNum > 3) {
+ _finished = true;
+ return;
+ }
+
+ // AY register mapping
+ _ch.channelNum = channelNum;
+ _ch.toneRegLo = (channelNum - 1) * 2; // A=0, B=2, C=4
+ _ch.toneRegHi = (channelNum - 1) * 2 + 1; // A=1, B=3, C=5
+ _ch.volReg = channelNum + 7; // A=8, B=9, C=10
+
+ // Configure mixer (register 7)
+ // Start with all disabled (0xFF), selectively enable per flags
+ // Bit 2 set in flags = DISABLE tone, Bit 3 set = DISABLE noise
+ byte mixer = 0xFF;
+ if (!(flags & 0x04))
+ mixer &= ~(1 << (channelNum - 1)); // Enable tone
+ if (!(flags & 0x08))
+ mixer &= ~(1 << (channelNum - 1 + 3)); // Enable noise
+ writeReg(7, mixer);
+
+ // Set AY tone period from entry[3-4]
+ _ch.period = period;
+ writeReg(_ch.toneRegLo, period & 0xFF);
+ writeReg(_ch.toneRegHi, period >> 8);
+
+ // Set AY volume from entry[5]
+ _ch.volume = volume;
+ writeReg(_ch.volReg, volume);
+
+ // Duration from entry[6]
+ _ch.duration = duration;
+
+ // Load volume envelope from "tone" table
+ // index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
+ int toneBase = toneIdx * 4;
+ _ch.volTripletTotal = _toneTable[toneBase];
+ _ch.volCurrentStep = 0;
+ _ch.volToneIdx = toneIdx;
+
+ // Load first volume triplet
+ int volOff = toneBase + 1;
+ _ch.volCounter = _toneTable[volOff];
+ _ch.volDelta = static_cast<int8>(_toneTable[volOff + 1]);
+ _ch.volLimit = _toneTable[volOff + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+
+ // Load pitch sweep from "envelope" table
+ // index * 4 stride, byte[0]=triplet_count, then {counter, delta, limit}
+ int envBase = envIdx * 4;
+ _ch.pitchTripletTotal = _envelopeTable[envBase];
+ _ch.pitchCurrentStep = 0;
+ _ch.pitchEnvIdx = envIdx;
+
+ // Load first pitch triplet
+ int pitchOff = envBase + 1;
+ _ch.pitchCounter = _envelopeTable[pitchOff];
+ _ch.pitchDelta = static_cast<int8>(_envelopeTable[pitchOff + 1]);
+ _ch.pitchLimit = _envelopeTable[pitchOff + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
+
+ _ch.finishedFlag = 0;
+ _ch.active = true;
+
+ debugC(1, kFreescapeDebugMedia, "CPC sound init: sound %d ch=%d mixer=0x%02x period=%d vol=%d dur=%d tone[%d] env[%d]",
+ soundNum, channelNum, mixer, period, volume, duration, toneIdx, envIdx);
+ debugC(1, kFreescapeDebugMedia, " vol envelope: triplets=%d counter=%d delta=%d limit=%d",
+ _ch.volTripletTotal, _ch.volCounter, _ch.volDelta, _ch.volLimit);
+ debugC(1, kFreescapeDebugMedia, " pitch sweep: triplets=%d counter=%d delta=%d limit=%d",
+ _ch.pitchTripletTotal, _ch.pitchCounter, _ch.pitchDelta, _ch.pitchLimit);
+ }
+
+ /**
+ * 300Hz interrupt-driven update. Updates pitch first, then volume.
+ */
+ void tickUpdate() {
+ if (!_ch.active) {
+ _finished = true;
+ return;
+ }
+
+ const byte *toneRaw = _toneTable;
+ const byte *envRaw = _envelopeTable;
+
+ // === PITCH UPDATE ===
+ _ch.pitchLimitCur--;
+ if (_ch.pitchLimitCur == 0) {
+ // Reload limit countdown
+ _ch.pitchLimitCur = _ch.pitchLimit;
+
+ // period += sign_extend(pitchDelta) with natural 16-bit wrapping
+ _ch.period += static_cast<int8>(_ch.pitchDelta);
+
+ // Write period to AY tone registers (AY masks coarse to 4 bits)
+ writeReg(_ch.toneRegLo, _ch.period & 0xFF);
+ writeReg(_ch.toneRegHi, _ch.period >> 8);
+
+ // Decrement pitch counter
+ _ch.pitchCounterCur--;
+ if (_ch.pitchCounterCur == 0) {
+ // Advance to next pitch triplet
+ _ch.pitchCurrentStep++;
+ if (_ch.pitchCurrentStep >= _ch.pitchTripletTotal) {
+ // All pitch triplets exhausted -> check duration
+ _ch.duration--;
+ if (_ch.duration == 0) {
+ // SHUTDOWN: silence and deactivate
+ writeReg(_ch.volReg, 0);
+ _ch.active = false;
+ _finished = true;
+ return;
+ }
+ // Duration > 0: restart BOTH volume and pitch from beginning
+
+ // Reload first volume triplet (from tone table)
+ int volOff = _ch.volToneIdx * 4 + 1;
+ _ch.volCounter = toneRaw[volOff];
+ _ch.volDelta = static_cast<int8>(toneRaw[volOff + 1]);
+ _ch.volLimit = toneRaw[volOff + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+
+ // Reset both position indices and done flag
+ _ch.volCurrentStep = 0;
+ _ch.pitchCurrentStep = 0;
+ _ch.finishedFlag = 0;
+
+ // Reload first pitch triplet (from envelope table)
+ int off = _ch.pitchEnvIdx * 4 + 1;
+ _ch.pitchCounter = envRaw[off];
+ _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
+ _ch.pitchLimit = envRaw[off + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
+ } else {
+ // Load next pitch triplet
+ int off = _ch.pitchEnvIdx * 4 + 1 + _ch.pitchCurrentStep * 3;
+ _ch.pitchCounter = envRaw[off];
+ _ch.pitchDelta = static_cast<int8>(envRaw[off + 1]);
+ _ch.pitchLimit = envRaw[off + 2];
+ _ch.pitchCounterCur = _ch.pitchCounter;
+ _ch.pitchLimitCur = _ch.pitchLimit;
+ }
+ }
+ }
+
+ // === VOLUME UPDATE ===
+ if (!_ch.finishedFlag) {
+ _ch.volLimitCur--;
+ if (_ch.volLimitCur == 0) {
+ // Reload limit countdown
+ _ch.volLimitCur = _ch.volLimit;
+
+ // volume = (volume + volDelta) & 0x0F
+ _ch.volume = (_ch.volume + _ch.volDelta) & 0x0F;
+ writeReg(_ch.volReg, _ch.volume);
+
+ // Decrement volume counter
+ _ch.volCounterCur--;
+ if (_ch.volCounterCur == 0) {
+ // Advance to next volume triplet
+ _ch.volCurrentStep++;
+ if (_ch.volCurrentStep >= _ch.volTripletTotal) {
+ // All volume triplets exhausted -> set finished flag
+ // NOTE: Does NOT shutdown channel - pitch continues
+ _ch.finishedFlag = 1;
+ } else {
+ // Load next volume triplet
+ int off = _ch.volToneIdx * 4 + 1 + _ch.volCurrentStep * 3;
+ _ch.volCounter = toneRaw[off];
+ _ch.volDelta = static_cast<int8>(toneRaw[off + 1]);
+ _ch.volLimit = toneRaw[off + 2];
+ _ch.volCounterCur = _ch.volCounter;
+ _ch.volLimitCur = _ch.volLimit;
+ }
+ }
+ }
+ }
+ }
+};
+
+void FreescapeEngine::playSoundCPC(int index, Audio::SoundHandle &handle) {
+ if (_soundsCPCSoundDefTable.empty()) {
+ debugC(1, kFreescapeDebugMedia, "CPC sound tables not loaded");
+ return;
+ }
+ debugC(1, kFreescapeDebugMedia, "Playing CPC sound %d", index);
+ CPCSfxStream *stream = new CPCSfxStream(index,
+ _soundsCPCSoundDefTable.data(), _soundsCPCSoundDefTable.size(),
+ _soundsCPCToneTable.data(), _soundsCPCEnvelopeTable.data());
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
+}
+
void FreescapeEngine::playSoundDrillerZX(int index, Audio::SoundHandle &handle) {
debugC(1, kFreescapeDebugMedia, "Playing Driller ZX sound %d", index);
Common::Array<soundUnitZX> soundUnits;
Commit: e860d27488c129aeebeedd73237f07846fa2ffa5
https://github.com/scummvm/scummvm/commit/e860d27488c129aeebeedd73237f07846fa2ffa5
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: implement sound for Total Eclipse CPC (demo)
Changed paths:
engines/freescape/games/eclipse/cpc.cpp
diff --git a/engines/freescape/games/eclipse/cpc.cpp b/engines/freescape/games/eclipse/cpc.cpp
index 0428bdb884c..c7683addc7c 100644
--- a/engines/freescape/games/eclipse/cpc.cpp
+++ b/engines/freescape/games/eclipse/cpc.cpp
@@ -30,6 +30,21 @@ namespace Freescape {
void EclipseEngine::initCPC() {
_viewArea = Common::Rect(36 + 3, 24 + 8, 284, 130 + 3);
+ // Sound mappings from TEPROG.BIN disassembly (sub_6D19h call sites)
+ _soundIndexShoot = 5; // 0x5D80: LD A,05h; CALL 6D19h (type 0x16 destroy)
+ _soundIndexCollide = 12; // 0x5192/0x5239: deferred via (0CFD9h)
+ _soundIndexStepDown = 12; // 0x5194/0x5239: small height drop within threshold
+ _soundIndexStepUp = 12; // same sound for step up (matches ZX version)
+ _soundIndexStart = 3; // 0x770F/7726/776A: game start transition
+ _soundIndexAreaChange = 7; // 0x63E0: deferred via (0CFD9h), type 0x12
+ _soundIndexStartFalling = 6; // 0x797E: falling handler, first phase
+ _soundIndexEndFalling = 8; // 0x79AC: falling handler, landing
+ _soundIndexFall = 5; // 0x7D25: death/game-over animation
+ _soundIndexNoShield = 5; // game-over conditions reuse death sound
+ _soundIndexFallen = 5;
+ _soundIndexTimeout = 5;
+ _soundIndexForceEndGame = 5;
+ _soundIndexCrushed = 5;
}
byte kCPCPaletteEclipseTitleData[4][3] = {
@@ -88,10 +103,12 @@ void EclipseEngine::loadAssetsCPCFullGame() {
loadFonts(&file, 0x60bc);
loadMessagesFixedSize(&file, 0x326, 16, 30);
load8bitBinary(&file, 0x62b4, 16);
+ // TODO: loadSoundsCPC for Eclipse 2 - need to determine table offsets from TE2.BI2
} else {
loadFonts(&file, 0x6076);
loadMessagesFixedSize(&file, 0x326, 16, 30);
load8bitBinary(&file, 0x626e, 16);
+ // TODO: loadSoundsCPC for full game - need to determine table offsets from TECODE.BIN
}
loadColorPalette();
@@ -123,6 +140,7 @@ void EclipseEngine::loadAssetsCPCDemo() {
loadMessagesFixedSize(&file, 0x362, 16, 23);
loadMessagesFixedSize(&file, 0x570b, 264, 5);
load8bitBinary(&file, 0x65c6, 16);
+ loadSoundsCPC(&file, 0x0805, 104, 0x086D, 165, 0x0772, 147);
loadColorPalette();
swapPalette(1);
Commit: 46c8cab355c5c9bae6216ec544a0a8eb3b3e2a24
https://github.com/scummvm/scummvm/commit/46c8cab355c5c9bae6216ec544a0a8eb3b3e2a24
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-06T21:03:06+01:00
Commit Message:
FREESCAPE: inherit from EmulatedChip
Changed paths:
audio/softsynth/ay8912.cpp
audio/softsynth/ay8912.h
engines/freescape/sound.cpp
diff --git a/audio/softsynth/ay8912.cpp b/audio/softsynth/ay8912.cpp
index f55a22bb137..4e80688f641 100644
--- a/audio/softsynth/ay8912.cpp
+++ b/audio/softsynth/ay8912.cpp
@@ -217,6 +217,11 @@ void AY8912Stream::setReg(int reg, unsigned char value) {
}
int AY8912Stream::readBuffer(int16 *buffer, const int numSamples) {
+ generateSamples(buffer, numSamples);
+ return numSamples;
+}
+
+void AY8912Stream::generateSamples(int16 *buffer, int numSamples) {
Common::StackLock lock(_mutex);
int mix_l, mix_r;
@@ -284,8 +289,6 @@ int AY8912Stream::readBuffer(int16 *buffer, const int numSamples) {
*bufPtr++ = mix_l;
*bufPtr++ = mix_r;
}
-
- return numSamples;
}
} // End of namespace Audio
diff --git a/audio/softsynth/ay8912.h b/audio/softsynth/ay8912.h
index f738c2af73a..ebccdda1f2f 100644
--- a/audio/softsynth/ay8912.h
+++ b/audio/softsynth/ay8912.h
@@ -22,12 +22,12 @@
#ifndef AUDIO_SOFTSYNTH_AY8912_H
#define AUDIO_SOFTSYNTH_AY8912_H
-#include "audio/audiostream.h"
+#include "audio/chip.h"
#include "common/mutex.h"
namespace Audio {
-class AY8912Stream : public AudioStream {
+class AY8912Stream : public EmulatedChip {
public:
enum ChipType {
AY_TYPE_AY,
@@ -57,6 +57,12 @@ public:
void setReg(int reg, unsigned char value);
void setRegs(const unsigned char *regs);
+ AudioStream *toAudioStream() { return this; }
+
+protected:
+ // EmulatedChip interface
+ void generateSamples(int16 *buffer, int numSamples) override;
+
private:
struct RegData {
int tone_a;
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
index 060522acac0..f1b284c6b9d 100644
--- a/engines/freescape/sound.cpp
+++ b/engines/freescape/sound.cpp
@@ -557,11 +557,11 @@ void FreescapeEngine::loadSoundsFx(Common::SeekableReadStream *file, int offset,
* Byte 2: limit - ticks between each application
*/
-class CPCSfxStream : public Audio::AudioStream {
+class CPCSfxStream : public Audio::AY8912Stream {
public:
CPCSfxStream(int index, const byte *soundDefTable, int soundDefTableSize,
const byte *toneTable, const byte *envelopeTable, int rate = 44100)
- : _ay(rate, 1000000), _rate(rate),
+ : AY8912Stream(rate, 1000000),
_soundDefTable(soundDefTable), _soundDefTableSize(soundDefTableSize),
_toneTable(toneTable), _envelopeTable(envelopeTable) {
_finished = false;
@@ -569,9 +569,9 @@ public:
// Reset all AY registers to match CPC init state
for (int r = 0; r < 14; r++)
- _ay.setReg(r, 0);
+ setReg(r, 0);
// Noise period from CPC init table (verified in binary)
- _ay.setReg(6, 0x07);
+ setReg(6, 0x07);
memset(&_ch, 0, sizeof(_ch));
setupSound(index);
@@ -585,7 +585,7 @@ public:
// AY8912Stream is stereo: readBuffer counts int16 values (2 per frame).
// CPC interrupts fire at 300Hz (6 per frame). The update routine is called
// unconditionally at every interrupt, NOT inside the 50Hz divider.
- int samplesPerTick = (_rate / 300) * 2;
+ int samplesPerTick = (getRate() / 300) * 2;
while (samplesGenerated < numSamples && !_finished) {
// Generate samples until next tick
@@ -593,7 +593,7 @@ public:
int toGenerate = MIN(numSamples - samplesGenerated, remaining);
if (toGenerate > 0) {
- _ay.readBuffer(buffer + samplesGenerated, toGenerate);
+ generateSamples(buffer + samplesGenerated, toGenerate);
samplesGenerated += toGenerate;
_tickSampleCount += toGenerate;
}
@@ -608,14 +608,10 @@ public:
return samplesGenerated;
}
- bool isStereo() const override { return true; }
bool endOfData() const override { return _finished; }
bool endOfStream() const override { return _finished; }
- int getRate() const override { return _rate; }
private:
- Audio::AY8912Stream _ay;
- int _rate;
bool _finished;
int _tickSampleCount; // Samples generated in current tick
@@ -667,7 +663,7 @@ private:
} _ch;
void writeReg(int reg, byte val) {
- _ay.setReg(reg, val);
+ setReg(reg, val);
}
void setupSound(int index) {
@@ -891,7 +887,7 @@ void FreescapeEngine::playSoundCPC(int index, Audio::SoundHandle &handle) {
CPCSfxStream *stream = new CPCSfxStream(index,
_soundsCPCSoundDefTable.data(), _soundsCPCSoundDefTable.size(),
_soundsCPCToneTable.data(), _soundsCPCEnvelopeTable.data());
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream->toAudioStream(), -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
}
void FreescapeEngine::playSoundDrillerZX(int index, Audio::SoundHandle &handle) {
More information about the Scummvm-git-logs
mailing list