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];
}