[Scummvm-git-logs] scummvm master -> 6ed7bd1263933c6c1b4349fafe8099de1fb19cf8
bluegr
noreply at scummvm.org
Sat Aug 31 14:36:12 UTC 2024
This automated email contains information about 4 new commits which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .
Summary:
7c074453d1 AGI: Move AgiNote to sound_sarien.cpp
9b7604f006 AGI: Cleanup AgiSound
919bc46dcb AGI: Add Apple II and CoCo3 sound generators
6ed7bd1263 AGI: Add Apple II and CoCo3 sound-blocking behavior
Commit: 7c074453d17370ce4a68d4cccd499999021f45e1
https://github.com/scummvm/scummvm/commit/7c074453d17370ce4a68d4cccd499999021f45e1
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-31T17:36:08+03:00
Commit Message:
AGI: Move AgiNote to sound_sarien.cpp
Changed paths:
engines/agi/sound.h
engines/agi/sound_sarien.cpp
diff --git a/engines/agi/sound.h b/engines/agi/sound.h
index 5acbe467606..7972db1df3b 100644
--- a/engines/agi/sound.h
+++ b/engines/agi/sound.h
@@ -38,25 +38,6 @@ namespace Agi {
#define SOUND_EMU_COCO3 6
#define SOUND_EMU_MIDI 7
-/**
- * AGI sound note structure.
- */
-struct AgiNote {
- uint16 duration; ///< Note duration
- uint16 freqDiv; ///< Note frequency divisor (10-bit)
- uint8 attenuation; ///< Note volume attenuation (4-bit)
-
- /** Reads an AgiNote through the given pointer. */
- void read(const uint8 *ptr) {
- duration = READ_LE_UINT16(ptr);
- uint16 freqByte0 = *(ptr + 2); // Bits 4-9 of the frequency divisor
- uint16 freqByte1 = *(ptr + 3); // Bits 0-3 of the frequency divisor
- // Merge the frequency divisor's bits together into a single variable
- freqDiv = ((freqByte0 & 0x3F) << 4) | (freqByte1 & 0x0F);
- attenuation = *(ptr + 4) & 0x0F;
- }
-};
-
/**
* AGI sound resource types.
* It's probably coincidence that all the values here are powers of two
diff --git a/engines/agi/sound_sarien.cpp b/engines/agi/sound_sarien.cpp
index 3c7d9b1b310..912718830b4 100644
--- a/engines/agi/sound_sarien.cpp
+++ b/engines/agi/sound_sarien.cpp
@@ -63,6 +63,25 @@ static const int16 waveformMac[WAVEFORM_SIZE] = {
-175, -172, -165, -159, -137, -114, -67, -19
};
+/**
+ * AGI sound note structure.
+ */
+struct AgiNote {
+ uint16 duration; ///< Note duration
+ uint16 freqDiv; ///< Note frequency divisor (10-bit)
+ uint8 attenuation; ///< Note volume attenuation (4-bit)
+
+ /** Reads an AgiNote through the given pointer. */
+ void read(const uint8 *ptr) {
+ duration = READ_LE_UINT16(ptr);
+ uint16 freqByte0 = *(ptr + 2); // Bits 4-9 of the frequency divisor
+ uint16 freqByte1 = *(ptr + 3); // Bits 0-3 of the frequency divisor
+ // Merge the frequency divisor's bits together into a single variable
+ freqDiv = ((freqByte0 & 0x3F) << 4) | (freqByte1 & 0x0F);
+ attenuation = *(ptr + 4) & 0x0F;
+ }
+};
+
SoundGenSarien::SoundGenSarien(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer), _chn() {
_sndBuffer = (int16 *)calloc(2, BUFFER_SIZE);
Commit: 9b7604f0066ee891d6902f342b68c7fb5189b485
https://github.com/scummvm/scummvm/commit/9b7604f0066ee891d6902f342b68c7fb5189b485
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-31T17:36:08+03:00
Commit Message:
AGI: Cleanup AgiSound
Changed paths:
engines/agi/sound.cpp
engines/agi/sound.h
engines/agi/sound_2gs.cpp
engines/agi/sound_2gs.h
engines/agi/sound_midi.cpp
engines/agi/sound_midi.h
diff --git a/engines/agi/sound.cpp b/engines/agi/sound.cpp
index c2a06e4a964..2e834a0b516 100644
--- a/engines/agi/sound.cpp
+++ b/engines/agi/sound.cpp
@@ -51,19 +51,19 @@ AgiSound *AgiSound::createFromRawResource(uint8 *data, uint32 len, int resnum, i
uint16 type = READ_LE_UINT16(data);
// For V1 sound resources
- if (type != AGI_SOUND_SAMPLE && (type & 0xFF) == 0x01)
- return new PCjrSound(data, len, resnum);
+ if (type != AGI_SOUND_SAMPLE && (type & 0xFF) == AGI_SOUND_4CHN)
+ return new PCjrSound(resnum, data, len, AGI_SOUND_4CHN);
switch (type) { // Create a sound object based on the type
case AGI_SOUND_SAMPLE:
- return new IIgsSample(data, len, resnum);
+ return new IIgsSample(resnum, data, len, type);
case AGI_SOUND_MIDI:
- return new IIgsMidi(data, len, resnum);
+ return new IIgsMidi(resnum, data, len, type);
case AGI_SOUND_4CHN:
if (soundemu == SOUND_EMU_MIDI) {
- return new MIDISound(data, len, resnum);
+ return new AgiSound(resnum, data, len, type);
} else {
- return new PCjrSound(data, len, resnum);
+ return new PCjrSound(resnum, data, len, type);
}
default:
break;
@@ -73,19 +73,12 @@ AgiSound *AgiSound::createFromRawResource(uint8 *data, uint32 len, int resnum, i
return nullptr;
}
-PCjrSound::PCjrSound(uint8 *data, uint32 len, int resnum) : AgiSound() {
- _data = data; // Save the resource pointer
- _len = len; // Save the resource's length
- _type = READ_LE_UINT16(data); // Read sound resource's type
+PCjrSound::PCjrSound(byte resourceNr, byte *data, uint32 length, uint16 type) :
+ AgiSound(resourceNr, data, length, type) {
- // Detect V1 sound resources
- if ((_type & 0xFF) == 0x01)
- _type = AGI_SOUND_4CHN;
-
- _isValid = (_type == AGI_SOUND_4CHN) && (_data != nullptr) && (_len >= 2);
-
- if (!_isValid) // Check for errors
- warning("Error creating PCjr 4-channel sound from resource %d (Type %d, length %d)", resnum, _type, len);
+ bool isValid = (_type == AGI_SOUND_4CHN) && (_data != nullptr) && (_length >= 2);
+ if (!isValid) // Check for errors
+ warning("Error creating PCjr 4-channel sound from resource %d (Type %d, length %d)", _resourceNr, _type, _length);
}
const uint8 *PCjrSound::getVoicePointer(uint voiceNum) {
@@ -129,19 +122,22 @@ void SoundMgr::unloadSound(int resnum) {
* @param flag the flag that is wished to be set true when finished
*/
void SoundMgr::startSound(int resnum, int flag) {
- debugC(3, kDebugLevelSound, "startSound(resnum = %d, flag = %d)", resnum, flag);
-
- if (_vm->_game.sounds[resnum] == nullptr) // Is this needed at all?
+ AgiSound *sound = _vm->_game.sounds[resnum];
+ debugC(3, kDebugLevelSound, "startSound(resnum = %d, flag = %d, type = %d)", resnum, flag, sound ? sound->type() : 0);
+ if (sound == nullptr) {
+ warning("startSound: sound %d does not exist", resnum);
return;
+ }
stopSound();
- AgiSoundEmuType type = (AgiSoundEmuType)_vm->_game.sounds[resnum]->type();
- if (type != AGI_SOUND_SAMPLE && type != AGI_SOUND_MIDI && type != AGI_SOUND_4CHN)
+ // This check handles an Apple IIgs sample with an invalid header
+ if (!sound->isValid()) {
+ warning("startSound: sound %d is invalid", resnum);
return;
- debugC(3, kDebugLevelSound, " type = %d", type);
+ }
- _vm->_game.sounds[resnum]->play();
+ sound->play();
_playingSound = resnum;
_soundGen->play(resnum);
diff --git a/engines/agi/sound.h b/engines/agi/sound.h
index 7972db1df3b..ce020fb2f95 100644
--- a/engines/agi/sound.h
+++ b/engines/agi/sound.h
@@ -73,12 +73,22 @@ public:
*/
class AgiSound {
public:
- AgiSound() : _isPlaying(false), _isValid(false) {}
- virtual ~AgiSound() {}
+ AgiSound(byte resourceNr, byte *data, uint32 length, uint16 type) :
+ _resourceNr(resourceNr),
+ _data(data),
+ _length(length),
+ _type(type),
+ _isPlaying(false) {}
+
+ virtual ~AgiSound() { free(_data); }
+
virtual void play() { _isPlaying = true; }
virtual void stop() { _isPlaying = false; }
virtual bool isPlaying() { return _isPlaying; }
- virtual uint16 type() = 0;
+ byte *getData() { return _data; }
+ uint32 getLength() { return _length; }
+ virtual uint16 type() { return _type; }
+ virtual bool isValid() { return true; }
/**
* A named constructor for creating different types of AgiSound objects
@@ -91,22 +101,17 @@ public:
static AgiSound *createFromRawResource(uint8 *data, uint32 len, int resnum, int soundemu);
protected:
- bool _isPlaying; ///< Is the sound playing?
- bool _isValid; ///< Is this a valid sound object?
+ byte _resourceNr;
+ byte *_data;
+ uint32 _length;
+ uint16 _type;
+ bool _isPlaying;
};
class PCjrSound : public AgiSound {
public:
- PCjrSound(uint8 *data, uint32 len, int resnum);
- ~PCjrSound() override { free(_data); }
- uint16 type() override { return _type; }
+ PCjrSound(byte resourceNr, byte *data, uint32 length, uint16 type);
const uint8 *getVoicePointer(uint voiceNum);
- uint8 *getData() { return _data; }
- uint32 getLength() { return _len; }
-protected:
- uint8 *_data; ///< Raw sound resource data
- uint32 _len; ///< Length of the raw sound resource
- uint16 _type; ///< Sound resource type
};
class SoundMgr {
diff --git a/engines/agi/sound_2gs.cpp b/engines/agi/sound_2gs.cpp
index 838449e3491..2521ea0dde3 100644
--- a/engines/agi/sound_2gs.cpp
+++ b/engines/agi/sound_2gs.cpp
@@ -456,16 +456,15 @@ void SoundGen2GS::setProgramChangeMapping(const IIgsMidiProgramMapping *mapping)
_progToInst = mapping;
}
-IIgsMidi::IIgsMidi(uint8 *data, uint32 len, int resnum) : AgiSound() {
- _data = data; // Save the resource pointer
+IIgsMidi::IIgsMidi(byte resourceNr, byte *data, uint32 length, uint16 type) :
+ AgiSound(resourceNr, data, length, type) {
+
_ptr = _data + 2; // Set current position to just after the header
- _len = len; // Save the resource's length
- _type = READ_LE_UINT16(data); // Read sound resource's type
_ticks = 0;
- _isValid = (_type == AGI_SOUND_MIDI) && (_data != nullptr) && (_len >= 2);
+ bool isValid = (_type == AGI_SOUND_MIDI) && (_data != nullptr) && (_length >= 2);
- if (!_isValid) // Check for errors
- warning("Error creating Apple IIGS midi sound from resource %d (Type %d, length %d)", resnum, _type, len);
+ if (!isValid) // Check for errors
+ warning("Error creating Apple IIGS midi sound from resource %d (Type %d, length %d)", _resourceNr, _type, _length);
}
/**
@@ -481,8 +480,10 @@ static bool convertWave(Common::SeekableReadStream &source, int8 *dest, uint len
return !(source.eos() || source.err());
}
-IIgsSample::IIgsSample(uint8 *data, uint32 len, int16 resourceNr) : AgiSound() {
- Common::MemoryReadStream stream(data, len, DisposeAfterUse::YES);
+IIgsSample::IIgsSample(byte resourceNr, byte *data, uint32 length, uint16 type) :
+ _isValid(false), AgiSound(resourceNr, data, length, type) {
+
+ Common::MemoryReadStream stream(_data, _length, DisposeAfterUse::NO);
_sample = nullptr;
@@ -521,7 +522,7 @@ IIgsSample::IIgsSample(uint8 *data, uint32 len, int16 resourceNr) : AgiSound() {
}
if (!_isValid) // Check for errors
- warning("Error creating Apple IIGS sample from resource %d (Type %d, length %d)", resourceNr, _header.type, len);
+ warning("Error creating Apple IIGS sample from resource %d (Type %d, length %d)", resourceNr, _header.type, _length);
}
diff --git a/engines/agi/sound_2gs.h b/engines/agi/sound_2gs.h
index 1cced30f22b..993533ba764 100644
--- a/engines/agi/sound_2gs.h
+++ b/engines/agi/sound_2gs.h
@@ -148,30 +148,27 @@ public:
class IIgsMidi : public AgiSound {
public:
- IIgsMidi(uint8 *data, uint32 len, int resnum);
- ~IIgsMidi() override { if (_data != NULL) free(_data); }
- uint16 type() override { return _type; }
+ IIgsMidi(byte resourceNr, byte *data, uint32 length, uint16 type);
virtual const uint8 *getPtr() { return _ptr; }
virtual void setPtr(const uint8 *ptr) { _ptr = ptr; }
virtual void rewind() { _ptr = _data + 2; _ticks = 0; }
protected:
- uint8 *_data; ///< Raw sound resource data
const uint8 *_ptr; ///< Pointer to the current position in the MIDI data
- uint32 _len; ///< Length of the raw sound resource
- uint16 _type; ///< Sound resource type
public:
uint _ticks; ///< MIDI song position in ticks (1/60ths of a second)
};
class IIgsSample : public AgiSound {
public:
- IIgsSample(uint8 *data, uint32 len, int16 resourceNr);
+ IIgsSample(byte resourceNr, byte *data, uint32 length, uint16 type);
~IIgsSample() override { delete[] _sample; }
uint16 type() override { return _header.type; }
const IIgsSampleHeader &getHeader() const { return _header; }
+ bool isValid() override { return _isValid; }
protected:
IIgsSampleHeader _header; ///< Apple IIGS AGI sample header
int8 *_sample; ///< Sample data (8-bit signed format)
+ bool _isValid;
};
/** Apple IIGS MIDI program change to instrument number mapping. */
diff --git a/engines/agi/sound_midi.cpp b/engines/agi/sound_midi.cpp
index 54c7aee6d52..a8c54088ceb 100644
--- a/engines/agi/sound_midi.cpp
+++ b/engines/agi/sound_midi.cpp
@@ -57,16 +57,6 @@ namespace Agi {
static uint32 convertSND2MIDI(byte *snddata, byte **data);
-MIDISound::MIDISound(uint8 *data, uint32 len, int resnum) : AgiSound() {
- _data = data; // Save the resource pointer
- _len = len; // Save the resource's length
- _type = READ_LE_UINT16(data); // Read sound resource's type
- _isValid = (_type == AGI_SOUND_4CHN) && (_data != nullptr) && (_len >= 2);
-
- if (!_isValid) // Check for errors
- warning("Error creating MIDI sound from resource %d (Type %d, length %d)", resnum, _type, len);
-}
-
SoundGenMIDI::SoundGenMIDI(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer), _isGM(false) {
MidiPlayer::createDriver(MDT_MIDI | MDT_ADLIB);
@@ -115,16 +105,14 @@ void SoundGenMIDI::endOfTrack() {
}
void SoundGenMIDI::play(int resnum) {
- MIDISound *track;
-
stop();
_isGM = true;
- track = (MIDISound *)_vm->_game.sounds[resnum];
+ AgiSound *track = _vm->_game.sounds[resnum];
// Convert AGI Sound data to MIDI
- int midiMusicSize = convertSND2MIDI(track->_data, &_midiData);
+ int midiMusicSize = convertSND2MIDI(track->getData(), &_midiData);
MidiParser *parser = MidiParser::createParser_SMF();
if (parser->loadMusic(_midiData, midiMusicSize)) {
diff --git a/engines/agi/sound_midi.h b/engines/agi/sound_midi.h
index 5ed976117a6..34cf20cdb9c 100644
--- a/engines/agi/sound_midi.h
+++ b/engines/agi/sound_midi.h
@@ -30,18 +30,6 @@
namespace Agi {
-class MIDISound : public AgiSound {
-public:
- MIDISound(uint8 *data, uint32 len, int resnum);
- ~MIDISound() override { free(_data); }
- uint16 type() override { return _type; }
- uint8 *_data; ///< Raw sound resource data
- uint32 _len; ///< Length of the raw sound resource
-
-protected:
- uint16 _type; ///< Sound resource type
-};
-
class SoundGenMIDI : public SoundGen, public Audio::MidiPlayer {
public:
SoundGenMIDI(AgiBase *vm, Audio::Mixer *pMixer);
Commit: 919bc46dcb0b5f94e0903c1a3938d1e7f89c6eac
https://github.com/scummvm/scummvm/commit/919bc46dcb0b5f94e0903c1a3938d1e7f89c6eac
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-31T17:36:08+03:00
Commit Message:
AGI: Add Apple II and CoCo3 sound generators
Changed paths:
A engines/agi/sound_a2.cpp
A engines/agi/sound_a2.h
engines/agi/agi.cpp
engines/agi/detection_tables.h
engines/agi/module.mk
engines/agi/sound.cpp
engines/agi/sound.h
engines/agi/sound_coco3.cpp
engines/agi/sound_coco3.h
diff --git a/engines/agi/agi.cpp b/engines/agi/agi.cpp
index 5b3c0f5c05a..19b442abfef 100644
--- a/engines/agi/agi.cpp
+++ b/engines/agi/agi.cpp
@@ -480,9 +480,11 @@ void AgiEngine::initialize() {
// drivers, and I'm not sure what they are. For now, they might
// as well be called "PC Speaker" and "Not PC Speaker".
- // If used platform is Apple IIGS then we must use Apple IIGS sound emulation
- // because Apple IIGS AGI games use only Apple IIGS specific sound resources.
- if (getPlatform() == Common::kPlatformApple2GS) {
+ // If platform is Apple or CoCo3 then their sound emulation must be used.
+ // The sound resources in these games have platform-specific formats.
+ if (getPlatform() == Common::kPlatformApple2) {
+ _soundemu = SOUND_EMU_APPLE2;
+ } else if (getPlatform() == Common::kPlatformApple2GS) {
_soundemu = SOUND_EMU_APPLE2GS;
} else if (getPlatform() == Common::kPlatformCoCo3) {
_soundemu = SOUND_EMU_COCO3;
diff --git a/engines/agi/detection_tables.h b/engines/agi/detection_tables.h
index e9048b636c8..a8cf6c125b2 100644
--- a/engines/agi/detection_tables.h
+++ b/engines/agi/detection_tables.h
@@ -456,7 +456,7 @@ static const AGIGameDescription gameDescriptions[] = {
// TRAC #13494
GAME_LPS("kq3", "2.14 1988-03-15 3.5\"", "87956c92d23f53d81bf2ee9e08fdc64c", 390, Common::ES_ESP, 0x2936, GID_KQ3, Common::kPlatformDOS),
- // King's Quest 3 (CoCo3 158k/360k) 1.0C [AGI 2.023]
+ // King's Quest 3 (CoCo3 158k/360k) 1.0C 6/27/88 [AGI 2.023]
// Official port by Sierra
GAME_PS("kq3", "", "5a6be7d16b1c742c369ef5cc64fefdd2", 429, 0x2440, GID_KQ3, Common::kPlatformCoCo3),
diff --git a/engines/agi/module.mk b/engines/agi/module.mk
index cf8269f2ed4..de26a8709fa 100644
--- a/engines/agi/module.mk
+++ b/engines/agi/module.mk
@@ -29,6 +29,7 @@ MODULE_OBJS := \
saveload.o \
sound.o \
sound_2gs.o \
+ sound_a2.o \
sound_coco3.o \
sound_midi.o \
sound_pcjr.o \
diff --git a/engines/agi/sound.cpp b/engines/agi/sound.cpp
index 2e834a0b516..1848e8b03c3 100644
--- a/engines/agi/sound.cpp
+++ b/engines/agi/sound.cpp
@@ -22,6 +22,7 @@
#include "agi/agi.h"
#include "agi/sound_2gs.h"
+#include "agi/sound_a2.h"
#include "agi/sound_coco3.h"
#include "agi/sound_midi.h"
#include "agi/sound_sarien.h"
@@ -48,6 +49,15 @@ SoundGen::~SoundGen() {
AgiSound *AgiSound::createFromRawResource(uint8 *data, uint32 len, int resnum, int soundemu) {
if (data == nullptr || len < 2) // Check for too small resource or no resource at all
return nullptr;
+
+ // Handle platform-specific formats that can't be detected by contents.
+ // These formats have no headers or predictable first bytes.
+ if (soundemu == SOUND_EMU_APPLE2) {
+ return new AgiSound(resnum, data, len, AGI_SOUND_APPLE2);
+ } else if (soundemu == SOUND_EMU_COCO3) {
+ return new AgiSound(resnum, data, len, AGI_SOUND_COCO3);
+ }
+
uint16 type = READ_LE_UINT16(data);
// For V1 sound resources
@@ -200,6 +210,9 @@ SoundMgr::SoundMgr(AgiBase *agi, Audio::Mixer *pMixer) {
case SOUND_EMU_PCJR:
_soundGen = new SoundGenPCJr(_vm, pMixer);
break;
+ case SOUND_EMU_APPLE2:
+ _soundGen = new SoundGenA2(_vm, pMixer);
+ break;
case SOUND_EMU_APPLE2GS:
_soundGen = new SoundGen2GS(_vm, pMixer);
break;
diff --git a/engines/agi/sound.h b/engines/agi/sound.h
index ce020fb2f95..2fdfffdf23e 100644
--- a/engines/agi/sound.h
+++ b/engines/agi/sound.h
@@ -34,20 +34,22 @@ namespace Agi {
#define SOUND_EMU_PCJR 2
#define SOUND_EMU_MAC 3
#define SOUND_EMU_AMIGA 4
-#define SOUND_EMU_APPLE2GS 5
-#define SOUND_EMU_COCO3 6
-#define SOUND_EMU_MIDI 7
+#define SOUND_EMU_APPLE2 5
+#define SOUND_EMU_APPLE2GS 6
+#define SOUND_EMU_COCO3 7
+#define SOUND_EMU_MIDI 8
/**
* AGI sound resource types.
- * It's probably coincidence that all the values here are powers of two
- * as they're simply the different used values in AGI sound resources'
- * starts (The first 16-bit little endian word, to be precise).
+ * These values are the first 16-bit LE words of each resource's header,
+ * except for Apple II and CoCo3, which do not have headers.
*/
enum AgiSoundEmuType {
AGI_SOUND_SAMPLE = 0x0001,
AGI_SOUND_MIDI = 0x0002,
- AGI_SOUND_4CHN = 0x0008
+ AGI_SOUND_4CHN = 0x0008,
+ AGI_SOUND_APPLE2 = 0xffff,
+ AGI_SOUND_COCO3 = 0xfffe
};
class SoundMgr;
diff --git a/engines/agi/sound_a2.cpp b/engines/agi/sound_a2.cpp
new file mode 100644
index 00000000000..2e808a7f643
--- /dev/null
+++ b/engines/agi/sound_a2.cpp
@@ -0,0 +1,220 @@
+/* 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/mixer.h"
+
+#include "agi/agi.h"
+#include "agi/sound_a2.h"
+
+namespace Agi {
+
+// SoundGenA2 plays Apple II sounds.
+//
+// Apple II AGI sounds are a series of monotonic notes. They sound similar to
+// PC speaker versions, but they use a different resource format, and sound
+// playback is a blocking operation.
+//
+// The sound resource's values are based on the number of 6502 CPU cycles
+// consumed by AGI's play-note routine and the speed of the CPU. Playback was
+// driven by the engine's own inner loops instead of a timer, so games are
+// blocked until a sound is completed or interrupted by a key press.
+//
+// Common::PCSpeaker is used for sound generation. It produces significantly
+// louder volumes than the other AGI sound generators, so I've lowered the
+// mixer volume for consistency.
+
+#define A2_MIXER_VOLUME 20
+
+static void calculateNote(uint16 clickCount, uint16 delayCount, float &freq, uint32 &duration_usec);
+static uint32 calculateDelayCycles(uint16 delayCount);
+static uint32 calculateTotalCycles(uint32 delayCycles, uint16 delayCount, uint16 clickCount);
+
+SoundGenA2::SoundGenA2(AgiBase *vm, Audio::Mixer *pMixer) :
+ _isPlaying(false),
+ SoundGen(vm, pMixer) {
+
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, A2_MIXER_VOLUME, 0, DisposeAfterUse::NO, true);
+}
+
+SoundGenA2::~SoundGenA2() {
+ _mixer->stopHandle(*_soundHandle);
+}
+
+void SoundGenA2::play(int resnum) {
+ Common::StackLock lock(_mutex);
+
+ if (_vm->_game.sounds[resnum] == nullptr ||
+ _vm->_game.sounds[resnum]->type() != AGI_SOUND_APPLE2) {
+ error("Apple II sound %d not loaded", resnum);
+ }
+
+ _speaker.stop();
+
+ // parse and enqueue all notes
+ AgiSound *sound = _vm->_game.sounds[resnum];
+ byte *data = sound->getData();
+ uint32 dataLength = sound->getLength();
+ for (uint32 i = 0; i + 4 < dataLength; i += 4) {
+ uint16 clickCount = READ_LE_UINT16(&data[i]);
+ uint16 delayCount = READ_LE_UINT16(&data[i + 2]);
+ if (clickCount == 0xffff) {
+ break;
+ }
+
+ float freq;
+ uint32 duration_usec;
+ calculateNote(clickCount, delayCount, freq, duration_usec);
+
+ if (delayCount != 0) {
+ _speaker.playQueue(Audio::PCSpeaker::kWaveFormSquare, freq, duration_usec);
+ } else {
+ _speaker.playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, duration_usec);
+ }
+ }
+
+ _isPlaying = true;
+}
+
+void SoundGenA2::stop() {
+ Common::StackLock lock(_mutex);
+
+ _speaker.stop();
+ _isPlaying = false;
+}
+
+int SoundGenA2::readBuffer(int16 *buffer, const int numSamples) {
+ Common::StackLock lock(_mutex);
+
+ // if not playing then there are no samples
+ if (!_isPlaying) {
+ return 0;
+ }
+
+ // fill the buffer with PCSpeaker samples
+ int result = _speaker.readBuffer(buffer, numSamples);
+
+ // if PCSpeaker is no longer playing then sound is finished
+ if (!_speaker.isPlaying()) {
+ _isPlaying = false;
+ _vm->_sound->soundIsFinished();
+ }
+
+ return result;
+}
+
+// Apple II note calculations
+//
+// Each note is four bytes. Each byte controls how many iterations a loop makes
+// in AGI's play-note routine. If the last two bytes are zero then the "click"
+// instruction (LDA $C030) is skipped.
+//
+// The four bytes are conceptually two 16-bit little-endian values: the number
+// of clicks to perform and the delay before each click.
+//
+// Calculating a note's frequency and duration requires calculating the number
+// of CPU cycles spent delaying before each click and the number of CPU cycles
+// spent in the play-note routine, and then applying the CPU speed.
+//
+// play-note routine from Black Cauldron:
+//
+// 6583:A5 12 LDA VALTYP+1 0012 ; a = delayCount[0]
+// 6585:05 13 ORA GARFLG 0013 ; a |= delayCount[1]
+// 6587:85 14 STA SUBFLG 0014 ; playNote = delayCount != 0
+// ----------- begin counting cycles -----------
+// 6589:A6 13 LDX GARFLG 0013 ; x = delayCount[1]
+// 658B:A4 12 LDY VALTYP+1 0012 ; y = delayCount[0]
+// 658D:88 DEY ; x--
+// 658E:D0 FD BNE $658D 658D
+// 6590:CA DEX ; y--
+// 6591:10 FA BPL $658D 658D
+// 6593:A5 14 LDA SUBFLG 0014 ; a = playNote
+// 6595:F0 03 BEQ $659A 659A ; skip click if !playNote
+// 6597:AD 30 C0 LDA SPKR C030 ; *click*
+// 659A:C6 10 DEC DIMFLG 0010 ; clickCount[0]--
+// 659C:D0 EB BNE $6589 6589
+// 659E:C6 11 DEC VALTYP 0011 ; clickCount[1]--
+// 65A0:10 E7 BPL $6589 6589
+// ----------- end counting cycles -------------
+// 65A2:60 RTS
+
+static void calculateNote(uint16 clickCount, uint16 delayCount, float &freq, uint32 &duration_usec) {
+ // calculate CPU cycles
+ uint32 delayCycles = calculateDelayCycles(delayCount);
+ uint32 totalCycles = calculateTotalCycles(delayCycles, delayCount, clickCount);
+
+ // frequency is half the time spent delaying before a click,
+ // because each click only toggles the speaker's state.
+ // the average 6502 CPU cycle at 1.023 MHz is 0.98 microseconds.
+ freq = 0.5f / (delayCycles * 0.00000098f);
+ duration_usec = (uint32)(totalCycles * 0.98f);
+}
+
+static uint32 calculateDelayCycles(uint16 delayCount) {
+ bool playNote = (delayCount != 0);
+ uint32 delayHighByte = delayCount >> 8;
+
+ uint32 cycles = 0;
+ cycles += 3; // LDX
+ cycles += 3; // LDY
+ if (playNote) {
+ cycles += (2 * delayCount); // DEY
+ int bneNoBranchCount = (delayCount / 256) + 1;
+ cycles += (3 * (delayCount - bneNoBranchCount)) + // BNE
+ (2 * bneNoBranchCount);
+ } else {
+ cycles += ((2 + 3) * 256) - 1; // DEY, BNE - 1 for last 2-cycle BNE
+ }
+ cycles += 2 * (delayHighByte + 1); // DEX
+ cycles += (3 * delayHighByte) + 2; // BPL (3 cycles, 2 cycles on last iteration)
+ cycles += 3; // LDA playNote
+ cycles += playNote ? 2 : 3; // BEQ (playNote)
+
+ return cycles;
+}
+
+static uint32 calculateTotalCycles(uint32 delayCycles, uint16 delayCount, uint16 clickCount) {
+ bool playNote = (delayCount != 0);
+ uint32 clickHighByte = clickCount >> 8;
+
+ // click count should never be zero, but if it were, then the low byte
+ // would wrap around and produce 256 clicks while the high byte would
+ // be correctly interpreted as zero.
+ if (clickCount == 0) {
+ clickCount = 256;
+ }
+
+ uint32 cycles = 0;
+ cycles += delayCycles * clickCount; // every click incurs delayCycles
+ if (playNote) {
+ cycles += (4 * clickCount); // every click incurs LDA SPKR (the click!)
+ }
+
+ cycles += 5 * clickCount; // DEC
+ int bneNoBranchCount = (clickCount / 256) + 1;
+ cycles += (3 * (clickCount - bneNoBranchCount)) + // BNE
+ (2 * bneNoBranchCount);
+ cycles += 5 * (clickHighByte + 1); // DEC
+ cycles += (3 * clickHighByte) + 2; // BPL (3 cycles, 2 cycles on last iteration)
+
+ return cycles;
+}
+
+} // End of namespace Agi
diff --git a/engines/agi/sound_a2.h b/engines/agi/sound_a2.h
new file mode 100644
index 00000000000..ea31364b75b
--- /dev/null
+++ b/engines/agi/sound_a2.h
@@ -0,0 +1,60 @@
+/* ScummVM - Graphic Adventure Engine
+ *
+ * ScummVM is the legal property of its developers, whose names
+ * are too numerous to list here. Please refer to the COPYRIGHT
+ * file distributed with this source distribution.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ */
+
+#ifndef AGI_SOUND_A2_H
+#define AGI_SOUND_A2_H
+
+#include "audio/audiostream.h"
+#include "audio/softsynth/pcspk.h"
+
+namespace Agi {
+
+class SoundGenA2 : public SoundGen, public Audio::AudioStream {
+public:
+ SoundGenA2(AgiBase *vm, Audio::Mixer *pMixer);
+ ~SoundGenA2() override;
+
+ void play(int resnum) override;
+ void stop() override;
+
+ int readBuffer(int16 *buffer, const int numSamples) override;
+
+ bool isStereo() const override {
+ return false;
+ }
+
+ bool endOfData() const override {
+ return false;
+ }
+
+ int getRate() const override {
+ return _speaker.getRate();
+ }
+
+private:
+ Common::Mutex _mutex;
+ bool _isPlaying;
+ Audio::PCSpeaker _speaker;
+};
+
+} // End of namespace Agi
+
+#endif /* AGI_SOUND_A2_H */
diff --git a/engines/agi/sound_coco3.cpp b/engines/agi/sound_coco3.cpp
index b3b6fcb9177..3fcc1797008 100644
--- a/engines/agi/sound_coco3.cpp
+++ b/engines/agi/sound_coco3.cpp
@@ -19,13 +19,31 @@
*
*/
-#include "agi/agi.h"
+#include "audio/mixer.h"
+#include "agi/agi.h"
#include "agi/sound_coco3.h"
namespace Agi {
-static int cocoFrequencies[] = {
+// SoundGenCoCo3 plays Tandy Color Computer sounds.
+//
+// CoCo3 AGI sounds are a series of monotonic notes. They sound similar to
+// PC speaker versions, but they use a different resource format. As with
+// the Apple II, sound playback is a blocking operation.
+//
+// The sound resource is a stream of four-byte notes. The frequency is a table
+// index. The volume is a boolean. The duration changed between interpreters;
+// originally the units were 1/10 of a second, then 1/60.
+//
+// Common::PCSpeaker is used for sound generation. It produces significantly
+// louder volumes than the other AGI sound generators, so I've lowered the
+// mixer volume for consistency.
+//
+// Thanks to Guillaume Major for documenting the sound format in their
+// conversion program, cc3snd.c.
+
+static const uint16 cocoFrequencies[] = {
130, 138, 146, 155, 164, 174, 184, 195, 207, 220, 233, 246,
261, 277, 293, 311, 329, 349, 369, 391, 415, 440, 466, 493,
523, 554, 587, 622, 659, 698, 739, 783, 830, 880, 932, 987,
@@ -33,44 +51,96 @@ static int cocoFrequencies[] = {
2093, 2217, 2349, 2489, 2637, 2793, 2959, 3135, 3322, 3520, 3729, 3951
};
-SoundGenCoCo3::SoundGenCoCo3(AgiBase *vm, Audio::Mixer *pMixer) : SoundGen(vm, pMixer) {
+#define COCO3_MIXER_VOLUME 20
+
+SoundGenCoCo3::SoundGenCoCo3(AgiBase *vm, Audio::Mixer *pMixer) :
+ _isPlaying(false),
+ SoundGen(vm, pMixer) {
+
+ _mixer->playStream(Audio::Mixer::kMusicSoundType, _soundHandle, this, -1, COCO3_MIXER_VOLUME, 0, DisposeAfterUse::NO, true);
}
SoundGenCoCo3::~SoundGenCoCo3() {
+ _mixer->stopHandle(*_soundHandle);
}
void SoundGenCoCo3::play(int resnum) {
- int i = cocoFrequencies[0]; // Silence warning
-
- i = i + 1;
-
-#if 0
- int i = 0;
- CoCoNote note;
-
- do {
- note.read(_chn[i].ptr);
-
- if (note.freq != 0xff) {
- playNote(0, cocoFrequencies[note.freq], note.volume);
+ Common::StackLock lock(_mutex);
+
+ if (_vm->_game.sounds[resnum] == nullptr ||
+ _vm->_game.sounds[resnum]->type() != AGI_SOUND_COCO3) {
+ error("CoCo3 sound %d not loaded", resnum);
+ }
+
+ _speaker.stop();
+
+ // KQ3 (Int. 2.023) stored the duration in 1/10 of a second.
+ // LSL1 (Int, 2.072) stored the duration in 1/60 of a second.
+ // Fan ports have been made using both interpreters, but our
+ // detection table doesn't capture this. For now, treat KQ3
+ // as the early interpreter all others as the later one.
+ // TODO: create detection heuristic
+ bool isEarlySound = (_vm->getGameID() == GID_KQ3);
+
+ // parse and enqueue all notes
+ AgiSound *sound = _vm->_game.sounds[resnum];
+ byte *data = sound->getData();
+ uint32 dataLength = sound->getLength();
+ for (uint32 i = 0; i + 4 < dataLength; i += 4) {
+ // the third byte is apparently unused, and always zero
+ byte freqIndex = data[i];
+ byte volume = data[i + 1];
+ byte duration = data[i + 3];
+ if (freqIndex == 0xff) {
+ break;
+ }
- uint32 start_time = _vm->_system->getMillis();
+ // get duration in ticks (1/60 of a second)
+ uint32 ticks = duration;
+ if (isEarlySound) {
+ ticks *= 6;
+ }
- while (_vm->_system->getMillis() < start_time + note.duration) {
- _vm->_system->updateScreen();
+ // convert ticks to microseconds for PCSpeaker
+ uint32 duration_usec = ticks * (1000000.0f / 60.0f);
- _vm->_system->delayMillis(10);
- }
+ // play if volume is non-zero (it's always 0x3f or zero)
+ if (volume != 0 && freqIndex < ARRAYSIZE(cocoFrequencies)) {
+ int freq = cocoFrequencies[freqIndex];
+ _speaker.playQueue(Audio::PCSpeaker::kWaveFormSquare, freq, duration_usec);
+ } else {
+ _speaker.playQueue(Audio::PCSpeaker::kWaveFormSilence, 0, duration_usec);
}
- } while (note.freq != 0xff);
-#endif
+ }
+
+ _isPlaying = true;
}
void SoundGenCoCo3::stop() {
+ Common::StackLock lock(_mutex);
+
+ _speaker.stop();
+ _isPlaying = false;
}
int SoundGenCoCo3::readBuffer(int16 *buffer, const int numSamples) {
- return numSamples;
+ Common::StackLock lock(_mutex);
+
+ // if not playing then there are no samples
+ if (!_isPlaying) {
+ return 0;
+ }
+
+ // fill the buffer with PCSpeaker samples
+ int result = _speaker.readBuffer(buffer, numSamples);
+
+ // if PCSpeaker is no longer playing then sound is finished
+ if (!_speaker.isPlaying()) {
+ _isPlaying = false;
+ _vm->_sound->soundIsFinished();
+ }
+
+ return result;
}
} // End of namespace Agi
diff --git a/engines/agi/sound_coco3.h b/engines/agi/sound_coco3.h
index b8e3c3b273c..f21b30f5781 100644
--- a/engines/agi/sound_coco3.h
+++ b/engines/agi/sound_coco3.h
@@ -23,22 +23,10 @@
#define AGI_SOUND_COCO3_H
#include "audio/audiostream.h"
+#include "audio/softsynth/pcspk.h"
namespace Agi {
-struct CoCoNote {
- uint8 freq;
- uint8 volume;
- uint16 duration; ///< Note duration
-
- /** Reads a CoCoNote through the given pointer. */
- void read(const uint8 *ptr) {
- freq = *ptr;
- volume = *(ptr + 1);
- duration = READ_LE_UINT16(ptr + 2);
- }
-};
-
class SoundGenCoCo3 : public SoundGen, public Audio::AudioStream {
public:
SoundGenCoCo3(AgiBase *vm, Audio::Mixer *pMixer);
@@ -47,7 +35,6 @@ public:
void play(int resnum) override;
void stop() override;
- // AudioStream API
int readBuffer(int16 *buffer, const int numSamples) override;
bool isStereo() const override {
@@ -59,9 +46,13 @@ public:
}
int getRate() const override {
- // FIXME: Ideally, we should use _sampleRate.
- return 22050;
+ return _speaker.getRate();
}
+
+private:
+ Common::Mutex _mutex;
+ bool _isPlaying;
+ Audio::PCSpeaker _speaker;
};
} // End of namespace Agi
Commit: 6ed7bd1263933c6c1b4349fafe8099de1fb19cf8
https://github.com/scummvm/scummvm/commit/6ed7bd1263933c6c1b4349fafe8099de1fb19cf8
Author: sluicebox (22204938+sluicebox at users.noreply.github.com)
Date: 2024-08-31T17:36:08+03:00
Commit Message:
AGI: Add Apple II and CoCo3 sound-blocking behavior
Changed paths:
engines/agi/agi.h
engines/agi/keyboard.cpp
engines/agi/op_cmd.cpp
engines/agi/sound.h
diff --git a/engines/agi/agi.h b/engines/agi/agi.h
index 347f4df8a73..e7530884fca 100644
--- a/engines/agi/agi.h
+++ b/engines/agi/agi.h
@@ -1036,6 +1036,7 @@ public:
int waitKey();
int waitAnyKey();
+ void waitAnyKeyOrFinishedSound();
void nonBlockingText_IsShown();
void nonBlockingText_Forget();
diff --git a/engines/agi/keyboard.cpp b/engines/agi/keyboard.cpp
index e38ca5d172e..a79b14b1fd9 100644
--- a/engines/agi/keyboard.cpp
+++ b/engines/agi/keyboard.cpp
@@ -673,6 +673,22 @@ int AgiEngine::waitAnyKey() {
return key;
}
+/**
+ * Waits on any key to be pressed or for a finished sound.
+ * This is used on platforms where sound playback would block the
+ * interpreter until the sound finished or was interrupted.
+ */
+void AgiEngine::waitAnyKeyOrFinishedSound() {
+ clearKeyQueue();
+
+ while (!(shouldQuit() || _restartGame || !_sound->isPlaying())) {
+ wait(10);
+ if (doPollKeyboard()) {
+ break;
+ }
+ }
+}
+
bool AgiEngine::isKeypress() {
processScummVMEvents();
return _keyQueueStart != _keyQueueEnd;
diff --git a/engines/agi/op_cmd.cpp b/engines/agi/op_cmd.cpp
index a8d98d07b5c..336965b916d 100644
--- a/engines/agi/op_cmd.cpp
+++ b/engines/agi/op_cmd.cpp
@@ -704,7 +704,20 @@ void cmdSound(AgiGame *state, AgiEngine *vm, uint8 *parameter) {
uint16 resourceNr = parameter[0];
uint16 flagNr = parameter[1];
- vm->_sound->startSound(resourceNr, flagNr);
+ if (vm->getPlatform() == Common::kPlatformApple2 ||
+ vm->getPlatform() == Common::kPlatformCoCo3) {
+ // Play the sound until it finishes or until a key is pressed.
+ // Sound playback is a blocking operation on these platforms.
+ // If sound is off then playback is not started.
+ if (vm->getFlag(VM_FLAG_SOUND_ON)) {
+ vm->_sound->startSound(resourceNr, flagNr);
+ vm->waitAnyKeyOrFinishedSound();
+ vm->_sound->stopSound();
+ }
+ vm->setFlag(flagNr, true);
+ } else {
+ vm->_sound->startSound(resourceNr, flagNr);
+ }
}
void cmdStopSound(AgiGame *state, AgiEngine *vm, uint8 *parameter) {
diff --git a/engines/agi/sound.h b/engines/agi/sound.h
index 2fdfffdf23e..2d87598e9a8 100644
--- a/engines/agi/sound.h
+++ b/engines/agi/sound.h
@@ -129,6 +129,7 @@ public:
void stopSound();
void soundIsFinished();
+ bool isPlaying() const { return _playingSound != -1; }
private:
int _endflag;
More information about the Scummvm-git-logs
mailing list