Closed dirkwhoffmann closed 1 year ago
For reference: Here is the setup code for all three filters in the filter pipeline:
double R, C, R1, R2, C1, C2, cutoff, qfactor;
if (amigaModel == MODEL_A500)
{
// A500 1-pole (6db/oct) RC low-pass filter:
R = 360.0; // R321 (360 ohm)
C = 1e-7; // C321 (0.1uF)
cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~4420.971Hz
setupOnePoleFilter(dPaulaOutputFreq, cutoff, &filterLo);
// A500 1-pole (6dB/oct) RC high-pass filter:
R = 1390.0; // R324 (1K ohm) + R325 (390 ohm)
C = 2.233e-5; // C334 (22uF) + C335 (0.33uF)
cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.128Hz
setupOnePoleFilter(dPaulaOutputFreq, cutoff, &filterHi);
}
else
{
/* Don't use the A1200 low-pass filter since its cutoff
** is well above human hearable range anyway (~34.4kHz).
** We don't do volume PWM, so we have nothing we need to
** filter away.
*/
useLowpassFilter = false;
// A1200 1-pole (6dB/oct) RC high-pass filter:
R = 1360.0; // R324 (1K ohm resistor) + R325 (360 ohm resistor)
C = 2.2e-5; // C334 (22uF capacitor)
cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.319Hz
setupOnePoleFilter(dPaulaOutputFreq, cutoff, &filterHi);
}
// 2-pole (12dB/oct) RC 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)
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
setupTwoPoleFilter(dPaulaOutputFreq, cutoff, qfactor, &filterLED);
Here are the implementations of the one pole and the two pole filter:
#include "pt2_math.h"
#include "pt2_rcfilters.h"
#define SMALL_NUMBER (1E-4)
/* 1-pole RC low-pass/high-pass filter, based on:
** https://www.musicdsp.org/en/latest/Filters/116-one-pole-lp-and-hp.html
*/
void setupOnePoleFilter(double audioRate, double cutOff, onePoleFilter_t *f)
{
if (cutOff >= audioRate/2.0)
cutOff = (audioRate/2.0) - SMALL_NUMBER;
const double a = 2.0 - pt2_cos((PT2_TWO_PI * cutOff) / audioRate);
const double b = a - pt2_sqrt((a * a) - 1.0);
f->a1 = 1.0 - b;
f->a2 = b;
}
void clearOnePoleFilterState(onePoleFilter_t *f)
{
f->tmpL = f->tmpR = 0.0;
}
void onePoleLPFilter(onePoleFilter_t *f, const double in, double *out)
{
f->tmpL = (f->a1 * in) + (f->a2 * f->tmpL);
*out = f->tmpL;
}
void onePoleLPFilterStereo(onePoleFilter_t *f, const double *in, double *out)
{
// left channel
f->tmpL = (f->a1 * in[0]) + (f->a2 * f->tmpL);
out[0] = f->tmpL;
// right channel
f->tmpR = (f->a1 * in[1]) + (f->a2 * f->tmpR);
out[1] = f->tmpR;
}
void onePoleHPFilter(onePoleFilter_t *f, const double in, double *out)
{
f->tmpL = (f->a1 * in) + (f->a2 * f->tmpL);
*out = in - f->tmpL;
}
void onePoleHPFilterStereo(onePoleFilter_t *f, const double *in, double *out)
{
// left channel
f->tmpL = (f->a1 * in[0]) + (f->a2 * f->tmpL);
out[0] = in[0] - f->tmpL;
// right channel
f->tmpR = (f->a1 * in[1]) + (f->a2 * f->tmpR);
out[1] = in[1] - f->tmpR;
}
/* 2-pole RC low-pass filter with Q factor, based on:
** https://www.musicdsp.org/en/latest/Filters/38-lp-and-hp-filter.html
*/
void setupTwoPoleFilter(double audioRate, double cutOff, double qFactor, twoPoleFilter_t *f)
{
if (cutOff >= audioRate/2.0)
cutOff = (audioRate/2.0) - SMALL_NUMBER;
const double a = 1.0 / pt2_tan((PT2_PI * cutOff) / audioRate);
const double b = 1.0 / qFactor;
f->a1 = 1.0 / (1.0 + b * a + a * a);
f->a2 = 2.0 * f->a1;
f->b1 = 2.0 * (1.0 - a*a) * f->a1;
f->b2 = (1.0 - b * a + a * a) * f->a1;
}
void clearTwoPoleFilterState(twoPoleFilter_t *f)
{
f->tmpL[0] = f->tmpL[1] = f->tmpL[2] = f->tmpL[3] = 0.0;
f->tmpR[0] = f->tmpR[1] = f->tmpR[2] = f->tmpR[3] = 0.0;
}
void twoPoleLPFilter(twoPoleFilter_t *f, const double in, double *out)
{
const double LOut = (f->a1 * in) + (f->a2 * f->tmpL[0]) + (f->a1 * f->tmpL[1]) - (f->b1 * f->tmpL[2]) - (f->b2 * f->tmpL[3]);
// shift states
f->tmpL[1] = f->tmpL[0];
f->tmpL[0] = in;
f->tmpL[3] = f->tmpL[2];
f->tmpL[2] = LOut;
// set output
*out = LOut;
}
void twoPoleLPFilterStereo(twoPoleFilter_t *f, const double *in, double *out)
{
const double LOut = (f->a1 * in[0]) + (f->a2 * f->tmpL[0]) + (f->a1 * f->tmpL[1]) - (f->b1 * f->tmpL[2]) - (f->b2 * f->tmpL[3]);
const double ROut = (f->a1 * in[1]) + (f->a2 * f->tmpR[0]) + (f->a1 * f->tmpR[1]) - (f->b1 * f->tmpR[2]) - (f->b2 * f->tmpR[3]);
// shift states
f->tmpL[1] = f->tmpL[0];
f->tmpL[0] = in[0];
f->tmpL[3] = f->tmpL[2];
f->tmpL[2] = LOut;
f->tmpR[1] = f->tmpR[0];
f->tmpR[0] = in[1];
f->tmpR[3] = f->tmpR[2];
f->tmpR[2] = ROut;
// set output
out[0] = LOut;
out[1] = ROut;
}
I've successfully ported the filter code from pt2-clone.
The new filter pipeline consists of three stages:
vAmiga supports the following filter types (OPT_FILTER_TYPE
):
FILTER_NONE
: No filter is applied.
FILTER_A500
: Runs all three filter stages, except stage 2 if the LED is dimmed.
FILTER_A1000
: Runs all three filter stages, no matter what.
FILTER_A1200
: Runs filter stage 2 and 3. Skips stage 2 if the power LED is dimmed.
FILTER_VAMIGA
: Runs the legacy filter which had been used up to version 2.4b1. This filter is deprecated and will be deleted in future.
The remaining filter types are meant for debugging:
FILTER_LOW
: Runs the low-pass filter, only.
FILTER_LED
: Runs the LED filter, only. Ignores the LED state.
FILTER_HIGH
: Runs the high-pass filter, only.
Option OPT_FILTER_ACTIVATION
is no longer needed and has been removed.
For debugging, the internal setting of the filter pipeline can be displayed in the RetroShell debugger (make sure to enter the debugger in RetroShell. Otherwise, only the configuration is shown):
Great, sounds about right on first try, but hard to tell :)
Don't know if it's intended, but FILTER_VAMIGA always enables the old filter, which might be useful for testing, but not how it behaved before (unless you had OPT_FILTER_ACTIVATION == FILTER_ALWAYS_ON).
Since it's hard (at least for me) to hear if it's done properly, maybe we could test it with a simple program that plays white noise with and without the LED filter enabled, if you can capture the output from an A500 for comparison? The filter response should show up more or less exactly in a spectrum plot for this case if I'm not mistaken.
Don't know if it's intended, but FILTER_VAMIGA always enables the old filter
Oops, no, this was unintended. The latest check-in fixes it.
maybe we could test it with a simple program that plays white noise with and without the LED filter enabled
Good idea. Is there an Amiga program that can play white noise out of the box? I do not know how to easily generate random numbers in an Amiga program 🙄.
Quick test program: whitenoise.zip -- WARNING LOUD!!!
Plays noise with LED filter disabled, then some silence (for easier separation), then same noise with LED filter enabled. Random data is just xorshift32. If you don't know how to generate pseudo-random data dust off your copy of TAOCP and get going 😄 (mine is in the basement so opted for something simple / newer).
Super quick analysis dumping raw output and checking the two different pars in audactity shows a clear difference, so I think this could be used for checking against real output:
Unfortunately, I only have this JBL speaker available which turned out to be too smart for this purpose. It only plays something if the incoming signal is considered to be "sound". It simply rejects to play the white noise.
How is that connected? Could you somehow hook up the output more directly to a computer input? On an A1200 it's easy with 2 x RCA output -> 3.5mm jack -> PC input.
with 2 x RCA output -> 3.5mm jack -> PC input.
"PC" ist the keyword here. The trick is not to use the Mac.
Stay tuned...
Rev. 5 Amiga 500:
Unfiltered:
Filtered:
This is what I get when vAmiga's audio output is fed into the loopback device and then recorded with Audacity:
Unfiltered:
Filtered:
This setup has the disadvantage that vAmiga's audio output in run through the Mac audio circuitry which is likely to do some filtering, too.
Super quick analysis dumping raw output and checking the two different pars in audactity
@mras0: Is the "raw output" coming from vAmiga or is the data coming from your A1200?
Is the "raw output" coming from vAmiga or is the data coming from your A1200?
It was done with the vAmiga code. In vAmigaSDL I have callback function for when the output buffer needs to be refilled, and I just added code to also write the data to a file:
amiga_.paula.muxer.copy(stream, len / sample_size);
sound.write((char*)stream, len);
I can't really claim any expertise in the audio domain, so take everything I write with a large grain of salt, but at a glance I think it looks OK. The unfiltered A500 case seems to hit -3dB at ~4KHz and that seems to match the data you got from vAmiga, and the increase in attenuation also seems to roughly match (but there is probably a better way to compare it than by looking at screenshots :) )
but at a glance I think it looks OK
That's my impression, too. I think we can consider it "good enough".
Part of v2.4
Now that you mention filtering, one thing I noticed is there's no filtering applied when the LED is off in vAmiga (which is the case for this test). According to https://eab.abime.net/showthread.php?t=112931 (and UAE source code) there are always low- and high-pass filter applied on A500. WinUAE doesn't seem to implement the high pass filter, but pt2-clone does (and has more information).
This is just info for you consideration, not something I expect you to do anything about :)
For reference the first couple of seconds form WinUAE & vAmigaSDL. For the latter I set the samplerate to 48000 and saved the audio output to a raw file containing (32-bit float stereo). In both cases I've normalized the output with audacity to make sure volume is the same. BeeMoved_samples.zip
Originally posted by @mras0 in https://github.com/dirkwhoffmann/vAmiga/issues/788#issuecomment-1485517848