shithub: pt2-clone

Download patch

ref: 5ccd729ea5a67c3c80fe3b117d4bd58c58226745
parent: b8e8e8a8ef726fbe428edc74bdd9de886863075c
author: Olav Sørensen <olav.sorensen@live.no>
date: Thu Sep 16 14:24:01 EDT 2021

Pushed v1.34 code

--- a/release/macos/protracker.ini
+++ b/release/macos/protracker.ini
@@ -203,12 +203,21 @@
 ;
 DOTTEDCENTER=TRUE
 
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+;        Syntax: TRUE or FALSE
+; Default value: FALSE
+;       Comment: This ProTracker command low-pass filters the current sample.
+;                It's a little used effect despite being present in original PT,
+;                and it was often replaced for syncing visuals with the music in
+;                demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
 [AUDIO SETTINGS]
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-;         OS is set to mix shared audio at 96kHz or higher.
+;       Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
 ;
 FREQUENCY=48000
 
--- a/release/other/protracker.ini
+++ b/release/other/protracker.ini
@@ -203,12 +203,21 @@
 ;
 DOTTEDCENTER=TRUE
 
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+;        Syntax: TRUE or FALSE
+; Default value: FALSE
+;       Comment: This ProTracker command low-pass filters the current sample.
+;                It's a little used effect despite being present in original PT,
+;                and it was often replaced for syncing visuals with the music in
+;                demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
 [AUDIO SETTINGS]
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-;         OS is set to mix shared audio at 96kHz or higher.
+;       Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
 ;
 FREQUENCY=48000
 
--- a/release/win32/protracker.ini
+++ b/release/win32/protracker.ini
@@ -203,12 +203,21 @@
 ;
 DOTTEDCENTER=TRUE
 
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+;        Syntax: TRUE or FALSE
+; Default value: FALSE
+;       Comment: This ProTracker command low-pass filters the current sample.
+;                It's a little used effect despite being present in original PT,
+;                and it was often replaced for syncing visuals with the music in
+;                demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
 [AUDIO SETTINGS]
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-;         OS is set to mix shared audio at 96kHz or higher.
+;       Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
 ;
 FREQUENCY=48000
 
--- a/release/win64/protracker.ini
+++ b/release/win64/protracker.ini
@@ -203,12 +203,21 @@
 ;
 DOTTEDCENTER=TRUE
 
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+;        Syntax: TRUE or FALSE
+; Default value: FALSE
+;       Comment: This ProTracker command low-pass filters the current sample.
+;                It's a little used effect despite being present in original PT,
+;                and it was often replaced for syncing visuals with the music in
+;                demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
 [AUDIO SETTINGS]
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-;         OS is set to mix shared audio at 96kHz or higher.
+;       Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
 ;
 FREQUENCY=48000
 
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -49,7 +49,7 @@
 static uint64_t tickTime64, tickTime64Frac;
 static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned;
 static double dPrngStateL, dPrngStateR, dSideFactor;
-static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES];
+static blep_t blep[AMIGA_VOICES];
 static rcFilter_t filterLoA500, filterHiA500, filterLoA1200, filterHiA1200;
 static ledFilter_t filterLED;
 static SDL_AudioDeviceID dev;
@@ -67,7 +67,7 @@
 audio_t audio;
 paulaVoice_t paula[AMIGA_VOICES];
 
-bool intMusic(void); // defined in pt_modplayer.c
+bool intMusic(void); // defined in pt2_replayer.c
 
 static void updateFilterFunc(void)
 {
@@ -89,6 +89,9 @@
 
 void setLEDFilter(bool state, bool doLockAudio)
 {
+	if (ledFilterEnabled == state)
+		return; // same state as before!
+
 	const bool audioWasntLocked = !audio.locked;
 	if (doLockAudio && audioWasntLocked)
 		lockAudio();
@@ -189,7 +192,6 @@
 
 	memset(&paula[ch], 0, sizeof (paulaVoice_t));
 	memset(&blep[ch], 0, sizeof (blep_t));
-	memset(&blepVol[ch], 0, sizeof (blep_t));
 
 	stopScope(ch); // it should be safe to clear the scope now
 	memset(&scope[ch], 0, sizeof (scope_t));
@@ -268,23 +270,22 @@
 		else
 			dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv;
 
-		v->dOldVoiceDelta = (dPeriodToDeltaDiv / realPeriod) * 0.5; // /2 since we do 2x oversampling
+		v->dOldVoiceDelta = dPeriodToDeltaDiv / realPeriod;
 
+		if (audio.oversamplingFlag || editor.isSMPRendering)
+			v->dOldVoiceDelta *= 0.5; // /2 since we do 2x oversampling
+
 		// for BLEP synthesis (prevents division in inner mix loop)
 		v->dOldVoiceDeltaMul = 1.0 / v->dOldVoiceDelta;
 	}
 
-	v->dDelta = v->dOldVoiceDelta;
+	v->dNewDelta = v->dOldVoiceDelta;
+	if (v->dLastDelta == 0.0) // for BLEP
+		v->dLastDelta = v->dNewDelta;
 
-	// for BLEP synthesis
-	v->dDeltaMul = v->dOldVoiceDeltaMul;
-
-	if (v->dLastDelta == 0.0)
-		v->dLastDelta = v->dDelta;
-
-	if (v->dLastDeltaMul == 0.0)
-		v->dLastDeltaMul = v->dDeltaMul;
-	// ------------------
+	v->dNewDeltaMul = v->dOldVoiceDeltaMul;
+	if (v->dLastDeltaMul == 0.0) // for BLEP
+		v->dLastDeltaMul = v->dNewDeltaMul;
 }
 
 void paulaSetVolume(int32_t ch, uint16_t vol)
@@ -293,13 +294,14 @@
 
 	int32_t realVol = vol;
 
-	// this is what WinUAE does, so I assume it's what Paula does too
+	// this is what WinUAE does
 	realVol &= 127;
 	if (realVol > 64)
 		realVol = 64;
-	// ----------------
+	// ------------------------
 
-	v->dVolume = realVol * (1.0 / 64.0);
+	// multiplying by this also scales the sample from -128..127 -> -1.0 .. ~0.99
+	v->dScaledVolume = realVol * (1.0 / (128.0 * 64.0));
 
 	if (editor.songPlaying)
 	{
@@ -356,7 +358,7 @@
 {
 	paulaVoice_t *v = &paula[ch];
 
-	v->active = false;
+	v->DMA_active = false;
 
 	if (editor.songPlaying)
 		v->syncFlags |= STOP_SCOPE;
@@ -376,12 +378,22 @@
 	if (length == 0)
 		length = 1+65535;
 
-	v->dPhase = 0.0;
+	v->dPhase = v->dLastPhase = 0.0;
 	v->pos = 0;
 	v->data = dat;
 	v->length = length;
-	v->active = true;
+	v->dDelta = v->dLastDelta = v->dNewDelta;
+	v->dDeltaMul = v->dLastDeltaMul = v->dNewDeltaMul;
 
+	/* Read first sample data point into cache now.
+	**
+	** (multiplying by dScaledVolume will also change the scale
+	**  from -128..127 to -1.0 .. ~0.99.)
+	*/
+	v->dCachedSamplePoint = v->data[0] * v->dScaledVolume;
+
+	v->DMA_active = true;
+
 	if (editor.songPlaying)
 	{
 		v->syncTriggerData = dat;
@@ -424,34 +436,32 @@
 void mixChannels(int32_t numSamples)
 {
 	double *dMixBufSelect[AMIGA_VOICES] = { dMixBufferL, dMixBufferR, dMixBufferR, dMixBufferL };
-	double dSmp, dVol;
-	blep_t *bSmp, *bVol;
-	paulaVoice_t *v;
 
 	memset(dMixBufferL, 0, numSamples * sizeof (double));
 	memset(dMixBufferR, 0, numSamples * sizeof (double));
 
-	v = paula;
-	bSmp = blep;
-	bVol = blepVol;
+	paulaVoice_t *v = paula;
+	blep_t *bSmp = blep;
 
-	for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++, bVol++)
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++)
 	{
-		if (!v->active || v->data == NULL)
+		/* We only need to test for a NULL-pointer once.
+		** When pointers are messed with in the tracker, the mixer
+		** is temporarily forced offline, and its voice pointers are
+		** cleared to prevent expired pointer addresses.
+		*/
+		if (!v->DMA_active || v->data == NULL)
 			continue;
 
-		double *dMixBuf = dMixBufSelect[i];
+		double *dMixBuf = dMixBufSelect[i]; // what output channel to mix into (L, R, R, L)
 		for (int32_t j = 0; j < numSamples; j++)
 		{
-			assert(v->data != NULL);
-			dSmp = v->data[v->pos] * (1.0 / 128.0);
-			dVol = v->dVolume;
-
+			double dSmp = v->dCachedSamplePoint;
 			if (dSmp != bSmp->dLastValue)
 			{
 				if (v->dLastDelta > v->dLastPhase)
 				{
-					// div->mul trick: v->dLastDeltaMul is 1.0 / v->dLastDelta
+					// v->dLastDeltaMul is (1.0 / v->dLastDelta) (pre-computed for speed, div -> mul)
 					blepAdd(bSmp, v->dLastPhase * v->dLastDeltaMul, bSmp->dLastValue - dSmp);
 				}
 
@@ -458,22 +468,21 @@
 				bSmp->dLastValue = dSmp;
 			}
 
-			if (dVol != bVol->dLastValue)
-			{
-				blepVolAdd(bVol, bVol->dLastValue - dVol);
-				bVol->dLastValue = dVol;
-			}
+			if (bSmp->samplesLeft > 0)
+				dSmp = blepRun(bSmp, dSmp);
 
-			if (bSmp->samplesLeft > 0) dSmp = blepRun(bSmp, dSmp);
-			if (bVol->samplesLeft > 0) dVol = blepRun(bVol, dVol);
+			dMixBuf[j] += dSmp;
 
-			dMixBuf[j] += dSmp * dVol;
-
 			v->dPhase += v->dDelta;
 			if (v->dPhase >= 1.0) // deltas can't be >= 1.0, so this is safe
 			{
 				v->dPhase -= 1.0;
 
+				// Paula only updates period (delta) during sample fetching
+				v->dDelta = v->dNewDelta;
+				v->dDeltaMul = v->dNewDeltaMul;
+				// --------------------------------------------------------
+
 				v->dLastPhase = v->dPhase;
 				v->dLastDelta = v->dDelta;
 				v->dLastDeltaMul = v->dDeltaMul;
@@ -486,6 +495,16 @@
 					v->length = v->newLength;
 					v->data = v->newData;
 				}
+
+				/* Read sample into cache now.
+				** Also change volume here as well. It has recently been
+				** discovered that Paula only updates its volume during period
+				** fetching (when it's reading the next sample point).
+				**
+				** (multiplying by dScaledVolume will also change the scale
+				**  from -128..127 to -1.0 .. ~0.99.)
+				*/
+				v->dCachedSamplePoint = v->data[v->pos] * v->dScaledVolume;
 			}
 		}
 	}
@@ -595,9 +614,77 @@
 }
 
 #define NORM_FACTOR 2.0 /* nominally correct, but can clip from high-pass filter overshoot */
+
 static inline void processMixedSamplesAmigaPanning(int32_t i, int16_t *out)
 {
 	int32_t smp32;
+	double dPrng;
+
+	double dL = dMixBufferL[i];
+	double dR = dMixBufferR[i];
+
+	// normalize w/ phase-inversion (A500/A1200 has a phase-inverted audio signal)
+	dL *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+	dR *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+
+	// left channel - 1-bit triangular dithering (high-pass filtered)
+	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+	dL = (dL + dPrng) - dPrngStateL;
+	dPrngStateL = dPrng;
+	smp32 = (int32_t)dL;
+	CLAMP16(smp32);
+	out[0] = (int16_t)smp32;
+
+	// right channel - 1-bit triangular dithering (high-pass filtered)
+	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+	dR = (dR + dPrng) - dPrngStateR;
+	dPrngStateR = dPrng;
+	smp32 = (int32_t)dR;
+	CLAMP16(smp32);
+	out[1] = (int16_t)smp32;
+}
+
+static inline void processMixedSamples(int32_t i, int16_t *out)
+{
+	int32_t smp32;
+	double dPrng;
+
+	double dL = dMixBufferL[i];
+	double dR = dMixBufferR[i];
+
+	// apply stereo separation
+	const double dOldL = dL;
+	const double dOldR = dR;
+	double dMid  = (dOldL + dOldR) * STEREO_NORM_FACTOR;
+	double dSide = (dOldL - dOldR) * dSideFactor;
+	dL = dMid + dSide;
+	dR = dMid - dSide;
+
+	// normalize w/ phase-inversion (A500/A1200 has a phase-inverted audio signal)
+	dL *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+	dR *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+
+	// left channel - 1-bit triangular dithering (high-pass filtered)
+	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+	dL = (dL + dPrng) - dPrngStateL;
+	dPrngStateL = dPrng;
+	smp32 = (int32_t)dL;
+	CLAMP16(smp32);
+	out[0] = (int16_t)smp32;
+
+	// right channel - 1-bit triangular dithering (high-pass filtered)
+	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+	dR = (dR + dPrng) - dPrngStateR;
+	dPrngStateR = dPrng;
+	smp32 = (int32_t)dR;
+	CLAMP16(smp32);
+	out[1] = (int16_t)smp32;
+}
+
+
+static inline void processMixedSamplesAmigaPanning_2x(int32_t i, int16_t *out) // 2x oversampling
+{
+	int32_t smp32;
 	double dPrng, dL, dR;
 
 	// 2x downsampling (decimation)
@@ -627,7 +714,7 @@
 	out[1] = (int16_t)smp32;
 }
 
-static inline void processMixedSamples(int32_t i, int16_t *out)
+static inline void processMixedSamples_2x(int32_t i, int16_t *out) // 2x oversampling
 {
 	int32_t smp32;
 	double dPrng, dL, dR;
@@ -677,7 +764,7 @@
 		if (editor.pat2SmpPos+samplesTodo > MAX_SAMPLE_LEN)
 			samplesTodo = MAX_SAMPLE_LEN-editor.pat2SmpPos;
 
-		// mix channels (at 2x rate, we do 2x oversampling)
+		// mix channels (with 2x oversampling, PAT2SMP needs it)
 		mixChannels(samplesTodo*2);
 
 		double *dOutStream = &editor.dPat2SmpBuf[editor.pat2SmpPos];
@@ -702,33 +789,63 @@
 	}
 	else
 	{
-		// mix and filter channels (at 2x rate, we do 2x oversampling)
-		mixChannels(numSamples*2);
-		processFiltersFunc(numSamples*2);
-
-		// downsample, normalize and dither
-		int16_t out[2];
-		int16_t *outStream = target;
-		if (stereoSeparation == 100)
+		if (audio.oversamplingFlag) // 2x oversampling
 		{
-			for (int32_t i = 0; i < numSamples; i++)
-			{
-				processMixedSamplesAmigaPanning(i, out); // also does 2x downsampling
+			// mix and filter channels (at 2x rate)
+			mixChannels(numSamples*2);
+			processFiltersFunc(numSamples*2);
 
-				*outStream++ = out[0];
-				*outStream++ = out[1];
+			// downsample, normalize and dither
+			int16_t out[2];
+			int16_t *outStream = target;
+			if (stereoSeparation == 100)
+			{
+				for (int32_t i = 0; i < numSamples; i++)
+				{
+					processMixedSamplesAmigaPanning_2x(i, out);
+					*outStream++ = out[0];
+					*outStream++ = out[1];
+				}
 			}
+			else
+			{
+				for (int32_t i = 0; i < numSamples; i++)
+				{
+					processMixedSamples_2x(i, out);
+					*outStream++ = out[0];
+					*outStream++ = out[1];
+				}
+			}
 		}
 		else
 		{
-			for (int32_t i = 0; i < numSamples; i++)
-			{
-				processMixedSamples(i, out); // also does 2x downsampling
+			// mix and filter channels
+			mixChannels(numSamples);
+			processFiltersFunc(numSamples);
 
-				*outStream++ = out[0];
-				*outStream++ = out[1];
+			// normalize and dither
+			int16_t out[2];
+			int16_t *outStream = target;
+			if (stereoSeparation == 100)
+			{
+				for (int32_t i = 0; i < numSamples; i++)
+				{
+					processMixedSamplesAmigaPanning(i, out);
+					*outStream++ = out[0];
+					*outStream++ = out[1];
+				}
 			}
+			else
+			{
+				for (int32_t i = 0; i < numSamples; i++)
+				{
+					processMixedSamples(i, out);
+					*outStream++ = out[0];
+					*outStream++ = out[1];
+				}
+			}
 		}
+
 	}
 }
 
@@ -872,9 +989,12 @@
 	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (same as A500)
 	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
 	*/
-	const double dAudioFreq = audio.outputRate * 2.0; // *2 because we do 2x oversampling
+	double dAudioFreq = audio.outputRate;
 	double R, C, R1, R2, C1, C2, fc, fb;
 
+	if (audio.oversamplingFlag)
+		dAudioFreq *= 2.0; // 2x oversampling
+
 	// A500 1-pole (6db/oct) static RC low-pass filter:
 	R = 360.0; // R321 (360 ohm)
 	C = 1e-7;  // C321 (0.1uF)
@@ -1021,7 +1141,10 @@
 	audio.audioBufferSize = have.samples;
 	audio.dPeriodToDeltaDiv = (double)PAULA_PAL_CLK / audio.outputRate;
 
-	updateReplayerTimingMode();
+	// we do 2x oversampling if the audio output rate is below 96kHz
+	audio.oversamplingFlag = (audio.outputRate < 96000);
+
+	updateReplayerTimingMode(); // also generates the BPM tables used below
 
 	const int32_t lowestBPM = 32;
 	const int32_t pat2SmpMaxSamples = (audio.bpmTable20kHz[lowestBPM-32] + (1LL + 31)) >> 32; // ceil (rounded upwards)
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -8,7 +8,7 @@
 {
 	volatile bool locked, isSampling;
 
-	bool forceSoundCardSilence;
+	bool forceSoundCardSilence, oversamplingFlag;
 	
 	uint32_t outputRate, audioBufferSize;
 	int64_t tickSampleCounter64, samplesPerTick64;
@@ -25,11 +25,13 @@
 
 typedef struct voice_t
 {
-	volatile bool active;
+	volatile bool DMA_active;
 
 	const int8_t *data, *newData;
 	int32_t length, newLength, pos;
-	double dVolume, dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase;
+
+	double dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase;
+	double dCachedSamplePoint, dScaledVolume, dNewDelta, dNewDeltaMul;
 
 	// period cache
 	int32_t oldPeriod;
--- a/src/pt2_config.c
+++ b/src/pt2_config.c
@@ -42,6 +42,7 @@
 	FILE *f;
 
 	// set default config values first
+	config.disableE8xEffect = false;
 	config.fullScreenStretch = false;
 	config.pattDots = false;
 	config.waveformCenterLine = true;
@@ -190,6 +191,13 @@
 		{
 			configLine = strtok(NULL, "\n");
 			continue;
+		}
+
+		// DISABLE_E8X (Karplus-Strong command)
+		else if (!_strnicmp(configLine, "DISABLE_E8X=", 12))
+		{
+			     if (!_strnicmp(&configLine[12], "TRUE",  4)) config.disableE8xEffect = true;
+			else if (!_strnicmp(&configLine[12], "FALSE", 5)) config.disableE8xEffect = false;
 		}
 
 		// HWMOUSE
--- a/src/pt2_config.h
+++ b/src/pt2_config.h
@@ -15,7 +15,7 @@
 	char *defModulesDir, *defSamplesDir;
 	bool waveformCenterLine, pattDots, compoMode, autoCloseDiskOp, hideDiskOpDates, hwMouse;
 	bool transDel, fullScreenStretch, vsyncOff, modDot, blankZeroFlag, realVuMeters, rememberPlayMode;
-	bool startInFullscreen, integerScaling;
+	bool startInFullscreen, integerScaling, disableE8xEffect;
 	int8_t stereoSeparation, videoScaleFactor, accidental;
 	uint8_t pixelFilter, filterModel;
 	uint16_t quantizeValue;
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -52,7 +52,7 @@
 	-1, 24, 26, 28
 };
 
-void setPattern(int16_t pattern); // pt_modplayer.c
+void setPattern(int16_t pattern); // pt2_replayer.c
 
 void jamAndPlaceSample(SDL_Scancode scancode,  bool normalMode);
 uint8_t quantizeCheck(uint8_t row);
--- a/src/pt2_header.h
+++ b/src/pt2_header.h
@@ -14,7 +14,7 @@
 #include "pt2_unicode.h"
 #include "pt2_palette.h"
 
-#define PROG_VER_STR "1.33"
+#define PROG_VER_STR "1.34"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -43,8 +43,8 @@
 static bool handleGeneralModes(SDL_Keycode keycode, SDL_Scancode scancode);
 bool handleTextEditMode(SDL_Scancode scancode);
 
-void sampleUpButton(void); // pt_mouse.c
-void sampleDownButton(void); // pt_mouse.c
+void sampleUpButton(void); // pt2_mouse.c
+void sampleDownButton(void); // pt2_mouse.c
 
 void gotoNextMulti(void)
 {
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -18,7 +18,7 @@
 
 #define TICKS_PER_RENDER_CHUNK 64
 
-// pt_modplayer.c
+// pt2_replayer.c
 void storeTempVariables(void);
 bool intMusic(void);
 // ---------------------
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -16,8 +16,8 @@
 #include "pt2_pat2smp.h"
 #include "pt2_downsamplers2x.h"
 
-bool intMusic(void); // pt_modplayer.c
-void storeTempVariables(void); // pt_modplayer.c
+bool intMusic(void); // pt2_replayer.c
+void storeTempVariables(void); // pt2_replayer.c
 
 void doPat2Smp(void)
 {
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -143,7 +143,7 @@
 	{
 		ch->n_funkoffset = 0;
 
-		if (ch->n_loopstart != NULL && ch->n_wavestart != NULL) // non-PT2 bug fix
+		if (ch->n_loopstart != NULL && ch->n_wavestart != NULL) // ProTracker bugfix
 		{
 			if (++ch->n_wavestart >= ch->n_loopstart + (ch->n_replen << 1))
 				ch->n_wavestart = ch->n_loopstart;
@@ -203,16 +203,34 @@
 	ch->n_wavecontrol = ((ch->n_cmd & 0xF) << 4) | (ch->n_wavecontrol & 0xF);
 }
 
+/* This is a little used effect, despite being present in original ProTracker.
+** E8x was sometimes entirely replaced with code used for demo fx syncing in
+** demo mod players, so it can be turned off by looking at DISABLE_E8X in
+** protracker.ini if you so desire.
+*/
 static void karplusStrong(moduleChannel_t *ch)
 {
-	/* This effect is definitely the least used PT effect there is!
-	** It trashes (filters) the sample data.
-	** The reason I'm not implementing it is because a lot of songs used
-	** E8x for syncing to demos/intros, and because I have never ever
-	** seen this effect being used intentionally.
-	*/
+	int8_t a, b;
 
-	(void)ch;
+	if (config.disableE8xEffect)
+		return;
+
+	if (ch->n_loopstart == NULL)
+		return; // ProTracker bugfix
+
+	int8_t *ptr8 = ch->n_loopstart;
+	int16_t end = ((ch->n_replen * 2) & 0xFFFF) - 2;
+	do
+	{
+		a = ptr8[0];
+		b = ptr8[1];
+		*ptr8++ = (a + b) >> 1;
+	}
+	while (--end >= 0);
+
+	a = ptr8[0];
+	b = ch->n_loopstart[0];
+	*ptr8 = (a + b) >> 1;
 }
 
 static void doRetrg(moduleChannel_t *ch)
@@ -247,17 +265,16 @@
 
 static void volumeSlide(moduleChannel_t *ch)
 {
-	uint8_t cmd = ch->n_cmd & 0xFF;
-
-	if ((cmd & 0xF0) == 0)
+	uint8_t param = ch->n_cmd & 0xFF;
+	if ((param & 0xF0) == 0)
 	{
-		ch->n_volume -= cmd & 0x0F;
+		ch->n_volume -= param & 0x0F;
 		if (ch->n_volume < 0)
 			ch->n_volume = 0;
 	}
 	else
 	{
-		ch->n_volume += cmd >> 4;
+		ch->n_volume += param >> 4;
 		if (ch->n_volume > 64)
 			ch->n_volume = 64;
 	}
@@ -306,6 +323,7 @@
 	if (song->tick == 0)
 	{
 		ch->n_glissfunk = ((ch->n_cmd & 0xF) << 4) | (ch->n_glissfunk & 0xF);
+
 		if ((ch->n_glissfunk & 0xF0) > 0)
 			updateFunk(ch);
 	}
@@ -313,7 +331,7 @@
 
 static void positionJump(moduleChannel_t *ch)
 {
-	modOrder = (ch->n_cmd & 0xFF) - 1; // 0xFF (B00) jumps to pat 0
+	modOrder = (ch->n_cmd & 0xFF) - 1; // B00 results in -1, but it safely jumps to order 0
 	pBreakPosition = 0;
 	posJumpAssert = true;
 }
@@ -360,9 +378,7 @@
 	uint8_t arpTick, arpNote;
 	const int16_t *periods;
 
-	assert(song->tick < 32);
-	arpTick = arpTickTable[song->tick]; // 0, 1, 2
-
+	arpTick = song->tick % 3; // 0, 1, 2
 	if (arpTick == 1)
 	{
 		arpNote = (uint8_t)(ch->n_cmd >> 4);
@@ -398,7 +414,7 @@
 	ch->n_period -= (ch->n_cmd & 0xFF) & lowMask;
 	lowMask = 0xFF;
 
-	if ((ch->n_period & 0xFFF) < 113) // PT BUG: unsigned comparison, underflow not clamped!
+	if ((ch->n_period & 0xFFF) < 113) // PT BUG: sign removed before comparison, underflow not clamped!
 		ch->n_period = (ch->n_period & 0xF000) | 113;
 
 	paulaSetPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
@@ -417,7 +433,11 @@
 
 static void filterOnOff(moduleChannel_t *ch)
 {
-	setLEDFilter(!(ch->n_cmd & 1), false);
+	if (song->tick == 0) // added this (just pointless to call this during all ticks!)
+	{
+		const bool filterOn = (ch->n_cmd & 1) ^ 1;
+		setLEDFilter(filterOn, false);
+	}
 }
 
 static void finePortaUp(moduleChannel_t *ch)
@@ -649,6 +669,7 @@
 
 	uint16_t newOffset = ch->n_sampleoffset << 7;
 
+	// this signed test is the reason for the 9xx "sample >64kB = silence" bug
 	if ((int16_t)newOffset < ch->n_length)
 	{
 		ch->n_length -= newOffset;
@@ -662,18 +683,19 @@
 
 static void E_Commands(moduleChannel_t *ch)
 {
-	const uint8_t cmd = (ch->n_cmd & 0xF0) >> 4;
-	switch (cmd)
+	const uint8_t ecmd = (ch->n_cmd & 0x00F0) >> 4;
+	switch (ecmd)
 	{
-		case 0x0: filterOnOff(ch);       break;
-		case 0x1: finePortaUp(ch);       break;
-		case 0x2: finePortaDown(ch);     break;
-		case 0x3: setGlissControl(ch);   break;
-		case 0x4: setVibratoControl(ch); break;
-		case 0x5: setFineTune(ch);       break;
-		case 0x6: jumpLoop(ch);          break;
-		case 0x7: setTremoloControl(ch); break;
-		case 0x8: karplusStrong(ch);     break;
+		case 0x0: filterOnOff(ch);       return;
+		case 0x1: finePortaUp(ch);       return;
+		case 0x2: finePortaDown(ch);     return;
+		case 0x3: setGlissControl(ch);   return;
+		case 0x4: setVibratoControl(ch); return;
+		case 0x5: setFineTune(ch);       return;
+		case 0x6: jumpLoop(ch);          return;
+		case 0x7: setTremoloControl(ch); return;
+		case 0x8: karplusStrong(ch);     return;
+		case 0xE: patternDelay(ch);      return;
 		default: break;
 	}
 
@@ -680,15 +702,14 @@
 	if (editor.muted[ch->n_chanindex])
 		return;
 
-	switch (cmd)
+	switch (ecmd)
 	{
-		case 0x9: retrigNote(ch);     break;
-		case 0xA: volumeFineUp(ch);   break;
-		case 0xB: volumeFineDown(ch); break;
-		case 0xC: noteCut(ch);        break;
-		case 0xD: noteDelay(ch);      break;
-		case 0xE: patternDelay(ch);   break;
-		case 0xF: funkIt(ch);         break;
+		case 0x9: retrigNote(ch);     return;
+		case 0xA: volumeFineUp(ch);   return;
+		case 0xB: volumeFineDown(ch); return;
+		case 0xC: noteCut(ch);        return;
+		case 0xD: noteDelay(ch);      return;
+		case 0xF: funkIt(ch);         return;
 		default: break;
 	}
 }
@@ -695,71 +716,74 @@
 
 static void checkMoreEffects(moduleChannel_t *ch)
 {
-	switch ((ch->n_cmd & 0xF00) >> 8)
+	const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+	switch (cmd)
 	{
-		case 0x9: sampleOffset(ch); break;
-		case 0xB: positionJump(ch); break;
+		case 0x9: sampleOffset(ch); return; // note the returns here, not breaks!
+		case 0xB: positionJump(ch); return; 
+		case 0xD: patternBreak(ch); return;
+		case 0xE: E_Commands(ch);   return;
+		case 0xF: setSpeed(ch);     return;
+		default: break;
+	}
 
-		case 0xC:
-		{
-			if (!editor.muted[ch->n_chanindex])
-				volumeChange(ch);
-		}
-		break;
+	if (editor.muted[ch->n_chanindex])
+		return;
 
-		case 0xD: patternBreak(ch); break;
-		case 0xE: E_Commands(ch);   break;
-		case 0xF: setSpeed(ch);     break;
-
-		default:
-		{
-			if (!editor.muted[ch->n_chanindex])
-				paulaSetPeriod(ch->n_chanindex, ch->n_period);
-		}
-		break;
+	if (cmd == 0xC)
+	{
+		volumeChange(ch);
+		return;
 	}
+
+	paulaSetPeriod(ch->n_chanindex, ch->n_period);
 }
 
-static void checkEffects(moduleChannel_t *ch)
+static void chkefx2(moduleChannel_t *ch)
 {
-	if (editor.muted[ch->n_chanindex])
-		return;
-
 	updateFunk(ch);
 
-	const uint8_t effect = (ch->n_cmd & 0xF00) >> 8;
-	if ((ch->n_cmd & 0xFFF) > 0)
+	if ((ch->n_cmd & 0xFFF) == 0)
+		return;
+
+	const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+	switch (cmd)
 	{
-		switch (effect)
-		{
-			case 0x0: arpeggio(ch);            break;
-			case 0x1: portaUp(ch);             break;
-			case 0x2: portaDown(ch);           break;
-			case 0x3: tonePortamento(ch);      break;
-			case 0x4: vibrato(ch);             break;
-			case 0x5: tonePlusVolSlide(ch);    break;
-			case 0x6: vibratoPlusVolSlide(ch); break;
-			case 0xE: E_Commands(ch);          break;
+		case 0x0: arpeggio(ch);            return; // note the returns here, not breaks!
+		case 0x1: portaUp(ch);             return;
+		case 0x2: portaDown(ch);           return;
+		case 0x3: tonePortamento(ch);      return;
+		case 0x4: vibrato(ch);             return;
+		case 0x5: tonePlusVolSlide(ch);    return;
+		case 0x6: vibratoPlusVolSlide(ch); return;
+		case 0xE: E_Commands(ch);          return;
+		default: break;
+	}
 
-			case 0x7:
-			{
-				paulaSetPeriod(ch->n_chanindex, ch->n_period);
-				tremolo(ch);
-			}
-			break;
+	paulaSetPeriod(ch->n_chanindex, ch->n_period);
 
-			case 0xA:
-			{
-				paulaSetPeriod(ch->n_chanindex, ch->n_period);
-				volumeSlide(ch);
-			}
-			break;
+	if (cmd == 0x7)
+		tremolo(ch);
+	else if (cmd == 0xA)
+		volumeSlide(ch);
+}
 
-			default: paulaSetPeriod(ch->n_chanindex, ch->n_period); break;
-		}
-	}
+static void checkEffects(moduleChannel_t *ch)
+{
+	if (editor.muted[ch->n_chanindex])
+		return;
 
-	if (effect != 0x7)
+	chkefx2(ch);
+
+	/* This is not very clear in the original PT replayer code,
+	** but the tremolo effect skips chkefx2()'s return address
+	** in the stack so that it jumps to checkEffects()'s return
+	** address instead of ending up here. In other words, volume
+	** is not updated here after tremolo (it's done inside the
+	** tremolo routine itself).
+	*/
+	const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+	if (cmd != 0x7)
 		paulaSetVolume(ch->n_chanindex, ch->n_volume);
 }
 
@@ -865,7 +889,7 @@
 			ch->n_wavestart = ch->n_start;
 		}
 
-		// non-PT2 quirk
+		// non-PT2 requirement (set safe sample space for uninitialized voices - f.ex. "the ultimate beeper.mod")
 		if (ch->n_length == 0)
 			ch->n_loopstart = ch->n_wavestart = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
 	}
@@ -879,7 +903,7 @@
 		}
 		else
 		{
-			cmd = (ch->n_cmd & 0xF00) >> 8;
+			cmd = (ch->n_cmd & 0x0F00) >> 8;
 			if (cmd == 3 || cmd == 5)
 			{
 				setVUMeterHeight(ch);
@@ -1032,7 +1056,7 @@
 		song->tick++;
 
 	bool readNewNote = false;
-	if ((unsigned)song->tick >= (unsigned)song->speed)
+	if ((uint32_t)song->tick >= (uint32_t)song->speed)
 	{
 		song->tick = 0;
 		readNewNote = true;
--- a/src/pt2_sampling.c
+++ b/src/pt2_sampling.c
@@ -3,7 +3,7 @@
 ** have the proper knowledge on this stuff.
 **
 ** Some functions like sin() may be different depending on
-** math library implementation, but we don't use pt_math.c
+** math library implementation, but we don't use pt2_math.c
 ** replacements for speed reasons.
 */
 
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -267,4 +267,4 @@
 extern cursor_t cursor;
 extern ui_t ui;
 
-extern module_t *song; // pt_main.c
+extern module_t *song; // pt2_main.c
--- a/src/pt2_tables.c
+++ b/src/pt2_tables.c
@@ -86,13 +86,6 @@
 	0xB4, 0xA1, 0x8D, 0x78, 0x61, 0x4A, 0x31, 0x18
 };
 
-const uint8_t arpTickTable[32] =
-{
-	0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,
-	0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,
-	0,1
-};
-
 const int16_t periodTable[(37*16)+15] =
 {
 	856,808,762,720,678,640,604,570,538,508,480,453,
--- a/src/pt2_tables.h
+++ b/src/pt2_tables.h
@@ -14,7 +14,6 @@
 extern const char *noteNames3[2+36];
 extern const char *noteNames4[2+36];
 extern const uint8_t vibratoTable[32];
-extern const uint8_t arpTickTable[32];
 extern const int16_t periodTable[(37*16)+15];
 extern int8_t pNoteTable[32];
 extern const uint64_t musicTimeTab64[256-32];
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -2111,7 +2111,7 @@
 	renderSprites();
 	SDL_UpdateTexture(video.texture, NULL, video.frameBuffer, SCREEN_W * sizeof (int32_t));
 
-	// SDL2 bug on Windows (?): This function consumes ever-increasing memory if the program is minimized
+	// SDL 2.0.14 bug on Windows (?): This function consumes ever-increasing memory if the program is minimized
 	if (!minimized)
 		SDL_RenderClear(video.renderer);
 
--- a/vs2019_project/pt2-clone/protracker.ini
+++ b/vs2019_project/pt2-clone/protracker.ini
@@ -203,12 +203,21 @@
 ;
 DOTTEDCENTER=TRUE
 
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+;        Syntax: TRUE or FALSE
+; Default value: FALSE
+;       Comment: This ProTracker command low-pass filters the current sample.
+;                It's a little used effect despite being present in original PT,
+;                and it was often replaced for syncing visuals with the music in
+;                demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
 [AUDIO SETTINGS]
 ; Audio output frequency
 ;        Syntax: Number, in hertz
 ; Default value: 48000
-;       Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-;         OS is set to mix shared audio at 96kHz or higher.
+;       Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
 ;
 FREQUENCY=48000