dirkwhoffmann / vAmiga

vAmiga is a user-friendly Amiga 500, 1000, 2000 emulator for macOS
https://dirkwhoffmann.github.io/vAmiga
Other
293 stars 24 forks source link

Implement more realistic audio filters #789

Closed dirkwhoffmann closed 1 year ago

dirkwhoffmann commented 1 year ago

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

dirkwhoffmann commented 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;
}
dirkwhoffmann commented 1 year ago

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):

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):

Bildschirm­foto 2023-03-28 um 17 59 53
mras0 commented 1 year ago

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.

dirkwhoffmann commented 1 year ago

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 🙄.

mras0 commented 1 year ago

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).

mras0 commented 1 year ago

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: image

dirkwhoffmann commented 1 year ago

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.

IMG_4886

mras0 commented 1 year ago

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.

dirkwhoffmann commented 1 year ago

with 2 x RCA output -> 3.5mm jack -> PC input.

"PC" ist the keyword here. The trick is not to use the Mac.

IMG_4887

Stay tuned...

dirkwhoffmann commented 1 year ago

Rev. 5 Amiga 500:

Unfiltered:

Bildschirm­foto 2023-03-29 um 13 27 02

Filtered:

Bildschirm­foto 2023-03-29 um 13 27 12
dirkwhoffmann commented 1 year ago

This is what I get when vAmiga's audio output is fed into the loopback device and then recorded with Audacity:

Unfiltered:

Bildschirmfoto 2023-03-29 um 14 33 47

Filtered:

Bildschirmfoto 2023-03-29 um 14 33 14

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?

mras0 commented 1 year ago

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 :) )

dirkwhoffmann commented 1 year ago

but at a glance I think it looks OK

That's my impression, too. I think we can consider it "good enough".

dirkwhoffmann commented 1 year ago

Part of v2.4