shithub: pt2-clone

Download patch

ref: 7d12cfe365ae8c4ef3ee6aaf41e963e1d5d63fb2
parent: c096b83b44248f0f714bd89b5091f419efd67335
author: Olav Sørensen <olav.sorensen@live.no>
date: Sun May 15 15:58:25 EDT 2022

Small audio filter changes

- The "LED" filter's quality factor (Q) should now be nominally correct
- Some minor changes to the audio filtering code

--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -1034,7 +1034,7 @@
 	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
 	*/
 	double dAudioFreq = audio.outputRate;
-	double R, C, R1, R2, C1, C2, fc, fb;
+	double R, C, R1, R2, C1, C2, cutoff, qfactor;
 
 	if (audio.oversamplingFlag)
 		dAudioFreq *= 2.0; // 2x oversampling
@@ -1042,41 +1042,41 @@
 	// A500 1-pole (6db/oct) static RC low-pass filter:
 	R = 360.0; // R321 (360 ohm)
 	C = 1e-7;  // C321 (0.1uF)
-	fc = 1.0 / (PT2_TWO_PI * R * C); // cutoff = ~4420.97Hz
-	calcRCFilterCoeffs(dAudioFreq, fc, &filterLoA500);
+	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~4420.971Hz
+	calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA500);
 
-	// A1200 1-pole (6db/oct) static RC low-pass filter:
+	// (optional) A1200 1-pole (6db/oct) static RC low-pass filter:
 	R = 680.0;  // R321 (680 ohm)
 	C = 6.8e-9; // C321 (6800pF)
-	fc = 1.0 / (PT2_TWO_PI * R * C); // cutoff = ~34419.32Hz
+	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~34419.322Hz
 
 	useA1200LowPassFilter = false;
-	if (dAudioFreq/2.0 > fc)
+	if (dAudioFreq/2.0 > cutoff)
 	{
-		calcRCFilterCoeffs(dAudioFreq, fc, &filterLoA1200);
+		calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA1200);
 		useA1200LowPassFilter = true;
 	}
 
-	// Sallen-Key filter ("LED" filter, same values on A500/A1200):
+	// Sallen-Key low-pass filter ("LED" filter, same values on A500/A1200):
 	R1 = 10000.0; // R322 (10K ohm)
 	R2 = 10000.0; // R323 (10K ohm)
 	C1 = 6.8e-9;  // C322 (6800pF)
 	C2 = 3.9e-9;  // C323 (3900pF)
-	fc = 1.0 / (PT2_TWO_PI * pt2_sqrt(R1 * R2 * C1 * C2)); // cutoff = ~3090.53Hz
-	fb = 0.125/2.0; // Fb = 0.125 : Q ~= 1/sqrt(2) (Butterworth) (8bb: was 0.125, but /2 gives a closer gain!)
-	calcLEDFilterCoeffs(dAudioFreq, fc, fb, &filterLED);
+	cutoff = 1.0 / (PT2_TWO_PI * pt2_sqrt(R1 * R2 * C1 * C2)); // ~3090.533Hz
+	qfactor = pt2_sqrt(R1 * R2 * C1 * C2) / (C2 * (R1 + R2)); // ~0.660225
+	calcLEDFilterCoeffs(dAudioFreq, cutoff, qfactor, &filterLED);
 
 	// A500 1-pole (6dB/oct) static RC high-pass filter:
 	R = 1390.0;   // R324 (1K ohm) + R325 (390 ohm)
 	C = 2.233e-5; // C334 (22uF) + C335 (0.33uF)
-	fc = 1.0 / (PT2_TWO_PI * R * C); // cutoff = ~5.13Hz
-	calcRCFilterCoeffs(dAudioFreq, fc, &filterHiA500);
+	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.128Hz
+	calcRCFilterCoeffs(dAudioFreq, cutoff, &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 / (PT2_TWO_PI * R * C); // cutoff = ~5.20Hz
-	calcRCFilterCoeffs(dAudioFreq, fc, &filterHiA1200);
+	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.205Hz
+	calcRCFilterCoeffs(dAudioFreq, cutoff, &filterHiA1200);
 }
 
 void mixerSetStereoSeparation(uint8_t percentage) // 0..100 (percentage)
--- 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.46"
+#define PROG_VER_STR "1.47"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
--- a/src/pt2_ledfilter.c
+++ b/src/pt2_ledfilter.c
@@ -1,79 +1,46 @@
-/* aciddose:
-** Imperfect Amiga "LED" filter implementation. This may be further improved in the future.
-** Based upon ideas posted by mystran @ the kvraudio.com forum.
-**
-** This filter may not function correctly used outside the fixed-cutoff context here!
+/* Fast and approximated implementation of the Amiga "LED" filter.
+** Based on https://www.musicdsp.org/en/latest/Filters/38-lp-and-hp-filter.html
 */
 
-#include <stdint.h>
 #include "pt2_math.h"
 #include "pt2_ledfilter.h"
 
-void clearLEDFilterState(ledFilter_t *filterLED)
+void clearLEDFilterState(ledFilter_t *f)
 {
-	filterLED->buffer[0] = filterLED->buffer[1] = 0.0; // left channel
-	filterLED->buffer[2] = filterLED->buffer[3] = 0.0; // right channel
+	f->LIn1 = f->LIn2 = f->LOut1 = f->LOut2 = 0.0;
+	f->RIn1 = f->RIn2 = f->ROut1 = f->ROut2 = 0.0;
 }
 
-static double sigmoid(double x, double coefficient)
+void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter)
 {
-	/* aciddose:
-	** Coefficient from:
-	**   0.0 to  inf (linear)
-	**  -1.0 to -inf (linear)
-	*/
-	return x / (x + coefficient) * (coefficient + 1.0);
-}
+	const double c = 1.0 / pt2_tan((PT2_PI * hz) / sr);
+	const double r = 1.0 / qfactor;
 
-void calcLEDFilterCoeffs(const double sr, const double hz, const double fb, ledFilter_t *filter)
-{
-	// 8bitbubsy: the tangent approximation is suitable for these input ranges
-	const double c = (hz < sr/2.0) ? pt2_tan((PT2_PI * hz) / sr) : 1.0;
-	const double g = 1.0 / (1.0 + c);
-
-	// aciddose: dirty compensation
-	const double s = 0.5;
-	const double t = 0.5;
-	const double ic = c > t ? 1.0 / ((1.0 - s*t) + s*c) : 1.0;
-	const double cg = c * g;
-	const double fbg = 1.0 / (1.0 + fb * cg*cg);
-
-	filter->c = c;
-	filter->ci = g;
-	filter->feedback = 2.0 * sigmoid(fb, 0.5);
-	filter->bg = fbg * filter->feedback * ic;
-	filter->cg = cg;
-	filter->c2 = c * 2.0;
+	filter->a1 = 1.0 / (1.0 + r * c + c * c);
+	filter->a2 = 2.0 * filter->a1;
+	filter->a3 = filter->a1;
+	filter->b1 = 2.0 * (1.0 - c*c) * filter->a1;
+	filter->b2 = (1.0 - r * c + c * c) * filter->a1;
 }
 
 void LEDFilter(ledFilter_t *f, const double *in, double *out)
 {
-	const double in_1 = DENORMAL_OFFSET;
-	const double in_2 = DENORMAL_OFFSET;
+	const double LOut = (f->a1 * in[0]) + (f->a2 * f->LIn1) + (f->a3 * f->LIn2) - (f->b1 * f->LOut1) - (f->b2 * f->LOut2);
+	const double ROut = (f->a1 * in[1]) + (f->a2 * f->RIn1) + (f->a3 * f->RIn2) - (f->b1 * f->ROut1) - (f->b2 * f->ROut2);
 
-	const double c = f->c;
-	const double g = f->ci;
-	const double cg = f->cg;
-	const double bg = f->bg;
-	const double c2 = f->c2;
+	// shift states
 
-	double *v = f->buffer;
+	f->LIn2 = f->LIn1;
+	f->LIn1 = in[0];
+	f->LOut2 = f->LOut1;
+	f->LOut1 = LOut;
 
-	// left channel
-	const double estimate_L = in_2 + g*(v[1] + c*(in_1 + g*(v[0] + c*in[0])));
-	const double y0_L = v[0]*g + in[0]*cg + in_1 + estimate_L * bg;
-	const double y1_L = v[1]*g + y0_L*cg + in_2;
+	f->RIn2 = f->RIn1;
+	f->RIn1 = in[1];
+	f->ROut2 = f->ROut1;
+	f->ROut1 = ROut;
 
-	v[0] += c2 * (in[0] - y0_L);
-	v[1] += c2 * (y0_L - y1_L);
-	out[0] = y1_L;
-
-	// right channel
-	const double estimate_R = in_2 + g*(v[3] + c*(in_1 + g*(v[2] + c*in[1])));
-	const double y0_R = v[2]*g + in[1]*cg + in_1 + estimate_R * bg;
-	const double y1_R = v[3]*g + y0_R*cg + in_2;
-
-	v[2] += c2 * (in[1] - y0_R);
-	v[3] += c2 * (y0_R - y1_R);
-	out[1] = y1_R;
+	// set output
+	out[0] = LOut;
+	out[1] = ROut;
 }
--- a/src/pt2_ledfilter.h
+++ b/src/pt2_ledfilter.h
@@ -1,14 +1,12 @@
 #pragma once
 
-#include <stdint.h>
-#include <stdbool.h>
-
 typedef struct ledFilter_t
 {
-	double buffer[4];
-	double c, ci, feedback, bg, cg, c2;
+	double LIn1, LIn2, LOut1, LOut2;
+	double RIn1, RIn2, ROut1, ROut2;
+	double a1, a2, a3, b1, b2;
 } ledFilter_t;
 
-void clearLEDFilterState(ledFilter_t *filterLED);
-void calcLEDFilterCoeffs(const double sr, const double hz, const double fb, ledFilter_t *filter);
+void clearLEDFilterState(ledFilter_t *f);
+void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter);
 void LEDFilter(ledFilter_t *f, const double *in, double *out);
--- a/src/pt2_math.c
+++ b/src/pt2_math.c
@@ -1,4 +1,4 @@
-/* Quite accurate approximation routines for sin/cos/sqrt/tan.
+/* Approximation routines for sin/cos/sqrt/tan.
 ** These should not be used in realtime, as they are too slow.
 */
 
--- a/src/pt2_math.h
+++ b/src/pt2_math.h
@@ -1,11 +1,5 @@
 #pragma once
 
-#include <stdint.h>
-#include <stdbool.h>
-
-// adding this prevents denormalized numbers, which is slow
-#define DENORMAL_OFFSET 1e-20
-
 #define PT2_PI 3.14159265358979323846264338327950288
 #define PT2_TWO_PI 6.28318530717958647692528676655900576
 
--- a/src/pt2_rcfilter.c
+++ b/src/pt2_rcfilter.c
@@ -5,15 +5,14 @@
 ** result in a cutoff that sounded slightly too low.
 */
 
-#include <stdint.h>
 #include "pt2_math.h"
 #include "pt2_rcfilter.h"
 
 void calcRCFilterCoeffs(double sr, double hz, rcFilter_t *f)
 {
-	const double a = (hz < sr/2.0) ? pt2_cos((PT2_TWO_PI * hz) / sr) : 1.0;
+	const double a = (hz < sr / 2.0) ? pt2_cos((PT2_TWO_PI * hz) / sr) : 1.0;
 	const double b = 2.0 - a;
-	const double c = b - pt2_sqrt((b*b)-1.0);
+	const double c = b - pt2_sqrt((b * b) - 1.0);
 
 	f->c1 = 1.0 - c;
 	f->c2 = c;
@@ -27,11 +26,11 @@
 void RCLowPassFilterStereo(rcFilter_t *f, const double *in, double *out)
 {
 	// left channel
-	f->tmp[0] = (f->c1*in[0] + f->c2*f->tmp[0]) + DENORMAL_OFFSET;
+	f->tmp[0] = (f->c1 * in[0]) + (f->c2 * f->tmp[0]);
 	out[0] = f->tmp[0];
 
 	// right channel
-	f->tmp[1] = (f->c1*in[1] + f->c2*f->tmp[1]) + DENORMAL_OFFSET;
+	f->tmp[1] = (f->c1 * in[1]) + (f->c2 * f->tmp[1]);
 	out[1] = f->tmp[1];
 }
 
@@ -38,22 +37,22 @@
 void RCHighPassFilterStereo(rcFilter_t *f, const double *in, double *out)
 {
 	// left channel
-	f->tmp[0] = (f->c1*in[0] + f->c2*f->tmp[0]) + DENORMAL_OFFSET;
-	out[0] = in[0]-f->tmp[0];
+	f->tmp[0] = (f->c1 * in[0]) + (f->c2 * f->tmp[0]);
+	out[0] = in[0] - f->tmp[0];
 
 	// right channel
-	f->tmp[1] = (f->c1*in[1] + f->c2*f->tmp[1]) + DENORMAL_OFFSET;
-	out[1] = in[1]-f->tmp[1];
+	f->tmp[1] = (f->c1 * in[1]) + (f->c2 * f->tmp[1]);
+	out[1] = in[1] - f->tmp[1];
 }
 
 void RCLowPassFilter(rcFilter_t *f, const double in, double *out)
 {
-	f->tmp[0] = (f->c1*in + f->c2*f->tmp[0]) + DENORMAL_OFFSET;
+	f->tmp[0] = (f->c1 * in) + (f->c2 * f->tmp[0]);
 	*out = f->tmp[0];
 }
 
 void RCHighPassFilter(rcFilter_t *f, const double in, double *out)
 {
-	f->tmp[0] = (f->c1*in + f->c2*f->tmp[0]) + DENORMAL_OFFSET;
-	*out = in-f->tmp[0];
+	f->tmp[0] = (f->c1 * in) + (f->c2 * f->tmp[0]);
+	*out = in - f->tmp[0];
 }