[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