shithub: pt2-clone

Download patch

ref: 871c59c2513acc611f3b1f4242453fc3bd1f6460
parent: 1e43b50e5d6817507fd8dc0849f052d4d7a6c0a8
author: Olav Sørensen <[email protected]>
date: Sun Dec 4 17:29:15 EST 2022

Pushed v1.54 code

--- a/src/pt2_amigafilters.c
+++ /dev/null
@@ -1,397 +1,0 @@
-/* Amiga 500 / Amiga 1200 filter implementation.
-**
-** Route:
-** Paula output -> low-pass filter -> LED filter (if turned on) -> high-pass filter (centering of waveform)
-*/
-
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_structs.h"
-#include "pt2_audio.h"
-#include "pt2_paula.h"
-#include "pt2_rcfilter.h"
-#include "pt2_math.h"
-#include "pt2_textout.h"
-
-typedef struct ledFilter_t
-{
-	double LIn1, LIn2, LOut1, LOut2;
-	double RIn1, RIn2, ROut1, ROut2;
-	double a1, a2, a3, b1, b2;
-} ledFilter_t;
-
-static int32_t filterModel;
-static bool ledFilterEnabled, useA1200LowPassFilter;
-static rcFilter_t filterLoA500, filterHiA500, filterLoA1200, filterHiA1200;
-static ledFilter_t filterLED;
-
-void (*processAmigaFilters)(double *, double *, int32_t); // globalized
-
-static void processFiltersA1200_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA1200_LED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA500_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA500_LED(double *dBufferL, double *dBufferR, int32_t numSamples);
-
-// --------------------------------------------------------
-// Crude LED filter implementation
-// --------------------------------------------------------
-
-void clearLEDFilterState(ledFilter_t *f)
-{
-	f->LIn1 = f->LIn2 = f->LOut1 = f->LOut2 = 0.0;
-	f->RIn1 = f->RIn2 = f->ROut1 = f->ROut2 = 0.0;
-}
-
-static void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter)
-{
-	const double c = 1.0 / pt2_tan((PT2_PI * hz) / sr);
-	const double r = 1.0 / qfactor;
-
-	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;
-}
-
-static void LEDFilter(ledFilter_t *f, const double *in, double *out)
-{
-	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);
-
-	// shift states
-
-	f->LIn2 = f->LIn1;
-	f->LIn1 = in[0];
-	f->LOut2 = f->LOut1;
-	f->LOut1 = LOut;
-
-	f->RIn2 = f->RIn1;
-	f->RIn1 = in[1];
-	f->ROut2 = f->ROut1;
-	f->ROut1 = ROut;
-
-	// set output
-	out[0] = LOut;
-	out[1] = ROut;
-}
-
-// --------------------------------------------------------
-// --------------------------------------------------------
-
-void setupAmigaFilters(double dAudioFreq)
-{
-	/* Amiga 500/1200 filter emulation
-	**
-	** aciddose:
-	** First comes a static low-pass 6dB formed by the supply current
-	** from the Paula's mixture of channels A+B / C+D into the opamp with
-	** 0.1uF capacitor and 360 ohm resistor feedback in inverting mode biased by
-	** dac vRef (used to center the output).
-	**
-	** R = 360 ohm
-	** C = 0.1uF
-	** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001)
-	**
-	** Under spice simulation the circuit yields -3dB = 4400Hz.
-	** In the Amiga 1200, the low-pass cutoff is ~34kHz, so the
-	** static low-pass filter is disabled in the mixer in A1200 mode.
-	**
-	** Next comes a bog-standard Sallen-Key filter ("LED") with:
-	** R1 = 10K ohm
-	** R2 = 10K ohm
-	** C1 = 6800pF
-	** C2 = 3900pF
-	** Q ~= 1/sqrt(2)
-	**
-	** This filter is optionally bypassed by an MPF-102 JFET chip when
-	** the LED filter is turned off.
-	**
-	** Under spice simulation the circuit yields -3dB = 2800Hz.
-	** 90 degrees phase = 3000Hz (so, should oscillate at 3kHz!)
-	**
-	** The buffered output of the Sallen-Key passes into an RC high-pass with:
-	** R = 1.39K ohm (1K ohm + 390 ohm)
-	** C = 22uF (also C = 330nF, for improved high-frequency)
-	**
-	** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022)
-	** Under spice simulation the circuit yields -3dB = 5.2Hz.
-	**
-	** 8bitbubsy:
-	** Keep in mind that many of the Amiga schematics that are floating around on
-	** the internet have wrong RC values! They were most likely very early schematics
-	** that didn't change before production (or changes that never reached production).
-	** This has been confirmed by measuring the components on several Amiga motherboards.
-	**
-	** Correct values for A500, >rev3 (?) (A500_R6.pdf):
-	** - 1-pole RC 6dB/oct low-pass: R=360 ohm, C=0.1uF
-	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF
-	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22.33uF (22+0.33)
-	**
-	** Correct values for A1200, all revs (A1200_R2.pdf):
-	** - 1-pole RC 6dB/oct low-pass: R=680 ohm, C=6800pF
-	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (same as A500)
-	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
-	*/
-	double R, C, R1, R2, C1, C2, cutoff, qfactor;
-
-	if (audio.oversamplingFlag)
-		dAudioFreq *= 2.0; // 2x oversampling
-
-	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
-		lockAudio();
-
-	// A500 1-pole (6db/oct) static 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
-	calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA500);
-
-	// (optional) A1200 1-pole (6db/oct) static RC low-pass filter:
-	R = 680.0;  // R321 (680 ohm)
-	C = 6.8e-9; // C321 (6800pF)
-	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~34419.322Hz
-
-	useA1200LowPassFilter = false;
-	if (dAudioFreq/2.0 > cutoff)
-	{
-		calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA1200);
-		useA1200LowPassFilter = true;
-	}
-
-	// 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)
-	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)
-	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)
-	cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.205Hz
-	calcRCFilterCoeffs(dAudioFreq, cutoff, &filterHiA1200);
-
-	if (audioWasntLocked)
-		unlockAudio();
-}
-
-void resetAmigaFilterStates(void)
-{
-	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
-		lockAudio();
-
-	clearRCFilterState(&filterLoA500);
-	clearRCFilterState(&filterLoA1200);
-	clearRCFilterState(&filterHiA500);
-	clearRCFilterState(&filterHiA1200);
-	clearLEDFilterState(&filterLED);
-
-	if (audioWasntLocked)
-		unlockAudio();
-}
-
-static void processFiltersA1200_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
-	if (useA1200LowPassFilter)
-	{
-		for (int32_t i = 0; i < numSamples; i++)
-		{
-			double dOut[2];
-
-			dOut[0] = dBufferL[i];
-			dOut[1] = dBufferR[i];
-
-			// low-pass filter
-			RCLowPassFilterStereo(&filterLoA1200, dOut, dOut);
-
-			// high-pass RC filter
-			RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
-			dBufferL[i] = dOut[0];
-			dBufferR[i] = dOut[1];
-		}
-	}
-	else
-	{
-		for (int32_t i = 0; i < numSamples; i++)
-		{
-			double dOut[2];
-
-			dOut[0] = dBufferL[i];
-			dOut[1] = dBufferR[i];
-
-			// high-pass RC filter
-			RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
-			dBufferL[i] = dOut[0];
-			dBufferR[i] = dOut[1];
-		}
-	}
-}
-
-static void processFiltersA1200_LED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
-	if (useA1200LowPassFilter)
-	{
-		for (int32_t i = 0; i < numSamples; i++)
-		{
-			double dOut[2];
-
-			dOut[0] = dBufferL[i];
-			dOut[1] = dBufferR[i];
-
-			// low-pass filter
-			RCLowPassFilterStereo(&filterLoA1200, dOut, dOut);
-
-			// "LED" Sallen-Key filter
-			LEDFilter(&filterLED, dOut, dOut);
-
-			// high-pass RC filter
-			RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
-			dBufferL[i] = dOut[0];
-			dBufferR[i] = dOut[1];
-		}
-	}
-	else
-	{
-		for (int32_t i = 0; i < numSamples; i++)
-		{
-			double dOut[2];
-
-			dOut[0] = dBufferL[i];
-			dOut[1] = dBufferR[i];
-
-			// "LED" Sallen-Key filter
-			LEDFilter(&filterLED, dOut, dOut);
-
-			// high-pass RC filter
-			RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
-			dBufferL[i] = dOut[0];
-			dBufferR[i] = dOut[1];
-		}
-	}
-}
-
-static void processFiltersA500_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
-	for (int32_t i = 0; i < numSamples; i++)
-	{
-		double dOut[2];
-
-		dOut[0] = dBufferL[i];
-		dOut[1] = dBufferR[i];
-
-		// low-pass RC filter
-		RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
-
-		// high-pass RC filter
-		RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
-
-		dBufferL[i] = dOut[0];
-		dBufferR[i] = dOut[1];
-	}
-}
-
-static void processFiltersA500_LED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
-	for (int32_t i = 0; i < numSamples; i++)
-	{
-		double dOut[2];
-
-		dOut[0] = dBufferL[i];
-		dOut[1] = dBufferR[i];
-
-		// low-pass RC filter
-		RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
-
-		// "LED" Sallen-Key filter
-		LEDFilter(&filterLED, dOut, dOut);
-
-		// high-pass RC filter
-		RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
-
-		dBufferL[i] = dOut[0];
-		dBufferR[i] = dOut[1];
-	}
-}
-
-static void updateAmigaFilterFunctions(void)
-{
-	if (filterModel == FILTERMODEL_A500)
-	{
-		if (ledFilterEnabled)
-			processAmigaFilters = processFiltersA500_LED;
-		else
-			processAmigaFilters = processFiltersA500_NoLED;
-	}
-	else // A1200
-	{
-		if (ledFilterEnabled)
-			processAmigaFilters = processFiltersA1200_LED;
-		else
-			processAmigaFilters = processFiltersA1200_NoLED;
-	}
-}
-
-void setAmigaFilterModel(uint8_t model)
-{
-	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
-		lockAudio();
-
-	filterModel = model;
-	updateAmigaFilterFunctions();
-
-	if (audioWasntLocked)
-		unlockAudio();
-}
-
-void setLEDFilter(bool state)
-{
-	if (ledFilterEnabled == state)
-		return; // same state as before!
-
-	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
-		lockAudio();
-
-	clearLEDFilterState(&filterLED);
-	ledFilterEnabled = editor.useLEDFilter;
-	updateAmigaFilterFunctions();
-
-	if (audioWasntLocked)
-		unlockAudio();
-}
-
-void toggleAmigaFilterModel(void)
-{
-	const bool audioWasntLocked = !audio.locked;
-	if (audioWasntLocked)
-		lockAudio();
-
-	resetAmigaFilterStates();
-
-	filterModel ^= 1;
-	updateAmigaFilterFunctions();
-
-	if (audioWasntLocked)
-		unlockAudio();
-
-	if (filterModel == FILTERMODEL_A500)
-		displayMsg("AUDIO: AMIGA 500");
-	else
-		displayMsg("AUDIO: AMIGA 1200");
-}
--- a/src/pt2_amigafilters.h
+++ /dev/null
@@ -1,12 +1,0 @@
-#pragma once
-
-#include <stdint.h>
-#include <stdbool.h>
-
-void setupAmigaFilters(double dAudioFreq);
-void resetAmigaFilterStates(void);
-void setAmigaFilterModel(uint8_t amigaModel);
-void setLEDFilter(bool state);
-void toggleAmigaFilterModel(void);
-
-extern void (*processAmigaFilters)(double *, double *, int32_t);
--- a/src/pt2_askbox.c
+++ b/src/pt2_askbox.c
@@ -4,7 +4,7 @@
 #include "pt2_visuals.h"
 #include "pt2_mouse.h"
 #include "pt2_structs.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
 #include "pt2_keyboard.h"
 #include "pt2_diskop.h"
 #include "pt2_mod2wav.h"
@@ -96,7 +96,7 @@
 
 	if (ui.diskOpScreenShown)
 	{
-		diskOpRenderFileList();
+		renderDiskOpScreen();
 	}
 	else if (ui.posEdScreenShown)
 	{
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -23,11 +23,10 @@
 #include "pt2_config.h"
 #include "pt2_textout.h"
 #include "pt2_scopes.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
 #include "pt2_downsample2x.h"
 #include "pt2_replayer.h"
 #include "pt2_paula.h"
-#include "pt2_amigafilters.h"
 
 // cumulative mid/side normalization factor (1/sqrt(2))*(1/sqrt(2))
 #define STEREO_NORM_FACTOR 0.5
@@ -36,41 +35,79 @@
 
 static uint8_t panningMode;
 static int32_t randSeed = INITIAL_DITHER_SEED, stereoSeparation = 100;
-static uint32_t audLatencyPerfValInt, audLatencyPerfValFrac;
-static uint64_t tickTime64, tickTime64Frac;
 static double *dMixBufferL, *dMixBufferR;
 static double dPrngStateL, dPrngStateR, dSideFactor;
 static SDL_AudioDeviceID dev;
 
-// for audio/video syncing
-static uint32_t tickTimeLen, tickTimeLenFrac;
-
 audio_t audio; // globalized
 
-static void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq)
+void setAmigaFilterModel(uint8_t model)
 {
-	double dInt, dFrac;
+	if (audio.amigaModel == model)
+		return; // same state as before!
 
-	if (audioFreq == 0)
-		return;
+	const bool audioWasntLocked = !audio.locked;
+	if (audioWasntLocked)
+		lockAudio();
 
-	const double dAudioLatencySecs = audioBufferSize / (double)audioFreq;
+	audio.amigaModel = model;
 
-	dFrac = modf(dAudioLatencySecs * hpcFreq.dFreq, &dInt);
+	const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
 
-	// integer part
-	audLatencyPerfValInt = (uint32_t)dInt;
+	if (audioWasntLocked)
+		unlockAudio();
+}
 
-	// fractional part (scaled to 0..2^32-1)
-	audLatencyPerfValFrac = (uint32_t)((dFrac * (UINT32_MAX+1.0)) + 0.5); // rounded
+void toggleAmigaFilterModel(void)
+{
+	const bool audioWasntLocked = !audio.locked;
+	if (audioWasntLocked)
+		lockAudio();
+
+	audio.amigaModel ^= 1;
+
+	const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
+
+	if (audioWasntLocked)
+		unlockAudio();
+
+	if (audio.amigaModel == MODEL_A500)
+		displayMsg("AUDIO: AMIGA 500");
+	else
+		displayMsg("AUDIO: AMIGA 1200");
 }
 
-void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac)
+void setLEDFilter(bool state)
 {
-	tickTimeLen = timeLen;
-	tickTimeLenFrac = timeLenFrac;
+	if (audio.ledFilterEnabled == state)
+		return; // same state as before!
+
+	const bool audioWasntLocked = !audio.locked;
+	if (audioWasntLocked)
+		lockAudio();
+
+	audio.ledFilterEnabled = state;
+	paulaWriteByte(0xBFE001, audio.ledFilterEnabled << 1);
+
+	if (audioWasntLocked)
+		unlockAudio();
 }
 
+void toggleLEDFilter(void)
+{
+	const bool audioWasntLocked = !audio.locked;
+	if (audioWasntLocked)
+		lockAudio();
+
+	audio.ledFilterEnabled ^= 1;
+	paulaWriteByte(0xBFE001, audio.ledFilterEnabled << 1);
+
+	if (audioWasntLocked)
+		unlockAudio();
+}
+
 void lockAudio(void)
 {
 	if (dev != 0)
@@ -252,9 +289,7 @@
 {
 	if (audio.oversamplingFlag) // 2x oversampling
 	{
-		// mix and filter channels (at 2x rate)
 		paulaGenerateSamples(dMixBufferL, dMixBufferR, numSamples*2);
-		processAmigaFilters(dMixBufferL, dMixBufferR, numSamples*2);
 
 		// downsample, normalize and dither
 		int16_t out[2];
@@ -280,9 +315,7 @@
 	}
 	else
 	{
-		// mix and filter channels
 		paulaGenerateSamples(dMixBufferL, dMixBufferR, numSamples);
-		processAmigaFilters(dMixBufferL, dMixBufferR, numSamples);
 
 		// normalize and dither
 		int16_t out[2];
@@ -308,53 +341,6 @@
 	}
 }
 
-static void fillVisualsSyncBuffer(void)
-{
-	chSyncData_t chSyncData;
-
-	if (audio.resetSyncTickTimeFlag)
-	{
-		audio.resetSyncTickTimeFlag = false;
-
-		tickTime64 = SDL_GetPerformanceCounter() + audLatencyPerfValInt;
-		tickTime64Frac = audLatencyPerfValFrac;
-	}
-
-	if (song != NULL)
-	{
-		moduleChannel_t *ch = song->channels;
-		paulaVoice_t *v = paula;
-		syncedChannel_t *sc = chSyncData.channels;
-
-		for (int32_t i = 0; i < PAULA_VOICES; i++, ch++, sc++, v++)
-		{
-			sc->flags = v->syncFlags | ch->syncFlags;
-			ch->syncFlags = v->syncFlags = 0; // clear sync flags
-
-			sc->volume = v->syncVolume;
-			sc->period = v->syncPeriod;
-			sc->triggerData = v->syncTriggerData;
-			sc->triggerLength = v->syncTriggerLength;
-			sc->newData = v->AUD_LC;
-			sc->newLength = v->AUD_LEN * 2;
-			sc->vuVolume = ch->syncVuVolume;
-			sc->analyzerVolume = ch->syncAnalyzerVolume;
-			sc->analyzerPeriod = ch->syncAnalyzerPeriod;
-		}
-
-		chSyncData.timestamp = tickTime64;
-		chQueuePush(chSyncData);
-	}
-
-	tickTime64 += tickTimeLen;
-	tickTime64Frac += tickTimeLenFrac;
-	if (tickTime64Frac > UINT32_MAX)
-	{
-		tickTime64Frac &= UINT32_MAX;
-		tickTime64++;
-	}
-}
-
 static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
 {
 	if (editor.mod2WavOngoing || editor.pat2SmpOngoing) // send silence to sound output device
@@ -374,7 +360,7 @@
 
 			if (editor.songPlaying)
 			{
-				intMusic();
+				intMusic(); // PT replayer ticker
 				fillVisualsSyncBuffer();
 			}
 
@@ -504,9 +490,10 @@
 	audio.outputRate = have.freq;
 	audio.audioBufferSize = have.samples;
 	audio.oversamplingFlag = (audio.outputRate < 96000); // we do 2x oversampling if the audio output rate is below 96kHz
+	audio.amigaModel = config.amigaModel;
 
-	const int32_t audioFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
-	const uint32_t maxSamplesToMix = (int32_t)ceil(audioFrequency / (REPLAYER_MIN_BPM / 2.5));
+	const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+	const uint32_t maxSamplesToMix = (int32_t)ceil(paulaMixFrequency / (REPLAYER_MIN_BPM / 2.5));
 
 	dMixBufferL = (double *)malloc((maxSamplesToMix + 1) * sizeof (double));
 	dMixBufferR = (double *)malloc((maxSamplesToMix + 1) * sizeof (double));
@@ -519,12 +506,10 @@
 		return false;
 	}
 
-	paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
 	audioSetStereoSeparation(config.stereoSeparation);
 	updateReplayerTimingMode(); // also generates the BPM table (audio.bpmTable)
-	setAmigaFilterModel(config.filterModel);
 	setLEDFilter(false);
-	setupAmigaFilters(audio.outputRate);
 	calcAudioLatencyVars(audio.audioBufferSize, audio.outputRate);
 
 	clearMixerDownsamplerStates();
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -6,12 +6,6 @@
 // for the low-pass/high-pass filters in the SAMPLER screen
 #define FILTERS_BASE_FREQ (PAULA_PAL_CLK / 214.0)
 
-enum
-{
-	AUDIO_NO_OVERSAMPLING = 0,
-	AUDIO_2X_OVERSAMPLING = 1
-};
-
 typedef struct audio_t
 {
 	volatile bool locked, isSampling;
@@ -18,7 +12,7 @@
 
 	bool ledFilterEnabled, oversamplingFlag;
 	
-	uint32_t outputRate, audioBufferSize;
+	uint32_t amigaModel, outputRate, audioBufferSize;
 	int64_t tickSampleCounter64, samplesPerTick64;
 	int64_t samplesPerTickTable[256-32]; // 32.32 fixed-point
 
@@ -29,6 +23,11 @@
 	bool resetSyncTickTimeFlag;
 	uint64_t tickLengthTable[224];
 } audio_t;
+
+void setAmigaFilterModel(uint8_t model);
+void toggleAmigaFilterModel(void);
+void setLEDFilter(bool state);
+void toggleLEDFilter(void);
 
 void updateReplayerTimingMode(void);
 void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac);
--- a/src/pt2_config.c
+++ b/src/pt2_config.c
@@ -43,7 +43,7 @@
 	config.fullScreenStretch = false;
 	config.pattDots = false;
 	config.waveformCenterLine = true;
-	config.filterModel = FILTERMODEL_A1200;
+	config.amigaModel = MODEL_A1200;
 	config.soundFrequency = 48000;
 	config.rememberPlayMode = false;
 	config.stereoSeparation = 20;
@@ -65,9 +65,7 @@
 	config.audioInputFrequency = 44100;
 	config.mod2WavOutputFreq = 44100;
 	config.keepEditModeAfterStepPlay = false;
-
 	config.maxSampleLength = 65534;
-	config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
 
 #ifndef _WIN32
 	getcwd(oldCwd, PATH_MAX);
@@ -205,15 +203,9 @@
 		else if (!_strnicmp(configLine, "64K_LIMIT=", 10))
 		{
 			if (!_strnicmp(&configLine[10], "TRUE", 4))
-			{
 				config.maxSampleLength = 65534;
-				config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
-			}
 			else if (!_strnicmp(&configLine[10], "FALSE", 5))
-			{
 				config.maxSampleLength = 131070;
-				config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
-			}
 		}
 
 		// NO_DWNSMP_ON_SMP_LOAD (no dialog for 2x downsample after >22kHz sample load)
@@ -407,22 +399,15 @@
 		// FILTERMODEL
 		else if (!_strnicmp(configLine, "FILTERMODEL=", 12))
 		{
-			     if (!_strnicmp(&configLine[12], "A500",  4)) config.filterModel = FILTERMODEL_A500;
-			else if (!_strnicmp(&configLine[12], "A1200", 5)) config.filterModel = FILTERMODEL_A1200;
+			     if (!_strnicmp(&configLine[12], "A500",  4)) config.amigaModel = MODEL_A500;
+			else if (!_strnicmp(&configLine[12], "A1200", 5)) config.amigaModel = MODEL_A1200;
 		}
 
 		// A500LOWPASSFILTER (deprecated, same as A4000LOWPASSFILTER)
 		else if (!_strnicmp(configLine, "A500LOWPASSFILTER=", 18))
 		{
-			     if (!_strnicmp(&configLine[18], "TRUE",  4)) config.filterModel = FILTERMODEL_A500;
-			else if (!_strnicmp(&configLine[18], "FALSE", 5)) config.filterModel = FILTERMODEL_A1200;
-		}
-
-		// A4000LOWPASSFILTER (deprecated)
-		else if (!_strnicmp(configLine, "A4000LOWPASSFILTER=", 19))
-		{
-			     if (!_strnicmp(&configLine[19], "TRUE",  4)) config.filterModel = FILTERMODEL_A500;
-			else if (!_strnicmp(&configLine[19], "FALSE", 5)) config.filterModel = FILTERMODEL_A1200;
+			     if (!_strnicmp(&configLine[18], "TRUE",  4)) config.amigaModel = MODEL_A500;
+			else if (!_strnicmp(&configLine[18], "FALSE", 5)) config.amigaModel = MODEL_A1200;
 		}
 
 		// SAMPLINGFREQ
--- a/src/pt2_config.h
+++ b/src/pt2_config.h
@@ -17,10 +17,10 @@
 	bool transDel, fullScreenStretch, vsyncOff, modDot, blankZeroFlag, realVuMeters, rememberPlayMode;
 	bool startInFullscreen, integerScaling, disableE8xEffect, noDownsampleOnSmpLoad, keepEditModeAfterStepPlay;
 	int8_t stereoSeparation, videoScaleFactor, accidental;
-	uint8_t pixelFilter, filterModel;
+	uint8_t pixelFilter, amigaModel;
 	uint16_t quantizeValue;
 	int32_t maxSampleLength;
-	uint32_t soundFrequency, soundBufferSize, audioInputFrequency, mod2WavOutputFreq, reservedSampleOffset;
+	uint32_t soundFrequency, soundBufferSize, audioInputFrequency, mod2WavOutputFreq;
 } config_t;
 
 extern config_t config; // pt2_config.c
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -25,6 +25,7 @@
 #include "pt2_chordmaker.h"
 #include "pt2_edit.h"
 #include "pt2_replayer.h"
+#include "pt2_visuals_sync.h"
 
 static const int8_t scancode2NoteLo[52] = // "USB usage page standard" order
 {
@@ -731,20 +732,36 @@
 			if (ch->n_length == 0)
 				ch->n_length = 1;
 
-			paulaSetVolume(chNum, ch->n_volume);
-			paulaSetPeriod(chNum, ch->n_period);
-			paulaSetData(chNum, ch->n_start);
-			paulaSetLength(chNum, ch->n_length);
+			const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
+			paulaWriteWord(voiceAddr + 8, ch->n_volume);
+			paulaWriteWord(voiceAddr + 6, ch->n_period);
+			paulaWritePtr(voiceAddr + 0, ch->n_start);
+			paulaWriteWord(voiceAddr + 4, ch->n_length);
 
 			if (!editor.muted[chNum])
-				paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+				paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
 			else
-				paulaSetDMACON(ch->n_dmabit); // voice DMA off
+				paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
 
 			// these take effect after the current DMA cycle is done
-			paulaSetData(chNum, ch->n_loopstart);
-			paulaSetLength(chNum, ch->n_replen);
+			paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+			paulaWriteWord(voiceAddr + 4, ch->n_replen);
 
+			// update tracker visuals
+
+			setVisualsVolume(chNum, ch->n_volume);
+			setVisualsPeriod(chNum, ch->n_period);
+			setVisualsDataPtr(chNum, ch->n_start);
+			setVisualsLength(chNum, ch->n_length);
+
+			if (!editor.muted[chNum])
+				setVisualsDMACON(0x8000 | ch->n_dmabit);
+			else
+				setVisualsDMACON(ch->n_dmabit);
+
+			setVisualsDataPtr(chNum, ch->n_loopstart);
+			setVisualsLength(chNum, ch->n_replen);
+
 			unlockAudio();
 		}
 
@@ -1059,19 +1076,35 @@
 
 	ch->n_samplenum = editor.currSample; // needed for sample playback/sampling line
 
-	paulaSetVolume(chNum, vol);
-	paulaSetPeriod(chNum, period);
-	paulaSetData(chNum, n_start);
-	paulaSetLength(chNum, n_length);
+	const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
+	paulaWriteWord(voiceAddr +  8, vol);
+	paulaWriteWord(voiceAddr + 6, period);
+	paulaWritePtr(voiceAddr + 0, n_start);
+	paulaWriteWord(voiceAddr + 4, n_length);
 
 	if (!editor.muted[chNum])
-		paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+		paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
 	else
-		paulaSetDMACON(ch->n_dmabit); // voice DMA off
+		paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
 
 	// these take effect after the current DMA cycle is done
-	paulaSetData(chNum, NULL); // NULL = reserved buffer (empty)
-	paulaSetLength(chNum, 1);
+	paulaWritePtr(voiceAddr + 0, NULL); // data
+	paulaWriteWord(voiceAddr + 4, 1); // length
+
+	// update tracker visuals
+
+	setVisualsVolume(chNum, vol);
+	setVisualsPeriod(chNum, period);
+	setVisualsDataPtr(chNum, n_start);
+	setVisualsLength(chNum, n_length);
+
+	if (!editor.muted[chNum])
+		setVisualsDMACON(0x8000 | ch->n_dmabit);
+	else
+		setVisualsDMACON(ch->n_dmabit);
+
+	setVisualsDataPtr(chNum, NULL);
+	setVisualsLength(chNum, 1);
 
 	unlockAudio();
 }
--- 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.53"
+#define PROG_VER_STR "1.54"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
@@ -90,10 +90,6 @@
 
 	TEMPFLAG_START = 1,
 	TEMPFLAG_DELAY = 2,
-
-	FILTERMODEL_A1200 = 0,
-	FILTERMODEL_A500 = 1,
-	FILTER_LED_ENABLED = 2,
 
 	NO_CARRY = 0,
 	DO_CARRY = 1,
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -19,7 +19,6 @@
 #include "pt2_edit.h"
 #include "pt2_sampler.h"
 #include "pt2_audio.h"
-#include "pt2_amigafilters.h"
 #include "pt2_tables.h"
 #include "pt2_module_saver.h"
 #include "pt2_sample_saver.h"
@@ -2264,17 +2263,12 @@
 			}
 			else if (keyb.leftCtrlPressed)
 			{
-				editor.useLEDFilter ^= 1;
-				if (editor.useLEDFilter)
-				{
-					setLEDFilter(true);
+				toggleLEDFilter();
+
+				if (audio.ledFilterEnabled)
 					displayMsg("LED FILTER ON");
-				}
 				else
-				{
-					setLEDFilter(false);
 					displayMsg("LED FILTER OFF");
-				}
 			}
 			else if (keyb.leftAltPressed)
 			{
--- /dev/null
+++ b/src/pt2_ledfilter.c
@@ -1,0 +1,44 @@
+// Crude Amiga "LED" filter implementation
+
+#include "pt2_ledfilter.h"
+#include "pt2_math.h"
+
+void clearLEDFilterState(ledFilter_t *f)
+{
+	f->LIn1 = f->LIn2 = f->LOut1 = f->LOut2 = 0.0;
+	f->RIn1 = f->RIn2 = f->ROut1 = f->ROut2 = 0.0;
+}
+
+void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter)
+{
+	const double c = 1.0 / pt2_tan((PT2_PI * hz) / sr);
+	const double r = 1.0 / qfactor;
+
+	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 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);
+
+	// shift states
+
+	f->LIn2 = f->LIn1;
+	f->LIn1 = in[0];
+	f->LOut2 = f->LOut1;
+	f->LOut1 = LOut;
+
+	f->RIn2 = f->RIn1;
+	f->RIn1 = in[1];
+	f->ROut2 = f->ROut1;
+	f->ROut1 = ROut;
+
+	// set output
+	out[0] = LOut;
+	out[1] = ROut;
+}
--- /dev/null
+++ b/src/pt2_ledfilter.h
@@ -1,0 +1,15 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef struct ledFilter_t
+{
+	double LIn1, LIn2, LOut1, LOut2;
+	double RIn1, RIn2, ROut1, ROut2;
+	double a1, a2, a3, b1, b2;
+} ledFilter_t;
+
+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_main.c
+++ b/src/pt2_main.c
@@ -31,7 +31,7 @@
 #include "pt2_scopes.h"
 #include "pt2_audio.h"
 #include "pt2_bmp.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
 #include "pt2_sampling.h"
 #include "pt2_askbox.h"
 #include "pt2_replayer.h"
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -8,7 +8,6 @@
 #include <stdbool.h>
 #include <sys/stat.h> // stat()
 #include "pt2_audio.h"
-#include "pt2_amigafilters.h"
 #include "pt2_mouse.h"
 #include "pt2_textout.h"
 #include "pt2_visuals.h"
@@ -135,8 +134,8 @@
 
 static void resetAudio(void)
 {
-	setupAmigaFilters(audio.outputRate);
-	paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+	const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
 	generateBpmTable(audio.outputRate, editor.timingMode == TEMPO_MODE_VBLANK);
 	clearMixerDownsamplerStates();
 	modSetTempo(song->currBPM, true); // update BPM (samples per tick) with the tracker's audio frequency
@@ -352,8 +351,8 @@
 
 	strncpy(lastFilename, filename, PATH_MAX-1);
 
-	const int32_t audioFrequency = config.mod2WavOutputFreq * 2; // *2 for oversampling
-	const uint32_t maxSamplesToMix = (int32_t)ceil(audioFrequency / (REPLAYER_MIN_BPM / 2.5));
+	const int32_t paulaMixFrequency = config.mod2WavOutputFreq * 2; // *2 for oversampling
+	const uint32_t maxSamplesToMix = (int32_t)ceil(paulaMixFrequency / (REPLAYER_MIN_BPM / 2.5));
 
 	mod2WavBuffer = (int16_t *)malloc(((TICKS_PER_RENDER_CHUNK * maxSamplesToMix) + 1) * sizeof (int16_t) * 2);
 	if (mod2WavBuffer == NULL)
@@ -367,8 +366,7 @@
 
 	// do some prep work
 	generateBpmTable(config.mod2WavOutputFreq, editor.timingMode == TEMPO_MODE_VBLANK);
-	setupAmigaFilters(config.mod2WavOutputFreq);
-	paulaSetOutputFrequency(config.mod2WavOutputFreq, AUDIO_2X_OVERSAMPLING);
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
 	storeTempVariables();
 	calcMod2WavTotalRows();
 	restartSong(); // this also updates BPM (samples per tick) with the MOD2WAV audio output rate
--- a/src/pt2_module_loader.c
+++ b/src/pt2_module_loader.c
@@ -20,7 +20,6 @@
 #include "pt2_replayer.h"
 #include "pt2_textout.h"
 #include "pt2_audio.h"
-#include "pt2_amigafilters.h"
 #include "pt2_helpers.h"
 #include "pt2_visuals.h"
 #include "pt2_sample_loader.h"
@@ -460,6 +459,10 @@
 				note->command = bytes[2] & 0x0F;
 				note->param = bytes[3];
 
+				// added sanitation not present in original PT
+				if (note->sample > 31)
+					note->sample = 0;
+
 				if (modFormat == FORMAT_STK)
 				{
 					if (note->command == 0xC || note->command == 0xD || note->command == 0xE)
@@ -930,7 +933,6 @@
 	editor.hiLowInstr = 0;
 
 	// disable LED filter after module load (real PT doesn't do this)
-	editor.useLEDFilter = false;
 	setLEDFilter(false);
 
 	updateWindowTitle(MOD_NOT_MODIFIED);
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -246,7 +246,8 @@
 
 	// do some prep work
 	generateBpmTable(dPat2SmpFreq, editor.timingMode == TEMPO_MODE_VBLANK);
-	paulaSetOutputFrequency(dPat2SmpFreq, AUDIO_2X_OVERSAMPLING);
+	paulaSetup(dPat2SmpFreq*2.0, MODEL_A1200);
+	paulaDisableFilters();
 	storeTempVariables();
 	restartSong(); // this also updates BPM (samples per tick) with the PAT2SMP audio output rate
 	clearMixerDownsamplerStates();
@@ -289,7 +290,8 @@
 	song->currRow = song->row = oldRow; // set back old row
 
 	// set back audio configurations
-	paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+	const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+	paulaSetup(paulaMixFrequency, audio.amigaModel);
 	generateBpmTable(audio.outputRate, editor.timingMode == TEMPO_MODE_VBLANK);
 	clearMixerDownsamplerStates();
 	resetSong(); // this also updates BPM (samples per tick) with the tracker's audio output rate
--- a/src/pt2_paula.c
+++ b/src/pt2_paula.c
@@ -1,39 +1,176 @@
 /* Simple Paula emulator by 8bitbubsy (with BLEP synthesis by aciddose).
-** The Amiga filters are handled in pt2_amigafilters.c
-**
 ** Limitation: The audio output frequency can't be below 31389Hz ( ceil(PAULA_PAL_CLK / 113.0) )
+**
+** WARNING: These functions must not be called while paulaGenerateSamples() is running!
+**          If so, lock the audio first so that you're sure it's not running.
 */
 
 #include <stdint.h>
 #include <stdbool.h>
-#include "pt2_audio.h"
 #include "pt2_paula.h"
 #include "pt2_blep.h"
-#include "pt2_sync.h"
-#include "pt2_scopes.h" 
-#include "pt2_config.h"
+#include "pt2_rcfilter.h"
+#include "pt2_ledfilter.h"
+#include "pt2_math.h"
 
-paulaVoice_t paula[PAULA_VOICES]; // globalized
+typedef struct voice_t
+{
+	volatile bool DMA_active;
 
+	// internal registers
+	bool DMATriggerFlag, nextSampleStage;
+	int8_t AUD_DAT[2]; // DMA data buffer
+	const int8_t *location; // current location
+	uint16_t lengthCounter; // current length
+	int32_t sampleCounter; // how many bytes left in AUD_DAT
+	double dSample; // currently held sample point (multiplied by volume)
+	double dDelta, dPhase;
+
+	// for BLEP synthesis
+	double dLastDelta, dLastPhase, dLastDeltaMul, dBlepOffset, dDeltaMul;
+
+	// registers modified by Paula functions
+	const int8_t *AUD_LC; // location (data pointer)
+	uint16_t AUD_LEN;
+	double AUD_PER_delta, AUD_PER_deltamul;
+	double AUD_VOL;
+} paulaVoice_t;
+
+static bool useLEDFilter, useLowpassFilter, useHighpassFilter;
+static int8_t nullSample[0xFFFF*2]; // buffer for NULL data pointer
+static double dPaulaOutputFreq, dPeriodToDeltaDiv;
 static blep_t blep[PAULA_VOICES];
-static double dPeriodToDeltaDiv;
+static rcFilter_t filterLo, filterHi;
+static ledFilter_t filterLED;
+static paulaVoice_t paula[PAULA_VOICES];
 
-void paulaSetOutputFrequency(double dAudioFreq, bool oversampling2x)
+void paulaSetup(double dOutputFreq, uint32_t amigaModel)
 {
-	dPeriodToDeltaDiv = PAULA_PAL_CLK / dAudioFreq;
-	if (oversampling2x)
-		dPeriodToDeltaDiv /= 2.0;
+	if (dOutputFreq <= 0.0)
+		dOutputFreq = 44100.0;
+
+	dPaulaOutputFreq = dOutputFreq;
+	dPeriodToDeltaDiv = PAULA_PAL_CLK / dPaulaOutputFreq;
+
+	useLowpassFilter = useHighpassFilter = true;
+	clearRCFilterState(&filterLo);
+	clearRCFilterState(&filterHi);
+	clearLEDFilterState(&filterLED);
+
+	/* Amiga 500/1200 filter emulation
+	**
+	** aciddose:
+	** First comes a static low-pass 6dB formed by the supply current
+	** from the Paula's mixture of channels A+B / C+D into the opamp with
+	** 0.1uF capacitor and 360 ohm resistor feedback in inverting mode biased by
+	** dac vRef (used to center the output).
+	**
+	** R = 360 ohm
+	** C = 0.1uF
+	** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001)
+	**
+	** Under spice simulation the circuit yields -3dB = 4400Hz.
+	** In the Amiga 1200, the low-pass cutoff is ~34kHz, so the
+	** static low-pass filter is disabled in the mixer in A1200 mode.
+	**
+	** Next comes a bog-standard Sallen-Key filter ("LED") with:
+	** R1 = 10K ohm
+	** R2 = 10K ohm
+	** C1 = 6800pF
+	** C2 = 3900pF
+	** Q ~= 1/sqrt(2)
+	**
+	** This filter is optionally bypassed by an MPF-102 JFET chip when
+	** the LED filter is turned off.
+	**
+	** Under spice simulation the circuit yields -3dB = 2800Hz.
+	** 90 degrees phase = 3000Hz (so, should oscillate at 3kHz!)
+	**
+	** The buffered output of the Sallen-Key passes into an RC high-pass with:
+	** R = 1.39K ohm (1K ohm + 390 ohm)
+	** C = 22uF (also C = 330nF, for improved high-frequency)
+	**
+	** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022)
+	** Under spice simulation the circuit yields -3dB = 5.2Hz.
+	**
+	** 8bitbubsy:
+	** Keep in mind that many of the Amiga schematics that are floating around on
+	** the internet have wrong RC values! They were most likely very early schematics
+	** that didn't change before production (or changes that never reached production).
+	** This has been confirmed by measuring the components on several Amiga motherboards.
+	**
+	** Correct values for A500, >rev3 (?) (A500_R6.pdf):
+	** - 1-pole RC 6dB/oct low-pass: R=360 ohm, C=0.1uF
+	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF
+	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22.33uF (22+0.33)
+	**
+	** Correct values for A1200, all revs (A1200_R2.pdf):
+	** - 1-pole RC 6dB/oct low-pass: R=680 ohm, C=6800pF
+	** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (same as A500)
+	** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
+	*/
+	double R, C, R1, R2, C1, C2, cutoff, qfactor;
+
+	if (amigaModel == MODEL_A500)
+	{
+		// A500 1-pole (6db/oct) static 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
+		calcRCFilterCoeffs(dPaulaOutputFreq, cutoff, &filterLo);
+
+		// 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)
+		cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.128Hz
+		calcRCFilterCoeffs(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) static RC high-pass filter:
+		R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
+		C = 2.2e-5; // C334 (22uF capacitor)
+		cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.205Hz
+		calcRCFilterCoeffs(dPaulaOutputFreq, cutoff, &filterHi);
+	}
+	
+	// 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)
+	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(dPaulaOutputFreq, cutoff, qfactor, &filterLED);
 }
 
-void paulaSetPeriod(int32_t ch, uint16_t period)
+void paulaDisableFilters(void) // disables low-pass/high-pass filter ("LED" filter is kept)
 {
+	useHighpassFilter = false;
+	useLowpassFilter = false;
+}
+
+int8_t *paulaGetNullSamplePtr(void)
+{
+	return nullSample;
+}
+
+static void audxper(int32_t ch, uint16_t period)
+{
 	paulaVoice_t *v = &paula[ch];
 
 	int32_t realPeriod = period;
 	if (realPeriod == 0)
-		realPeriod = 65535; // On Amiga: period 0 = one full cycle with period 65536, then period 65535 for the rest
+		realPeriod = 65536; // On Amiga: period 0 = period 65536 (1+65535)
 	else if (realPeriod < 113)
-		realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
+		realPeriod = 113; // close to what happens on real Amiga (and low-limit needed for BLEP synthesis)
 
 	// to be read on next sampling step (or on DMA trigger)
 	v->AUD_PER_delta = dPeriodToDeltaDiv / realPeriod;
@@ -46,74 +183,29 @@
 
 	if (v->dLastDeltaMul == 0.0)
 		v->dLastDeltaMul = v->AUD_PER_deltamul;
-
-	// handle visualizers
-
-	if (editor.songPlaying)
-	{
-		v->syncPeriod = realPeriod;
-		v->syncFlags |= SET_SCOPE_PERIOD;
-	}
-	else
-	{
-		scopeSetPeriod(ch, realPeriod);
-	}
 }
 
-void paulaSetVolume(int32_t ch, uint16_t vol)
+static void audxvol(int32_t ch, uint16_t vol)
 {
-	paulaVoice_t *v = &paula[ch];
-
 	int32_t realVol = vol & 127;
 	if (realVol > 64)
 		realVol = 64;
 
-	// multiplying sample point by this also scales the sample from -128..127 -> -1.0 .. ~0.99
-	v->AUD_VOL = realVol * (1.0 / (128.0 * 64.0));
-
-	// handle visualizers
-
-	if (editor.songPlaying)
-	{
-		v->syncVolume = (uint8_t)realVol;
-		v->syncFlags |= SET_SCOPE_VOLUME;
-	}
-	else
-	{
-		scope[ch].volume = (uint8_t)realVol;
-	}
+	// multiplying sample point by this also scales the sample from -128..127 -> -1.000 .. ~0.992
+	paula[ch].AUD_VOL = realVol * (1.0 / (128.0 * 64.0));
 }
 
-void paulaSetLength(int32_t ch, uint16_t len)
+static void audxlen(int32_t ch, uint16_t len)
 {
-	paulaVoice_t *v = &paula[ch];
-
-	v->AUD_LEN = len;
-
-	// handle visualizers
-
-	if (editor.songPlaying)
-		v->syncFlags |= SET_SCOPE_LENGTH;
-	else
-		scope[ch].newLength = len * 2;
+	paula[ch].AUD_LEN = len;
 }
 
-void paulaSetData(int32_t ch, const int8_t *src)
+static void audxdat(int32_t ch, const int8_t *src)
 {
-	paulaVoice_t *v = &paula[ch];
-
-	// if pointer is NULL, use empty 128kB sample slot after sample 31 in the tracker
 	if (src == NULL)
-		src = &song->sampleData[config.reservedSampleOffset];
+		src = nullSample;
 
-	v->AUD_LC = src;
-
-	// handle visualizers
-
-	if (editor.songPlaying)
-		v->syncFlags |= SET_SCOPE_DATA;
-	else
-		scope[ch].newData = src;
+	paula[ch].AUD_LC = src;
 }
 
 static inline void refetchPeriod(paulaVoice_t *v) // Paula stage
@@ -131,13 +223,12 @@
 	v->nextSampleStage = true;
 }
 
-static void startPaulaDMA(int32_t ch)
+static void startDMA(int32_t ch)
 {
 	paulaVoice_t *v = &paula[ch];
 
-	// if pointer is NULL, use empty 128kB sample slot after sample 31 in the tracker
 	if (v->AUD_LC == NULL)
-		v->AUD_LC = &song->sampleData[config.reservedSampleOffset];
+		v->AUD_LC = nullSample;
 
 	// immediately update AUD_LC/AUD_LEN
 	v->location = v->AUD_LC;
@@ -151,56 +242,108 @@
 	v->dPhase = 0.0; // kludge: must be cleared *after* refetchPeriod()
 
 	v->DMA_active = true;
+}
 
-	// handle visualizers
+static void stopDMA(int32_t ch)
+{
+	paula[ch].DMA_active = false;
+}
 
-	if (editor.songPlaying)
+void paulaWriteByte(uint32_t address, uint16_t data8)
+{
+	if (address == 0)
+		return;
+
+	switch (address)
 	{
-		v->syncTriggerData = v->AUD_LC;
-		v->syncTriggerLength = v->AUD_LEN * 2;
-		v->syncFlags |= TRIGGER_SCOPE;
+		// CIA-A ("LED" filter control only)
+		case 0xBFE001:
+		{
+			const bool oldLedFilterState = useLEDFilter;
+
+			useLEDFilter = !!(data8 & 2);
+
+			if (useLEDFilter != oldLedFilterState)
+				clearLEDFilterState(&filterLED);
+		}
+		break;
+	
+		// AUDxVOL ( byte-write to AUDxVOL works on most Amigas (not 68040/68060) )
+		case 0xDFF0A8: audxvol(0, data8); break;
+		case 0xDFF0B8: audxvol(1, data8); break;
+		case 0xDFF0C8: audxvol(2, data8); break;
+		case 0xDFF0D8: audxvol(3, data8); break;
+
+		default: return;
 	}
-	else
-	{
-		scope_t *s = &scope[ch];
-		s->newData = v->AUD_LC;
-		s->newLength = v->AUD_LEN * 2;
-		scopeTrigger(ch);
-	}
 }
 
-static void stopPaulaDMA(int32_t ch)
+void paulaWriteWord(uint32_t address, uint16_t data16)
 {
-	paulaVoice_t *v = &paula[ch];
+	if (address == 0)
+		return;
 
-	v->DMA_active = false;
+	switch (address)
+	{
+		// DMACON
+		case 0xDFF096:
+		{
+			if (data16 & 0x8000)
+			{
+				// set
+				if (data16 & 1) startDMA(0);
+				if (data16 & 2) startDMA(1);
+				if (data16 & 4) startDMA(2);
+				if (data16 & 8) startDMA(3);
+			}
+			else
+			{
+				// clear
+				if (data16 & 1) stopDMA(0);
+				if (data16 & 2) stopDMA(1);
+				if (data16 & 4) stopDMA(2);
+				if (data16 & 8) stopDMA(3);
+			}
+		}
+		break;
 
-	// handle visualizers
- 
-	if (editor.songPlaying)
-		v->syncFlags |= STOP_SCOPE;
-	else
-		scope[ch].active = false;
+		// AUDxLEN
+		case 0xDFF0A4: audxlen(0, data16); break;
+		case 0xDFF0B4: audxlen(1, data16); break;
+		case 0xDFF0C4: audxlen(2, data16); break;
+		case 0xDFF0D4: audxlen(3, data16); break;
+
+		// AUDxPER
+		case 0xDFF0A6: audxper(0, data16); break;
+		case 0xDFF0B6: audxper(1, data16); break;
+		case 0xDFF0C6: audxper(2, data16); break;
+		case 0xDFF0D6: audxper(3, data16); break;
+
+		// AUDxVOL
+		case 0xDFF0A8: audxvol(0, data16); break;
+		case 0xDFF0B8: audxvol(1, data16); break;
+		case 0xDFF0C8: audxvol(2, data16); break;
+		case 0xDFF0D8: audxvol(3, data16); break;
+
+		default: return;
+	}
 }
 
-void paulaSetDMACON(uint16_t bits) // $DFF096 register write (only controls paula DMAs)
+void paulaWritePtr(uint32_t address, const void *ptr)
 {
-	if (bits & 0x8000)
+	if (address == 0)
+		return;
+
+	switch (address)
 	{
-		// set
-		if (bits & 1) startPaulaDMA(0);
-		if (bits & 2) startPaulaDMA(1);
-		if (bits & 4) startPaulaDMA(2);
-		if (bits & 8) startPaulaDMA(3);
+		// AUDxDAT
+		case 0xDFF0A0: audxdat(0, ptr); break;
+		case 0xDFF0B0: audxdat(1, ptr); break;
+		case 0xDFF0C0: audxdat(2, ptr); break;
+		case 0xDFF0D0: audxdat(3, ptr); break;
+
+		default: return;
 	}
-	else
-	{
-		// clear
-		if (bits & 1) stopPaulaDMA(0);
-		if (bits & 2) stopPaulaDMA(1);
-		if (bits & 4) stopPaulaDMA(2);
-		if (bits & 8) stopPaulaDMA(3);
-	}
 }
 
 static inline void nextSample(paulaVoice_t *v, blep_t *b)
@@ -248,13 +391,20 @@
 	v->sampleCounter--;
 }
 
+// output is -4.00 .. 3.97 (can be louder because of high-pass filter)
 void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples)
 {
 	double *dMixBufSelect[PAULA_VOICES] = { dOutL, dOutR, dOutR, dOutL };
 
+	if (numSamples <= 0)
+		return;
+
+	// clear mix buffer block
 	memset(dOutL, 0, numSamples * sizeof (double));
 	memset(dOutR, 0, numSamples * sizeof (double));
 
+	// mix samples
+
 	paulaVoice_t *v = paula;
 	blep_t *b = blep;
 
@@ -269,10 +419,10 @@
 			if (v->nextSampleStage)
 			{
 				v->nextSampleStage = false;
-				nextSample(v, b); // inlined
+				nextSample(v, b);
 			}
 
-			double dSample = v->dSample; // current Paula sample, pre-multiplied by volume, scaled to -1.0 .. 0.9921875
+			double dSample = v->dSample; // current sample, pre-multiplied by vol, scaled to -1.0 .. 0.992
 			if (b->samplesLeft > 0)
 				dSample = blepRun(b, dSample);
 
@@ -282,8 +432,26 @@
 			if (v->dPhase >= 1.0)
 			{
 				v->dPhase -= 1.0;
-				refetchPeriod(v); // inlined
+				refetchPeriod(v);
 			}
 		}
+	}
+
+	// apply Amiga filters
+	for (int32_t i = 0; i < numSamples; i++)
+	{
+		double dOut[2] = { dOutL[i], dOutR[i] };
+
+		if (useLowpassFilter)
+			RCLowPassFilterStereo(&filterLo, dOut, dOut);
+
+		if (useLEDFilter)
+			LEDFilter(&filterLED, dOut, dOut);
+
+		if (useHighpassFilter)
+			RCHighPassFilterStereo(&filterHi, dOut, dOut);
+
+		dOutL[i] = dOut[0];
+		dOutR[i] = dOut[1];
 	}
 }
--- a/src/pt2_paula.h
+++ b/src/pt2_paula.h
@@ -4,37 +4,12 @@
 #include <stdbool.h>
 #include "pt2_header.h"
 
-typedef struct voice_t
+enum
 {
-	volatile bool DMA_active;
+	MODEL_A1200 = 0,
+	MODEL_A500  = 1,
+};
 
-	// internal values (don't modify directly!)
-	bool DMATriggerFlag, nextSampleStage;
-	int8_t AUD_DAT[2]; // DMA data buffer
-	const int8_t *location; // current location
-	uint16_t lengthCounter; // current length
-	int32_t sampleCounter; // how many bytes left in AUD_DAT
-	double dSample; // current sample point
-
-	// registers modified by Paula functions
-	const int8_t *AUD_LC; // location
-	uint16_t AUD_LEN; // length (in words)
-	double AUD_PER_delta, AUD_PER_deltamul; // delta
-	double AUD_VOL; // volume
-
-	double dDelta, dPhase;
-
-	// for BLEP synthesis
-	double dLastDelta, dLastPhase, dLastDeltaMul, dBlepOffset, dDeltaMul;
-
-	// used for pt2_sync.c (visualizers)
-	uint8_t syncFlags;
-	uint8_t syncVolume;
-	int32_t syncPeriod;
-	int32_t syncTriggerLength;
-	const int8_t *syncTriggerData;
-} paulaVoice_t;
-
 #define PAULA_VOICES 4
 #define PAULA_PAL_CLK AMIGA_PAL_CCK_HZ
 #define PAL_PAULA_MIN_PERIOD 113
@@ -42,12 +17,14 @@
 #define PAL_PAULA_MAX_HZ (PAULA_PAL_CLK / (double)PAL_PAULA_MIN_PERIOD)
 #define PAL_PAULA_MAX_SAFE_HZ (PAULA_PAL_CLK / (double)PAL_PAULA_MIN_SAFE_PERIOD)
 
-void paulaSetOutputFrequency(double dAudioFreq, bool oversampling2x);
-void paulaSetDMACON(uint16_t bits); // $DFF096 register write (only controls paula DMA)
-void paulaSetPeriod(int32_t ch, uint16_t period);
-void paulaSetVolume(int32_t ch, uint16_t vol);
-void paulaSetLength(int32_t ch, uint16_t len);
-void paulaSetData(int32_t ch, const int8_t *src);
-void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples);
+void paulaSetup(double dOutputFreq, uint32_t amigaModel);
+void paulaDisableFilters(void); // disables low-pass & high-pass filters ("LED" filter is kept)
 
-extern paulaVoice_t paula[PAULA_VOICES]; // pt2_paula.c
+int8_t *paulaGetNullSamplePtr(void);
+
+void paulaWriteByte(uint32_t address, uint16_t data8);
+void paulaWriteWord(uint32_t address, uint16_t data16);
+void paulaWritePtr(uint32_t address, const void *ptr);
+
+// output is -4.00 .. 3.97 (can be louder because of high-pass filter)
+void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples);
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -13,8 +13,8 @@
 #include "pt2_config.h"
 #include "pt2_visuals.h"
 #include "pt2_scopes.h"
-#include "pt2_sync.h"
-#include "pt2_amigafilters.h"
+#include "pt2_paula.h"
+#include "pt2_visuals_sync.h"
 
 static bool posJumpAssert, pBreakFlag, modRenderDone;
 static bool doStopSong; // from F00 (Set Speed)
@@ -52,8 +52,12 @@
 		{
 			const moduleSample_t *s = &song->samples[editor.currSample];
 
-			paulaSetData(i, ch->n_start + s->loopStart);
-			paulaSetLength(i, (uint16_t)(s->loopLength >> 1));
+			const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+			paulaWritePtr(voiceAddr + 0, ch->n_start + s->loopStart);
+			paulaWriteWord(voiceAddr + 4, (uint16_t)(s->loopLength >> 1));
+
+			setVisualsDataPtr(i, ch->n_start + s->loopStart);
+			setVisualsLength(i, (uint16_t)(s->loopLength >> 1));
 		}
 	}
 
@@ -67,13 +71,19 @@
 	if (audioWasntLocked)
 		lockAudio();
 
-	paulaSetDMACON(0x000F); // turn off all voice DMAs
+	paulaWriteWord(0xDFF096, 0x000F); // turn off all voice DMAs
+	setVisualsDMACON(0x000F);
 
+	// clear all volumes
 	for (int32_t i = 0; i < PAULA_VOICES; i++)
-		paulaSetVolume(i, 0);
+	{
+		const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+		paulaWriteWord(voiceAddr + 8, 0);
 
+		setVisualsVolume(i, 0);
+	}
+
 	// reset dithering/filter states (so that every playback session is identical)
-	resetAmigaFilterStates();
 	resetAudioDithering();
 
 	if (audioWasntLocked)
@@ -289,19 +299,28 @@
 
 static void doRetrg(moduleChannel_t *ch)
 {
-	paulaSetDMACON(ch->n_dmabit); // voice DMA off
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
 
-	paulaSetData(ch->n_chanindex, ch->n_start); // n_start is increased on 9xx
-	paulaSetLength(ch->n_chanindex, ch->n_length);
-	paulaSetPeriod(ch->n_chanindex, ch->n_period);
+	paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
+	paulaWritePtr(voiceAddr + 0, ch->n_start); // n_start is increased on 9xx
+	paulaWriteWord(voiceAddr + 4, ch->n_length);
+	paulaWriteWord(voiceAddr + 6, ch->n_period);
+	paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
+	
+	// these take effect after the current DMA cycle is done
+	paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+	paulaWriteWord(voiceAddr + 4, ch->n_replen);
 
-	paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+	// update tracker visuals
 
-	// these take effect after the current DMA cycle is done
-	paulaSetData(ch->n_chanindex, ch->n_loopstart);
-	paulaSetLength(ch->n_chanindex, ch->n_replen);
+	setVisualsDMACON(ch->n_dmabit);
+	setVisualsDataPtr(ch->n_chanindex, ch->n_start);
+	setVisualsLength(ch->n_chanindex, ch->n_length);
+	setVisualsPeriod(ch->n_chanindex, ch->n_period);
+	setVisualsDMACON(0x8000 | ch->n_dmabit);
 
-	// set visuals
+	setVisualsDataPtr(ch->n_chanindex, ch->n_loopstart);
+	setVisualsLength(ch->n_chanindex, ch->n_replen);
 
 	ch->syncAnalyzerVolume = ch->n_volume;
 	ch->syncAnalyzerPeriod = ch->n_period;
@@ -434,6 +453,8 @@
 {
 	int32_t arpNote;
 
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+
 	int32_t arpTick = song->tick % 3; // 0, 1, 2
 	if (arpTick == 1)
 	{
@@ -445,7 +466,8 @@
 	}
 	else // arpTick 0
 	{
-		paulaSetPeriod(ch->n_chanindex, ch->n_period);
+		paulaWriteWord(voiceAddr + 6, ch->n_period);
+		setVisualsPeriod(ch->n_chanindex, ch->n_period);
 		return;
 	}
 
@@ -459,7 +481,8 @@
 	{
 		if (ch->n_period >= periods[baseNote])
 		{
-			paulaSetPeriod(ch->n_chanindex, periods[baseNote+arpNote]);
+			paulaWriteWord(voiceAddr + 6, periods[baseNote+arpNote]);
+			setVisualsPeriod(ch->n_chanindex, periods[baseNote+arpNote]);
 			break;
 		}
 	}
@@ -473,7 +496,10 @@
 	if ((ch->n_period & 0xFFF) < 113) // PT BUG: sign removed before comparison, underflow not clamped!
 		ch->n_period = (ch->n_period & 0xF000) | 113;
 
-	paulaSetPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 6, ch->n_period & 0xFFF);
+
+	setVisualsPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
 }
 
 static void portaDown(moduleChannel_t *ch)
@@ -484,7 +510,10 @@
 	if ((ch->n_period & 0xFFF) > 856)
 		ch->n_period = (ch->n_period & 0xF000) | 856;
 
-	paulaSetPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 6, ch->n_period & 0xFFF);
+
+	setVisualsPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
 }
 
 static void filterOnOff(moduleChannel_t *ch)
@@ -492,16 +521,8 @@
 	if (song->tick == 0) // added this (just pointless to call this during all ticks!)
 	{
 		const bool filterOn = (ch->n_cmd & 1) ^ 1;
-		if (filterOn)
-		{
-			editor.useLEDFilter = true;
-			setLEDFilter(true);
-		}
-		else
-		{
-			editor.useLEDFilter = false;
-			setLEDFilter(false);
-		}
+		paulaWriteByte(0xBFE001, filterOn << 1);
+		audio.ledFilterEnabled = filterOn;
 	}
 }
 
@@ -576,9 +597,12 @@
 		}
 	}
 
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+
 	if ((ch->n_glissfunk & 0xF) == 0)
 	{
-		paulaSetPeriod(ch->n_chanindex, ch->n_period);
+		paulaWriteWord(voiceAddr + 6, ch->n_period);
+		setVisualsPeriod(ch->n_chanindex, ch->n_period);
 	}
 	else
 	{
@@ -598,7 +622,8 @@
 			}
 		}
 
-		paulaSetPeriod(ch->n_chanindex, portaPointer[i]);
+		paulaWriteWord(voiceAddr + 6, portaPointer[i]);
+		setVisualsPeriod(ch->n_chanindex, portaPointer[i]);
 	}
 }
 
@@ -643,8 +668,11 @@
 	else
 		vibratoData = ch->n_period - vibratoData;
 
-	paulaSetPeriod(ch->n_chanindex, vibratoData);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 6, vibratoData); // period
 
+	setVisualsPeriod(ch->n_chanindex, vibratoData);
+
 	ch->n_vibratopos += (ch->n_vibratocmd >> 2) & 0x3C;
 }
 
@@ -715,8 +743,11 @@
 			tremoloData = 0;
 	}
 
-	paulaSetVolume(ch->n_chanindex, tremoloData);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 8, tremoloData); // volume
 
+	setVisualsVolume(ch->n_chanindex, tremoloData);
+
 	ch->n_tremolopos += (ch->n_tremolocmd >> 2) & 0x3C;
 }
 
@@ -794,7 +825,10 @@
 		return;
 	}
 
-	paulaSetPeriod(ch->n_chanindex, ch->n_period);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 6, ch->n_period);
+
+	setVisualsPeriod(ch->n_chanindex, ch->n_period);
 }
 
 static void chkefx2(moduleChannel_t *ch)
@@ -818,8 +852,11 @@
 		default: break;
 	}
 
-	paulaSetPeriod(ch->n_chanindex, ch->n_period);
+	const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+	paulaWriteWord(voiceAddr + 6, ch->n_period);
 
+	setVisualsPeriod(ch->n_chanindex, ch->n_period);
+
 	if (cmd == 0x7)
 		tremolo(ch);
 	else if (cmd == 0xA)
@@ -842,7 +879,12 @@
 	*/
 	const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
 	if (cmd != 0x7)
-		paulaSetVolume(ch->n_chanindex, ch->n_volume);
+	{
+		const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+		paulaWriteWord(voiceAddr + 8, ch->n_volume);
+
+		setVisualsVolume(ch->n_chanindex, ch->n_volume);
+	}
 }
 
 static void setPeriod(moduleChannel_t *ch)
@@ -862,24 +904,38 @@
 
 	if ((ch->n_cmd & 0xFF0) != 0xED0) // no note delay
 	{
-		paulaSetDMACON(ch->n_dmabit); // voice DMA off (turned on in setDMA() later)
+		paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off (turned on in setDMA() later)
 
 		if ((ch->n_wavecontrol & 0x04) == 0) ch->n_vibratopos = 0;
 		if ((ch->n_wavecontrol & 0x40) == 0) ch->n_tremolopos = 0;
 
-		paulaSetLength(ch->n_chanindex, ch->n_length);
-		paulaSetData(ch->n_chanindex, ch->n_start);
+		const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
 
+		paulaWriteWord(voiceAddr + 4, ch->n_length);
+		paulaWritePtr(voiceAddr + 0, ch->n_start);
+
 		if (ch->n_start == NULL)
 		{
 			ch->n_loopstart = NULL;
-			paulaSetLength(ch->n_chanindex, 1);
+			paulaWriteWord(voiceAddr + 4, 1); // length
 			ch->n_replen = 1;
 		}
 
-		paulaSetPeriod(ch->n_chanindex, ch->n_period);
+		paulaWriteWord(voiceAddr + 6, ch->n_period);
 
 		DMACONtemp |= ch->n_dmabit;
+
+		// update tracker visuals
+
+		setVisualsDMACON(ch->n_dmabit);
+
+		setVisualsLength(ch->n_chanindex, ch->n_length);
+		setVisualsDataPtr(ch->n_chanindex, ch->n_start);
+
+		if (ch->n_start == NULL)
+			setVisualsLength(ch->n_chanindex, 1);
+
+		setVisualsPeriod(ch->n_chanindex, ch->n_period);
 	}
 
 	checkMoreEffects(ch);
@@ -889,7 +945,7 @@
 {
 	if (editor.metroFlag && editor.metroChannel > 0)
 	{
-		if (ch->n_chanindex == editor.metroChannel-1 && (song->row % editor.metroSpeed) == 0)
+		if (ch->n_chanindex == (uint32_t)editor.metroChannel-1 && (song->row % editor.metroSpeed) == 0)
 		{
 			note->sample = 31;
 			note->period = (((song->row / editor.metroSpeed) % editor.metroSpeed) == 0) ? 160 : 214;
@@ -900,8 +956,13 @@
 static void playVoice(moduleChannel_t *ch)
 {
 	if (ch->n_note == 0 && ch->n_cmd == 0) // test period, command and command parameter
-		paulaSetPeriod(ch->n_chanindex, ch->n_period);
+	{
+		const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+		paulaWriteWord(voiceAddr + 6, ch->n_period);
 
+		setVisualsPeriod(ch->n_chanindex, ch->n_period);
+	}
+
 	note_t note = song->patterns[modPattern][(song->row * PAULA_VOICES) + ch->n_chanindex];
 
 	checkMetronome(ch, &note);
@@ -935,7 +996,7 @@
 
 		// non-PT2 requirement (set safe sample space for uninitialized voices - f.ex. "the ultimate beeper.mod")
 		if (ch->n_length == 0)
-			ch->n_loopstart = ch->n_wavestart = &song->sampleData[config.reservedSampleOffset]; // 128K reserved sample
+			ch->n_loopstart = ch->n_wavestart = paulaGetNullSamplePtr();
 	}
 
 	if ((ch->n_note & 0xFFF) > 0)
@@ -1120,8 +1181,10 @@
 	if (editor.muted[2]) DMACONtemp &= ~4;
 	if (editor.muted[3]) DMACONtemp &= ~8;
 
-	paulaSetDMACON(0x8000 | DMACONtemp);
+	paulaWriteWord(0xDFF096, 0x8000 | DMACONtemp); // start DMAs for selected voices
 
+	setVisualsDMACON(0x8000 | DMACONtemp);
+
 	moduleChannel_t *ch = song->channels;
 	for (int32_t i = 0; i < PAULA_VOICES; i++, ch++)
 	{
@@ -1135,8 +1198,12 @@
 		}
 
 		// these take effect after the current DMA cycle is done
-		paulaSetData(i, ch->n_loopstart);
-		paulaSetLength(i, ch->n_replen);
+		const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+		paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+		paulaWriteWord(voiceAddr + 4, ch->n_replen);
+
+		setVisualsDataPtr(i, ch->n_loopstart);
+		setVisualsLength(i, ch->n_replen);
 	}
 }
 
@@ -1209,7 +1276,11 @@
 			for (int32_t i = 0; i < PAULA_VOICES; i++, ch++)
 			{
 				playVoice(ch);
-				paulaSetVolume(i, ch->n_volume);
+
+				const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+				paulaWriteWord(voiceAddr + 8, ch->n_volume);
+
+				setVisualsVolume(i, ch->n_volume);
 			}
 
 			setDMA();
@@ -1583,7 +1654,6 @@
 	modSetSpeed(editor.initialSpeed);
 
 	// disable the LED filter after clearing the song (real PT2 doesn't do this)
-	editor.useLEDFilter = false;
 	setLEDFilter(false);
 
 	updateCurrSample();
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -19,6 +19,7 @@
 #include "pt2_rcfilter.h"
 #include "pt2_chordmaker.h"
 #include "pt2_replayer.h"
+#include "pt2_visuals_sync.h"
 
 #define CENTER_LINE_COLOR 0x303030
 #define MARK_COLOR_1 0x666666 /* inverted background */
@@ -1247,15 +1248,25 @@
 
 		lockAudio();
 
-		paulaSetDMACON(TToneBit); // voice DMA off
+		const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
 
-		paulaSetPeriod(chNum, periodTable[editor.tuningNote]);
-		paulaSetVolume(chNum, 64);
-		paulaSetData(chNum, tuneToneData);
-		paulaSetLength(chNum, sizeof (tuneToneData) / 2);
+		paulaWriteWord(0xDFF096, TToneBit); // voice DMA off
 
-		paulaSetDMACON(0x8000 | TToneBit); // voice DMA on
+		paulaWriteWord(voiceAddr + 6, periodTable[editor.tuningNote]);
+		paulaWriteWord(voiceAddr + 8, 64); // volume
+		paulaWritePtr(voiceAddr + 0, tuneToneData);
+		paulaWriteWord(voiceAddr + 4, sizeof (tuneToneData) / 2); // length
 
+		paulaWriteWord(0xDFF096, 0x8000 | TToneBit); // voice DMA on
+
+		// update tracker visuals
+		setVisualsDMACON(TToneBit);
+		setVisualsPeriod(chNum, periodTable[editor.tuningNote]);
+		setVisualsVolume(chNum, 64);
+		setVisualsDataPtr(chNum, tuneToneData);
+		setVisualsLength(chNum, sizeof (tuneToneData) / 2);
+		setVisualsDMACON(0x8000 | TToneBit);
+
 		unlockAudio();
 	}
 	else
@@ -1263,7 +1274,8 @@
 		// turn tuning tone off
 
 		lockAudio();
-		paulaSetDMACON(TToneBit); // voice DMA off
+		paulaWriteWord(0xDFF096, TToneBit); // voice DMA off
+		setVisualsDMACON(TToneBit);
 		unlockAudio();
 	}
 }
@@ -1772,26 +1784,51 @@
 	if (ch->n_length == 0)
 		ch->n_length = 1;
 
-	paulaSetVolume(chn, ch->n_volume);
-	paulaSetPeriod(chn, ch->n_period);
-	paulaSetData(chn, ch->n_start);
-	paulaSetLength(chn, ch->n_length);
+	const uint32_t voiceAddr = 0xDFF0A0 + (chn * 16);
 
+	paulaWriteWord(voiceAddr + 8, ch->n_volume);
+	paulaWriteWord(voiceAddr + 6, ch->n_period);
+	paulaWritePtr(voiceAddr + 0, ch->n_start);
+	paulaWriteWord(voiceAddr + 4, ch->n_length);
+
 	if (!editor.muted[chn])
-		paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+		paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
 	else
-		paulaSetDMACON(ch->n_dmabit); // voice DMA off
+		paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
 
 	// these take effect after the current DMA cycle is done
 	if (playWaveformFlag)
 	{
-		paulaSetData(chn, ch->n_loopstart);
-		paulaSetLength(chn, ch->n_replen);
+		paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+		paulaWriteWord(voiceAddr + 4, ch->n_replen);
 	}
 	else
 	{
-		paulaSetData(chn, NULL);
-		paulaSetLength(chn, 1);
+		paulaWritePtr(voiceAddr + 0, NULL); // data
+		paulaWriteWord(voiceAddr + 4, 1); // length
+	}
+
+	// update tracker visuals
+
+	setVisualsVolume(chn, ch->n_volume);
+	setVisualsPeriod(chn, ch->n_period);
+	setVisualsDataPtr(chn, ch->n_start);
+	setVisualsLength(chn, ch->n_length);
+
+	if (!editor.muted[chn])
+		setVisualsDMACON(0x8000 | ch->n_dmabit);
+	else
+		setVisualsDMACON(ch->n_dmabit);
+
+	if (playWaveformFlag)
+	{
+		setVisualsDataPtr(chn, ch->n_loopstart);
+		setVisualsLength(chn, ch->n_replen);
+	}
+	else
+	{
+		setVisualsDataPtr(chn, NULL);
+		setVisualsLength(chn, 1);
 	}
 
 	unlockAudio();
--- a/src/pt2_scopes.c
+++ b/src/pt2_scopes.c
@@ -88,23 +88,19 @@
 	scope[ch].dDelta = (PAULA_PAL_CLK / (double)SCOPE_HZ) / period;
 }
 
-void scopeTrigger(int32_t ch)
+void scopeTrigger(int32_t ch) // expects data & length variables to be set already
 {
 	volatile scope_t *sc = &scope[ch];
 	scope_t tempState = *sc; // cache it
 
-	const int8_t *newData = tempState.newData;
-	if (newData == NULL)
-		newData = &song->sampleData[config.reservedSampleOffset]; // 128K reserved sample
+	if (tempState.data == NULL)
+		tempState.data = paulaGetNullSamplePtr();
 
-	int32_t newLength = tempState.newLength; // in bytes, not words
-	if (newLength < 2)
-		newLength = 2; // for safety
+	if (tempState.length < 2)
+		tempState.length = 2; // for safety
 
 	tempState.dPhase = 0.0;
 	tempState.pos = 0;
-	tempState.data = newData;
-	tempState.length = newLength;
 	tempState.active = true;
 
 	/* Update live scope now.
@@ -240,14 +236,12 @@
 	{
 		scope_t tmpScope = *sc; // cache it
 
+		// clear scope background
+		fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
+
 		// render scope
 		if (tmpScope.active && tmpScope.data != NULL && tmpScope.volume != 0 && tmpScope.length > 0)
 		{
-			sc->emptyScopeDrawn = false;
-
-			// fill scope background
-			fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
-
 			// render scope data
 			int16_t scopeData;
 			int32_t pos = tmpScope.pos;
@@ -275,17 +269,10 @@
 				}
 			}
 		}
-		else if (!sc->emptyScopeDrawn)
+		else
 		{
-			// scope is inactive (or vol=0), draw empty scope once until it gets active again
-
-			// fill scope background
-			fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
-
-			// draw scope line
+			// draw centered scope line
 			hLine(scopeX, 71, SCOPE_WIDTH, fgColor);
-
-			sc->emptyScopeDrawn = true;
 		}
 
 		scopeX += SCOPE_WIDTH+8;
--- a/src/pt2_scopes.h
+++ b/src/pt2_scopes.h
@@ -7,14 +7,12 @@
 
 typedef struct scope_t
 {
-	const int8_t *data;
-	bool active, emptyScopeDrawn;
+	volatile bool active;
+	const int8_t *data, *newData;
 	uint8_t volume;
-	int32_t length, pos;
-
+	int32_t length, newLength;
+	int32_t pos;
 	double dDelta, dPhase;
-	const int8_t *newData;
-	int32_t newLength;
 } scope_t;
 
 void scopeSetPeriod(int32_t ch, int32_t period);
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -68,7 +68,7 @@
 
 typedef struct moduleChannel_t
 {
-	int8_t *n_start, *n_wavestart, *n_loopstart, n_chanindex, n_volume, n_dmabit;
+	int8_t *n_start, *n_wavestart, *n_loopstart, n_volume, n_dmabit;
 	int8_t n_toneportdirec, n_pattpos, n_loopcount;
 	uint8_t n_wavecontrol, n_glissfunk, n_sampleoffset, n_toneportspeed;
 	uint8_t n_vibratocmd, n_tremolocmd, n_finetune, n_funkoffset, n_samplenum;
@@ -75,7 +75,7 @@
 	uint8_t n_vibratopos, n_tremolopos;
 	int16_t n_period, n_note, n_wantedperiod;
 	uint16_t n_cmd, n_length, n_replen;
-	uint32_t n_scopedelta;
+	uint32_t n_scopedelta, n_chanindex;
 
 	// for pt2_sync.c
 	uint8_t syncFlags;
@@ -162,7 +162,7 @@
 	UNICHAR *fileNameTmpU, *currPathU, *modulesPathU, *samplesPathU;
 
 	bool errorMsgActive, errorMsgBlock, multiFlag, metroFlag, keypadToggle8CFlag, normalizeFiltersFlag;
-	bool sampleAllFlag, halfClipFlag, newOldFlag, pat2SmpHQ, mixFlag, useLEDFilter;
+	bool sampleAllFlag, halfClipFlag, newOldFlag, pat2SmpHQ, mixFlag;
 	bool modLoaded, autoInsFlag, repeatKeyFlag, sampleZero, tuningToneFlag;
 	bool stepPlayEnabled, stepPlayBackwards, blockBufferFlag, blockMarkFlag, didQuantize;
 	bool swapChannelFlag, configFound, chordLengthMin, rowVisitTable[MOD_ORDERS * MOD_ROWS];
--- a/src/pt2_sync.c
+++ /dev/null
@@ -1,190 +1,0 @@
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_sync.h"
-#include "pt2_scopes.h"
-#include "pt2_visuals.h"
-#include "pt2_tables.h"
-
-static volatile bool chQueueClearing;
-
-chSyncData_t *chSyncEntry; // globalized
-chSync_t chSync; // globalized
-
-void resetChSyncQueue(void)
-{
-	chSync.data[0].timestamp = 0;
-	chSync.writePos = 0;
-	chSync.readPos = 0;
-}
-
-static int32_t chQueueReadSize(void)
-{
-	while (chQueueClearing);
-
-	if (chSync.writePos > chSync.readPos)
-		return chSync.writePos - chSync.readPos;
-	else if (chSync.writePos < chSync.readPos)
-		return chSync.writePos - chSync.readPos + SYNC_QUEUE_LEN + 1;
-	else
-		return 0;
-}
-
-static int32_t chQueueWriteSize(void)
-{
-	int32_t size;
-
-	if (chSync.writePos > chSync.readPos)
-	{
-		size = chSync.readPos - chSync.writePos + SYNC_QUEUE_LEN;
-	}
-	else if (chSync.writePos < chSync.readPos)
-	{
-		chQueueClearing = true;
-
-		/* Buffer is full, reset the read/write pos. This is actually really nasty since
-		** read/write are two different threads, but because of timestamp validation it
-		** shouldn't be that dangerous.
-		** It will also create a small visual stutter while the buffer is getting filled,
-		** though that is barely noticable on normal buffer sizes, and it takes a minute
-		** or two at max BPM between each time (when queue size is default, 8191)
-		*/
-		chSync.data[0].timestamp = 0;
-		chSync.readPos = 0;
-		chSync.writePos = 0;
-
-		size = SYNC_QUEUE_LEN;
-
-		chQueueClearing = false;
-	}
-	else
-	{
-		size = SYNC_QUEUE_LEN;
-	}
-
-	return size;
-}
-
-bool chQueuePush(chSyncData_t t)
-{
-	if (!chQueueWriteSize())
-		return false;
-
-	assert(chSync.writePos <= SYNC_QUEUE_LEN);
-	chSync.data[chSync.writePos] = t;
-	chSync.writePos = (chSync.writePos + 1) & SYNC_QUEUE_LEN;
-
-	return true;
-}
-
-static bool chQueuePop(void)
-{
-	if (!chQueueReadSize())
-		return false;
-
-	chSync.readPos = (chSync.readPos + 1) & SYNC_QUEUE_LEN;
-	assert(chSync.readPos <= SYNC_QUEUE_LEN);
-
-	return true;
-}
-
-static chSyncData_t *chQueuePeek(void)
-{
-	if (!chQueueReadSize())
-		return NULL;
-
-	assert(chSync.readPos <= SYNC_QUEUE_LEN);
-	return &chSync.data[chSync.readPos];
-}
-
-static uint64_t getChQueueTimestamp(void)
-{
-	if (!chQueueReadSize())
-		return 0;
-
-	assert(chSync.readPos <= SYNC_QUEUE_LEN);
-	return chSync.data[chSync.readPos].timestamp;
-}
-
-void updateChannelSyncBuffer(void)
-{
-	uint8_t updateFlags[PAULA_VOICES];
-
-	chSyncEntry = NULL;
-
-	memset(updateFlags, 0, sizeof (updateFlags)); // this is needed
-
-	const uint64_t frameTime64 = SDL_GetPerformanceCounter();
-
-	// handle channel sync queue
-
-	while (chQueueClearing);
-	while (chQueueReadSize() > 0)
-	{
-		if (frameTime64 < getChQueueTimestamp())
-			break; // we have no more stuff to render for now
-
-		chSyncEntry = chQueuePeek();
-		if (chSyncEntry == NULL)
-			break;
-
-		for (int32_t i = 0; i < PAULA_VOICES; i++)
-			updateFlags[i] |= chSyncEntry->channels[i].flags; // yes, OR the status
-
-		if (!chQueuePop())
-			break;
-	}
-
-	/* Extra validation because of possible issues when the buffer is full
-	** and positions are being reset, which is not entirely thread safe.
-	*/
-	if (chSyncEntry != NULL && chSyncEntry->timestamp == 0)
-		chSyncEntry = NULL;
-
-	// do actual updates
-	if (chSyncEntry != NULL)
-	{
-		scope_t *s = scope;
-		syncedChannel_t *c = chSyncEntry->channels;
-		for (int32_t ch = 0; ch < PAULA_VOICES; ch++, s++, c++)
-		{
-			const uint8_t flags = updateFlags[ch];
-			if (flags == 0)
-				continue;
-
-			if (flags & SET_SCOPE_VOLUME)
-				scope[ch].volume = c->volume;
-
-			if (flags & SET_SCOPE_PERIOD)
-				scopeSetPeriod(ch, c->period);
-
-			// the following handling order is important, don't change it!
-
-			if (flags & STOP_SCOPE)
-				scope[ch].active = false;
-
-			if (flags & TRIGGER_SCOPE)
-			{
-				s->newData = c->triggerData;
-				s->newLength = c->triggerLength;
-				scopeTrigger(ch);
-			}
-
-			if (flags & SET_SCOPE_DATA)
-				scope[ch].newData = c->newData;
-
-			if (flags & SET_SCOPE_LENGTH)
-				scope[ch].newLength = c->newLength;
-
-			// ---------------------------------------------------------------
-
-			if (flags & UPDATE_ANALYZER)
-				updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod);
-
-			if (flags & UPDATE_VUMETER) // for fake VU-meters only
-			{
-				if (c->vuVolume <= 64)
-					editor.vuMeterVolumes[ch] = vuMeterHeights[c->vuVolume];
-			}
-		}
-	}
-}
--- a/src/pt2_sync.h
+++ /dev/null
@@ -1,51 +1,0 @@
-#pragma once
-
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_paula.h"
-
-enum // flags
-{
-	SET_SCOPE_VOLUME = 1,
-	SET_SCOPE_PERIOD = 2,
-	SET_SCOPE_DATA = 4,
-	SET_SCOPE_LENGTH = 8,
-	TRIGGER_SCOPE = 16,
-	STOP_SCOPE = 32,
-
-	UPDATE_VUMETER = 64,
-	UPDATE_ANALYZER = 128
-};
-
-// 2^n-1 - don't change this! Queue buffer is already ~1MB in size
-#define SYNC_QUEUE_LEN 8191
-
-typedef struct syncedChannel_t
-{
-	uint8_t flags;
-	const int8_t *triggerData, *newData;
-	int32_t triggerLength, newLength;
-	uint8_t volume, vuVolume, analyzerVolume;
-	uint16_t analyzerPeriod;
-	int32_t period;
-} syncedChannel_t;
-
-typedef struct chSyncData_t
-{
-	syncedChannel_t channels[PAULA_VOICES];
-	uint64_t timestamp;
-} chSyncData_t;
-
-typedef struct chSync_t
-{
-	volatile int32_t readPos, writePos;
-	chSyncData_t data[SYNC_QUEUE_LEN + 1];
-} chSync_t;
-
-void resetChSyncQueue(void);
-bool chQueuePush(chSyncData_t t);
-void updateChannelSyncBuffer(void);
-
-extern chSyncData_t *chSyncEntry; // pt2_sync.c
-extern chSync_t chSync; // pt2_sync.c
-
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -1073,9 +1073,6 @@
 		srcPtr += SCREEN_W;
 		dstPtr += SCREEN_W;
 	}
-
-	for (int32_t i = 0; i < PAULA_VOICES; i++)
-		scope[i].emptyScopeDrawn = false;
 }
 
 void renderSpectrumAnalyzerBg(void)
--- /dev/null
+++ b/src/pt2_visuals_sync.c
@@ -1,0 +1,388 @@
+// used for syncing audio from Paula writes to tracker visuals
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_audio.h"
+#include "pt2_visuals_sync.h"
+#include "pt2_scopes.h"
+#include "pt2_visuals.h"
+#include "pt2_tables.h"
+
+typedef struct syncVoice_t
+{
+	const int8_t *newData, *data;
+	uint8_t flags, volume;
+	uint16_t period;
+	uint16_t newLength, length;
+} syncVoice_t;
+
+static volatile bool chQueueClearing;
+static uint32_t audLatencyPerfValInt, audLatencyPerfValFrac;
+static uint64_t tickTime64, tickTime64Frac;
+static uint32_t tickTimeLen, tickTimeLenFrac;
+static syncVoice_t syncVoice[PAULA_VOICES];
+static chSyncData_t *chSyncEntry;
+static chSync_t chSync;
+
+static void startDMA(int32_t ch)
+{
+	syncVoice_t *sv = &syncVoice[ch];
+
+	if (editor.songPlaying)
+	{
+		sv->data = sv->newData;
+		sv->length = sv->newLength;
+		sv->flags |= TRIGGER_SCOPE;
+	}
+	else
+	{
+		scope_t *s = &scope[ch];
+		s->data = sv->newData;
+		s->length = sv->newLength * 2;
+		scopeTrigger(ch);
+	}
+}
+
+static void stopDMA(int32_t ch)
+{
+	if (editor.songPlaying)
+		syncVoice[ch].flags |= STOP_SCOPE;
+	else
+		scope[ch].active = false;
+}
+
+void setVisualsDMACON(uint16_t bits)
+{
+	if (bits & 0x8000)
+	{
+		// set
+		if (bits & 1) startDMA(0);
+		if (bits & 2) startDMA(1);
+		if (bits & 4) startDMA(2);
+		if (bits & 8) startDMA(3);
+	}
+	else
+	{
+		// clear
+		if (bits & 1) stopDMA(0);
+		if (bits & 2) stopDMA(1);
+		if (bits & 4) stopDMA(2);
+		if (bits & 8) stopDMA(3);
+	}
+}
+
+void setVisualsVolume(int32_t ch, uint16_t vol)
+{
+	int32_t realVol = vol & 127;
+	if (realVol > 64)
+		realVol = 64;
+
+	if (editor.songPlaying)
+	{
+		syncVoice_t *sv = &syncVoice[ch];
+
+		sv->volume = (uint8_t)realVol;
+		sv->flags |= SET_SCOPE_VOLUME;
+	}
+	else
+	{
+		scope[ch].volume = (uint8_t)realVol;
+	}
+}
+
+void setVisualsPeriod(int32_t ch, uint16_t period)
+{
+	if (period == 0)
+		period = 65535; // On Amiga: period 0 = one full cycle with period 65536, then period 65535 for the rest
+	else if (period < 113)
+		period = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
+
+	if (editor.songPlaying)
+	{
+		syncVoice_t *sv = &syncVoice[ch];
+
+		sv->period = period;
+		sv->flags |= SET_SCOPE_PERIOD;
+	}
+	else
+	{
+		scopeSetPeriod(ch, period);
+	}
+}
+
+void setVisualsLength(int32_t ch, uint16_t len)
+{
+	syncVoice_t *sv = &syncVoice[ch];
+
+	sv->newLength = len;
+
+	if (editor.songPlaying)
+		sv->flags |= SET_SCOPE_LENGTH;
+	else
+		scope[ch].newLength = len * 2;
+}
+
+void setVisualsDataPtr(int32_t ch, const int8_t *src)
+{
+	syncVoice_t *sv = &syncVoice[ch];
+
+	if (src == NULL)
+		src = paulaGetNullSamplePtr();
+
+	sv->newData = src;
+
+	if (editor.songPlaying)
+		sv->flags |= SET_SCOPE_DATA;
+	else
+		scope[ch].newData = src;
+}
+
+void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq)
+{
+	double dInt, dFrac;
+
+	if (audioFreq == 0)
+		return;
+
+	const double dAudioLatencySecs = audioBufferSize / (double)audioFreq;
+
+	dFrac = modf(dAudioLatencySecs * hpcFreq.dFreq, &dInt);
+
+	// integer part
+	audLatencyPerfValInt = (uint32_t)dInt;
+
+	// fractional part (scaled to 0..2^32-1)
+	audLatencyPerfValFrac = (uint32_t)((dFrac * (UINT32_MAX+1.0)) + 0.5); // rounded
+}
+
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac)
+{
+	tickTimeLen = timeLen;
+	tickTimeLenFrac = timeLenFrac;
+}
+
+void resetChSyncQueue(void)
+{
+	chSync.data[0].timestamp = 0;
+	chSync.writePos = 0;
+	chSync.readPos = 0;
+}
+
+static int32_t chQueueReadSize(void)
+{
+	while (chQueueClearing);
+
+	if (chSync.writePos > chSync.readPos)
+		return chSync.writePos - chSync.readPos;
+	else if (chSync.writePos < chSync.readPos)
+		return chSync.writePos - chSync.readPos + SYNC_QUEUE_LEN + 1;
+	else
+		return 0;
+}
+
+static int32_t chQueueWriteSize(void)
+{
+	int32_t size;
+
+	if (chSync.writePos > chSync.readPos)
+	{
+		size = chSync.readPos - chSync.writePos + SYNC_QUEUE_LEN;
+	}
+	else if (chSync.writePos < chSync.readPos)
+	{
+		chQueueClearing = true;
+
+		/* Buffer is full, reset the read/write pos. This is actually really nasty since
+		** read/write are two different threads, but because of timestamp validation it
+		** shouldn't be that dangerous.
+		** It will also create a small visual stutter while the buffer is getting filled,
+		** though that is barely noticable on normal buffer sizes, and it takes a minute
+		** or two at max BPM between each time (when queue size is default, 8191)
+		*/
+		chSync.data[0].timestamp = 0;
+		chSync.readPos = 0;
+		chSync.writePos = 0;
+
+		size = SYNC_QUEUE_LEN;
+
+		chQueueClearing = false;
+	}
+	else
+	{
+		size = SYNC_QUEUE_LEN;
+	}
+
+	return size;
+}
+
+bool chQueuePush(chSyncData_t t)
+{
+	if (!chQueueWriteSize())
+		return false;
+
+	assert(chSync.writePos <= SYNC_QUEUE_LEN);
+	chSync.data[chSync.writePos] = t;
+	chSync.writePos = (chSync.writePos + 1) & SYNC_QUEUE_LEN;
+
+	return true;
+}
+
+static bool chQueuePop(void)
+{
+	if (!chQueueReadSize())
+		return false;
+
+	chSync.readPos = (chSync.readPos + 1) & SYNC_QUEUE_LEN;
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+
+	return true;
+}
+
+static chSyncData_t *chQueuePeek(void)
+{
+	if (!chQueueReadSize())
+		return NULL;
+
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+	return &chSync.data[chSync.readPos];
+}
+
+static uint64_t getChQueueTimestamp(void)
+{
+	if (!chQueueReadSize())
+		return 0;
+
+	assert(chSync.readPos <= SYNC_QUEUE_LEN);
+	return chSync.data[chSync.readPos].timestamp;
+}
+
+void fillVisualsSyncBuffer(void)
+{
+	chSyncData_t chSyncData;
+
+	if (audio.resetSyncTickTimeFlag)
+	{
+		audio.resetSyncTickTimeFlag = false;
+
+		tickTime64 = SDL_GetPerformanceCounter() + audLatencyPerfValInt;
+		tickTime64Frac = audLatencyPerfValFrac;
+	}
+
+	if (song != NULL)
+	{
+		moduleChannel_t *ch = song->channels;
+		syncVoice_t *sv = syncVoice;
+		syncedChannel_t *sc = chSyncData.channels;
+
+		for (int32_t i = 0; i < PAULA_VOICES; i++, ch++, sc++, sv++)
+		{
+			sc->flags = sv->flags | ch->syncFlags;
+			ch->syncFlags = sv->flags = 0; // clear sync flags
+
+			sc->volume = sv->volume;
+			sc->period = sv->period;
+			sc->data = sv->data;
+			sc->length = sv->length;
+			sc->newData = sv->newData;
+			sc->newLength = sv->newLength;
+			sc->vuVolume = ch->syncVuVolume;
+			sc->analyzerVolume = ch->syncAnalyzerVolume;
+			sc->analyzerPeriod = ch->syncAnalyzerPeriod;
+		}
+
+		chSyncData.timestamp = tickTime64;
+		chQueuePush(chSyncData);
+	}
+
+	tickTime64 += tickTimeLen;
+	tickTime64Frac += tickTimeLenFrac;
+	if (tickTime64Frac > UINT32_MAX)
+	{
+		tickTime64Frac &= UINT32_MAX;
+		tickTime64++;
+	}
+}
+
+void updateChannelSyncBuffer(void)
+{
+	uint8_t updateFlags[PAULA_VOICES];
+
+	chSyncEntry = NULL;
+
+	*(uint32_t *)updateFlags = 0; // clear all channel update flags (this is needed)
+
+	const uint64_t frameTime64 = SDL_GetPerformanceCounter();
+
+	// handle channel sync queue
+
+	while (chQueueClearing);
+	while (chQueueReadSize() > 0)
+	{
+		if (frameTime64 < getChQueueTimestamp())
+			break; // we have no more stuff to render for now
+
+		chSyncEntry = chQueuePeek();
+		if (chSyncEntry == NULL)
+			break;
+
+		for (int32_t i = 0; i < PAULA_VOICES; i++)
+			updateFlags[i] |= chSyncEntry->channels[i].flags; // yes, OR the status
+
+		if (!chQueuePop())
+			break;
+	}
+
+	/* Extra validation because of possible issues when the buffer is full
+	** and positions are being reset, which is not entirely thread safe.
+	*/
+	if (chSyncEntry != NULL && chSyncEntry->timestamp == 0)
+		chSyncEntry = NULL;
+
+	// do actual updates
+	if (chSyncEntry != NULL)
+	{
+		scope_t *s = scope;
+		syncedChannel_t *c = chSyncEntry->channels;
+		for (int32_t ch = 0; ch < PAULA_VOICES; ch++, s++, c++)
+		{
+			const uint8_t flags = updateFlags[ch];
+			if (flags == 0)
+				continue;
+
+			if (flags & SET_SCOPE_VOLUME)
+				scope[ch].volume = c->volume;
+
+			if (flags & SET_SCOPE_PERIOD)
+				scopeSetPeriod(ch, c->period);
+
+			// the following handling order is important, don't change it!
+
+			if (flags & STOP_SCOPE)
+				scope[ch].active = false;
+
+			if (flags & TRIGGER_SCOPE)
+			{
+				s->data = c->data;
+				s->length = c->length * 2;
+				scopeTrigger(ch);
+			}
+
+			if (flags & SET_SCOPE_DATA)
+				scope[ch].newData = c->newData;
+
+			if (flags & SET_SCOPE_LENGTH)
+				scope[ch].newLength = c->newLength * 2;
+
+			// ---------------------------------------------------------------
+
+			if (flags & UPDATE_ANALYZER)
+				updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod);
+
+			if (flags & UPDATE_VUMETER) // for fake VU-meters only
+			{
+				if (c->vuVolume <= 64)
+					editor.vuMeterVolumes[ch] = vuMeterHeights[c->vuVolume];
+			}
+		}
+	}
+}
--- /dev/null
+++ b/src/pt2_visuals_sync.h
@@ -1,0 +1,65 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_paula.h"
+
+enum // flags
+{
+	SET_SCOPE_VOLUME = 1,
+	SET_SCOPE_PERIOD = 2,
+	SET_SCOPE_DATA = 4,
+	SET_SCOPE_LENGTH = 8,
+	TRIGGER_SCOPE = 16,
+	STOP_SCOPE = 32,
+
+	UPDATE_VUMETER = 64,
+	UPDATE_ANALYZER = 128
+};
+
+// 2^n-1 - don't change this! Total queue buffer length is already big.
+#define SYNC_QUEUE_LEN 8191
+
+#ifdef _MSC_VER
+#pragma pack(push)
+#pragma pack(1)
+#endif
+typedef struct syncedChannel_t // pack to save RAM
+{
+	const int8_t *data, *newData;
+	uint16_t length, newLength;
+	uint16_t period, analyzerPeriod;
+	uint8_t flags;
+	uint8_t volume, analyzerVolume, vuVolume;
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+syncedChannel_t;
+#ifdef _MSC_VER
+#pragma pack(pop)
+#endif
+
+typedef struct chSyncData_t
+{
+	syncedChannel_t channels[PAULA_VOICES];
+	uint64_t timestamp;
+} chSyncData_t;
+
+typedef struct chSync_t
+{
+	volatile int32_t readPos, writePos;
+	chSyncData_t data[SYNC_QUEUE_LEN + 1];
+} chSync_t;
+
+void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq);
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac);
+void fillVisualsSyncBuffer(void);
+void resetChSyncQueue(void);
+void updateChannelSyncBuffer(void);
+
+void setVisualsDMACON(uint16_t bits);
+void setVisualsVolume(int32_t ch, uint16_t vol);
+void setVisualsPeriod(int32_t ch, uint16_t period);
+void setVisualsLength(int32_t ch, uint16_t len);
+void setVisualsDataPtr(int32_t ch, const int8_t *src);
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj
@@ -237,7 +237,6 @@
     <Reference Include="System.Xml" />
   </ItemGroup>
   <ItemGroup>
-    <ClInclude Include="..\..\src\pt2_amigafilters.h" />
     <ClInclude Include="..\..\src\pt2_askbox.h" />
     <ClInclude Include="..\..\src\pt2_audio.h" />
     <ClInclude Include="..\..\src\pt2_blep.h" />
@@ -250,6 +249,7 @@
     <ClInclude Include="..\..\src\pt2_helpers.h" />
     <ClInclude Include="..\..\src\pt2_hpc.h" />
     <ClInclude Include="..\..\src\pt2_keyboard.h" />
+    <ClInclude Include="..\..\src\pt2_ledfilter.h" />
     <ClInclude Include="..\..\src\pt2_math.h" />
     <ClInclude Include="..\..\src\pt2_mod2wav.h" />
     <ClInclude Include="..\..\src\pt2_module_loader.h" />
@@ -268,7 +268,7 @@
     <ClInclude Include="..\..\src\pt2_sampling.h" />
     <ClInclude Include="..\..\src\pt2_scopes.h" />
     <ClInclude Include="..\..\src\pt2_structs.h" />
-    <ClInclude Include="..\..\src\pt2_sync.h" />
+    <ClInclude Include="..\..\src\pt2_visuals_sync.h" />
     <ClInclude Include="..\..\src\pt2_tables.h" />
     <ClInclude Include="..\..\src\pt2_textout.h" />
     <ClInclude Include="..\..\src\pt2_unicode.h" />
@@ -290,7 +290,6 @@
     <ClCompile Include="..\..\src\gfx\pt2_gfx_spectrum.c" />
     <ClCompile Include="..\..\src\gfx\pt2_gfx_tracker.c" />
     <ClCompile Include="..\..\src\gfx\pt2_gfx_vumeter.c" />
-    <ClCompile Include="..\..\src\pt2_amigafilters.c" />
     <ClCompile Include="..\..\src\pt2_askbox.c" />
     <ClCompile Include="..\..\src\pt2_audio.c" />
     <ClCompile Include="..\..\src\pt2_blep.c" />
@@ -302,6 +301,7 @@
     <ClCompile Include="..\..\src\pt2_helpers.c" />
     <ClCompile Include="..\..\src\pt2_hpc.c" />
     <ClCompile Include="..\..\src\pt2_keyboard.c" />
+    <ClCompile Include="..\..\src\pt2_ledfilter.c" />
     <ClCompile Include="..\..\src\pt2_main.c" />
     <ClCompile Include="..\..\src\pt2_math.c" />
     <ClCompile Include="..\..\src\pt2_mod2wav.c" />
@@ -321,7 +321,7 @@
     <ClCompile Include="..\..\src\pt2_sampling.c" />
     <ClCompile Include="..\..\src\pt2_scopes.c" />
     <ClCompile Include="..\..\src\pt2_structs.c" />
-    <ClCompile Include="..\..\src\pt2_sync.c" />
+    <ClCompile Include="..\..\src\pt2_visuals_sync.c" />
     <ClCompile Include="..\..\src\pt2_tables.c" />
     <ClCompile Include="..\..\src\pt2_textout.c" />
     <ClCompile Include="..\..\src\pt2_unicode.c" />
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
@@ -87,7 +87,7 @@
     <ClInclude Include="..\..\src\pt2_sampling.h">
       <Filter>headers</Filter>
     </ClInclude>
-    <ClInclude Include="..\..\src\pt2_sync.h">
+    <ClInclude Include="..\..\src\pt2_visuals_sync.h">
       <Filter>headers</Filter>
     </ClInclude>
     <ClInclude Include="..\..\src\pt2_rcfilter.h">
@@ -117,7 +117,7 @@
     <ClInclude Include="..\..\src\pt2_paula.h">
       <Filter>headers</Filter>
     </ClInclude>
-    <ClInclude Include="..\..\src\pt2_amigafilters.h">
+    <ClInclude Include="..\..\src\pt2_ledfilter.h">
       <Filter>headers</Filter>
     </ClInclude>
   </ItemGroup>
@@ -191,7 +191,7 @@
     <ClCompile Include="..\..\src\pt2_module_saver.c" />
     <ClCompile Include="..\..\src\pt2_replayer.c" />
     <ClCompile Include="..\..\src\pt2_sampling.c" />
-    <ClCompile Include="..\..\src\pt2_sync.c" />
+    <ClCompile Include="..\..\src\pt2_visuals_sync.c" />
     <ClCompile Include="..\..\src\pt2_rcfilter.c" />
     <ClCompile Include="..\..\src\pt2_chordmaker.c" />
     <ClCompile Include="..\..\src\pt2_downsample2x.c" />
@@ -200,7 +200,7 @@
     <ClCompile Include="..\..\src\pt2_xpk.c" />
     <ClCompile Include="..\..\src\pt2_askbox.c" />
     <ClCompile Include="..\..\src\pt2_paula.c" />
-    <ClCompile Include="..\..\src\pt2_amigafilters.c" />
+    <ClCompile Include="..\..\src\pt2_ledfilter.c" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\src\pt2-clone.rc" />