[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