Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement more realistic audio filters #789

Closed
dirkwhoffmann opened this issue Mar 28, 2023 · 14 comments
Closed

Implement more realistic audio filters #789

dirkwhoffmann opened this issue Mar 28, 2023 · 14 comments
Assignees
Labels
Enhancement New feature or request v2.4

Comments

@dirkwhoffmann
Copy link
Owner

dirkwhoffmann commented Mar 28, 2023

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 #788 (comment)

@dirkwhoffmann dirkwhoffmann added the Enhancement New feature or request label Mar 28, 2023
@dirkwhoffmann
Copy link
Owner Author

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 dirkwhoffmann self-assigned this Mar 28, 2023
@dirkwhoffmann
Copy link
Owner Author

I've successfully ported the filter code from pt2-clone.

The new filter pipeline consists of three stages:

  • Stage 1: A static low-pass filter
  • Stage 2: The so called "LED filter"
  • Stage 3: A static high-pass filter

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

Bildschirm­foto 2023-03-28 um 17 59 53

@mras0
Copy link

mras0 commented Mar 28, 2023

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
Copy link
Owner Author

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
Copy link

mras0 commented Mar 28, 2023

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
Copy link

mras0 commented Mar 28, 2023

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
Copy link
Owner Author

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
Copy link

mras0 commented Mar 28, 2023

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
Copy link
Owner Author

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
Copy link
Owner Author

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
Copy link
Owner Author

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
Copy link

mras0 commented Mar 29, 2023

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
Copy link
Owner Author

but at a glance I think it looks OK

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

@dirkwhoffmann
Copy link
Owner Author

Part of v2.4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Enhancement New feature or request v2.4
Projects
None yet
Development

No branches or pull requests

2 participants