shithub: pt2-clone

Download patch

ref: fc55f5c1b127721c26809a0732ba1b6893e9c6cc
parent: 71666cfc9fdb10b223032a30a4c0862af49c1e33
author: Olav Sørensen <olav.sorensen@live.no>
date: Sun Jun 7 14:22:22 EDT 2020

Pushed v1.18 code

- Bugfix: Pasting copied sample data to an empty sample didn't work! Also fixed
  an issue with the sample length not changing when pasting data.
- Bugfix: Scopes would never stop showing looped samples after channel muting
- Bugfix: The sampling position line in the sampler screen would not behave
  correctly during "sample swapping".
- Bugfix: Left/right/up/down cursor keys should not be repeated in keyrepeat
  mode (toggled with Caps Lock).
- Windows bugfix: The Windows key could get stuck if you held down ALT while
  pressing it.
- Windows bugfix: Num Lock now works ike it should when the program is in focus
  (yes, this ruins drumpad mode, but it never worked right to begin with).
- The "real VU-meter" bars are now sinking a bit faster
- Code cleanup

--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,5 @@
 *.opendb
 *.cod
 vs2019_project/pt2-clone/Debug/pt2-clone.vcxproj.FileListAbsolute.txt
+*.db-wal
+*.db-shm
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -337,12 +337,6 @@
 
 			paulaSetData(i, ch->n_start + s->loopStart);
 			paulaSetLength(i, s->loopLength >> 1);
-
-			if (!editor.songPlaying)
-			{
-				scopeSetData(i, ch->n_start + s->loopStart);
-				scopeSetLength(i, s->loopLength >> 1);
-			}
 		}
 	}
 }
@@ -388,15 +382,16 @@
 	const double dOldPanR = paula[ch].dPanR;
 
 	memset(&paula[ch], 0, sizeof (paulaVoice_t));
-	stopScope(ch);
+	memset(&blep[ch], 0, sizeof (blep_t));
+	memset(&blepVol[ch], 0, sizeof (blep_t));
 
-	// store old pans
+	stopScope(ch); // it should be safe to clear the scope now
+	memset(&scope[ch], 0, sizeof (scope_t));
+
+	// restore old pans
 	paula[ch].dPanL = dOldPanL;
 	paula[ch].dPanR = dOldPanR;
 
-	memset(&blep[ch], 0, sizeof (blep_t));
-	memset(&blepVol[ch], 0, sizeof (blep_t));
-
 	if (audioWasntLocked)
 		unlockAudio();
 }
@@ -432,21 +427,24 @@
 
 void paulaSetPeriod(int32_t ch, uint16_t period)
 {
-	int32_t realPeriod;
 	double dPeriodToDeltaDiv;
-	paulaVoice_t *v;
+	paulaVoice_t *v = &paula[ch];
 
-	v = &paula[ch];
-
-	v->syncPeriod = period; // used for pt2_sync.c
-	v->syncFlags |= UPDATE_PERIOD; // used for pt2_sync.c
-
-	if (period == 0)
+	int32_t realPeriod = period;
+	if (realPeriod == 0)
 		realPeriod = 1+65535; // confirmed behavior on real Amiga
-	else if (period < 113)
+	else if (realPeriod < 113)
 		realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
+
+	if (editor.songPlaying)
+	{
+		v->syncPeriod = realPeriod;
+		v->syncFlags |= SET_SCOPE_PERIOD;
+	}
 	else
-		realPeriod = period;
+	{
+		scopeSetPeriod(ch, realPeriod);
+	}
 
 	// if the new period was the same as the previous period, use cached deltas
 	if (realPeriod != oldPeriod)
@@ -478,48 +476,72 @@
 
 void paulaSetVolume(int32_t ch, uint16_t vol)
 {
-	paulaVoice_t *v;
+	paulaVoice_t *v = &paula[ch];
 
-	v = &paula[ch];
+	int32_t realVol = vol;
 
-	vol &= 127; // confirmed behavior on real Amiga
+	// confirmed behavior on real Amiga
+	realVol &= 127;
+	if (realVol > 64)
+		realVol = 64;
 
-	if (vol > 64)
-		vol = 64; // confirmed behavior on real Amiga
+	v->dVolume = realVol * (1.0 / 64.0);
 
-	v->dVolume = vol * (1.0 / 64.0);
-
-	v->syncVolume = (int8_t)vol; // used for pt2_sync.c
-	v->syncFlags |= UPDATE_VOLUME; // used for pt2_sync.c
+	if (editor.songPlaying)
+	{
+		v->syncVolume = (uint8_t)realVol;
+		v->syncFlags |= SET_SCOPE_VOLUME;
+	}
+	else
+	{
+		scope[ch].volume = (uint8_t)realVol;
+	}
 }
 
 void paulaSetLength(int32_t ch, uint16_t len)
 {
-	if (len == 0)
+	int32_t realLength = len;
+	if (realLength == 0)
 	{
-		len = 65535;
-		/* Confirmed behavior on real Amiga (also needed for safety).
-		** And yes, we have room for this, it will never overflow!
+		realLength = 1+65535;
+		/* Confirmed behavior on real Amiga. We have room for this
+		** even at the last sample slot, so it will never overflow!
+		**
+		** PS: I don't really know if it's possible for ProTracker to
+		** set a Paula length of 0, but I fully support this Paula
+		** behavior just in case.
 		*/
 	}
 
-	paula[ch].newLength = len << 1; // our mixer works with bytes, not words
-	paula[ch].syncFlags |= UPDATE_LENGTH; // for pt2_sync.c
+	realLength <<= 1; // we work with bytes, not words
+
+	paula[ch].newLength = realLength;
+	if (editor.songPlaying)
+		paula[ch].syncFlags |= SET_SCOPE_LENGTH;
+	else
+		scope[ch].newLength = realLength;
 }
 
 void paulaSetData(int32_t ch, const int8_t *src)
 {
-	// set voice data
 	if (src == NULL)
-		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
 
 	paula[ch].newData = src;
-	paula[ch].syncFlags |= UPDATE_DATA; // for pt2_sync.c
+	if (editor.songPlaying)
+		paula[ch].syncFlags |= SET_SCOPE_DATA;
+	else
+		scope[ch].newData = src;
 }
 
 void paulaStopDMA(int32_t ch)
 {
 	paula[ch].active = false;
+
+	if (editor.songPlaying)
+		paula[ch].syncFlags |= STOP_SCOPE;
+	else
+		scope[ch].active = false;
 }
 
 void paulaStartDMA(int32_t ch)
@@ -534,9 +556,9 @@
 
 	dat = v->newData;
 	if (dat == NULL)
-		dat = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+		dat = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
 
-	length = v->newLength;
+	length = v->newLength; // in bytes, not words
 	if (length < 2)
 		length = 2; // for safety
 
@@ -546,10 +568,18 @@
 	v->length = length;
 	v->active = true;
 
-	// for pt2_sync.c
-	v->syncTriggerData = dat;
-	v->syncTriggerLength = (uint16_t)(length >> 1);
-	v->syncFlags |= TRIGGER_SAMPLE;
+	if (editor.songPlaying)
+	{
+		v->syncTriggerData = dat;
+		v->syncTriggerLength = length;
+		v->syncFlags |= TRIGGER_SCOPE;
+	}
+	else
+	{
+		scope[ch].newData = dat;
+		scope[ch].newLength = length;
+		scopeTrigger(ch);
+	}
 }
 
 void toggleA500Filters(void)
@@ -1015,7 +1045,7 @@
 		s->triggerData = v->syncTriggerData;
 		s->triggerLength = v->syncTriggerLength;
 		s->newData = v->newData;
-		s->newLength = (uint16_t)(v->newLength >> 1);
+		s->newLength = v->newLength;
 		s->vuVolume = c->syncVuVolume;
 		s->analyzerVolume = c->syncAnalyzerVolume;
 		s->analyzerPeriod = c->syncAnalyzerPeriod;
@@ -1139,7 +1169,7 @@
 
 	if (audio.outputRate >= 96000) // cutoff is too high for 44.1kHz/48kHz
 	{
-		// A1200 one-pole 6db/oct static RC low-pass filter:
+		// A1200 1-pole (6db/oct) static RC low-pass filter:
 		R = 680.0;  // R321 (680 ohm resistor)
 		C = 6.8e-9; // C321 (6800pf capacitor)
 		fc = 1.0 / (2.0 * M_PI * R * C);
@@ -1146,7 +1176,7 @@
 		calcRCFilterCoeffs(audio.outputRate, fc, &filterLoA1200);
 	}
 
-	// A500 one-pole 6db/oct static RC low-pass filter:
+	// A500 1-pole (6db/oct) static RC low-pass filter:
 	R = 360.0; // R321 (360 ohm resistor)
 	C = 1e-7;  // C321 (0.1uF capacitor)
 	fc = 1.0 / (2.0 * M_PI * R * C);
@@ -1161,7 +1191,7 @@
 	fb = 0.125; // Fb = 0.125 : Q ~= 1/sqrt(2)
 	calcLEDFilterCoeffs(audio.outputRate, fc, fb, &filterLED);
 
-	// A1200 one-pole 6db/oct static RC high-pass filter:
+	// A1200 1-pole (6db/oct) static RC high-pass filter:
 	R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
 	C = 2.2e-5; // C334 (22uF capacitor)
 	fc = 1.0 / (2.0 * M_PI * R * C);
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -40,9 +40,9 @@
 
 	// used for pt2_sync.c
 	uint8_t syncFlags;
-	int8_t syncVolume;
-	uint16_t syncPeriod;
-	uint16_t syncTriggerLength;
+	uint8_t syncVolume;
+	int32_t syncPeriod;
+	int32_t syncTriggerLength;
 	const int8_t *syncTriggerData;
 } paulaVoice_t;
 
--- a/src/pt2_diskop.c
+++ b/src/pt2_diskop.c
@@ -900,7 +900,7 @@
 					modFree();
 
 					song = newSong;
-					setupNewMod();
+					setupLoadedMod();
 					song->loaded = true;
 
 					statusAllRight();
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -929,34 +929,14 @@
 	paulaSetData(ch, n_start);
 	paulaSetLength(ch, n_length);
 
-	if (!editor.songPlaying)
-	{
-		scopeSetVolume(ch, vol);
-		scopeSetPeriod(ch, period);
-		scopeSetData(ch, n_start);
-		scopeSetLength(ch, n_length);
-	}
-
 	if (!editor.muted[ch])
-	{
 		paulaStartDMA(ch);
-		if (!editor.songPlaying)
-			scopeTrigger(ch);
-	}
 	else
-	{
 		paulaStopDMA(ch);
-	}
 
 	// these take effect after the current DMA cycle is done
 	paulaSetData(ch, NULL);
 	paulaSetLength(ch, 1);
-
-	if (!editor.songPlaying)
-	{
-		scopeSetData(ch, NULL);
-		scopeSetLength(ch, 1);
-	}
 }
 
 void jamAndPlaceSample(SDL_Scancode scancode, bool normalMode)
@@ -1006,34 +986,14 @@
 			paulaSetData(ch, chn->n_start);
 			paulaSetLength(ch, chn->n_length);
 
-			if (!editor.songPlaying)
-			{
-				scopeSetVolume(ch, chn->n_volume);
-				scopeSetPeriod(ch, chn->n_period);
-				scopeSetData(ch, chn->n_start);
-				scopeSetLength(ch, chn->n_length);
-			}
-
 			if (!editor.muted[ch])
-			{
 				paulaStartDMA(ch);
-				if (!editor.songPlaying)
-					scopeTrigger(ch);
-			}
 			else
-			{
 				paulaStopDMA(ch);
-			}
 
 			// these take effect after the current DMA cycle is done
 			paulaSetData(ch, chn->n_loopstart);
 			paulaSetLength(ch, chn->n_replen);
-
-			if (!editor.songPlaying)
-			{
-				scopeSetData(ch, chn->n_loopstart);
-				scopeSetLength(ch, chn->n_replen);
-			}
 		}
 
 		// normalMode = normal keys, or else keypad keys (in jam mode)
--- 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.17"
+#define PROG_VER_STR "1.18"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
@@ -36,13 +36,13 @@
 */
 #define VBLANK_HZ 60
 
-/* Scopes are clocked at 64Hz instead of 60Hz to prevent +/- interference
-** from monitors not being exactly 60Hz (and unstable non-vsync mode).
-** Sadly the scopes might midly flicker from this.
+/* Scopes are clocked at 64Hz instead of 60Hz to prevent the small +/- Hz 
+** interference from monitors not being exactly 60Hz (and unstable non-vsync mode).
+** Sadly, the scopes might midly flicker from this in some cases.
 */
 #define SCOPE_HZ 64
 
-#define AMIGA_PAL_VBLANK_HZ 50
+#define AMIGA_PAL_VBLANK_HZ 49.9204092835
 
 #define FONT_BMP_WIDTH 
 #define FONT_CHAR_W 8 // actual data length is 7, includes right spacing (1px column)
@@ -54,8 +54,10 @@
 #define MAX_PATTERNS 100
 
 #define MAX_SAMPLE_LEN 65534
-#define RESERVED_SAMPLE_OFFSET (31 * MAX_SAMPLE_LEN)
 
+// for NULL pointers
+#define RESERVED_SAMPLE_OFFSET ((31+1) * MAX_SAMPLE_LEN)
+
 #define AMIGA_VOICES 4
 #define SCOPE_WIDTH 40
 #define SCOPE_HEIGHT 33
@@ -65,6 +67,7 @@
 
 #define POSED_LIST_SIZE 12
 
+
 // main crystal oscillator
 #define AMIGA_PAL_XTAL_HZ 28375160
 
@@ -71,6 +74,9 @@
 #define PAULA_PAL_CLK (AMIGA_PAL_XTAL_HZ / 8)
 #define CIA_PAL_CLK (AMIGA_PAL_XTAL_HZ / 40)
 
+#define PAL_PAULA_MIN_SAFE_PERIOD 124
+#define PAL_PAULA_MAX_SAFE_HZ (PAULA_PAL_CLK / (double)PAL_PAULA_MIN_SAFE_PERIOD)
+
 #define FILTERS_BASE_FREQ (PAULA_PAL_CLK / 214.0)
 
 #define KEYB_REPEAT_DELAY 17
@@ -206,6 +212,8 @@
 	TEXT_EDIT_DECIMAL = 1,
 	TEXT_EDIT_HEX = 2
 };
+
+int8_t *allocMemForAllSamples(void); // pt2_replayer.c
 
 void restartSong(void);
 void resetSong(void);
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -73,67 +73,42 @@
 }
 
 #if defined _WIN32 && !defined _DEBUG
-/* For taking control over windows key if the program has focus.
+/* For taking control over the windows key if the program has focus.
 ** Warning: Don't do this in debug mode, it will completely ruin the keyboard input
 ** latency (in the OS in general) when the debugger is breaking.
 */
 LRESULT CALLBACK lowLevelKeyboardProc(int32_t nCode, WPARAM wParam, LPARAM lParam)
 {
-	SDL_Event inputEvent;
 	SDL_Window *window = video.window;
 
-	if (window == NULL || nCode < 0 || nCode != HC_ACTION) // do not process message
-		return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
-
-	bool bEatKeystroke = false;
-
-	KBDLLHOOKSTRUCT *p = (KBDLLHOOKSTRUCT *)lParam;
-	switch (wParam)
+	if (nCode == HC_ACTION && window != NULL)
 	{
-		case WM_KEYUP:
-		case WM_KEYDOWN:
+		switch (wParam)
 		{
-			const bool windowHasFocus = SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS;
-
-			bEatKeystroke = windowHasFocus && (p->vkCode == VK_LWIN || p->vkCode == VK_NUMLOCK);
-			if (!bEatKeystroke)
-				break;
-
-			memset(&inputEvent, 0, sizeof (SDL_Event));
-
-			const bool keyDown = (wParam == WM_KEYDOWN);
-			if (keyDown)
+			case WM_KEYUP:
+			case WM_KEYDOWN:
+			case WM_SYSKEYUP: // needed to prevent stuck Windows key if used with ALT
 			{
-				if (windowsKeyIsDown)
-					break; // Windows-key is already down (XXX: Do we need this check?)
+				const bool windowHasFocus = SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS;
+				if (!windowHasFocus)
+				{
+					windowsKeyIsDown = false;
+					break;
+				}
 
-				inputEvent.type = SDL_KEYDOWN;
-				inputEvent.key.type = SDL_KEYDOWN;
-				inputEvent.key.state = SDL_PRESSED;
-				windowsKeyIsDown = true;
+				if (((KBDLLHOOKSTRUCT *)lParam)->vkCode != VK_LWIN)
+					break;
+
+				windowsKeyIsDown = (wParam == WM_KEYDOWN);
+				return 1; // eat keystroke
 			}
-			else
-			{
-				inputEvent.type = SDL_KEYUP;
-				inputEvent.key.type = SDL_KEYUP;
-				inputEvent.key.state = SDL_RELEASED;
-				windowsKeyIsDown = false;
-			}
+			break;
 
-			inputEvent.key.keysym.sym = SDLK_LGUI;
-			inputEvent.key.keysym.scancode = SDL_SCANCODE_LGUI;
-			inputEvent.key.keysym.mod = SDL_GetModState();
-			inputEvent.key.timestamp = SDL_GetTicks();
-			inputEvent.key.windowID = SDL_GetWindowID(window);
-
-			SDL_PushEvent(&inputEvent);
+			default: break;
 		}
-		break;
-
-		default: break;
 	}
 
-	return bEatKeystroke ? 1 : CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
+	return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
 }
 #endif
 
@@ -366,12 +341,12 @@
 		return;
 	}
 
-	// kludge to allow certain repeat-keys to use custom repeat/delay values
-	if (editor.repeatKeyFlag && keyb.repeatKey && scancode == keyb.lastRepKey &&
-		(keyb.leftAltPressed || keyb.leftAmigaPressed || keyb.leftCtrlPressed))
-	{
+	// these keys should not allow to be repeated in keyrepeat mode (caps lock)
+	const bool illegalKeys = keyb.leftAltPressed || keyb.leftAmigaPressed || keyb.leftCtrlPressed
+		|| scancode == SDL_SCANCODE_LEFT || scancode == SDL_SCANCODE_RIGHT
+		|| scancode == SDL_SCANCODE_UP   || scancode == SDL_SCANCODE_DOWN;
+	if (editor.repeatKeyFlag && keyb.repeatKey && scancode == keyb.lastRepKey && illegalKeys)
 		return;
-	}
 
 	if (scancode == SDL_SCANCODE_KP_PLUS)
 		keyb.keypadEnterPressed = true;
@@ -428,6 +403,8 @@
 		}
 	}
 
+	// XXX: This really needs some refactoring, it's messy and not logical
+
 	if (!handleGeneralModes(keycode, scancode)) return;
 	if (!handleTextEditMode(scancode)) return;
 	if (ui.samplerVolBoxShown || ui.samplingBoxShown) return;
@@ -3453,9 +3430,8 @@
 		break;
 	}
 
-	// repeat keys at 50Hz rate
-
-	const uint64_t keyRepeatDelta = ((uint64_t)AMIGA_PAL_VBLANK_HZ << 32) / VBLANK_HZ;
+	// repeat keys at 49.92Hz (Amiga PAL) rate
+	const uint64_t keyRepeatDelta = (uint64_t)(((UINT32_MAX + 1.0) * (AMIGA_PAL_VBLANK_HZ / (double)VBLANK_HZ)) + 0.5);
 
 	keyb.repeatFrac += keyRepeatDelta; // 32.32 fixed-point counter
 	if (keyb.repeatFrac > 0xFFFFFFFF)
--- a/src/pt2_main.c
+++ b/src/pt2_main.c
@@ -267,7 +267,7 @@
 
 	setupSprites();
 
-	song = createNewMod();
+	song = createEmptyMod();
 	if (song == NULL)
 	{
 		cleanUp();
@@ -327,6 +327,7 @@
 	setupWaitVBL();
 	while (editor.programRunning)
 	{
+		sinkVisualizerBars();
 		updateChannelSyncBuffer();
 		readMouseXY();
 		readKeyModifiers(); // set/clear CTRL/ALT/SHIFT/AMIGA key states
@@ -342,7 +343,6 @@
 
 		renderFrame();
 		flipFrame();
-		sinkVisualizerBars();
 	}
 
 	cleanUp();
--- a/src/pt2_module_loader.c
+++ b/src/pt2_module_loader.c
@@ -585,8 +585,7 @@
 		}
 	}
 
-	// allocate sample data (+2 sample slots for overflow safety (Paula and scopes))
-	newMod->sampleData = (int8_t *)calloc(MOD_SAMPLES + 2, MAX_SAMPLE_LEN);
+	newMod->sampleData = allocMemForAllSamples();
 	if (newMod->sampleData == NULL)
 	{
 		statusOutOfMemory();
@@ -905,7 +904,7 @@
 	return true;
 }
 
-void setupNewMod(void)
+void setupLoadedMod(void)
 {
 	int8_t i;
 
@@ -988,7 +987,7 @@
 		song->loaded = false;
 		modFree();
 		song = newSong;
-		setupNewMod();
+		setupLoadedMod();
 		song->loaded = true;
 	}
 	else
@@ -1120,7 +1119,7 @@
 			modFree();
 
 			song = newSong;
-			setupNewMod();
+			setupLoadedMod();
 			song->loaded = true;
 
 			statusAllRight();
@@ -1183,7 +1182,7 @@
 	loadDroppedFile(oldFullPath, oldFullPathLen, oldAutoPlay, false);
 }
 
-module_t *createNewMod(void)
+module_t *createEmptyMod(void)
 {
 	uint8_t i;
 	module_t *newMod;
@@ -1199,23 +1198,23 @@
 			goto oom;
 	}
 
-	// +2 sample slots for overflow safety (Paula and scopes)
-	newMod->sampleData = (int8_t *)calloc(MOD_SAMPLES + 2, MAX_SAMPLE_LEN);
+	newMod->sampleData = allocMemForAllSamples();
 	if (newMod->sampleData == NULL)
 		goto oom;
 
 	newMod->header.numOrders = 1;
 
-	for (i = 0; i < MOD_SAMPLES; i++)
+	moduleSample_t *s = newMod->samples;
+	for (i = 0; i < MOD_SAMPLES; i++, s++)
 	{
-		newMod->samples[i].offset = MAX_SAMPLE_LEN * i;
-		newMod->samples[i].loopLength = 2;
+		s->offset = MAX_SAMPLE_LEN * i;
+		s->loopLength = 2;
 
 		// setup GUI text pointers
-		newMod->samples[i].volumeDisp = &newMod->samples[i].volume;
-		newMod->samples[i].lengthDisp = &newMod->samples[i].length;
-		newMod->samples[i].loopStartDisp = &newMod->samples[i].loopStart;
-		newMod->samples[i].loopLengthDisp = &newMod->samples[i].loopLength;
+		s->volumeDisp = &s->volume;
+		s->lengthDisp = &s->length;
+		s->loopStartDisp = &s->loopStart;
+		s->loopLengthDisp = &s->loopLength;
 	}
 
 	for (i = 0; i < AMIGA_VOICES; i++)
--- a/src/pt2_module_loader.h
+++ b/src/pt2_module_loader.h
@@ -10,6 +10,6 @@
 void loadModFromArg(char *arg);
 void loadDroppedFile(char *fullPath, uint32_t fullPathLen, bool autoPlay, bool songModifiedCheck);
 void loadDroppedFile2(void);
-module_t *createNewMod(void);
+module_t *createEmptyMod(void);
 module_t *modLoad(UNICHAR *fileName);
-void setupNewMod(void);
+void setupLoadedMod(void);
--- a/src/pt2_pat2smp.h
+++ b/src/pt2_pat2smp.h
@@ -2,7 +2,7 @@
 
 #include "pt2_header.h"
 
-#define PAT2SMP_HI_PERIOD 124 /* A-3 finetune +4, 28604.99Hz */
+#define PAT2SMP_HI_PERIOD 124 /* A-3 finetune +4, 28603.99Hz */
 #define PAT2SMP_LO_PERIOD 160 /* F-3 finetune +1, 22168.09Hz */
 
 #define PAT2SMP_HI_FREQ (PAULA_PAL_CLK / (double)PAT2SMP_HI_PERIOD)
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -35,6 +35,25 @@
 	0x10, 0x13, 0x16, 0x1A, 0x20, 0x2B, 0x40, 0x80
 };
 
+int8_t *allocMemForAllSamples(void)
+{
+	/* Allocate memoru for all sample data blocks.
+	**
+	** We need three extra sample slots:
+	** The 1st is extra safety padding since setting a Paula length of 0
+	** results in reading (1+65535)*2 bytes. The 2nd and 3rd (64K*2 = 1x 128K)
+	** are reserved for NULL pointers. This is needed for emulating a PT quirk.
+	**
+	** We have a padding of 4 bytes at the end for length=0 quirk safety.
+	**
+	** PS: I don't really know if it's possible for ProTracker to set a Paula
+	** length of 0, but I fully support this Paula behavior just in case.
+	*/
+	const size_t allocLen = ((MOD_SAMPLES + 3) * MAX_SAMPLE_LEN) + 4;
+
+	return (int8_t *)calloc(1, allocLen);
+}
+
 void modSetSpeed(uint8_t speed)
 {
 	song->speed = speed;
@@ -105,11 +124,15 @@
 	if (vol > 64)
 		vol = 64;
 
-	ch->syncVuVolume = vol;
-	ch->syncFlags |= UPDATE_VUMETER;
-
 	if (!editor.songPlaying)
+	{
 		editor.vuMeterVolumes[ch->n_chanindex] = vuMeterHeights[vol];
+	}
+	else
+	{
+		ch->syncVuVolume = vol;
+		ch->syncFlags |= UPDATE_VUMETER;
+	}
 }
 
 static void updateFunk(moduleChannel_t *ch)
@@ -853,7 +876,7 @@
 
 		// non-PT2 quirk
 		if (ch->n_length == 0)
-			ch->n_loopstart = ch->n_wavestart = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+			ch->n_loopstart = ch->n_wavestart = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
 	}
 
 	if ((ch->n_note & 0xFFF) > 0)
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -1501,12 +1501,6 @@
 		paulaSetData(ch, tuneToneData);
 		paulaSetLength(ch, sizeof (tuneToneData) / 2);
 		paulaStartDMA(ch);
-
-		scopeSetPeriod(ch, periodTable[editor.tuningNote]);
-		scopeSetVolume(ch, 64);
-		scopeSetData(ch, tuneToneData);
-		scopeSetLength(ch, sizeof (tuneToneData) / 2);
-		scopeTrigger(ch);
 	}
 	else
 	{
@@ -1909,7 +1903,7 @@
 		readPos += markStart;
 	}
 
-	// copy buffer
+	// copy actual buffer
 	memcpy(&tmpBuf[readPos], sampler.copyBuf, sampler.copyBufSize);
 
 	// copy end part
@@ -1925,7 +1919,7 @@
 	if (newLength > MAX_SAMPLE_LEN)
 		newLength = MAX_SAMPLE_LEN;
 
-	sampler.samLength = (uint16_t)newLength;
+	sampler.samLength = s->length = (uint16_t)newLength;
 
 	if (s->loopLength > 2) // loop enabled?
 	{
@@ -1974,6 +1968,8 @@
 	}
 
 	memcpy(&song->sampleData[s->offset], tmpBuf, s->length);
+
+	// clear data after sample's length (if present)
 	if (s->length < MAX_SAMPLE_LEN)
 		memset(&song->sampleData[s->offset+s->length], 0, MAX_SAMPLE_LEN - s->length);
 
@@ -2036,24 +2032,10 @@
 	paulaSetData(chn, ch->n_start);
 	paulaSetLength(chn, ch->n_length);
 
-	if (!editor.songPlaying)
-	{
-		scopeSetVolume(chn, ch->n_volume);
-		scopeSetPeriod(chn, ch->n_period);
-		scopeSetData(chn, ch->n_start);
-		scopeSetLength(chn, ch->n_length);
-	}
-
 	if (!editor.muted[chn])
-	{
 		paulaStartDMA(chn);
-		if (!editor.songPlaying)
-			scopeTrigger(chn);
-	}
 	else
-	{
 		paulaStopDMA(chn);
-	}
 
 	// these take effect after the current DMA cycle is done
 	if (playWaveformFlag)
@@ -2060,23 +2042,11 @@
 	{
 		paulaSetData(chn, ch->n_loopstart);
 		paulaSetLength(chn, ch->n_replen);
-
-		if (!editor.songPlaying)
-		{
-			scopeSetData(chn, ch->n_loopstart);
-			scopeSetLength(chn, ch->n_replen);
-		}
 	}
 	else
 	{
 		paulaSetData(chn, NULL);
 		paulaSetLength(chn, 1);
-
-		if (!editor.songPlaying)
-		{
-			scopeSetData(chn, NULL);
-			scopeSetLength(chn, 1);
-		}
 	}
 
 	updateSpectrumAnalyzer(ch->n_volume, ch->n_period);
@@ -2854,24 +2824,18 @@
 
 void drawSamplerLine(void)
 {
-	uint8_t i;
-	int32_t pos;
-
 	hideSprite(SPRITE_SAMPLING_POS_LINE);
 	if (!ui.samplerScreenShown || ui.samplerVolBoxShown || ui.samplerFiltersBoxShown)
 		return;
 
-	for (i = 0; i < AMIGA_VOICES; i++)
+	for (int32_t ch = 0; ch < AMIGA_VOICES; ch++)
 	{
-		if (song->channels[i].n_samplenum == editor.currSample && !editor.muted[i])
+		int32_t pos = getSampleReadPos(ch);
+		if (pos >= 0)
 		{
-			pos = getSampleReadPos(i, editor.currSample);
-			if (pos >= 0)
-			{
-				pos = 3 + smpPos2Scr(pos);
-				if (pos >= 3 && pos <= 316)
-					setSpritePos(SPRITE_SAMPLING_POS_LINE, pos, 138);
-			}
+			pos = 3 + smpPos2Scr(pos);
+			if (pos >= 3 && pos <= 316)
+				setSpritePos(SPRITE_SAMPLING_POS_LINE, pos, 138);
 		}
 	}
 }
--- a/src/pt2_sampling.c
+++ b/src/pt2_sampling.c
@@ -312,7 +312,9 @@
 {
 	char str[16];
 	sprintf(str, "%05dHZ", roundedOutputFrequency);
-	textOutBg(262, 208, str, roundedOutputFrequency <= 28604 ? video.palette[PAL_GENTXT] : 0x8C0F0F, video.palette[PAL_GENBKG]);
+
+	const int32_t maxSafeFrequency = (int32_t)(PAL_PAULA_MAX_SAFE_HZ + 0.5); // rounded
+	textOutBg(262, 208, str, roundedOutputFrequency <= maxSafeFrequency ? video.palette[PAL_GENTXT] : 0x8C0F0F, video.palette[PAL_GENBKG]);
 }
 
 static void drawSamplingModeCross(void)
--- a/src/pt2_scopes.c
+++ b/src/pt2_scopes.c
@@ -35,89 +35,81 @@
 	oldPeriod = -1;
 }
 
-int32_t getSampleReadPos(int32_t ch, uint8_t smpNum)
+// this is quite hackish, but fixes sample swapping issues
+static int32_t getSampleSlotFromReadAddress(const int8_t *sampleReadAddress)
 {
-	const int8_t *data;
-	volatile bool active;
-	volatile int32_t pos;
-	volatile scope_t *sc;
+	assert(song != NULL);
+	const int8_t *sampleData = song->sampleData;
+	const int32_t sampleSlotSize = MAX_SAMPLE_LEN;
 
-	moduleSample_t *s;
-	
-	sc = &scope[ch];
-
-	// cache some stuff
-	active = sc->active;
-	data = sc->data;
-	pos = sc->pos;
-
-	if (!active || data == NULL || pos <= 2) // pos 0..2 = sample loop area for non-looping samples
+	if (sampleData == NULL) // shouldn't really happen, but just in case
 		return -1;
 
-	s = &song->samples[smpNum];
+	int32_t sampleSlot = 30; // start at last slot
 
-	// hackish way of getting real scope/sampling position
-	pos = (int32_t)(&data[pos] - &song->sampleData[s->offset]);
-	if (pos < 0 || pos >= s->length)
-		return -1;
+	const int8_t *sampleBaseAddress = &sampleData[sampleSlot * sampleSlotSize];
+	if (sampleReadAddress == NULL || sampleReadAddress >= sampleBaseAddress+sampleSlotSize)
+		return -1; // out of range
 
-	return pos;
-}
+	for (; sampleSlot >= 0; sampleSlot--)
+	{
+		if (sampleReadAddress >= sampleBaseAddress)
+			break;
 
-void scopeSetVolume(int32_t ch, uint16_t vol)
-{
-	vol &= 127; // confirmed behavior on real Amiga
+		sampleBaseAddress -= sampleSlotSize;
+	}
 
-	if (vol > 64)
-		vol = 64; // confirmed behavior on real Amiga
-
-	scope[ch].volume = (uint8_t)vol;
+	return sampleSlot; // 0..30, or -1 if out of range
 }
 
-void scopeSetPeriod(int32_t ch, uint16_t period)
+int32_t getSampleReadPos(int32_t ch) // used for the sampler screen
 {
-	int32_t realPeriod;
+	// cache some stuff
+	const scope_t *sc = &scope[ch];
+	const bool active = sc->active;
+	const int8_t *data = sc->data;
+	const int32_t pos = sc->pos;
+	const int32_t len = sc->length;
 
-	if (period == 0)
-		realPeriod = 1+65535; // confirmed behavior on real Amiga
-	else if (period < 113)
-		realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
-	else
-		realPeriod = period;
+	if (song == NULL || !active || data == NULL)
+		return -1;
 
-	// if the new period was the same as the previous period, use cached deltas
-	if (realPeriod != oldPeriod)
-	{
-		oldPeriod = realPeriod;
+	/* Because the scopes work like the Paula emulation, we have a DATA
+	** and LENGTH variable, which are not static. This means that we have
+	** to get creative to get the absolute sampling position.
+	*/
 
-		// this period is not cached, calculate scope delta
+	int32_t sample = getSampleSlotFromReadAddress(data);
+	if (sample != editor.currSample)
+		return -1; // sample is not the one we're seeing in the sampler screen
 
-		const float fPeriodToScopeDeltaDiv = PAULA_PAL_CLK / (float)SCOPE_HZ;
-		fOldScopeDelta = fPeriodToScopeDeltaDiv / realPeriod;
-	}
+	const moduleSample_t *s = &song->samples[sample];
+	const int8_t *sampleReadAddress = &data[pos];
+	const int8_t *sampleBaseAddress = &song->sampleData[s->offset];
+	const int32_t realPos = (int32_t)(sampleReadAddress - sampleBaseAddress);
 
-	scope[ch].fDelta = fOldScopeDelta;
-}
+	// return -1 if sample has no loop and read length is 2 (playing sample "loop" area)
+	const bool loopEnabled = (s->loopStart + s->loopLength) > 2;
+	if (!loopEnabled && len == 2)
+		return -1;
 
-void scopeSetData(int32_t ch, const int8_t *src)
-{
-	// set voice data
-	if (src == NULL)
-		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+	if (realPos < 0 || realPos >= s->length)
+		return -1;
 
-	scope[ch].newData = src;
+	return realPos;
 }
 
-void scopeSetLength(int32_t ch, uint16_t len)
+void scopeSetPeriod(int32_t ch, int32_t period)
 {
-	if (len == 0)
+	// if the new period was the same as the previous period, use cached deltas
+	if (period != oldPeriod)
 	{
-		len = 65535;
-		/* Confirmed behavior on real Amiga (also needed for safety).
-		** And yes, we have room for this, it will never overflow!
-		*/
+		oldPeriod = period;
+		const float fPeriodToScopeDeltaDiv = PAULA_PAL_CLK / (float)SCOPE_HZ;
+		fOldScopeDelta = fPeriodToScopeDeltaDiv / period;
 	}
-	scope[ch].newLength = len << 1;
+
+	scope[ch].fDelta = fOldScopeDelta;
 }
 
 void scopeTrigger(int32_t ch)
@@ -127,11 +119,11 @@
 
 	const int8_t *newData = tempState.newData;
 	if (newData == NULL)
-		newData = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+		newData = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
 
-	int32_t newLength = tempState.newLength;
+	int32_t newLength = tempState.newLength; // in bytes, not words
 	if (newLength < 2)
-		newLength = 2;
+		newLength = 2; // for safety
 
 	tempState.fPhase = 0.0f;
 	tempState.pos = 0;
@@ -180,7 +172,7 @@
 			tempState.pos -= tempState.length;
 
 			tempState.length = tempState.newLength;
-			if (tempState.length > 0)
+			if (tempState.pos >= tempState.length && tempState.length > 0)
 				tempState.pos %= tempState.length;
 
 			tempState.data = tempState.newData;
@@ -202,7 +194,7 @@
 	// sink VU-meters first
 	for (int32_t i = 0; i < AMIGA_VOICES; i++)
 	{
-		editor.realVuMeterVolumes[i] -= 3;
+		editor.realVuMeterVolumes[i] -= 4;
 		if (editor.realVuMeterVolumes[i] < 0)
 			editor.realVuMeterVolumes[i] = 0;
 	}
@@ -260,41 +252,34 @@
 
 void drawScopes(void)
 {
-	int16_t scopeData;
-	int32_t i, x;
-	uint32_t *scopeDrawPtr;
-	volatile scope_t *sc;
-	scope_t tmpScope;
+	volatile scope_t *sc = scope; // cache it
+	int32_t scopeX = 128;
 
-	scopeDrawPtr = &video.frameBuffer[(71 * SCREEN_W) + 128];
-
+	const uint32_t bgColor = video.palette[PAL_BACKGRD];
 	const uint32_t fgColor = video.palette[PAL_QADSCP];
 
-	sc = scope;
-
 	scopesDisplayingFlag = true;
-	for (i = 0; i < AMIGA_VOICES; i++, sc++)
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++)
 	{
-		tmpScope = *sc; // cache it
+		scope_t tmpScope = *sc; // cache it
 
 		// render scope
 		if (tmpScope.active && tmpScope.data != NULL && tmpScope.volume != 0 && tmpScope.length > 0)
 		{
-			// scope is active
-
 			sc->emptyScopeDrawn = false;
 
 			// fill scope background
-			fillRect(128 + (i * (SCOPE_WIDTH + 8)), 55, SCOPE_WIDTH, SCOPE_HEIGHT, video.palette[PAL_BACKGRD]);
+			fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
 
 			// render scope data
-
+			int16_t scopeData;
 			int32_t pos = tmpScope.pos;
 			int32_t length = tmpScope.length;
 			const int16_t volume = -(tmpScope.volume << 7);
 			const int8_t *data = tmpScope.data;
+			uint32_t *scopeDrawPtr = &video.frameBuffer[(71 * SCREEN_W) + scopeX];
 
-			for (x = 0; x < SCOPE_WIDTH; x++)
+			for (int32_t x = 0; x < SCOPE_WIDTH; x++)
 			{
 				scopeData = 0;
 				if (data != NULL)
@@ -302,37 +287,30 @@
 
 				scopeDrawPtr[(scopeData * SCREEN_W) + x] = fgColor;
 
-				pos++;
-				if (pos >= length)
+				if (++pos >= length)
 				{
 					pos = 0;
 
-					/* Read cycle done, temporarily update the display data/length variables
-					** before the scope thread does it.
-					*/
+					// read cycle done, update the drawing data/length variables
 					length = tmpScope.newLength;
 					data = tmpScope.newData;
 				}
 			}
 		}
-		else
+		else if (!sc->emptyScopeDrawn)
 		{
-			// scope is inactive, draw empty scope once until it gets active again
+			// scope is inactive (or vol=0), draw empty scope once until it gets active again
 
-			if (!sc->emptyScopeDrawn)
-			{
-				// fill scope background
-				fillRect(128 + (i * (SCOPE_WIDTH + 8)), 55, SCOPE_WIDTH, SCOPE_HEIGHT, video.palette[PAL_BACKGRD]);
+			// fill scope background
+			fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
 
-				// draw scope line
-				for (x = 0; x < SCOPE_WIDTH; x++)
-					scopeDrawPtr[x] = fgColor;
+			// draw scope line
+			hLine(scopeX, 71, SCOPE_WIDTH, fgColor);
 
-				sc->emptyScopeDrawn = true;
-			}
+			sc->emptyScopeDrawn = true;
 		}
 
-		scopeDrawPtr += SCOPE_WIDTH+8;
+		scopeX += SCOPE_WIDTH+8;
 	}
 	scopesDisplayingFlag = false;
 }
--- a/src/pt2_scopes.h
+++ b/src/pt2_scopes.h
@@ -23,13 +23,10 @@
 
 void resetCachedScopePeriod(void);
 
-void scopeSetVolume(int32_t ch, uint16_t vol);
-void scopeSetPeriod(int32_t ch, uint16_t period);
-void scopeSetData(int32_t ch, const int8_t *src);
-void scopeSetLength(int32_t ch, uint16_t len);
+void scopeSetPeriod(int32_t ch, int32_t period);
 void scopeTrigger(int32_t ch);
 
-int32_t getSampleReadPos(int32_t ch, uint8_t smpNum);
+int32_t getSampleReadPos(int32_t ch);
 void updateScopes(void);
 void drawScopes(void);
 bool initScopes(void);
--- a/src/pt2_sync.c
+++ b/src/pt2_sync.c
@@ -147,31 +147,34 @@
 	{
 		scope_t *s = scope;
 		syncedChannel_t *c = chSyncEntry->channels;
-		for (int32_t i = 0; i < AMIGA_VOICES; i++, s++, c++)
+		for (int32_t ch = 0; ch < AMIGA_VOICES; ch++, s++, c++)
 		{
-			const uint8_t flags = updateFlags[i];
+			const uint8_t flags = updateFlags[ch];
 			if (flags == 0)
 				continue;
 
-			if (flags & UPDATE_VOLUME)
-				scopeSetVolume(i, c->volume);
+			if (flags & SET_SCOPE_VOLUME)
+				scope[ch].volume = c->volume;
 
-			if (flags & UPDATE_PERIOD)
-				scopeSetPeriod(i, c->period);
+			if (flags & SET_SCOPE_PERIOD)
+				scopeSetPeriod(ch, c->period);
 
-			if (flags & TRIGGER_SAMPLE)
+			if (flags & TRIGGER_SCOPE)
 			{
 				s->newData = c->triggerData;
-				s->newLength = c->triggerLength << 1;
-				scopeTrigger(i);
+				s->newLength = c->triggerLength;
+				scopeTrigger(ch);
 			}
 
-			if (flags & UPDATE_DATA)
-				scopeSetData(i, c->newData);
+			if (flags & SET_SCOPE_DATA)
+				scope[ch].newData = c->newData;
 
-			if (flags & UPDATE_LENGTH)
-				scopeSetLength(i, c->newLength);
+			if (flags & SET_SCOPE_LENGTH)
+				scope[ch].newLength = c->newLength;
 
+			if (flags & STOP_SCOPE)
+				scope[ch].active = false;
+
 			if (flags & UPDATE_ANALYZER)
 				updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod);
 
@@ -178,7 +181,7 @@
 			if (flags & UPDATE_VUMETER) // for fake VU-meters only
 			{
 				if (c->vuVolume <= 64)
-					editor.vuMeterVolumes[i] = vuMeterHeights[c->vuVolume];
+					editor.vuMeterVolumes[ch] = vuMeterHeights[c->vuVolume];
 			}
 		}
 	}
--- a/src/pt2_sync.h
+++ b/src/pt2_sync.h
@@ -6,13 +6,15 @@
 
 enum // flags
 {
-	UPDATE_VOLUME = 1,
-	UPDATE_PERIOD = 2,
-	TRIGGER_SAMPLE = 4,
-	UPDATE_DATA = 8,
-	UPDATE_LENGTH = 16,
-	UPDATE_VUMETER = 32,
-	UPDATE_ANALYZER = 64
+	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
@@ -22,9 +24,10 @@
 {
 	uint8_t flags;
 	const int8_t *triggerData, *newData;
-	uint16_t triggerLength, newLength;
+	int32_t triggerLength, newLength;
 	uint8_t volume, vuVolume, analyzerVolume;
-	uint16_t period, analyzerPeriod;
+	uint16_t analyzerPeriod;
+	int32_t period;
 } syncedChannel_t;
 
 typedef struct chSyncData_t
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -2186,10 +2186,10 @@
 {
 	int32_t i;
 
-	// sink stuff @ 50Hz rate
+	// sink stuff @ 49.92Hz (Amiga PAL) rate
 
 	static uint64_t counter50Hz;
-	const uint64_t counter50HzDelta = ((uint64_t)AMIGA_PAL_VBLANK_HZ << 32) / VBLANK_HZ;
+	const uint64_t counter50HzDelta = (uint64_t)(((UINT32_MAX + 1.0) * (AMIGA_PAL_VBLANK_HZ / (double)VBLANK_HZ)) + 0.5);
 
 	counter50Hz += counter50HzDelta; // 32.32 fixed-point counter
 	if (counter50Hz > 0xFFFFFFFF)