shithub: pt2-clone

Download patch

ref: 6fe2950c2bca6718a6d876d6fb626bb07a98c79d
parent: f94686bf4da0bc46cd4f08ca9aa420cc2d75779f
author: Olav Sørensen <olav.sorensen@live.no>
date: Fri Dec 18 14:56:04 EST 2020

A500 low-pass filter is now much closer to a real typical A500

--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -35,6 +35,7 @@
 #include "pt2_structs.h"
 #include "pt2_rcfilter.h"
 #include "pt2_ledfilter.h"
+#include "pt2_downsamplers2x.h"
 
 #define INITIAL_DITHER_SEED 0x12345000
 
@@ -46,9 +47,9 @@
 static uint32_t audLatencyPerfValInt, audLatencyPerfValFrac;
 static uint64_t tickTime64, tickTime64Frac;
 static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned, dOldVoiceDelta, dOldVoiceDeltaMul;
-static double dPrngStateL, dPrngStateR;
+static double dPrngStateL, dPrngStateR, dLState[2], dRState[2];
 static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES];
-static rcFilter_t filterLoA500, filterLoA1200, filterHiA500, filterHiA1200;
+static rcFilter_t filterLoA500, filterHiA500, filterHiA1200;
 static ledFilter_t filterLED;
 static SDL_AudioDeviceID dev;
 
@@ -188,7 +189,6 @@
 		mixerKillVoice(i);
 
 	clearRCFilterState(&filterLoA500);
-	clearRCFilterState(&filterLoA1200);
 	clearRCFilterState(&filterHiA500);
 	clearRCFilterState(&filterHiA1200);
 	clearLEDFilterState(&filterLED);
@@ -372,7 +372,6 @@
 		lockAudio();
 
 	clearRCFilterState(&filterLoA500);
-	clearRCFilterState(&filterLoA1200);
 	clearRCFilterState(&filterHiA500);
 	clearRCFilterState(&filterHiA1200);
 	clearLEDFilterState(&filterLED);
@@ -465,6 +464,12 @@
 	dPrngStateR = 0.0;
 }
 
+void resetAudioDownsamplingStates(void)
+{
+	dLState[0] = dLState[1] = 0.0;
+	dRState[0] = dRState[1] = 0.0;
+}
+
 static inline int32_t random32(void)
 {
 	// LCG random 32-bit generator (quite good and fast)
@@ -473,44 +478,56 @@
 	return randSeed;
 }
 
-static inline void processMixedSamples(int32_t i, int16_t *out)
+static void processMixedSamples(int32_t i, int16_t *out)
 {
 	int32_t smp32;
-	double dOut[2], dPrng;
+	double dPrng, dOut[2], dMixL[2], dMixR[2];
 
-	dOut[0] = dMixBufferL[i];
-	dOut[1] = dMixBufferR[i];
-
-	if (filterModel == FILTERMODEL_A500)
+	// we run the filters at 2x the audio output rate for more precision
+	for (int32_t j = 0; j < 2; j++)
 	{
-		// A500 low-pass RC filter
-		RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
+		// zero-padding (yes, this makes sense)
+		dOut[0] = (j == 0) ? dMixBufferL[i] : 0.0;
+		dOut[1] = (j == 0) ? dMixBufferR[i] : 0.0;
 
-		// "LED" Sallen-Key filter
-		if (ledFilterEnabled)
-			LEDFilter(&filterLED, dOut, dOut);
+		if (filterModel == FILTERMODEL_A500)
+		{
+			// A500 low-pass RC filter
+			RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
 
-		// A500 high-pass RC filter
-		RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
-	}
-	else
-	{
-		// A1200 low-pass RC filter
-		if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
-			RCLowPassFilterStereo(&filterLoA1200, dOut, dOut);
+			// "LED" Sallen-Key filter
+			if (ledFilterEnabled)
+				LEDFilter(&filterLED, dOut, dOut);
 
-		// "LED" Sallen-Key filter
-		if (ledFilterEnabled)
-			LEDFilter(&filterLED, dOut, dOut);
+			// A500 high-pass RC filter
+			RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
+		}
+		else
+		{
+			// A1200 low-pass filter is ignored (we don't want it)
 
-		// A1200 high-pass RC filter
-		RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
+			// "LED" Sallen-Key filter
+			if (ledFilterEnabled)
+				LEDFilter(&filterLED, dOut, dOut);
+
+			// A1200 high-pass RC filter
+			RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
+		}
+
+		dMixL[j] = dOut[0];
+		dMixR[j] = dOut[1];
 	}
 
-	// normalize and flip phase (A500/A1200 has a phase-inverted audio signal)
-	dOut[0] *= -INT16_MAX / (double)AMIGA_VOICES;
-	dOut[1] *= -INT16_MAX / (double)AMIGA_VOICES;
+#define NORMALIZE_DOWNSAMPLE 2.0
 
+	// 2x "all-pass halfband" downsampling
+	dOut[0] = d2x(dMixL, dLState);
+	dOut[1] = d2x(dMixR, dRState);
+
+	// normalize and invert phase (A500/A1200 has a phase-inverted audio signal)
+	dOut[0] *= NORMALIZE_DOWNSAMPLE * (-INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= NORMALIZE_DOWNSAMPLE * (-INT16_MAX / (double)AMIGA_VOICES);
+
 	// left channel - 1-bit triangular dithering (high-pass filtered)
 	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
 	dOut[0] = (dOut[0] + dPrng) - dPrngStateL;
@@ -564,6 +581,7 @@
 		for (i = 0; i < numSamples; i++)
 		{
 			processMixedSamples(i, out);
+
 			*outStream++ = out[0];
 			*outStream++ = out[1];
 		}
@@ -647,7 +665,7 @@
 			samplesToMix = remainingTick;
 
 		outputAudio(streamOut, samplesToMix);
-		streamOut += samplesToMix << 1;
+		streamOut += samplesToMix<<1;
 
 		samplesLeft -= samplesToMix;
 		audio.dTickSampleCounter -= samplesToMix;
@@ -711,24 +729,48 @@
 	** - RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF (f=5.204Hz)
 	*/
 
+	// we run the filters at twice the frequency for improved precision (zero-padding)
+	const uint32_t audioFreq = audio.outputRate * 2;
+
 	double R, C, R1, R2, C1, C2, fc, fb;
 	const double pi = 4.0 * atan(1.0); // M_PI can not be trusted
 
+	/*
+	** 8bitbubsy:
+	** Hackish low-pass cutoff compensation to better match Amiga 500 when
+	** we use "lower" audio output rates. This has been loosely hand-picked
+	** after looking at many frequency analyses on a sine-sweep test module
+	** rendered on 7 different Amiga 500 machines (and taking the average).
+	** Don't try to make sense of this magic constant, and it should only be
+	** used within this very specific application!
+	**
+	** The reason we want this bias is because our digital RC filter is not
+	** that precise at lower audio output rates. It would otherwise lead to a
+	** slight unwanted cut of treble near the cutoff we aim for. It was easily
+	** audible, and especially visible on a plotted frequency spectrum.
+	**
+	** 1100Hz is the magic value I found that seems to be good. Higher than that
+	** would allow too much treble to pass.
+	**
+	** Scaling it like this is 'acceptable' (confirmed with further frequency analyses
+	** at output rates of 48, 96 and 192).
+	*/
+	double dLPCutoffBias = 1100.0 * (44100.0 / audio.outputRate);
+
 	// A500 1-pole (6db/oct) static RC low-pass filter:
 	R = 360.0; // R321 (360 ohm resistor)
 	C = 1e-7;  // C321 (0.1uF capacitor)
-	fc = 1.0 / (2.0 * pi * R * C);
-	calcRCFilterCoeffs(audio.outputRate, fc, &filterLoA500);
+	fc = (1.0 / (2.0 * pi * R * C)) + dLPCutoffBias;
+	calcRCFilterCoeffs(audioFreq, fc, &filterLoA500);
+	
+	/*
+	** 8bitbubsy:
+	** We don't handle Amiga 1200's ~34kHz low-pass filter as it's not really
+	** needed. The reason it was still present in the A1200 (despite its high
+	** non-audible cutoff) was to filter away high-frequency noise from Paula's
+	** PWM (volume modulation). We don't do PWM for volume in the PT2 clone.
+	*/
 
-	// A1200 1-pole (6dB/oct) static RC low-pass filter:
-	if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
-	{
-		R = 680.0;  // R321 (680 ohm resistor)
-		C = 6.8e-9; // C321 (6800pf capacitor)
-		fc = 1.0 / (2.0 * pi * R * C);
-		calcRCFilterCoeffs(audio.outputRate, fc, &filterLoA1200);
-	}
-
 	// Sallen-Key filter ("LED" filter, same RC values on A500 and A1200):
 	R1 = 10000.0; // R322 (10K ohm resistor)
 	R2 = 10000.0; // R323 (10K ohm resistor)
@@ -736,19 +778,19 @@
 	C2 = 3.9e-9;  // C323 (3900pF capacitor)
 	fc = 1.0 / (2.0 * pi * sqrt(R1 * R2 * C1 * C2));
 	fb = 0.125; // Fb = 0.125 : Q ~= 1/sqrt(2)
-	calcLEDFilterCoeffs(audio.outputRate, fc, fb, &filterLED);
+	calcLEDFilterCoeffs(audioFreq, fc, fb, &filterLED);
 
 	// A500 1-pole (6dB/oct) static RC high-pass filter:
 	R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
 	C = 2.233e-5; // C334 (22uF capacitor) + C335 (0.33�F capacitor)
 	fc = 1.0 / (2.0 * pi * R * C);
-	calcRCFilterCoeffs(audio.outputRate, fc, &filterHiA500);
+	calcRCFilterCoeffs(audioFreq, fc, &filterHiA500);
 
 	// A1200 1-pole (6dB/oct) static RC high-pass filter:
 	R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
 	C = 2.2e-5; // C334 (22uF capacitor)
 	fc = 1.0 / (2.0 * pi * R * C);
-	calcRCFilterCoeffs(audio.outputRate, fc, &filterHiA1200);
+	calcRCFilterCoeffs(audioFreq, fc, &filterHiA1200);
 }
 
 void recalcFilterCoeffs(int32_t outputRate) // for MOD2WAV
@@ -761,7 +803,6 @@
 	audio.outputRate = outputRate;
 
 	clearRCFilterState(&filterLoA500);
-	clearRCFilterState(&filterLoA1200);
 	clearRCFilterState(&filterHiA500);
 	clearRCFilterState(&filterHiA1200);
 	clearLEDFilterState(&filterLED);
@@ -904,8 +945,8 @@
 
 	const int32_t maxSamplesToMix = MAX(pat2SmpMaxSamples, MAX(mod2WavMaxSamples, renderMaxSamples));
 
-	dMixBufferLUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
-	dMixBufferRUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
+	dMixBufferLUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double) * 8, 256);
+	dMixBufferRUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double) * 8, 256);
 
 	if (dMixBufferLUnaligned == NULL || dMixBufferRUnaligned == NULL)
 	{
@@ -928,6 +969,7 @@
 
 	calcAudioLatencyVars(audio.audioBufferSize, audio.outputRate);
 
+	resetAudioDownsamplingStates();
 	audio.resetSyncTickTimeFlag = true;
 	SDL_PauseAudioDevice(dev, false);
 	return true;
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -72,6 +72,7 @@
 void turnOffVoices(void);
 void mixerCalcVoicePans(uint8_t stereoSeparation);
 void outputAudio(int16_t *target, int32_t numSamples);
+void resetAudioDownsamplingStates(void);
 
 extern audio_t audio; // pt2_audio.c
 extern paulaVoice_t paula[AMIGA_VOICES]; // pt2_audio.c
--- a/src/pt2_downsample2x.c
+++ b/src/pt2_downsample2x.c
@@ -25,7 +25,7 @@
 	return out;
 }
 
-static double d2x(const double *input, double *b)
+double d2x(const double *input, double *b)
 {
 	return (f(input[0], &b[0], 0.150634765625) + f(input[1], &b[1], -0.3925628662109375)) * 0.5;
 }
--- a/src/pt2_downsamplers2x.h
+++ b/src/pt2_downsamplers2x.h
@@ -4,6 +4,8 @@
 
 // all-pass halfband filters
 
+double d2x(const double *input, double *b);
+
 // Warning: These can exceed -1.0 .. 1.0 because of undershoot/overshoot!
 
 void downsample2xFloat(float *buffer, int32_t originalLength);
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -123,6 +123,8 @@
 	fwrite(&wavHeader, sizeof (wavHeader_t), 1, f);
 	fclose(f);
 
+	resetAudioDownsamplingStates();
+
 	ui.mod2WavFinished = true;
 	ui.updateMod2WavDialog = true;