[Scummvm-git-logs] scummvm master -> 7a52f26df7459b7316a7189f8fdd0248a0887797
neuromancer
noreply at scummvm.org
Sat Feb 21 10:17:25 UTC 2026
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://api.github.com/repos/scummvm/scummvm .
Summary:
a2d59d70be FREESCAPE: refactored sound handling code per platform
854bacdd52 FREESCAPE: moved driller sounds for zx into its own file
6843d0ba79 FREESCAPE: initial implementation of castle master amiga sounds
7a52f26df7 FREESCAPE: fixes for castle master amiga sounds
Commit: a2d59d70be6f0331e0b206e03c23af147fd1bbcb
https://github.com/scummvm/scummvm/commit/a2d59d70be6f0331e0b206e03c23af147fd1bbcb
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-21T11:07:35+01:00
Commit Message:
FREESCAPE: refactored sound handling code per platform
Changed paths:
A engines/freescape/sound/common.cpp
A engines/freescape/sound/cpc.cpp
A engines/freescape/sound/dos.cpp
A engines/freescape/sound/zx.cpp
R engines/freescape/sound.cpp
engines/freescape/module.mk
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 8108c47c156..f8f74aebce1 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -48,7 +48,10 @@ MODULE_OBJS := \
objects/group.o \
objects/sensor.o \
sweepAABB.o \
- sound.o \
+ sound/common.o \
+ sound/cpc.o \
+ sound/dos.o \
+ sound/zx.o \
ui.o \
unpack.o \
wb.o
diff --git a/engines/freescape/sound.cpp b/engines/freescape/sound.cpp
deleted file mode 100644
index f1b284c6b9d..00000000000
--- a/engines/freescape/sound.cpp
+++ /dev/null
@@ -1,992 +0,0 @@
-/* ScummVM - Graphic Adventure Engine
- *
- * ScummVM is the legal property of its developers, whose names
- * are too numerous to list here. Please refer to the COPYRIGHT
- * file distributed with this source distribution.
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- *
- */
-
-#include "common/file.h"
-#include "audio/audiostream.h"
-#include "audio/decoders/raw.h"
-#include "audio/softsynth/ay8912.h"
-
-#include "freescape/freescape.h"
-#include "freescape/games/eclipse/eclipse.h"
-
-namespace Freescape {
-
-void FreescapeEngine::loadSpeakerFxZX(Common::SeekableReadStream *file, int sfxTable, int sfxData) {
- debugC(1, kFreescapeDebugParser, "Reading sound table for ZX");
- int numberSounds = 25;
-
- if (isDark())
- numberSounds = 34;
-
- if (isEclipse() && (_variant & GF_ZX_DEMO_MICROHOBBY))
- numberSounds = 21;
-
- for (int i = 1; i < numberSounds; i++) {
- debugC(1, kFreescapeDebugParser, "Reading sound table entry: %d ", i);
- _soundsSpeakerFxZX[i] = new Common::Array<soundUnitZX>();
- int soundIdx = (i - 1) * 4;
- file->seek(sfxTable + soundIdx);
-
- byte SFXtempStruct[8] = {};
-
- uint8 dataIndex = file->readByte();
- uint16 soundValue = file->readUint16LE();
- SFXtempStruct[0] = file->readByte();
-
- file->seek(sfxData + dataIndex * 4);
- uint8 soundType = file->readByte();
- int original_sound_ptr = sfxData + dataIndex * 4 + 1;
- int sound_ptr = original_sound_ptr;
- uint8 soundSize = 0;
- int16 repetitions = 0;
- debugC(1, kFreescapeDebugParser, "dataIndex: %x, value: %x, SFXtempStruct[0]: %x, type: %x", dataIndex, soundValue, SFXtempStruct[0], soundType);
- if (soundType == 0xff)
- break;
-
- if ((soundType & 0x80) == 0) {
- SFXtempStruct[6] = 0;
- SFXtempStruct[4] = soundType;
-
- while (true) {
- while (true) {
- file->seek(sound_ptr);
- //debug("start sound ptr: %x", sound_ptr);
- soundSize = file->readByte();
- SFXtempStruct[1] = soundSize;
- SFXtempStruct[2] = file->readByte();
- SFXtempStruct[3] = file->readByte();
-
- for (int j = 0; j <= 7; j++)
- debugC(1, kFreescapeDebugParser, "SFXtempStruct[%d]: %x", j, SFXtempStruct[j]);
-
- do {
- uint32 var9 = 0xffffff & (SFXtempStruct[3] * 0xd0);
- uint32 var10 = var9 / soundValue;
-
- var9 = 0xffffff & (7 * soundValue);
- uint16 var5 = (0xffff & var9) - 0x1e;
- if ((short)var5 < 0)
- var5 = 1;
-
- soundUnitZX soundUnit;
- soundUnit.isRaw = false;
- soundUnit.freqTimesSeconds = (var10 & 0xffff) + 1;
- soundUnit.tStates = var5;
- soundUnit.multiplier = 10;
- //debug("playSFX(%x, %x)", soundUnit.freqTimesSeconds, soundUnit.tStates);
- _soundsSpeakerFxZX[i]->push_back(soundUnit);
- int16 var4 = 0;
-
- if ((SFXtempStruct[2] & 0x80) != 0) {
- var4 = 0xff;
- }
- //debug("var4: %d", var4);
- //debug("soundValue delta: %d", int16(((var4 << 8) | SFXtempStruct[2])));
- soundValue = soundValue + int16(((var4 << 8) | SFXtempStruct[2]));
- //debug("soundValue: %x", soundValue);
- soundSize = soundSize - 1;
- } while (soundSize != 0);
- SFXtempStruct[5] = SFXtempStruct[5] + 1;
- if (SFXtempStruct[5] == SFXtempStruct[4])
- break;
-
- sound_ptr = original_sound_ptr + SFXtempStruct[5] * 3;
- //debug("sound ptr: %x", sound_ptr);
- }
-
- soundSize = SFXtempStruct[0];
- SFXtempStruct[0] = soundSize - 1;
- sound_ptr = original_sound_ptr;
- if ((soundSize - 1) == 0)
- break;
- SFXtempStruct[5] = 0;
- }
- } else if (soundType & 0x80) {
- file->seek(sound_ptr);
- for (int j = 1; j <= 7; j++) {
- SFXtempStruct[j] = file->readByte();
- debugC(1, kFreescapeDebugParser, "SFXtempStruct[%d]: %x", j, SFXtempStruct[j]);
- }
- soundSize = SFXtempStruct[0];
- repetitions = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
- uint16 var5 = soundValue;
- //debug("Repetitions: %x", repetitions);
- if ((soundType & 0x7f) == 1) {
- do {
- do {
- soundUnitZX soundUnit;
- soundUnit.isRaw = false;
- soundUnit.tStates = var5;
- soundUnit.freqTimesSeconds = SFXtempStruct[3] | (SFXtempStruct[4] << 8);
- soundUnit.multiplier = 1.8f;
- //debug("playSFX(%x, %x)", soundUnit.freqTimesSeconds, soundUnit.tStates);
- _soundsSpeakerFxZX[i]->push_back(soundUnit);
- repetitions = repetitions - 1;
- var5 = var5 + (SFXtempStruct[5] | (SFXtempStruct[6] << 8));
-
- } while ((byte)((byte)repetitions | (byte)((uint16)repetitions >> 8)) != 0);
- soundSize = soundSize - 1;
- repetitions = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
- var5 = soundValue;
- } while (soundSize != 0);
- } else if ((soundType & 0x7f) == 2) {
- repetitions = SFXtempStruct[1] | (SFXtempStruct[0] << 8);
- debugC(1, kFreescapeDebugParser, "Raw sound, repetitions: %x", repetitions);
- uint16 sVar7 = SFXtempStruct[3];
- soundType = 0;
- soundSize = SFXtempStruct[2];
- uint16 silenceSize = SFXtempStruct[4];
- bool cond1 = (SFXtempStruct[4] != 0 && SFXtempStruct[4] != 2);
- bool cond2 = SFXtempStruct[4] == 2;
- bool cond3 = SFXtempStruct[4] == 0;
-
- assert(cond1 || cond2 || cond3);
- do {
- soundUnitZX soundUnit;
- soundUnit.isRaw = true;
- int totalSize = soundSize + sVar7;
- soundUnit.rawFreq = 0.1f;
- soundUnit.rawLengthus = totalSize;
- _soundsSpeakerFxZX[i]->push_back(soundUnit);
- //debugN("%x ", silenceSize);
- soundUnit.rawFreq = 0;
- soundUnit.rawLengthus = silenceSize;
- _soundsSpeakerFxZX[i]->push_back(soundUnit);
- repetitions = repetitions + -1;
- soundSize = SFXtempStruct[5] + soundSize;
-
- if (cond1)
- silenceSize = (repetitions & 0xff) | (repetitions >> 8);
- else if (cond2)
- silenceSize = (repetitions & 0xff);
- else
- silenceSize = soundSize;
-
- //debug("soundSize: %x", soundSize);
- //sVar7 = (uint16)bVar9 << 8;
- } while (repetitions != 0);
- //debug("\n");
- //if (i == 15)
- // assert(0);
- } else {
- debugC(1, kFreescapeDebugParser, "Sound type: %x", soundType);
- bool beep = false;
- do {
- soundType = 0;
- uint16 uVar2 = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
- uint8 cVar3 = 0;
- do {
- //debug("start cycle %d:", cVar3);
- //ULA_PORT = bVar4;
- //bVar4 = bVar4 ^ 0x10;
- beep = !beep;
- repetitions = (((uint16)soundType * 0x100 + (uint16)soundType * -2) -
- (uint16)((uint16)soundType * 0x100 < (uint16)soundType)) + (uVar2 & 0xff);
- uint8 bVar9 = (byte)repetitions;
- uint8 bVar8 = (byte)((uint16)repetitions >> 8);
- uint8 bVar1 = bVar9 - bVar8;
- soundType = bVar1;
- if (bVar8 <= bVar9) {
- bVar1 = bVar1 - 1;
- soundType = bVar1;
- }
- //debug("wait %d", bVar1);
- assert(bVar1 > 0);
- soundUnitZX soundUnit;
- soundUnit.isRaw = false;
- soundUnit.freqTimesSeconds = beep ? 1000 : 0;
- soundUnit.tStates = beep ? 437500 / 1000 - 30.125 : 0;
- soundUnit.multiplier = float(bVar1) / 500;
- _soundsSpeakerFxZX[i]->push_back(soundUnit);
-
- // No need to wait
- //do {
- // bVar1 = bVar1 - 1;
- //} while (bVar1 != 0);
- cVar3 = (char)(uVar2 >> 8) + -1;
- uVar2 = (((uint16)cVar3) << 8) | (uint8)uVar2;
- } while (cVar3 != '\0');
- soundSize = soundSize + -1;
- } while (soundSize != '\0');
- }
- }
- }
- //assert(0);
-}
-
-void FreescapeEngine::loadSpeakerFxDOS(Common::SeekableReadStream *file, int offsetFreq, int offsetTable, int numberSounds) {
- debugC(1, kFreescapeDebugParser, "Reading PC speaker sound table for DOS");
- for (int i = 1; i <= numberSounds; i++) {
- debugC(1, kFreescapeDebugParser, "Reading sound table entry: %d ", i);
- int soundIdx = (i - 1) * 4;
- file->seek(offsetFreq + soundIdx);
- uint16 index = file->readByte();
- if (index == 0xff)
- continue;
- uint iVar = index * 5;
-
- uint16 frequencyStart = file->readUint16LE();
- uint8 repetitions = file->readByte();
- debugC(1, kFreescapeDebugParser, "Frequency start: %d ", frequencyStart);
- debugC(1, kFreescapeDebugParser, "Repetitions: %d ", repetitions);
-
- uint8 frequencyStepsNumber = 0;
- uint16 frequencyStep = 0;
-
- file->seek(offsetTable + iVar);
- uint8 lastIndex = file->readByte();
- debugC(1, kFreescapeDebugParser, "0x%x %d (lastIndex)", offsetTable - 0x200, lastIndex);
-
- frequencyStepsNumber = file->readByte();
- debugC(1, kFreescapeDebugParser, "0x%x %d (frequency steps)", offsetTable + 1 - 0x200, frequencyStepsNumber);
-
- int basePtr = offsetTable + iVar + 1;
- debugC(1, kFreescapeDebugParser, "0x%x (basePtr)", basePtr - 0x200);
-
- frequencyStep = file->readUint16LE();
- debugC(1, kFreescapeDebugParser, "0x%x %d (steps number)", offsetTable + 2 - 0x200, (int16)frequencyStep);
-
- uint8 frequencyDuration = file->readByte();
- debugC(1, kFreescapeDebugParser, "0x%x %d (frequency duration)", offsetTable + 4 - 0x200, frequencyDuration);
-
- soundSpeakerFx *speakerFxInfo = new soundSpeakerFx();
- _soundsSpeakerFx[i] = speakerFxInfo;
-
- speakerFxInfo->frequencyStart = frequencyStart;
- speakerFxInfo->repetitions = repetitions;
- speakerFxInfo->frequencyStepsNumber = frequencyStepsNumber;
- speakerFxInfo->frequencyStep = frequencyStep;
- speakerFxInfo->frequencyDuration = frequencyDuration;
-
- for (int j = 1; j < lastIndex; j++) {
-
- soundSpeakerFx *speakerFxInfoAdditionalStep = new soundSpeakerFx();
- speakerFxInfoAdditionalStep->frequencyStart = 0;
- speakerFxInfoAdditionalStep->repetitions = 0;
-
- file->seek(basePtr + 4 * j);
- debugC(1, kFreescapeDebugParser, "Reading at %x", basePtr + 4 * j - 0x200);
- frequencyStepsNumber = file->readByte();
- debugC(1, kFreescapeDebugParser, "%d (steps number)", frequencyStepsNumber);
- frequencyStep = file->readUint16LE();
- debugC(1, kFreescapeDebugParser, "%d (frequency step)", (int16)frequencyStep);
- frequencyDuration = file->readByte();
- debugC(1, kFreescapeDebugParser, "%d (frequency duration)", frequencyDuration);
-
- speakerFxInfoAdditionalStep->frequencyStepsNumber = frequencyStepsNumber;
- speakerFxInfoAdditionalStep->frequencyStep = frequencyStep;
- speakerFxInfoAdditionalStep->frequencyDuration = frequencyDuration;
- speakerFxInfo->additionalSteps.push_back(speakerFxInfoAdditionalStep);
- }
- debugC(1, kFreescapeDebugParser, "\n");
- }
-}
-
-void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle) {
- if (index < 0) {
- debugC(1, kFreescapeDebugMedia, "Sound not specified");
- return;
- }
-
- if (_syncSound)
- waitForSounds();
-
- _syncSound = sync;
-
- debugC(1, kFreescapeDebugMedia, "Playing sound %d with sync: %d", index, sync);
- if (isAmiga() || isAtariST()) {
- playSoundFx(index, sync);
- return;
- }
-
- if (isDOS()) {
- soundSpeakerFx *speakerFxInfo = _soundsSpeakerFx[index];
- if (speakerFxInfo)
- playSoundDOS(speakerFxInfo, sync, handle);
- else
- debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d is not available", index);
-
- return;
- } else if (isSpectrum()) {
- if (isDriller())
- playSoundDrillerZX(index, handle);
- else
- playSoundZX(_soundsSpeakerFxZX[index], handle);
- return;
- } else if (isCPC()) {
- playSoundCPC(index, handle);
- 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);
- _syncSound = sync;
-}
-void FreescapeEngine::playWav(const Common::Path &filename) {
-
- Common::SeekableReadStream *s = _dataBundle->createReadStreamForMember(filename);
- if (!s) {
- debugC(1, kFreescapeDebugMedia, "WARNING: Sound %s not found", filename.toString().c_str());
- return;
- }
- Audio::AudioStream *stream = Audio::makeWAVStream(s, DisposeAfterUse::YES);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, stream);
-}
-
-void FreescapeEngine::playMusic(const Common::Path &filename) {
- Audio::SeekableAudioStream *stream = nullptr;
- stream = Audio::SeekableAudioStream::openStreamFile(filename);
- if (stream) {
- _mixer->stopHandle(_musicHandle);
- Audio::LoopingAudioStream *loop = new Audio::LoopingAudioStream(stream, 0);
- _mixer->playStream(Audio::Mixer::kMusicSoundType, &_musicHandle, loop);
- }
-}
-
-void FreescapeEngine::playSoundFx(int index, bool sync) {
- if (_soundsFx.size() == 0) {
- debugC(1, kFreescapeDebugMedia, "WARNING: Sounds are not loaded");
- return;
- }
-
- if (index < 0 || index >= int(_soundsFx.size())) {
- debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d not available", index);
- return;
- }
-
- int size = _soundsFx[index]->size;
- int sampleRate = _soundsFx[index]->sampleRate;
- int repetitions = _soundsFx[index]->repetitions;
- byte *data = _soundsFx[index]->data;
-
- if (size > 4) {
- Audio::SeekableAudioStream *s = Audio::makeRawStream(data, size, sampleRate, Audio::FLAG_16BITS, DisposeAfterUse::NO);
- Audio::AudioStream *stream = new Audio::LoopingAudioStream(s, repetitions);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, stream);
- } else
- debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d is empty", index);
-}
-
-void FreescapeEngine::stopAllSounds(Audio::SoundHandle &handle) {
- debugC(1, kFreescapeDebugMedia, "Stopping sound");
- _mixer->stopHandle(handle);
-}
-
-void FreescapeEngine::waitForSounds() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
- while (_mixer->isSoundHandleActive(_soundFxHandle))
- waitInLoop(10);
- else {
- while (!_speaker->endOfStream())
- waitInLoop(10);
- }
-}
-
-bool FreescapeEngine::isPlayingSound() {
- if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
- return _mixer->isSoundHandleActive(_soundFxHandle);
-
- return (!_speaker->endOfStream());
-}
-
-void FreescapeEngine::playSilence(int duration, bool sync) {
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, 1000 * 10 * duration);
- _mixer->stopHandle(_soundFxHandle);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
-}
-
-void FreescapeEngine::queueSoundConst(double hzFreq, int duration) {
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, 1000 * 10 * duration);
-}
-
-uint16 FreescapeEngine::playSoundDOSSpeaker(uint16 frequencyStart, soundSpeakerFx *speakerFxInfo) {
- uint8 frequencyStepsNumber = speakerFxInfo->frequencyStepsNumber;
- int16 frequencyStep = speakerFxInfo->frequencyStep;
- uint8 frequencyDuration = speakerFxInfo->frequencyDuration;
-
- int16 freq = frequencyStart;
- int waveDurationMultipler = 1800;
- int waveDuration = waveDurationMultipler * (frequencyDuration + 1);
-
- while (true) {
- if (freq > 0) {
- float hzFreq = 1193180.0 / freq;
- debugC(1, kFreescapeDebugMedia, "raw %d, hz: %f, duration: %d", freq, hzFreq, waveDuration);
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, waveDuration);
- }
- if (frequencyStepsNumber > 0) {
- // Ascending initial portions of cycle
- freq += frequencyStep;
- frequencyStepsNumber--;
- } else
- break;
- }
-
- return freq;
-}
-
-void FreescapeEngine::playSoundZX(Common::Array<soundUnitZX> *data, Audio::SoundHandle &handle) {
- for (auto &it : *data) {
- soundUnitZX value = it;
-
- if (value.isRaw) {
- debugC(1, kFreescapeDebugMedia, "raw hz: %f, duration: %d", value.rawFreq, value.rawLengthus);
- if (value.rawFreq == 0) {
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 1, 5 * value.rawLengthus);
- continue;
- }
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, value.rawFreq, 5 * value.rawLengthus);
- } else {
- if (value.freqTimesSeconds == 0 && value.tStates == 0) {
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 1, 1000 * value.multiplier);
- continue;
- }
-
- float hzFreq = 1 / ((value.tStates + 30.125) / 437500.0);
- float waveDuration = value.freqTimesSeconds / hzFreq;
- waveDuration = value.multiplier * 1000 * (waveDuration + 1);
- debugC(1, kFreescapeDebugMedia, "non raw hz: %f, duration: %f", hzFreq, waveDuration);
- _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, waveDuration);
- }
- }
-
- _mixer->stopHandle(_soundFxHandle);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
-}
-
-void FreescapeEngine::playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle) {
- uint freq = speakerFxInfo->frequencyStart;
-
- for (int i = 0; i < speakerFxInfo->repetitions; i++) {
- freq = playSoundDOSSpeaker(freq, speakerFxInfo);
-
- for (auto &it : speakerFxInfo->additionalSteps) {
- assert(it);
- freq = playSoundDOSSpeaker(freq, it);
- }
- }
-
- _mixer->stopHandle(handle);
- _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
-}
-
-void FreescapeEngine::loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) {
- file->seek(offset);
- soundFx *sound = nullptr;
- _soundsFx[0] = sound;
- for (int i = 1; i < number + 1; i++) {
- sound = (soundFx *)malloc(sizeof(soundFx));
- int zero = file->readUint16BE();
- assert(zero == 0);
- int size = file->readUint16BE();
- float sampleRate = float(file->readUint16BE()) / 2;
- debugC(1, kFreescapeDebugParser, "Loading sound: %d (size: %d, sample rate: %f) at %" PRIx64, i, size, sampleRate, file->pos());
- byte *data = (byte *)malloc(size * sizeof(byte));
- file->read(data, size);
- sound->sampleRate = sampleRate;
- sound->size = size;
- sound->data = (byte *)data;
- sound->repetitions = 1;
- _soundsFx[i] = sound;
- }
-}
-
-/**
- * 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::AY8912Stream {
-public:
- CPCSfxStream(int index, const byte *soundDefTable, int soundDefTableSize,
- const byte *toneTable, const byte *envelopeTable, int rate = 44100)
- : AY8912Stream(rate, 1000000),
- _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++)
- setReg(r, 0);
- // Noise period from CPC init table (verified in binary)
- 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 = (getRate() / 300) * 2;
-
- while (samplesGenerated < numSamples && !_finished) {
- // Generate samples until next tick
- int remaining = samplesPerTick - _tickSampleCount;
- int toGenerate = MIN(numSamples - samplesGenerated, remaining);
-
- if (toGenerate > 0) {
- generateSamples(buffer + samplesGenerated, toGenerate);
- samplesGenerated += toGenerate;
- _tickSampleCount += toGenerate;
- }
-
- // Run interrupt handler at 300Hz tick boundary
- if (_tickSampleCount >= samplesPerTick) {
- _tickSampleCount -= samplesPerTick;
- tickUpdate();
- }
- }
-
- return samplesGenerated;
- }
-
- bool endOfData() const override { return _finished; }
- bool endOfStream() const override { return _finished; }
-
-private:
- 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) {
- 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->toAudioStream(), -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;
-
- 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
diff --git a/engines/freescape/sound/common.cpp b/engines/freescape/sound/common.cpp
new file mode 100644
index 00000000000..fbb45cad403
--- /dev/null
+++ b/engines/freescape/sound/common.cpp
@@ -0,0 +1,171 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "common/file.h"
+#include "audio/audiostream.h"
+#include "audio/decoders/raw.h"
+#include "audio/decoders/wave.h"
+
+#include "freescape/freescape.h"
+
+namespace Freescape {
+
+void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle) {
+ if (index < 0) {
+ debugC(1, kFreescapeDebugMedia, "Sound not specified");
+ return;
+ }
+
+ if (_syncSound)
+ waitForSounds();
+
+ _syncSound = sync;
+
+ debugC(1, kFreescapeDebugMedia, "Playing sound %d with sync: %d", index, sync);
+ if (isAmiga() || isAtariST()) {
+ playSoundFx(index, sync);
+ return;
+ }
+
+ if (isDOS()) {
+ soundSpeakerFx *speakerFxInfo = _soundsSpeakerFx[index];
+ if (speakerFxInfo)
+ playSoundDOS(speakerFxInfo, sync, handle);
+ else
+ debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d is not available", index);
+
+ return;
+ } else if (isSpectrum()) {
+ if (isDriller())
+ playSoundDrillerZX(index, handle);
+ else
+ playSoundZX(_soundsSpeakerFxZX[index], handle);
+ return;
+ } else if (isCPC()) {
+ playSoundCPC(index, handle);
+ 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);
+ _syncSound = sync;
+}
+void FreescapeEngine::playWav(const Common::Path &filename) {
+
+ Common::SeekableReadStream *s = _dataBundle->createReadStreamForMember(filename);
+ if (!s) {
+ debugC(1, kFreescapeDebugMedia, "WARNING: Sound %s not found", filename.toString().c_str());
+ return;
+ }
+ Audio::AudioStream *stream = Audio::makeWAVStream(s, DisposeAfterUse::YES);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, stream);
+}
+
+void FreescapeEngine::playMusic(const Common::Path &filename) {
+ Audio::SeekableAudioStream *stream = nullptr;
+ stream = Audio::SeekableAudioStream::openStreamFile(filename);
+ if (stream) {
+ _mixer->stopHandle(_musicHandle);
+ Audio::LoopingAudioStream *loop = new Audio::LoopingAudioStream(stream, 0);
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, &_musicHandle, loop);
+ }
+}
+
+void FreescapeEngine::playSoundFx(int index, bool sync) {
+ if (_soundsFx.size() == 0) {
+ debugC(1, kFreescapeDebugMedia, "WARNING: Sounds are not loaded");
+ return;
+ }
+
+ if (index < 0 || index >= int(_soundsFx.size())) {
+ debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d not available", index);
+ return;
+ }
+
+ int size = _soundsFx[index]->size;
+ int sampleRate = _soundsFx[index]->sampleRate;
+ int repetitions = _soundsFx[index]->repetitions;
+ byte *data = _soundsFx[index]->data;
+
+ if (size > 4) {
+ Audio::SeekableAudioStream *s = Audio::makeRawStream(data, size, sampleRate, Audio::FLAG_16BITS, DisposeAfterUse::NO);
+ Audio::AudioStream *stream = new Audio::LoopingAudioStream(s, repetitions);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, stream);
+ } else
+ debugC(1, kFreescapeDebugMedia, "WARNING: Sound %d is empty", index);
+}
+
+void FreescapeEngine::stopAllSounds(Audio::SoundHandle &handle) {
+ debugC(1, kFreescapeDebugMedia, "Stopping sound");
+ _mixer->stopHandle(handle);
+}
+
+void FreescapeEngine::waitForSounds() {
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
+ while (_mixer->isSoundHandleActive(_soundFxHandle))
+ waitInLoop(10);
+ else {
+ while (!_speaker->endOfStream())
+ waitInLoop(10);
+ }
+}
+
+bool FreescapeEngine::isPlayingSound() {
+ if (_usePrerecordedSounds || isAmiga() || isAtariST() || isCPC())
+ return _mixer->isSoundHandleActive(_soundFxHandle);
+
+ return (!_speaker->endOfStream());
+}
+
+void FreescapeEngine::playSilence(int duration, bool sync) {
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, 1000 * 10 * duration);
+ _mixer->stopHandle(_soundFxHandle);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &_soundFxHandle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+}
+
+void FreescapeEngine::queueSoundConst(double hzFreq, int duration) {
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, 1000 * 10 * duration);
+}
+
+void FreescapeEngine::loadSoundsFx(Common::SeekableReadStream *file, int offset, int number) {
+ file->seek(offset);
+ soundFx *sound = nullptr;
+ _soundsFx[0] = sound;
+ for (int i = 1; i < number + 1; i++) {
+ sound = (soundFx *)malloc(sizeof(soundFx));
+ int zero = file->readUint16BE();
+ assert(zero == 0);
+ int size = file->readUint16BE();
+ float sampleRate = float(file->readUint16BE()) / 2;
+ debugC(1, kFreescapeDebugParser, "Loading sound: %d (size: %d, sample rate: %f) at %" PRIx64, i, size, sampleRate, file->pos());
+ byte *data = (byte *)malloc(size * sizeof(byte));
+ file->read(data, size);
+ sound->sampleRate = sampleRate;
+ sound->size = size;
+ sound->data = (byte *)data;
+ sound->repetitions = 1;
+ _soundsFx[i] = sound;
+ }
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/sound/cpc.cpp b/engines/freescape/sound/cpc.cpp
new file mode 100644
index 00000000000..36ca4e327bc
--- /dev/null
+++ b/engines/freescape/sound/cpc.cpp
@@ -0,0 +1,407 @@
+/* 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 "freescape/freescape.h"
+
+namespace Freescape {
+
+/**
+ * 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::AY8912Stream {
+public:
+ CPCSfxStream(int index, const byte *soundDefTable, int soundDefTableSize,
+ const byte *toneTable, const byte *envelopeTable, int rate = 44100)
+ : AY8912Stream(rate, 1000000),
+ _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++)
+ setReg(r, 0);
+ // Noise period from CPC init table (verified in binary)
+ 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 = (getRate() / 300) * 2;
+
+ while (samplesGenerated < numSamples && !_finished) {
+ // Generate samples until next tick
+ int remaining = samplesPerTick - _tickSampleCount;
+ int toGenerate = MIN(numSamples - samplesGenerated, remaining);
+
+ if (toGenerate > 0) {
+ generateSamples(buffer + samplesGenerated, toGenerate);
+ samplesGenerated += toGenerate;
+ _tickSampleCount += toGenerate;
+ }
+
+ // Run interrupt handler at 300Hz tick boundary
+ if (_tickSampleCount >= samplesPerTick) {
+ _tickSampleCount -= samplesPerTick;
+ tickUpdate();
+ }
+ }
+
+ return samplesGenerated;
+ }
+
+ bool endOfData() const override { return _finished; }
+ bool endOfStream() const override { return _finished; }
+
+private:
+ 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) {
+ 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->toAudioStream(), -1, kFreescapeDefaultVolume, 0, DisposeAfterUse::YES);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/sound/dos.cpp b/engines/freescape/sound/dos.cpp
new file mode 100644
index 00000000000..8b6ee48e84e
--- /dev/null
+++ b/engines/freescape/sound/dos.cpp
@@ -0,0 +1,136 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/freescape.h"
+
+namespace Freescape {
+
+void FreescapeEngine::loadSpeakerFxDOS(Common::SeekableReadStream *file, int offsetFreq, int offsetTable, int numberSounds) {
+ debugC(1, kFreescapeDebugParser, "Reading PC speaker sound table for DOS");
+ for (int i = 1; i <= numberSounds; i++) {
+ debugC(1, kFreescapeDebugParser, "Reading sound table entry: %d ", i);
+ int soundIdx = (i - 1) * 4;
+ file->seek(offsetFreq + soundIdx);
+ uint16 index = file->readByte();
+ if (index == 0xff)
+ continue;
+ uint iVar = index * 5;
+
+ uint16 frequencyStart = file->readUint16LE();
+ uint8 repetitions = file->readByte();
+ debugC(1, kFreescapeDebugParser, "Frequency start: %d ", frequencyStart);
+ debugC(1, kFreescapeDebugParser, "Repetitions: %d ", repetitions);
+
+ uint8 frequencyStepsNumber = 0;
+ uint16 frequencyStep = 0;
+
+ file->seek(offsetTable + iVar);
+ uint8 lastIndex = file->readByte();
+ debugC(1, kFreescapeDebugParser, "0x%x %d (lastIndex)", offsetTable - 0x200, lastIndex);
+
+ frequencyStepsNumber = file->readByte();
+ debugC(1, kFreescapeDebugParser, "0x%x %d (frequency steps)", offsetTable + 1 - 0x200, frequencyStepsNumber);
+
+ int basePtr = offsetTable + iVar + 1;
+ debugC(1, kFreescapeDebugParser, "0x%x (basePtr)", basePtr - 0x200);
+
+ frequencyStep = file->readUint16LE();
+ debugC(1, kFreescapeDebugParser, "0x%x %d (steps number)", offsetTable + 2 - 0x200, (int16)frequencyStep);
+
+ uint8 frequencyDuration = file->readByte();
+ debugC(1, kFreescapeDebugParser, "0x%x %d (frequency duration)", offsetTable + 4 - 0x200, frequencyDuration);
+
+ soundSpeakerFx *speakerFxInfo = new soundSpeakerFx();
+ _soundsSpeakerFx[i] = speakerFxInfo;
+
+ speakerFxInfo->frequencyStart = frequencyStart;
+ speakerFxInfo->repetitions = repetitions;
+ speakerFxInfo->frequencyStepsNumber = frequencyStepsNumber;
+ speakerFxInfo->frequencyStep = frequencyStep;
+ speakerFxInfo->frequencyDuration = frequencyDuration;
+
+ for (int j = 1; j < lastIndex; j++) {
+
+ soundSpeakerFx *speakerFxInfoAdditionalStep = new soundSpeakerFx();
+ speakerFxInfoAdditionalStep->frequencyStart = 0;
+ speakerFxInfoAdditionalStep->repetitions = 0;
+
+ file->seek(basePtr + 4 * j);
+ debugC(1, kFreescapeDebugParser, "Reading at %x", basePtr + 4 * j - 0x200);
+ frequencyStepsNumber = file->readByte();
+ debugC(1, kFreescapeDebugParser, "%d (steps number)", frequencyStepsNumber);
+ frequencyStep = file->readUint16LE();
+ debugC(1, kFreescapeDebugParser, "%d (frequency step)", (int16)frequencyStep);
+ frequencyDuration = file->readByte();
+ debugC(1, kFreescapeDebugParser, "%d (frequency duration)", frequencyDuration);
+
+ speakerFxInfoAdditionalStep->frequencyStepsNumber = frequencyStepsNumber;
+ speakerFxInfoAdditionalStep->frequencyStep = frequencyStep;
+ speakerFxInfoAdditionalStep->frequencyDuration = frequencyDuration;
+ speakerFxInfo->additionalSteps.push_back(speakerFxInfoAdditionalStep);
+ }
+ debugC(1, kFreescapeDebugParser, "\n");
+ }
+}
+
+uint16 FreescapeEngine::playSoundDOSSpeaker(uint16 frequencyStart, soundSpeakerFx *speakerFxInfo) {
+ uint8 frequencyStepsNumber = speakerFxInfo->frequencyStepsNumber;
+ int16 frequencyStep = speakerFxInfo->frequencyStep;
+ uint8 frequencyDuration = speakerFxInfo->frequencyDuration;
+
+ int16 freq = frequencyStart;
+ int waveDurationMultipler = 1800;
+ int waveDuration = waveDurationMultipler * (frequencyDuration + 1);
+
+ while (true) {
+ if (freq > 0) {
+ float hzFreq = 1193180.0 / freq;
+ debugC(1, kFreescapeDebugMedia, "raw %d, hz: %f, duration: %d", freq, hzFreq, waveDuration);
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, waveDuration);
+ }
+ if (frequencyStepsNumber > 0) {
+ // Ascending initial portions of cycle
+ freq += frequencyStep;
+ frequencyStepsNumber--;
+ } else
+ break;
+ }
+
+ return freq;
+}
+
+void FreescapeEngine::playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle) {
+ uint freq = speakerFxInfo->frequencyStart;
+
+ for (int i = 0; i < speakerFxInfo->repetitions; i++) {
+ freq = playSoundDOSSpeaker(freq, speakerFxInfo);
+
+ for (auto &it : speakerFxInfo->additionalSteps) {
+ assert(it);
+ freq = playSoundDOSSpeaker(freq, it);
+ }
+ }
+
+ _mixer->stopHandle(handle);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/sound/zx.cpp b/engines/freescape/sound/zx.cpp
new file mode 100644
index 00000000000..33672f49186
--- /dev/null
+++ b/engines/freescape/sound/zx.cpp
@@ -0,0 +1,357 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/freescape.h"
+#include "freescape/games/eclipse/eclipse.h"
+
+namespace Freescape {
+
+void FreescapeEngine::loadSpeakerFxZX(Common::SeekableReadStream *file, int sfxTable, int sfxData) {
+ debugC(1, kFreescapeDebugParser, "Reading sound table for ZX");
+ int numberSounds = 25;
+
+ if (isDark())
+ numberSounds = 34;
+
+ if (isEclipse() && (_variant & GF_ZX_DEMO_MICROHOBBY))
+ numberSounds = 21;
+
+ for (int i = 1; i < numberSounds; i++) {
+ debugC(1, kFreescapeDebugParser, "Reading sound table entry: %d ", i);
+ _soundsSpeakerFxZX[i] = new Common::Array<soundUnitZX>();
+ int soundIdx = (i - 1) * 4;
+ file->seek(sfxTable + soundIdx);
+
+ byte SFXtempStruct[8] = {};
+
+ uint8 dataIndex = file->readByte();
+ uint16 soundValue = file->readUint16LE();
+ SFXtempStruct[0] = file->readByte();
+
+ file->seek(sfxData + dataIndex * 4);
+ uint8 soundType = file->readByte();
+ int original_sound_ptr = sfxData + dataIndex * 4 + 1;
+ int sound_ptr = original_sound_ptr;
+ uint8 soundSize = 0;
+ int16 repetitions = 0;
+ debugC(1, kFreescapeDebugParser, "dataIndex: %x, value: %x, SFXtempStruct[0]: %x, type: %x", dataIndex, soundValue, SFXtempStruct[0], soundType);
+ if (soundType == 0xff)
+ break;
+
+ if ((soundType & 0x80) == 0) {
+ SFXtempStruct[6] = 0;
+ SFXtempStruct[4] = soundType;
+
+ while (true) {
+ while (true) {
+ file->seek(sound_ptr);
+ //debug("start sound ptr: %x", sound_ptr);
+ soundSize = file->readByte();
+ SFXtempStruct[1] = soundSize;
+ SFXtempStruct[2] = file->readByte();
+ SFXtempStruct[3] = file->readByte();
+
+ for (int j = 0; j <= 7; j++)
+ debugC(1, kFreescapeDebugParser, "SFXtempStruct[%d]: %x", j, SFXtempStruct[j]);
+
+ do {
+ uint32 var9 = 0xffffff & (SFXtempStruct[3] * 0xd0);
+ uint32 var10 = var9 / soundValue;
+
+ var9 = 0xffffff & (7 * soundValue);
+ uint16 var5 = (0xffff & var9) - 0x1e;
+ if ((short)var5 < 0)
+ var5 = 1;
+
+ soundUnitZX soundUnit;
+ soundUnit.isRaw = false;
+ soundUnit.freqTimesSeconds = (var10 & 0xffff) + 1;
+ soundUnit.tStates = var5;
+ soundUnit.multiplier = 10;
+ //debug("playSFX(%x, %x)", soundUnit.freqTimesSeconds, soundUnit.tStates);
+ _soundsSpeakerFxZX[i]->push_back(soundUnit);
+ int16 var4 = 0;
+
+ if ((SFXtempStruct[2] & 0x80) != 0) {
+ var4 = 0xff;
+ }
+ //debug("var4: %d", var4);
+ //debug("soundValue delta: %d", int16(((var4 << 8) | SFXtempStruct[2])));
+ soundValue = soundValue + int16(((var4 << 8) | SFXtempStruct[2]));
+ //debug("soundValue: %x", soundValue);
+ soundSize = soundSize - 1;
+ } while (soundSize != 0);
+ SFXtempStruct[5] = SFXtempStruct[5] + 1;
+ if (SFXtempStruct[5] == SFXtempStruct[4])
+ break;
+
+ sound_ptr = original_sound_ptr + SFXtempStruct[5] * 3;
+ //debug("sound ptr: %x", sound_ptr);
+ }
+
+ soundSize = SFXtempStruct[0];
+ SFXtempStruct[0] = soundSize - 1;
+ sound_ptr = original_sound_ptr;
+ if ((soundSize - 1) == 0)
+ break;
+ SFXtempStruct[5] = 0;
+ }
+ } else if (soundType & 0x80) {
+ file->seek(sound_ptr);
+ for (int j = 1; j <= 7; j++) {
+ SFXtempStruct[j] = file->readByte();
+ debugC(1, kFreescapeDebugParser, "SFXtempStruct[%d]: %x", j, SFXtempStruct[j]);
+ }
+ soundSize = SFXtempStruct[0];
+ repetitions = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
+ uint16 var5 = soundValue;
+ //debug("Repetitions: %x", repetitions);
+ if ((soundType & 0x7f) == 1) {
+ do {
+ do {
+ soundUnitZX soundUnit;
+ soundUnit.isRaw = false;
+ soundUnit.tStates = var5;
+ soundUnit.freqTimesSeconds = SFXtempStruct[3] | (SFXtempStruct[4] << 8);
+ soundUnit.multiplier = 1.8f;
+ //debug("playSFX(%x, %x)", soundUnit.freqTimesSeconds, soundUnit.tStates);
+ _soundsSpeakerFxZX[i]->push_back(soundUnit);
+ repetitions = repetitions - 1;
+ var5 = var5 + (SFXtempStruct[5] | (SFXtempStruct[6] << 8));
+
+ } while ((byte)((byte)repetitions | (byte)((uint16)repetitions >> 8)) != 0);
+ soundSize = soundSize - 1;
+ repetitions = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
+ var5 = soundValue;
+ } while (soundSize != 0);
+ } else if ((soundType & 0x7f) == 2) {
+ repetitions = SFXtempStruct[1] | (SFXtempStruct[0] << 8);
+ debugC(1, kFreescapeDebugParser, "Raw sound, repetitions: %x", repetitions);
+ uint16 sVar7 = SFXtempStruct[3];
+ soundType = 0;
+ soundSize = SFXtempStruct[2];
+ uint16 silenceSize = SFXtempStruct[4];
+ bool cond1 = (SFXtempStruct[4] != 0 && SFXtempStruct[4] != 2);
+ bool cond2 = SFXtempStruct[4] == 2;
+ bool cond3 = SFXtempStruct[4] == 0;
+
+ assert(cond1 || cond2 || cond3);
+ do {
+ soundUnitZX soundUnit;
+ soundUnit.isRaw = true;
+ int totalSize = soundSize + sVar7;
+ soundUnit.rawFreq = 0.1f;
+ soundUnit.rawLengthus = totalSize;
+ _soundsSpeakerFxZX[i]->push_back(soundUnit);
+ //debugN("%x ", silenceSize);
+ soundUnit.rawFreq = 0;
+ soundUnit.rawLengthus = silenceSize;
+ _soundsSpeakerFxZX[i]->push_back(soundUnit);
+ repetitions = repetitions + -1;
+ soundSize = SFXtempStruct[5] + soundSize;
+
+ if (cond1)
+ silenceSize = (repetitions & 0xff) | (repetitions >> 8);
+ else if (cond2)
+ silenceSize = (repetitions & 0xff);
+ else
+ silenceSize = soundSize;
+
+ //debug("soundSize: %x", soundSize);
+ //sVar7 = (uint16)bVar9 << 8;
+ } while (repetitions != 0);
+ //debug("\n");
+ //if (i == 15)
+ // assert(0);
+ } else {
+ debugC(1, kFreescapeDebugParser, "Sound type: %x", soundType);
+ bool beep = false;
+ do {
+ soundType = 0;
+ uint16 uVar2 = SFXtempStruct[1] | (SFXtempStruct[2] << 8);
+ uint8 cVar3 = 0;
+ do {
+ //debug("start cycle %d:", cVar3);
+ //ULA_PORT = bVar4;
+ //bVar4 = bVar4 ^ 0x10;
+ beep = !beep;
+ repetitions = (((uint16)soundType * 0x100 + (uint16)soundType * -2) -
+ (uint16)((uint16)soundType * 0x100 < (uint16)soundType)) + (uVar2 & 0xff);
+ uint8 bVar9 = (byte)repetitions;
+ uint8 bVar8 = (byte)((uint16)repetitions >> 8);
+ uint8 bVar1 = bVar9 - bVar8;
+ soundType = bVar1;
+ if (bVar8 <= bVar9) {
+ bVar1 = bVar1 - 1;
+ soundType = bVar1;
+ }
+ //debug("wait %d", bVar1);
+ assert(bVar1 > 0);
+ soundUnitZX soundUnit;
+ soundUnit.isRaw = false;
+ soundUnit.freqTimesSeconds = beep ? 1000 : 0;
+ soundUnit.tStates = beep ? 437500 / 1000 - 30.125 : 0;
+ soundUnit.multiplier = float(bVar1) / 500;
+ _soundsSpeakerFxZX[i]->push_back(soundUnit);
+
+ // No need to wait
+ //do {
+ // bVar1 = bVar1 - 1;
+ //} while (bVar1 != 0);
+ cVar3 = (char)(uVar2 >> 8) + -1;
+ uVar2 = (((uint16)cVar3) << 8) | (uint8)uVar2;
+ } while (cVar3 != '\0');
+ soundSize = soundSize + -1;
+ } while (soundSize != '\0');
+ }
+ }
+ }
+ //assert(0);
+}
+
+void FreescapeEngine::playSoundZX(Common::Array<soundUnitZX> *data, Audio::SoundHandle &handle) {
+ for (auto &it : *data) {
+ soundUnitZX value = it;
+
+ if (value.isRaw) {
+ debugC(1, kFreescapeDebugMedia, "raw hz: %f, duration: %d", value.rawFreq, value.rawLengthus);
+ if (value.rawFreq == 0) {
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 1, 5 * value.rawLengthus);
+ continue;
+ }
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, value.rawFreq, 5 * value.rawLengthus);
+ } else {
+ if (value.freqTimesSeconds == 0 && value.tStates == 0) {
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSilence, 1, 1000 * value.multiplier);
+ continue;
+ }
+
+ float hzFreq = 1 / ((value.tStates + 30.125) / 437500.0);
+ float waveDuration = value.freqTimesSeconds / hzFreq;
+ waveDuration = value.multiplier * 1000 * (waveDuration + 1);
+ debugC(1, kFreescapeDebugMedia, "non raw hz: %f, duration: %f", hzFreq, waveDuration);
+ _speaker->playQueue(Audio::PCSpeaker::kWaveFormSquare, hzFreq, waveDuration);
+ }
+ }
+
+ _mixer->stopHandle(_soundFxHandle);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
+}
+
+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: 854bacdd52227c29c45c0e3dd30951112ba86f5c
https://github.com/scummvm/scummvm/commit/854bacdd52227c29c45c0e3dd30951112ba86f5c
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-21T11:07:35+01:00
Commit Message:
FREESCAPE: moved driller sounds for zx into its own file
Changed paths:
A engines/freescape/games/driller/sounds.cpp
engines/freescape/freescape.h
engines/freescape/games/driller/driller.h
engines/freescape/module.mk
engines/freescape/sound/common.cpp
engines/freescape/sound/zx.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index 4e44f39eabd..c7b559b8130 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -489,7 +489,6 @@ public:
uint16 playSoundDOSSpeaker(uint16 startFrequency, soundSpeakerFx *speakerFxInfo);
void playSoundDOS(soundSpeakerFx *speakerFxInfo, bool sync, Audio::SoundHandle &handle);
- void playSoundDrillerZX(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);
@@ -498,6 +497,7 @@ public:
void loadSpeakerFxZX(Common::SeekableReadStream *file, int sfxTable, int sfxData);
Common::HashMap<uint16, soundSpeakerFx *> _soundsSpeakerFx;
+ virtual void playSoundZX(int index, Audio::SoundHandle &handle);
void playSoundZX(Common::Array<soundUnitZX> *data, Audio::SoundHandle &handle);
Common::HashMap<uint16, Common::Array<soundUnitZX>*> _soundsSpeakerFxZX;
diff --git a/engines/freescape/games/driller/driller.h b/engines/freescape/games/driller/driller.h
index ac9281c9097..e57f131fddc 100644
--- a/engines/freescape/games/driller/driller.h
+++ b/engines/freescape/games/driller/driller.h
@@ -61,6 +61,7 @@ public:
void gotoArea(uint16 areaID, int entranceID) override;
+ void playSoundZX(int index, Audio::SoundHandle &handle) override;
void drawInfoMenu() override;
void drawSensorShoot(Sensor *sensor) override;
void drawCompass(Graphics::Surface *surface, int x, int y, double degrees, double magnitude, double fov, uint32 color);
diff --git a/engines/freescape/games/driller/sounds.cpp b/engines/freescape/games/driller/sounds.cpp
new file mode 100644
index 00000000000..60f02efb49c
--- /dev/null
+++ b/engines/freescape/games/driller/sounds.cpp
@@ -0,0 +1,125 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#include "freescape/freescape.h"
+#include "freescape/games/driller/driller.h"
+
+namespace Freescape {
+
+void DrillerEngine::playSoundZX(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;
+ }
+
+ FreescapeEngine::playSoundZX(&soundUnits, handle);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index f8f74aebce1..4cc77bfef9e 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -28,6 +28,7 @@ MODULE_OBJS := \
games/driller/cpc.o \
games/driller/dos.o \
games/driller/driller.o \
+ games/driller/sounds.o \
games/driller/zx.o \
games/eclipse/atari.o \
games/eclipse/c64.o \
diff --git a/engines/freescape/sound/common.cpp b/engines/freescape/sound/common.cpp
index fbb45cad403..ee50f2cd4dd 100644
--- a/engines/freescape/sound/common.cpp
+++ b/engines/freescape/sound/common.cpp
@@ -54,10 +54,7 @@ void FreescapeEngine::playSound(int index, bool sync, Audio::SoundHandle &handle
return;
} else if (isSpectrum()) {
- if (isDriller())
- playSoundDrillerZX(index, handle);
- else
- playSoundZX(_soundsSpeakerFxZX[index], handle);
+ playSoundZX(index, handle);
return;
} else if (isCPC()) {
playSoundCPC(index, handle);
diff --git a/engines/freescape/sound/zx.cpp b/engines/freescape/sound/zx.cpp
index 33672f49186..301dded776e 100644
--- a/engines/freescape/sound/zx.cpp
+++ b/engines/freescape/sound/zx.cpp
@@ -256,102 +256,8 @@ void FreescapeEngine::playSoundZX(Common::Array<soundUnitZX> *data, Audio::Sound
_mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, _speaker, -1, Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::NO);
}
-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);
+void FreescapeEngine::playSoundZX(int index, Audio::SoundHandle &handle) {
+ playSoundZX(_soundsSpeakerFxZX[index], handle);
}
} // namespace Freescape
Commit: 6843d0ba79b97d29458a6add767a5762a09ad294
https://github.com/scummvm/scummvm/commit/6843d0ba79b97d29458a6add767a5762a09ad294
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-21T11:07:35+01:00
Commit Message:
FREESCAPE: initial implementation of castle master amiga sounds
Changed paths:
A engines/freescape/sound/amiga.cpp
engines/freescape/freescape.h
engines/freescape/games/castle/amiga.cpp
engines/freescape/module.mk
engines/freescape/sound.h
engines/freescape/sound/common.cpp
diff --git a/engines/freescape/freescape.h b/engines/freescape/freescape.h
index c7b559b8130..6a16947929d 100644
--- a/engines/freescape/freescape.h
+++ b/engines/freescape/freescape.h
@@ -506,6 +506,11 @@ public:
Common::Array<byte> _soundsCPCEnvelopeTable;
Common::Array<byte> _soundsCPCSoundDefTable;
+ void loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds);
+ void playSoundAmiga(int index, Audio::SoundHandle &handle);
+ Common::Array<AmigaSfxEntry> _amigaSfxTable;
+ Common::Array<AmigaDmaSample> _amigaDmaSamples;
+
int _soundIndexShoot;
int _soundIndexCollide;
int _soundIndexStepDown;
diff --git a/engines/freescape/games/castle/amiga.cpp b/engines/freescape/games/castle/amiga.cpp
index f0af67820e7..7526481f599 100644
--- a/engines/freescape/games/castle/amiga.cpp
+++ b/engines/freescape/games/castle/amiga.cpp
@@ -310,6 +310,10 @@ void CastleEngine::loadAssetsAmigaDemo() {
}
}
+ // Load synthesized sound effects from command table
+ // Table at file offset 0x1469E (memory 0x14682), 30 entries
+ loadSoundsAmigaDemo(&file, 0x1469E, 30);
+
// Load embedded ProTracker module for background music
// Module is at file offset 0x3D5A6 (memory 0x3D58A), ~86260 bytes
static const int kModOffset = 0x3D5A6;
diff --git a/engines/freescape/module.mk b/engines/freescape/module.mk
index 4cc77bfef9e..ea12765c2a4 100644
--- a/engines/freescape/module.mk
+++ b/engines/freescape/module.mk
@@ -49,6 +49,7 @@ MODULE_OBJS := \
objects/group.o \
objects/sensor.o \
sweepAABB.o \
+ sound/amiga.o \
sound/common.o \
sound/cpc.o \
sound/dos.o \
diff --git a/engines/freescape/sound.h b/engines/freescape/sound.h
index f937a6e3c5e..ab420cc8c4b 100644
--- a/engines/freescape/sound.h
+++ b/engines/freescape/sound.h
@@ -52,6 +52,15 @@ struct soundSpeakerFx {
Common::Array<struct soundSpeakerFx *>additionalSteps;
};
+struct AmigaSfxEntry {
+ byte priority;
+ Common::Array<uint16> commands;
+};
+
+struct AmigaDmaSample {
+ Common::Array<int8> data;
+};
+
// TODO: Migrate to Audio::PCSpeaker
class SizedPCSpeaker : public Audio::PCSpeakerStream {
public:
diff --git a/engines/freescape/sound/amiga.cpp b/engines/freescape/sound/amiga.cpp
new file mode 100644
index 00000000000..c01711b928f
--- /dev/null
+++ b/engines/freescape/sound/amiga.cpp
@@ -0,0 +1,450 @@
+/* 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/audiostream.h"
+#include "audio/mods/module.h"
+#include "audio/mods/paula.h"
+
+#include "common/memstream.h"
+
+#include "freescape/freescape.h"
+
+namespace Freescape {
+
+/**
+ * Amiga Sound Effect Synthesizer
+ *
+ * Synthesizes sound effects from a command stream, emulating the Castle Master
+ * Amiga demo's custom sound engine. All 4 Amiga audio channels play the same
+ * 64-byte square wave buffer (alternating +64/-64 signed bytes).
+ *
+ * Command format: 16-bit big-endian words.
+ * Bits 15-12: command type
+ * Bits 11-0: parameter
+ *
+ * Period commands (set absolute frequency):
+ * 0x0xxx: AUD1 period = xxx (0 disables channel)
+ * 0x1xxx: AUD2 period = xxx
+ * 0x2xxx: AUD3 period = xxx
+ * 0x3xxx: AUD0 period = xxx
+ *
+ * Relative period commands (pitch bend):
+ * 0x8xxx: AUD1 period += sign_extend_12(xxx)
+ * 0x9xxx: AUD2 period += sign_extend_12(xxx)
+ * 0xAxxx: AUD3 period += sign_extend_12(xxx)
+ * 0xBxxx: AUD0 period += sign_extend_12(xxx)
+ *
+ * Volume commands (channel select in bits 11-8):
+ * 0x4Yxx: set volume = xx (Y=1: AUD1, Y=2: AUD2, else: AUD0+AUD3)
+ * 0xCYxx: volume += sign_extend_8(xx) (same channel mapping)
+ *
+ * Control commands:
+ * 0x5NNN: play note (reads 3 extra words; 3rd word = DMA repeat count)
+ * 0x6xxx: delay xxx VBI ticks (50Hz)
+ * 0x7000: full stop (silence all, end stream)
+ * 0x7001: pause until DMA playback completes
+ * 0x7002: loop (decrement counter, jump to saved position if > 0)
+ * 0xDxxx: save loop position, set loop counter = xxx
+ * 0xFxxx: end (stop interpreter)
+ *
+ * Tone frequency: 3,546,895 / (period * 2) Hz.
+ * Paula plays bytes at 3,546,895 / period, and the 0x40/0xC0 waveform
+ * alternates every byte (2 samples per cycle), adding a /2.
+ */
+class AmigaSfxStream : public Audio::Paula {
+public:
+ AmigaSfxStream(const uint16 *commands, int numCommands, const Common::Array<AmigaDmaSample> *dmaSamples, int rate = 44100)
+ : Audio::Paula(false, rate, rate / 50),
+ _dmaSamples(dmaSamples),
+ _cmdPos(0), _delay(0), _paused(false),
+ _dmaCounter(0), _dmaAud0Active(false), _loopPos(0), _loopCounter(0),
+ _graceCounter(0) {
+
+ _commands.resize(numCommands);
+ for (int i = 0; i < numCommands; i++)
+ _commands[i] = commands[i];
+
+ for (int i = 0; i < 64; i += 2) {
+ _squareWave[i] = 64;
+ _squareWave[i + 1] = -64;
+ }
+
+ // FUN_2520 init values.
+ static const uint16 initPeriods[4] = { 0x1A1, 0x1AB, 0x1B5, 0x1BF };
+ for (int ch = 0; ch < 4; ch++) {
+ _periodShadow[ch] = initPeriods[ch];
+ _volumeShadow[ch] = 0;
+
+ setChannelSampleStart(ch, _squareWave);
+ setChannelSampleLen(ch, 0x20); // 32 words = 64 bytes
+ setChannelPeriod(ch, _periodShadow[ch]);
+ setChannelVolume(ch, 0);
+ // FUN_2520 writes DMACON=0x000F (clear audio DMA bits), so channels
+ // are configured but disabled until command handlers enable them.
+ disableChannel(ch);
+ }
+ startPaula();
+ }
+
+private:
+ void interrupt() override {
+ tickUpdate();
+ }
+
+ Common::Array<uint16> _commands;
+ const Common::Array<AmigaDmaSample> *_dmaSamples;
+ int _cmdPos;
+ int _delay; // -1 = stopped, 0 = execute next, >0 = waiting
+ bool _paused; // Waiting for DMA completion
+ int _dmaCounter; // DMA ticks remaining (approximate)
+ bool _dmaAud0Active;
+ int _loopPos; // Saved command position for looping
+ int _loopCounter; // Loop iterations remaining
+ int _graceCounter; // Ticks to keep playing after END before finishing
+ uint16 _periodShadow[4];
+ int _volumeShadow[4];
+ int8 _squareWave[64];
+
+ /**
+ * Map period command nibble (0-3) to internal channel index.
+ * Command 0 -> AUD1 (ch 1), 1 -> AUD2 (ch 2), 2 -> AUD3 (ch 3), 3 -> AUD0 (ch 0)
+ */
+ static int periodCmdToChannel(int nibble) {
+ return (nibble + 1) & 3;
+ }
+
+ uint8 clampVolume(int value) const {
+ return (uint8)CLIP<int>(value, 0, 64);
+ }
+
+ void setAbsolutePeriod(int ch, uint16 period) {
+ if (ch == 0 && !_dmaAud0Active) {
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ }
+ _periodShadow[ch] = period;
+ setChannelPeriod(ch, period);
+ enableChannel(ch);
+ }
+
+ void setRelativePeriod(int ch, int16 delta) {
+ if (ch == 0 && !_dmaAud0Active) {
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ }
+ uint16 newPeriod = (uint16)(_periodShadow[ch] + delta);
+ if (newPeriod == 0) {
+ disableChannel(ch);
+ return;
+ }
+ _periodShadow[ch] = newPeriod;
+ setChannelPeriod(ch, newPeriod);
+ enableChannel(ch);
+ }
+
+ void setAbsoluteVolume(int sel, uint8 vol) {
+ if (sel == 1) {
+ _volumeShadow[1] = vol;
+ setChannelVolume(1, clampVolume(_volumeShadow[1]));
+ } else if (sel == 2) {
+ _volumeShadow[2] = vol;
+ setChannelVolume(2, clampVolume(_volumeShadow[2]));
+ } else {
+ _volumeShadow[0] = vol;
+ _volumeShadow[3] = vol;
+ setChannelVolume(0, clampVolume(_volumeShadow[0]));
+ setChannelVolume(3, clampVolume(_volumeShadow[3]));
+ }
+ }
+
+ void addRelativeVolume(int sel, int8 delta) {
+ if (sel == 1) {
+ _volumeShadow[1] += delta;
+ setChannelVolume(1, clampVolume(_volumeShadow[1]));
+ } else if (sel == 2) {
+ _volumeShadow[2] += delta;
+ setChannelVolume(2, clampVolume(_volumeShadow[2]));
+ } else {
+ _volumeShadow[0] += delta;
+ _volumeShadow[3] += delta;
+ setChannelVolume(0, clampVolume(_volumeShadow[0]));
+ setChannelVolume(3, clampVolume(_volumeShadow[3]));
+ }
+ }
+
+ void tickUpdate() {
+ if (_dmaCounter > 0) {
+ _dmaCounter--;
+ if (_dmaCounter == 0 && _dmaAud0Active) {
+ disableChannel(0);
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ _dmaAud0Active = false;
+ }
+ }
+
+ if (_paused) {
+ if (_dmaCounter <= 0)
+ _paused = false;
+ else
+ return;
+ }
+
+ if (_delay < 0) {
+ // After END command, allow a grace period so short sounds
+ // remain audible (original hardware keeps channels playing
+ // until next sound trigger silences them).
+ if (_graceCounter > 0) {
+ _graceCounter--;
+ return;
+ }
+ _dmaAud0Active = false;
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ stopPaula();
+ return;
+ }
+
+ if (_delay > 0) {
+ _delay--;
+ return;
+ }
+
+ // _delay == 0: execute commands
+ executeCommands();
+ }
+
+ void executeCommands() {
+ // Process commands until we hit a delay, stop, or end
+ while (_cmdPos < (int)_commands.size()) {
+ uint16 cmd = _commands[_cmdPos++];
+ int nibble = (cmd >> 12) & 0xF;
+ int param = cmd & 0xFFF;
+
+ switch (nibble) {
+ case 0: case 1: case 2: case 3: {
+ // Set absolute period
+ int ch = periodCmdToChannel(nibble);
+ if (param == 0) {
+ disableChannel(ch);
+ } else {
+ setAbsolutePeriod(ch, (uint16)param);
+ }
+ break;
+ }
+
+ case 4: {
+ // Set volume
+ int sel = (param >> 8) & 0xF;
+ int vol = param & 0xFF;
+ setAbsoluteVolume(sel, (uint8)vol);
+ break;
+ }
+
+ case 5: {
+ // Play note: NNN selects a sample, 3 extra words follow.
+ // FUN_26C2 does SUBQ #1, D0 (D0=NNN). If D0<0 (NNN=0) -> NO-OP.
+ // NNN>0: triggers DMA playback of a sample buffer on AUD0.
+ // Extra words: D2=start offset, D4=end trim, D3=repeat count.
+ // DMA plays buffer (D3+1) times total (SUBQ #1 + BPL counting).
+ if (_cmdPos + 3 <= (int)_commands.size()) {
+ uint16 startOffset = _commands[_cmdPos++]; // D2
+ uint16 endTrim = _commands[_cmdPos++]; // D4
+ uint16 dmaCount = _commands[_cmdPos++]; // D3
+ if (param > 0 && dmaCount > 0 && _periodShadow[0] > 0) {
+ int bufSize = 256;
+ if (_dmaSamples && param < (int)_dmaSamples->size()) {
+ const AmigaDmaSample &sample = (*_dmaSamples)[param];
+ if (!sample.data.empty()) {
+ int start = MIN<int>(startOffset, sample.data.size());
+ int trim = MIN<int>(endTrim, sample.data.size() - start);
+ int playLen = sample.data.size() - start - trim;
+ if (playLen > 1) {
+ const int8 *src = sample.data.data() + start;
+ // AUD0LC/AUD0LEN are reloaded on each DMA completion,
+ // so the selected segment repeats in full.
+ setChannelData(0, src, src, playLen, playLen);
+ bufSize = playLen;
+ }
+ }
+ }
+
+ double durationSec = (dmaCount + 1) * bufSize * _periodShadow[0] / Audio::Paula::kPalPaulaClock;
+ _dmaCounter = (int)(durationSec * 50.0) + 1;
+ _dmaAud0Active = true;
+ enableChannel(0);
+ }
+ }
+ break;
+ }
+
+ case 6:
+ // Delay
+ _delay = param;
+ return;
+
+ case 7:
+ if (param == 0x000) {
+ // Full stop: silence all channels
+ for (int ch = 0; ch < 4; ch++) {
+ _volumeShadow[ch] = 0;
+ setChannelVolume(ch, 0);
+ disableChannel(ch);
+ }
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ _dmaAud0Active = false;
+ _delay = -1;
+ stopPaula();
+ return;
+ } else if (param == 0x001) {
+ // Pause: wait for DMA completion
+ _paused = true;
+ return;
+ } else if (param == 0x002) {
+ // Loop: decrement counter, jump back if > 0
+ _loopCounter--;
+ if (_loopCounter > 0)
+ _cmdPos = _loopPos;
+ break;
+ }
+ break;
+
+ case 8: case 9: case 0xA: case 0xB: {
+ // Relative period (pitch bend)
+ int ch = periodCmdToChannel(nibble - 8);
+ // Sign-extend 12-bit parameter
+ int16 delta = (int16)(param << 4) >> 4;
+ setRelativePeriod(ch, delta);
+ break;
+ }
+
+ case 0xC: {
+ // Relative volume
+ int sel = (param >> 8) & 0xF;
+ int8 delta = (int8)(param & 0xFF);
+ addRelativeVolume(sel, delta);
+ break;
+ }
+
+ case 0xD:
+ // Save loop position and set counter
+ _loopPos = _cmdPos;
+ _loopCounter = param;
+ break;
+
+ case 0xF:
+ // End: stop interpreter but let channels keep playing.
+ // On real Amiga hardware, audio DMA channels loop their
+ // waveform buffer continuously until the next FUN_2652 call
+ // silences them. playSoundAmiga() calls stopHandle() before
+ // playing a new sound, matching this behavior.
+ // Grace period of 25 ticks (500ms) approximates the typical
+ // inter-sound gap during gameplay.
+ _delay = -1;
+ _graceCounter = 25;
+ return;
+
+ default:
+ break;
+ }
+ }
+
+ // Ran out of commands
+ _delay = -1;
+ _dmaAud0Active = false;
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ setChannelOffset(0, Audio::Paula::Offset(0));
+ stopPaula();
+ }
+};
+
+void FreescapeEngine::loadSoundsAmigaDemo(Common::SeekableReadStream *file, int offset, int numSounds) {
+ file->seek(offset);
+ _amigaSfxTable.clear();
+ for (int i = 0; i < numSounds; i++) {
+ AmigaSfxEntry entry;
+ uint16 header = file->readUint16BE();
+ entry.priority = header >> 8;
+ int numWords = header & 0xFF;
+ entry.commands.resize(numWords);
+ for (int j = 0; j < numWords; j++)
+ entry.commands[j] = file->readUint16BE();
+ _amigaSfxTable.push_back(entry);
+ debugC(1, kFreescapeDebugParser, "Amiga SFX %d: priority=%d, commands=%d", i, entry.priority, numWords);
+ }
+ debugC(1, kFreescapeDebugParser, "Loaded %d Amiga sound effects", numSounds);
+
+ // Prepare DMA sample set for 0x5 commands from the embedded ProTracker module.
+ // Parameter N uses index N (1-based), so keep index 0 empty.
+ _amigaDmaSamples.clear();
+ _amigaDmaSamples.resize(12);
+
+ static const int kModOffset = 0x3D5A6;
+ if (file->size() > kModOffset + 1084) {
+ int modSize = file->size() - kModOffset;
+ Common::Array<byte> modBytes;
+ modBytes.resize(modSize);
+ file->seek(kModOffset);
+ file->read(modBytes.data(), modSize);
+
+ Common::MemoryReadStream modStream(modBytes.data(), modBytes.size());
+ Modules::Module module;
+ if (module.load(modStream, 0)) {
+ for (int i = 1; i <= 10; i++) {
+ const Modules::sample_t &sample = module.sample[i - 1];
+ if (sample.len > 0 && sample.data) {
+ _amigaDmaSamples[i].data.resize(sample.len);
+ memcpy(_amigaDmaSamples[i].data.data(), sample.data, sample.len);
+ }
+ }
+ }
+ }
+}
+
+void FreescapeEngine::playSoundAmiga(int index, Audio::SoundHandle &handle) {
+ if (index < 0 || index >= (int)_amigaSfxTable.size()) {
+ debugC(1, kFreescapeDebugMedia, "Amiga sound %d out of range (have %d)", index, (int)_amigaSfxTable.size());
+ return;
+ }
+
+ const AmigaSfxEntry &entry = _amigaSfxTable[index];
+ if (entry.commands.empty()) {
+ debugC(1, kFreescapeDebugMedia, "Amiga sound %d has no commands", index);
+ return;
+ }
+
+ debugC(1, kFreescapeDebugMedia, "Playing Amiga sound %d (priority=%d, commands=%d)",
+ index, entry.priority, (int)entry.commands.size());
+
+ AmigaSfxStream *stream = new AmigaSfxStream(entry.commands.data(), entry.commands.size(), &_amigaDmaSamples);
+ _mixer->stopHandle(handle);
+ _mixer->playStream(Audio::Mixer::kSFXSoundType, &handle, stream, -1,
+ Audio::Mixer::kMaxChannelVolume, 0, DisposeAfterUse::YES);
+}
+
+} // namespace Freescape
diff --git a/engines/freescape/sound/common.cpp b/engines/freescape/sound/common.cpp
index ee50f2cd4dd..8c59906ca41 100644
--- a/engines/freescape/sound/common.cpp
+++ b/engines/freescape/sound/common.cpp
@@ -89,6 +89,11 @@ void FreescapeEngine::playMusic(const Common::Path &filename) {
}
void FreescapeEngine::playSoundFx(int index, bool sync) {
+ if (!_amigaSfxTable.empty()) {
+ playSoundAmiga(index, _soundFxHandle);
+ return;
+ }
+
if (_soundsFx.size() == 0) {
debugC(1, kFreescapeDebugMedia, "WARNING: Sounds are not loaded");
return;
Commit: 7a52f26df7459b7316a7189f8fdd0248a0887797
https://github.com/scummvm/scummvm/commit/7a52f26df7459b7316a7189f8fdd0248a0887797
Author: neuromancer (gustavo.grieco at gmail.com)
Date: 2026-02-21T11:07:35+01:00
Commit Message:
FREESCAPE: fixes for castle master amiga sounds
Changed paths:
engines/freescape/sound/amiga.cpp
diff --git a/engines/freescape/sound/amiga.cpp b/engines/freescape/sound/amiga.cpp
index c01711b928f..e03e8d8b0d7 100644
--- a/engines/freescape/sound/amiga.cpp
+++ b/engines/freescape/sound/amiga.cpp
@@ -92,6 +92,7 @@ public:
for (int ch = 0; ch < 4; ch++) {
_periodShadow[ch] = initPeriods[ch];
_volumeShadow[ch] = 0;
+ _channelEnabled[ch] = false;
setChannelSampleStart(ch, _squareWave);
setChannelSampleLen(ch, 0x20); // 32 words = 64 bytes
@@ -121,6 +122,7 @@ private:
int _graceCounter; // Ticks to keep playing after END before finishing
uint16 _periodShadow[4];
int _volumeShadow[4];
+ bool _channelEnabled[4]; // Tracks DMA enable state per channel
int8 _squareWave[64];
/**
@@ -136,30 +138,34 @@ private:
}
void setAbsolutePeriod(int ch, uint16 period) {
- if (ch == 0 && !_dmaAud0Active) {
- setChannelSampleStart(0, _squareWave);
- setChannelSampleLen(0, 0x20);
- setChannelOffset(0, Audio::Paula::Offset(0));
- }
_periodShadow[ch] = period;
setChannelPeriod(ch, period);
- enableChannel(ch);
+ if (!_channelEnabled[ch]) {
+ // Channel was off -> enable DMA (like writing DMACON with SET bit).
+ // Restore square wave for AUD0 if not playing a DMA sample.
+ if (ch == 0 && !_dmaAud0Active) {
+ setChannelSampleStart(0, _squareWave);
+ setChannelSampleLen(0, 0x20);
+ }
+ enableChannel(ch);
+ _channelEnabled[ch] = true;
+ }
+ // If already enabled, just the period register update above is
+ // sufficient. On real hardware, writing AUDxPER only changes the
+ // DMA fetch rate without restarting the buffer position.
}
void setRelativePeriod(int ch, int16 delta) {
- if (ch == 0 && !_dmaAud0Active) {
- setChannelSampleStart(0, _squareWave);
- setChannelSampleLen(0, 0x20);
- setChannelOffset(0, Audio::Paula::Offset(0));
- }
+ // Original only writes to shadow + period register.
+ // Does NOT touch DMACON - channel retains its current enable state.
uint16 newPeriod = (uint16)(_periodShadow[ch] + delta);
+ _periodShadow[ch] = newPeriod;
if (newPeriod == 0) {
disableChannel(ch);
+ _channelEnabled[ch] = false;
return;
}
- _periodShadow[ch] = newPeriod;
setChannelPeriod(ch, newPeriod);
- enableChannel(ch);
}
void setAbsoluteVolume(int sel, uint8 vol) {
@@ -197,6 +203,7 @@ private:
_dmaCounter--;
if (_dmaCounter == 0 && _dmaAud0Active) {
disableChannel(0);
+ _channelEnabled[0] = false;
setChannelSampleStart(0, _squareWave);
setChannelSampleLen(0, 0x20);
setChannelOffset(0, Audio::Paula::Offset(0));
@@ -248,7 +255,9 @@ private:
// Set absolute period
int ch = periodCmdToChannel(nibble);
if (param == 0) {
+ _periodShadow[ch] = 0;
disableChannel(ch);
+ _channelEnabled[ch] = false;
} else {
setAbsolutePeriod(ch, (uint16)param);
}
@@ -295,6 +304,7 @@ private:
_dmaCounter = (int)(durationSec * 50.0) + 1;
_dmaAud0Active = true;
enableChannel(0);
+ _channelEnabled[0] = true;
}
}
break;
@@ -310,6 +320,7 @@ private:
// Full stop: silence all channels
for (int ch = 0; ch < 4; ch++) {
_volumeShadow[ch] = 0;
+ _channelEnabled[ch] = false;
setChannelVolume(ch, 0);
disableChannel(ch);
}
More information about the Scummvm-git-logs
mailing list