[Scummvm-git-logs] scummvm master -> 9f5b5024679296ee9ce891fa1ff5b70158b683ea

sluicebox noreply at scummvm.org
Fri Dec 6 07:24:49 UTC 2024


This automated email contains information about 1 new commit which have been
pushed to the 'scummvm' repo located at https://github.com/scummvm/scummvm .

Summary:
9f5b502467 SCI32: Implement GK1 CD DPCM8 speech audio repair


Commit: 9f5b5024679296ee9ce891fa1ff5b70158b683ea
    https://github.com/scummvm/scummvm/commit/9f5b5024679296ee9ce891fa1ff5b70158b683ea
Author: AllTinker (49541155+TheAllTinker at users.noreply.github.com)
Date: 2024-12-05T23:24:45-08:00

Commit Message:
SCI32: Implement GK1 CD DPCM8 speech audio repair

Changed paths:
    engines/sci/detection.h
    engines/sci/detection_options.h
    engines/sci/detection_tables.h
    engines/sci/sound/decoders/sol.cpp
    engines/sci/sound/decoders/sol.h


diff --git a/engines/sci/detection.h b/engines/sci/detection.h
index ae5df995f24..6652ee28da7 100644
--- a/engines/sci/detection.h
+++ b/engines/sci/detection.h
@@ -44,6 +44,7 @@ namespace Sci {
 #define GAMEOPTION_SQ1_BEARDED_MUSICIANS    GUIO_GAMEOPTIONS16
 #define GAMEOPTION_TTS                      GUIO_GAMEOPTIONS17
 #define GAMEOPTION_ENABLE_GMM_SAVE          GUIO_GAMEOPTIONS18
+#define GAMEOPTION_GK1_ENABLE_AUDIO_POPFIX	GUIO_GAMEOPTIONS19
 
 enum SciGameId {
 	GID_ALL,
diff --git a/engines/sci/detection_options.h b/engines/sci/detection_options.h
index 9bcadf2c4f0..adccff49587 100644
--- a/engines/sci/detection_options.h
+++ b/engines/sci/detection_options.h
@@ -242,6 +242,18 @@ const ADExtraGuiOptionsMap optionsList[] = {
 		}
  	},
 
+	{
+		GAMEOPTION_GK1_ENABLE_AUDIO_POPFIX,
+		{
+			_s("Repair speech audio"),
+			_s("Detect and attempt to repair overflows in DPCM8 audio, which cause noticeable pops and crackles."),
+			"audio_popfix_enabled",
+			true,
+			0,
+			0
+		}
+	},
+
 	AD_EXTRA_GUI_OPTIONS_TERMINATOR
 };
 
diff --git a/engines/sci/detection_tables.h b/engines/sci/detection_tables.h
index 78f68c50861..c199a96890d 100644
--- a/engines/sci/detection_tables.h
+++ b/engines/sci/detection_tables.h
@@ -873,15 +873,17 @@ static const struct ADGameDescription SciGameDescriptions[] = {
 							  GAMEOPTION_ORIGINAL_SAVELOAD, \
 							  GAMEOPTION_TTS, \
 							  GAMEOPTION_ENABLE_GMM_SAVE)
-#define GUIO_GK1_CD_DOS GUIO5(GUIO_LINKSPEECHTOSFX, \
+#define GUIO_GK1_CD_DOS GUIO6(GUIO_LINKSPEECHTOSFX, \
 							  GAMEOPTION_ORIGINAL_SAVELOAD, \
 							  GAMEOPTION_HIGH_RESOLUTION_GRAPHICS, \
 							  GAMEOPTION_HQ_VIDEO, \
-							  GAMEOPTION_ENABLE_GMM_SAVE)
-#define GUIO_GK1_CD_WIN GUIO4(GUIO_LINKSPEECHTOSFX, \
+							  GAMEOPTION_ENABLE_GMM_SAVE, \
+							  GAMEOPTION_GK1_ENABLE_AUDIO_POPFIX)
+#define GUIO_GK1_CD_WIN GUIO5(GUIO_LINKSPEECHTOSFX, \
 							  GAMEOPTION_ORIGINAL_SAVELOAD, \
 							  GAMEOPTION_HQ_VIDEO, \
-							  GAMEOPTION_ENABLE_GMM_SAVE)
+							  GAMEOPTION_ENABLE_GMM_SAVE, \
+							  GAMEOPTION_GK1_ENABLE_AUDIO_POPFIX)
 #define GUIO_GK1_MAC    GUIO3(GUIO_NOSPEECH, \
 							  GAMEOPTION_TTS, \
 							  GAMEOPTION_ENABLE_GMM_SAVE)
diff --git a/engines/sci/sound/decoders/sol.cpp b/engines/sci/sound/decoders/sol.cpp
index 8dcf670e9ae..1dfd5b4c2f6 100644
--- a/engines/sci/sound/decoders/sol.cpp
+++ b/engines/sci/sound/decoders/sol.cpp
@@ -24,6 +24,7 @@
 #include "audio/decoders/raw.h"
 #include "common/substream.h"
 #include "common/util.h"
+#include "common/config-manager.h"
 #include "sci/sci.h"
 #include "sci/sound/decoders/sol.h"
 #include "sci/resource/resource.h"
@@ -47,7 +48,14 @@ static const uint16 tableDPCM16[128] = {
 	0x0F00, 0x1000, 0x1400, 0x1800, 0x1C00, 0x2000, 0x3000, 0x4000
 };
 
-static const byte tableDPCM8[8] = { 0, 1, 2, 3, 6, 10, 15, 21 };
+// Each 4-bit nibble indexes into this table to refer to one of the 16 delta values.
+// deDPCM8Nibble() currently uses the first 8 values, with logic to order the negative
+//  deltas differently depending on the exact encoding used.
+// deDPCM8NibbleWithRepair() uses the whole table, since it matches the order of "old"
+//  encoding as-is. This saves a tiny bit of computation in the more complex function.
+static const int8 tableDPCM8[16] = {
+	0, 1, 2, 3, 6, 10, 15, 21, -21, -15, -10, -6, -3, -2, -1, -0
+};
 
 /**
  * Decompresses one channel of 16-bit DPCM compressed audio.
@@ -112,16 +120,118 @@ static void deDPCM8Nibble(int16 *out, uint8 &sample, uint8 delta) {
 	*out = ((lastSample + sample) << 7) ^ 0x8000;
 }
 
+/**
+ * Decompresses one half of an 8-bit DPCM compressed audio
+ * byte. Attempts to repair overflows on the fly.
+ */
+static void deDPCM8NibbleWithRepair(int16 *const out, uint8 &sample, const uint8 delta,
+									uint8 &repairState, uint8 &preRepairSample) {
+	const uint8 lastSample = sample;
+
+	// In Gabriel Knight: Sins of the Fathers CD, the DPCM8-encoded speech contains overflows.
+	// Overflows wrap from positive to negative, or negative to positive. This continues
+	// until the wave settles at a new (wrapped) zero DC offset.
+
+	// We can't look ahead to see where the wave "reconnects" to valid data, so we
+	// decay the wave on an artificial slope until it does. This seems to take on average
+	// around 9-10 samples. The slope value below was chosen through analysing spectrographs
+	// of the decoded/repaired wave data to find the slope which removed the pops most
+	// cleanly across a test selection of game speech.
+
+#define REPAIR_SLOPE 12
+
+	switch (repairState) {
+	case 0: {
+		const int16 newSampleOverflow = (int16)sample + tableDPCM8[delta & 15];
+
+		if (newSampleOverflow > 255) {
+			// Positive overflow has occurred; begin artificial negative slope.
+			repairState = 1;
+			sample = lastSample - REPAIR_SLOPE;
+			// We also begin tracking the un-repaired waveform, so we can tell when to stop.
+			preRepairSample = (uint8)newSampleOverflow;
+
+			debugC(1, kDebugLevelSound, "DPCM8 OVERFLOW (+)");
+
+		} else if (newSampleOverflow < 0) {
+			// Negative overflow has occurred; begin artificial positive slope.
+			repairState = 2;
+			sample = lastSample + REPAIR_SLOPE;
+			// We also begin tracking the un-repaired waveform, so we can tell when to stop.
+			preRepairSample = (uint8)newSampleOverflow;
+
+			debugC(1, kDebugLevelSound, "DPCM8 OVERFLOW (-)");
+
+		} else {
+			sample = (uint8)newSampleOverflow;
+		}
+	}	break;
+	case 1: {
+		// Check for a slope wrap. This circumstance should never happen in reality;
+		// the unrepaired wave would somehow need to be stuck near minimum
+		// value over the entire course of the slope.
+		if (lastSample < REPAIR_SLOPE)
+			warning("Negative slope wrap!");
+
+		const uint8 slopeSample = lastSample - REPAIR_SLOPE;
+		preRepairSample += tableDPCM8[delta & 15];
+
+		// Stop the repair if the artificial slope has intersected with real data.
+		if (preRepairSample >= slopeSample) {
+			// Return to real data.
+			repairState = 0;
+			sample = preRepairSample;
+		} else {
+			sample = slopeSample;
+		}
+	}	break;
+	case 2: {
+		// Check for a slope wrap. This circumstance should never happen in reality;
+		// the unrepaired wave would somehow need to be stuck near maximum
+		// value over the entire course of the slope.
+		if (lastSample > (255 - REPAIR_SLOPE))
+			warning("Positive slope wrap!");
+
+		const uint8 slopeSample = lastSample + REPAIR_SLOPE;
+		preRepairSample += tableDPCM8[delta & 15];
+
+		// Stop the repair if the artificial slope has intersected with real data.
+		if (preRepairSample <= slopeSample) {
+			// Return to real data.
+			repairState = 0;
+			sample = preRepairSample;
+		} else {
+			sample = slopeSample;
+		}
+	}	break;
+	default:
+		warning("Invalid repair state!");
+		repairState = 0;
+		break;
+	}
+
+	*out = ((lastSample + sample) << 7) ^ 0x8000;
+}
+
 /**
  * Decompresses 8-bit DPCM compressed audio. Each byte read
  * outputs two samples into the decompression buffer.
  */
 template <bool OLD>
-static void deDPCM8Mono(int16 *out, Common::ReadStream &audioStream, uint32 numBytes, uint8 &sample) {
-	for (uint32 i = 0; i < numBytes; ++i) {
-		const uint8 delta = audioStream.readByte();
-		deDPCM8Nibble<OLD>(out++, sample, delta >> 4);
-		deDPCM8Nibble<OLD>(out++, sample, delta & 0xf);
+static void deDPCM8Mono(int16 *out, Common::ReadStream &audioStream, const uint32 numBytes, uint8 &sample,
+						const bool popfixEnabled, uint8 &repairState, uint8 &preRepairSample) {
+	if (popfixEnabled) {
+		for (uint32 i = 0; i < numBytes; ++i) {
+			const uint8 delta = audioStream.readByte();
+			deDPCM8NibbleWithRepair(out++, sample, delta >> 4, repairState, preRepairSample);
+			deDPCM8NibbleWithRepair(out++, sample, delta & 0xf, repairState, preRepairSample);
+		}
+	} else {
+		for (uint32 i = 0; i < numBytes; ++i) {
+			const uint8 delta = audioStream.readByte();
+			deDPCM8Nibble<OLD>(out++, sample, delta >> 4);
+			deDPCM8Nibble<OLD>(out++, sample, delta & 0xf);
+		}
 	}
 }
 
@@ -140,7 +250,9 @@ SOLStream<STEREO, S16BIT, OLDDPCM8>::SOLStream(Common::SeekableReadStream *strea
 	_stream(stream, disposeAfterUse),
 	_sampleRate(sampleRate),
 	// SSCI aligns the size of SOL data to 32 bits
-	_rawDataSize(rawDataSize & ~3) {
+	_rawDataSize(rawDataSize & ~3),
+	// The pop fix is only verified with (relevant to?) "old" DPCM8, so we enforce that here.
+	_popfixDPCM8(ConfMan.getBool("audio_popfix_enabled") && OLDDPCM8) {
 		if (S16BIT) {
 			_dpcmCarry16.l = _dpcmCarry16.r = 0;
 		} else {
@@ -201,7 +313,8 @@ int SOLStream<STEREO, S16BIT, OLDDPCM8>::readBuffer(int16 *buffer, const int num
 		if (STEREO) {
 			deDPCM8Stereo(buffer, *_stream, bytesToRead, _dpcmCarry8.l, _dpcmCarry8.r);
 		} else {
-			deDPCM8Mono<OLDDPCM8>(buffer, *_stream, bytesToRead, _dpcmCarry8.l);
+			deDPCM8Mono<OLDDPCM8>(buffer, *_stream, bytesToRead, _dpcmCarry8.l,
+								  _popfixDPCM8.enabled, _popfixDPCM8.state, _popfixDPCM8.preRepairSample);
 		}
 	}
 
diff --git a/engines/sci/sound/decoders/sol.h b/engines/sci/sound/decoders/sol.h
index 1542ed1dba5..9997b64e666 100644
--- a/engines/sci/sound/decoders/sol.h
+++ b/engines/sci/sound/decoders/sol.h
@@ -51,6 +51,21 @@ private:
 	 */
 	int32 _rawDataSize;
 
+	/**
+	 * DPCM8 pop (overflow) fix working data:
+	 *  - Whether or not the fix is enabled, set once on construction.
+	 *  - The current state of the repair (0 = inactive, 1 = positive, 2 = negative).
+	 *  - The sample state without repair, to detect where repairing should stop.
+	 */
+	struct PopFixData {
+		const bool enabled;
+		uint8 state;
+		uint8 preRepairSample;
+
+		PopFixData(const bool e):
+			enabled(e), state(0), preRepairSample(0) {}
+	} _popfixDPCM8;
+
 	/**
 	 * The last sample from the previous DPCM decode.
 	 */




More information about the Scummvm-git-logs mailing list