shithub: pt2-clone

Download patch

ref: 527d14082914c733a5f60d7361db69897cc761d3
parent: 4205fc20cb6541818c9ffc3ea62e69bce0f7cee6
author: Olav Sørensen <olav.sorensen@live.no>
date: Thu May 7 16:26:52 EDT 2020

Pushed v1.15 code

- Bugfix: The EFx (Invert Loop) effect didn't work on the whole sample loop, but
  only on the half of it.
- Bugfix: The quadrascope didn't show the volume changes from the 7xy (Tremolo)
  effect.
- Windows bugfix: Certain key modifiers didn't work (bug appeared in v1.13)
- The "real VU-meters" mode now acts more like real VU-meters by showing average
  amplitudes instead of peak amplitudes. This mode can be toggled on/off by
  pressing ALT+F11 or changing a setting in protracker.ini.
- The quadrascope code has been refactored a little bit to be slightly simpler
- Some other cosmetic changes to the code (to better match the PT source code,
  which makes it easier to verify that it's correct).

--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -314,15 +314,13 @@
 
 void mixerUpdateLoops(void) // updates Paula loop (+ scopes)
 {
-	moduleChannel_t *ch;
-	moduleSample_t *s;
-
 	for (int32_t i = 0; i < AMIGA_VOICES; i++)
 	{
-		ch = &song->channels[i];
+		const moduleChannel_t *ch = &song->channels[i];
 		if (ch->n_samplenum == editor.currSample)
 		{
-			s = &song->samples[editor.currSample];
+			const moduleSample_t *s = &song->samples[editor.currSample];
+
 			paulaSetData(i, ch->n_start + s->loopStart);
 			paulaSetLength(i, s->loopLength >> 1);
 		}
@@ -406,7 +404,7 @@
 	if (period == 0)
 		realPeriod = 1+65535; // confirmed behavior on real Amiga
 	else if (period < 113)
-		realPeriod = 113; // confirmed behavior on real Amiga
+		realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
 	else
 		realPeriod = period;
 
@@ -417,9 +415,6 @@
 
 		// this period is not cached, calculate mixer/scope deltas
 
-#if SCOPE_HZ != 64
-#error Scope Hz is not 64 (2^n), change rate calc. to use doubles+round in pt2_scope.c
-#endif
 		// during PAT2SMP or doing MOD2WAV, use different audio output rates
 		if (editor.isSMPRendering)
 			dPeriodToDeltaDiv = editor.pat2SmpHQ ? (PAULA_PAL_CLK / 28836.0) : (PAULA_PAL_CLK / 22168.0);
@@ -428,17 +423,19 @@
 		else
 			dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv;
 
+		const double dPeriodToScopeDeltaDiv = ((double)PAULA_PAL_CLK * SCOPE_FRAC_SCALE) / SCOPE_HZ;
+
 		// cache these
 		dOldVoiceDelta = dPeriodToDeltaDiv / realPeriod;
+		oldScopeDelta = (int32_t)((dPeriodToScopeDeltaDiv / realPeriod) + 0.5);
 		dOldVoiceDeltaMul = 1.0 / dOldVoiceDelta; // for BLEP synthesis
-		oldScopeDelta = (PAULA_PAL_CLK * (65536UL / SCOPE_HZ)) / realPeriod;
 	}
 
 	v->dDelta = dOldVoiceDelta;
-	v->dDeltaMul = dOldVoiceDeltaMul; // for BLEP synthesis
-	setScopeDelta(ch, oldScopeDelta);
+	scope[ch].delta = oldScopeDelta;
 
 	// for BLEP synthesis
+	v->dDeltaMul = dOldVoiceDeltaMul;
 	if (v->dLastDelta == 0.0) v->dLastDelta = v->dDelta;
 	if (v->dLastDeltaMul == 0.0) v->dLastDeltaMul = v->dDeltaMul;
 }
@@ -451,6 +448,7 @@
 		vol = 64; // confirmed behavior on real Amiga
 
 	paula[ch].dVolume = vol * (1.0 / 64.0);
+	scope[ch].volume = (uint8_t)vol;
 }
 
 void paulaSetLength(int32_t ch, uint16_t len)
@@ -463,40 +461,21 @@
 		*/
 	}
 
-	// our mixer works with bytes, not words. Multiply by two
-	scopeExt[ch].newLength = paula[ch].newLength = len << 1;
+	scope[ch].newLength = paula[ch].newLength = len << 1; // our mixer works with bytes, not words
 }
 
 void paulaSetData(int32_t ch, const int8_t *src)
 {
-	uint8_t smp;
-	moduleSample_t *s;
-	scopeChannelExt_t *se, tmp;
-
-	smp = song->channels[ch].n_samplenum;
-	assert(smp <= 30);
-	s = &song->samples[smp];
-
 	// set voice data
 	if (src == NULL)
 		src = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
 
-	paula[ch].newData = src;
-
-	// set external scope data
-	se = &scopeExt[ch];
-	tmp = *se; // cache it
-
-	tmp.newData = src;
-	tmp.newLoopFlag = (s->loopStart + s->loopLength) > 2;
-	tmp.newLoopStart = s->loopStart;
-
-	*se = tmp; // update it
+	scope[ch].newData = paula[ch].newData = src;
 }
 
 void paulaStopDMA(int32_t ch)
 {
-	scopeExt[ch].active = paula[ch].active = false;
+	scope[ch].active = paula[ch].active = false;
 }
 
 void paulaStartDMA(int32_t ch)
@@ -504,8 +483,6 @@
 	const int8_t *dat;
 	int32_t length;
 	paulaVoice_t *v;
-	scopeChannel_t s, *sc;
-	scopeChannelExt_t *se;
 
 	// trigger voice
 
@@ -525,30 +502,7 @@
 	v->length = length;
 	v->active = true;
 
-	// trigger scope
-
-	sc = &scope[ch];
-	se = &scopeExt[ch];
-	s = *sc; // cache it
-
-	dat = se->newData;
-	if (dat == NULL)
-		dat = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
-
-	s.length = length;
-	s.data = dat;
-
-	s.pos = 0;
-	s.posFrac = 0;
-
-	// data/length is already set from replayer thread (important)
-	s.loopFlag = se->newLoopFlag;
-	s.loopStart = se->newLoopStart;
-
-	se->didSwapData = false;
-	se->active = true;
-
-	*sc = s; // update it
+	scopeTrigger(ch, length);
 }
 
 void toggleA500Filters(void)
@@ -1074,9 +1028,9 @@
 	calcLEDFilterCoeffs(audio.outputRate, fc, fb, &filterLED);
 
 	// A500/A1200 one-pole 6db/oct static RC high-pass filter:
-	R = 1000.0 + 390.0;  // R324 (1K ohm resistor) + R325 (390 ohm resistor)
-	C = 2.2e-5;          // C334 (22uF capacitor) (+ C324 (0.33uF capacitor) if A500)
-	fc = 1.0 / (2.0 * M_PI * R * C); // ~5.20Hz
+	R = 1000.0 + 390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
+	C = 2.2e-5;         // C334 (22uF capacitor) (+ C324 (0.33uF capacitor) if A500)
+	fc = 1.0 / (2.0 * M_PI * R * C); // ~5.2Hz
 	calcRCFilterCoeffs(audio.outputRate, fc, &filterHi);
 }
 
--- 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.14"
+#define PROG_VER_STR "1.15"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -60,6 +60,7 @@
 	keyb.leftAltPressed = (modState & KMOD_LALT) ? true : false;
 	keyb.shiftPressed = (modState & (KMOD_LSHIFT + KMOD_RSHIFT)) ? true : false;
 
+
 #ifdef __APPLE__
 	keyb.leftCommandPressed = (modState & KMOD_LGUI) ? true : false;
 #endif
@@ -84,6 +85,8 @@
 	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)
 	{
@@ -92,9 +95,9 @@
 		{
 			const bool windowHasFocus = SDL_GetWindowFlags(window) & SDL_WINDOW_INPUT_FOCUS;
 
-			const bool bEatKeystroke = windowHasFocus && p->vkCode == VK_LWIN;
+			bEatKeystroke = windowHasFocus && (p->vkCode == VK_LWIN || p->vkCode == VK_NUMLOCK);
 			if (!bEatKeystroke)
-				return CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
+				break;
 
 			memset(&inputEvent, 0, sizeof (SDL_Event));
 
@@ -107,7 +110,6 @@
 				inputEvent.type = SDL_KEYDOWN;
 				inputEvent.key.type = SDL_KEYDOWN;
 				inputEvent.key.state = SDL_PRESSED;
-
 				windowsKeyIsDown = true;
 			}
 			else
@@ -115,7 +117,6 @@
 				inputEvent.type = SDL_KEYUP;
 				inputEvent.key.type = SDL_KEYUP;
 				inputEvent.key.state = SDL_RELEASED;
-
 				windowsKeyIsDown = false;
 			}
 
@@ -132,7 +133,7 @@
 		default: break;
 	}
 
-	return true;
+	return bEatKeystroke ? 1 : CallNextHookEx(g_hKeyboardHook, nCode, wParam, lParam);
 }
 #endif
 
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -71,14 +71,17 @@
 		pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
 	}
 
-	for (int32_t i = 0; i < AMIGA_VOICES; i++)
+	if (song != NULL)
 	{
-		moduleChannel_t *c = &song->channels[i];
+		for (int32_t i = 0; i < AMIGA_VOICES; i++)
+		{
+			moduleChannel_t *c = &song->channels[i];
 
-		c->n_wavecontrol = 0;
-		c->n_glissfunk = 0;
-		c->n_finetune = 0;
-		c->n_loopcount = 0;
+			c->n_wavecontrol = 0;
+			c->n_glissfunk = 0;
+			c->n_finetune = 0;
+			c->n_loopcount = 0;
+		}
 	}
 }
 
@@ -129,7 +132,7 @@
 
 		if (ch->n_loopstart != NULL && ch->n_wavestart != NULL) // non-PT2 bug fix
 		{
-			if (++ch->n_wavestart >= ch->n_loopstart+ch->n_replen)
+			if (++ch->n_wavestart >= ch->n_loopstart + (ch->n_replen << 1))
 				ch->n_wavestart = ch->n_loopstart;
 
 			*ch->n_wavestart = -1 - *ch->n_wavestart;
@@ -166,13 +169,9 @@
 	else
 	{
 		if (ch->n_loopcount == 0)
-		{
 			ch->n_loopcount = ch->n_cmd & 0xF;
-		}
 		else if (--ch->n_loopcount == 0)
-		{
 			return;
-		}
 
 		pBreakPosition = ch->n_pattpos;
 		pBreakFlag = true;
@@ -520,26 +519,25 @@
 	tonePortNoChange(ch);
 }
 
-static void vibratoNoChange(moduleChannel_t *ch)
+static void vibrato2(moduleChannel_t *ch)
 {
-	uint8_t vibratoTemp;
-	int16_t vibratoData;
+	uint16_t vibratoData;
 
-	vibratoTemp = (ch->n_vibratopos / 4) & 31;
-	vibratoData = ch->n_wavecontrol & 3;
+	const uint8_t vibratoPos = (ch->n_vibratopos >> 2) & 0x1F;
+	const uint8_t vibratoType = ch->n_wavecontrol & 3;
 
-	if (vibratoData == 0)
+	if (vibratoType == 0)
 	{
-		vibratoData = vibratoTable[vibratoTemp];
+		vibratoData = vibratoTable[vibratoPos];
 	}
 	else
 	{
-		if (vibratoData == 1)
+		if (vibratoType == 1)
 		{
-			if (ch->n_vibratopos < 0)
-				vibratoData = 255 - (vibratoTemp * 8);
+			if (ch->n_vibratopos < 128)
+				vibratoData = vibratoPos << 3;
 			else
-				vibratoData = vibratoTemp * 8;
+				vibratoData = 255 - (vibratoPos << 3);
 		}
 		else
 		{
@@ -547,30 +545,27 @@
 		}
 	}
 
-	vibratoData = (vibratoData * (ch->n_vibratocmd & 0xF)) / 128;
+	vibratoData = (vibratoData * (ch->n_vibratocmd & 0xF)) >> 7;
 
-	if (ch->n_vibratopos < 0)
-		vibratoData = ch->n_period - vibratoData;
-	else
+	if (ch->n_vibratopos < 128)
 		vibratoData = ch->n_period + vibratoData;
+	else
+		vibratoData = ch->n_period - vibratoData;
 
 	paulaSetPeriod(ch->n_chanindex, vibratoData);
 
-	ch->n_vibratopos += ((ch->n_vibratocmd >> 4) * 4);
+	ch->n_vibratopos += (ch->n_vibratocmd >> 2) & 0x3C;
 }
 
 static void vibrato(moduleChannel_t *ch)
 {
-	if ((ch->n_cmd & 0xFF) > 0)
-	{
-		if ((ch->n_cmd & 0x0F) > 0)
-			ch->n_vibratocmd = (ch->n_vibratocmd & 0xF0) | (ch->n_cmd & 0x0F);
+	if ((ch->n_cmd & 0x0F) > 0)
+		ch->n_vibratocmd = (ch->n_vibratocmd & 0xF0) | (ch->n_cmd & 0x0F);
 
-		if ((ch->n_cmd & 0xF0) > 0)
-			ch->n_vibratocmd = (ch->n_cmd & 0xF0) | (ch->n_vibratocmd & 0x0F);
-	}
+	if ((ch->n_cmd & 0xF0) > 0)
+		ch->n_vibratocmd = (ch->n_cmd & 0xF0) | (ch->n_vibratocmd & 0x0F);
 
-	vibratoNoChange(ch);
+	vibrato2(ch);
 }
 
 static void tonePlusVolSlide(moduleChannel_t *ch)
@@ -581,39 +576,35 @@
 
 static void vibratoPlusVolSlide(moduleChannel_t *ch)
 {
-	vibratoNoChange(ch);
+	vibrato2(ch);
 	volumeSlide(ch);
 }
 
 static void tremolo(moduleChannel_t *ch)
 {
-	int8_t tremoloTemp;
 	int16_t tremoloData;
 
-	if ((ch->n_cmd & 0xFF) > 0)
-	{
-		if ((ch->n_cmd & 0x0F) > 0)
-			ch->n_tremolocmd = (ch->n_tremolocmd & 0xF0) | (ch->n_cmd & 0x0F);
+	if ((ch->n_cmd & 0x0F) > 0)
+		ch->n_tremolocmd = (ch->n_tremolocmd & 0xF0) | (ch->n_cmd & 0x0F);
 
-		if ((ch->n_cmd & 0xF0) > 0)
-			ch->n_tremolocmd = (ch->n_cmd & 0xF0) | (ch->n_tremolocmd & 0x0F);
-	}
+	if ((ch->n_cmd & 0xF0) > 0)
+		ch->n_tremolocmd = (ch->n_cmd & 0xF0) | (ch->n_tremolocmd & 0x0F);
 
-	tremoloTemp = (ch->n_tremolopos / 4) & 31;
-	tremoloData = (ch->n_wavecontrol >> 4) & 3;
+	const uint8_t tremoloPos = (ch->n_tremolopos >> 2) & 0x1F;
+	const uint8_t tremoloType = (ch->n_wavecontrol >> 4) & 3;
 
-	if (!tremoloData)
+	if (tremoloType == 0)
 	{
-		tremoloData = vibratoTable[tremoloTemp];
+		tremoloData = vibratoTable[tremoloPos];
 	}
 	else
 	{
-		if (tremoloData == 1)
+		if (tremoloType == 1)
 		{
-			if (ch->n_vibratopos < 0) // PT bug, should've been n_tremolopos
-				tremoloData = 255 - (tremoloTemp * 8);
+			if (ch->n_vibratopos < 128) // PT bug, should've been ch->n_tremolopos
+				tremoloData = tremoloPos << 3;
 			else
-				tremoloData = tremoloTemp * 8;
+				tremoloData = 255 - (tremoloPos << 3);
 		}
 		else
 		{
@@ -621,39 +612,37 @@
 		}
 	}
 
-	tremoloData = (tremoloData * (ch->n_tremolocmd & 0xF)) / 64;
+	tremoloData = ((uint16_t)tremoloData * (ch->n_tremolocmd & 0xF)) >> 6;
 
-	if (ch->n_tremolopos < 0)
+	if (ch->n_tremolopos < 128)
 	{
-		tremoloData = ch->n_volume - tremoloData;
-		if (tremoloData < 0)
-			tremoloData = 0;
-	}
-	else
-	{
 		tremoloData = ch->n_volume + tremoloData;
 		if (tremoloData > 64)
 			tremoloData = 64;
 	}
+	else
+	{
+		tremoloData = ch->n_volume - tremoloData;
+		if (tremoloData < 0)
+			tremoloData = 0;
+	}
 
 	paulaSetVolume(ch->n_chanindex, tremoloData);
 
-	ch->n_tremolopos += (ch->n_tremolocmd >> 4) * 4;
+	ch->n_tremolopos += (ch->n_tremolocmd >> 2) & 0x3C;
 }
 
 static void sampleOffset(moduleChannel_t *ch)
 {
-	uint16_t newOffset;
-
 	if ((ch->n_cmd & 0xFF) > 0)
 		ch->n_sampleoffset = ch->n_cmd & 0xFF;
 
-	newOffset = ch->n_sampleoffset << 7;
+	uint16_t newOffset = ch->n_sampleoffset << 7;
 
-	if ((int16_t)newOffset < (int16_t)ch->n_length)
+	if ((int16_t)newOffset < ch->n_length)
 	{
 		ch->n_length -= newOffset;
-		ch->n_start += newOffset*2;
+		ch->n_start += newOffset << 1;
 	}
 	else
 	{
@@ -663,9 +652,7 @@
 
 static void E_Commands(moduleChannel_t *ch)
 {
-	uint8_t cmd;
-
-	cmd = (ch->n_cmd & 0xF0) >> 4;
+	const uint8_t cmd = (ch->n_cmd & 0xF0) >> 4;
 	switch (cmd)
 	{
 		case 0x0: filterOnOff(ch);       break;
@@ -725,15 +712,12 @@
 
 static void checkEffects(moduleChannel_t *ch)
 {
-	uint8_t effect;
-
 	if (editor.muted[ch->n_chanindex])
 		return;
 
 	updateFunk(ch);
 
-	effect = (ch->n_cmd & 0xF00) >> 8;
-
+	const uint8_t effect = (ch->n_cmd & 0xF00) >> 8;
 	if ((ch->n_cmd & 0xFFF) > 0)
 	{
 		switch (effect)
@@ -771,10 +755,9 @@
 
 static void setPeriod(moduleChannel_t *ch)
 {
-	uint8_t i;
-	uint16_t note;
+	int32_t i;
 
-	note = ch->n_note & 0xFFF;
+	uint16_t note = ch->n_note & 0xFFF;
 	for (i = 0; i < 37; i++)
 	{
 		// periodTable[36] = 0, so i=36 is safe
@@ -782,7 +765,7 @@
 			break;
 	}
 
-	// BUG: yes it's 'safe' if i=37 because of padding at the end of period table
+	// yes it's safe if i=37 because of zero-padding
 	ch->n_period = periodTable[(ch->n_finetune * 37) + i];
 
 	if ((ch->n_cmd & 0xFF0) != 0xED0) // no note delay
@@ -850,16 +833,17 @@
 		s = &song->samples[ch->n_samplenum];
 
 		ch->n_start = &song->sampleData[s->offset];
-		ch->n_finetune = s->fineTune;
+		ch->n_finetune = s->fineTune & 0xF;
 		ch->n_volume = s->volume;
-		ch->n_length = s->length / 2;
-		ch->n_replen = s->loopLength / 2;
+		ch->n_length = s->length >> 1;
+		ch->n_replen = s->loopLength >> 1;
 
-		if (s->loopStart > 0)
+		const uint16_t repeat = s->loopStart >> 1;
+		if (repeat > 0)
 		{
-			ch->n_loopstart = ch->n_start + s->loopStart;
+			ch->n_loopstart = ch->n_start + (repeat << 1);
 			ch->n_wavestart = ch->n_loopstart;
-			ch->n_length = (s->loopStart / 2) + ch->n_replen;
+			ch->n_length = repeat + ch->n_replen;
 		}
 		else
 		{
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -1539,9 +1539,6 @@
 		paulaSetData(editor.tuningChan, tuneToneData);
 		paulaSetLength(editor.tuningChan, sizeof (tuneToneData) / 2);
 		paulaStartDMA(editor.tuningChan);
-
-		// force loop flag on for scopes
-		scopeExt[editor.tuningChan].newLoopFlag = scope[editor.tuningChan].loopFlag = true;
 	}
 	else
 	{
--- a/src/pt2_scopes.c
+++ b/src/pt2_scopes.c
@@ -21,75 +21,94 @@
 
 // this uses code that is not entirely thread safe, but I have never had any issues so far...
 
-static volatile bool scopesReading;
+static volatile bool scopesUpdatingFlag, scopesDisplayingFlag;
 static uint32_t scopeTimeLen, scopeTimeLenFrac;
 static uint64_t timeNext64, timeNext64Frac;
 static SDL_Thread *scopeThread;
 
-scopeChannel_t scope[AMIGA_VOICES]; // global
-scopeChannelExt_t scopeExt[AMIGA_VOICES]; // global
+scope_t scope[AMIGA_VOICES]; // global
 
 int32_t getSampleReadPos(int32_t ch, uint8_t smpNum)
 {
 	const int8_t *data;
-	int32_t pos;
-	scopeChannel_t *sc;
+	volatile bool active;
+	volatile int32_t pos;
+	volatile scope_t *sc;
+
 	moduleSample_t *s;
 	
 	sc = &scope[ch];
 
 	// cache some stuff
+	active = sc->active;
 	data = sc->data;
 	pos = sc->pos;
 
-	if (scopeExt[ch].active && pos >= 2)
-	{
-		s = &song->samples[smpNum];
+	if (!active || data == NULL || pos <= 2) // pos 0..2 = sample loop area for non-looping samples
+		return -1;
 
-		/* Get real sampling position regardless of where the scope data points to
-		** sc->data changes during loop, offset and so on, so this has to be done
-		** (sadly, because it's really hackish).
-		*/
-		pos = (int32_t)(&data[pos] - &song->sampleData[s->offset]);
-		if (pos >= s->length)
-			return -1;
+	s = &song->samples[smpNum];
 
-		return pos;
-	}
+	// 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;
 
-	return -1;
+	return pos;
 }
 
-void setScopeDelta(int32_t ch, uint32_t delta)
+void scopeTrigger(int32_t ch, int32_t length)
 {
-	scope[ch].delta = delta;
+	volatile scope_t *sc = &scope[ch];
+	scope_t tempState = *sc; // cache it
+
+	const int8_t *newData = tempState.newData;
+	if (newData == NULL)
+		newData = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // dummy sample
+
+	if (length < 2)
+	{
+		sc->active = false;
+		return;
+	}
+
+	tempState.posFrac = 0;
+	tempState.pos = 0;
+	tempState.data = newData;
+	tempState.length = length;
+	tempState.active = true;
+
+	/* Update live scope now.
+	** In theory it -can- be written to in the middle of a cached read,
+	** then the read thread writes its own non-updated cached copy back and
+	** the trigger never happens. So far I have never seen it happen,
+	** so it's probably very rare. Yes, this is not good coding...
+	*/
+	*sc = tempState;
 }
 
 void updateScopes(void)
 {
-	scopeChannel_t *sc, tmp;
-	scopeChannelExt_t *se, tmpExt;
+	scope_t tempState;
 
 	if (editor.isWAVRendering)
 		return;
 
-	for (int32_t i = 0; i < AMIGA_VOICES; i++)
+	volatile scope_t *sc = scope;
+
+	scopesUpdatingFlag = true;
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++)
 	{
-		sc = &scope[i];
-		se = &scopeExt[i];
+		tempState = *sc; // cache it
 
-		// cache these
-		tmp = *sc;
-		tmpExt = *se;
-
-		if (!tmpExt.active)
+		if (!tempState.active)
 			continue; // scope is not active
 
-		tmp.posFrac += tmp.delta;
-		tmp.pos += tmp.posFrac >> 16;
-		tmp.posFrac &= 0xFFFF;
+		tempState.posFrac += tempState.delta;
+		tempState.pos += tempState.posFrac >> SCOPE_FRAC_BITS;
+		tempState.posFrac &= SCOPE_FRAC_MASK;
 
-		if (tmp.pos >= tmp.length)
+		if (tempState.pos >= tempState.length)
 		{
 			// sample reached end, simulate Paula register update (sample swapping)
 
@@ -96,37 +115,30 @@
 			/* Wrap pos around one time with current length, then set new length
 			** and wrap around it (handles one-shot loops and sample swapping).
 			*/
-			tmp.pos -= tmp.length;
-			tmp.length = tmpExt.newLength;
+			tempState.pos -= tempState.length;
 
-			if (tmp.length > 0)
-				tmp.pos %= tmp.length;
+			tempState.length = tempState.newLength;
+			if (tempState.length > 0)
+				tempState.pos %= tempState.length;
 
-			tmp.data = tmpExt.newData;
-			tmp.loopFlag = tmpExt.newLoopFlag;
-			tmp.loopStart = tmpExt.newLoopStart;
-
-			se->didSwapData = true;
+			tempState.data = tempState.newData;
 		}
 
-		*sc = tmp; // update it
+		*sc = tempState; // update scope state
 	}
+	scopesUpdatingFlag = false;
 }
 
-/* This routine gets the average sample peak through the running scope voices.
-** This gives a much more smooth and stable result than getting the peak from
-** the mixer, and we don't care about including filters/BLEP in the peak calculation.
+/* This routine gets the average sample amplitude through the running scope voices.
+** This gives a somewhat more stable result than getting the peak from the mixer,
+** and we don't care about including filters/BLEP in the peak calculation.
 */
 static void updateRealVuMeters(void) 
 {
-	bool didSwapData;
-	int16_t volume;
-	int32_t i, x, readPos, samplesToScan, smpDat, smpPeak;
-	scopeChannel_t tmpScope, *sc;
-	scopeChannelExt_t *se;
+	scope_t tmpScope, *sc;
 
 	// sink VU-meters first
-	for (i = 0; i < AMIGA_VOICES; i++)
+	for (int32_t i = 0; i < AMIGA_VOICES; i++)
 	{
 		editor.realVuMeterVolumes[i] -= 3;
 		if (editor.realVuMeterVolumes[i] < 0)
@@ -134,206 +146,157 @@
 	}
 
 	// get peak sample data from running scope voices
-	for (i = 0; i < AMIGA_VOICES; i++)
+	sc = scope;
+	for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++)
 	{
-		sc = &scope[i];
-		se = &scopeExt[i];
+		tmpScope = *sc; // cache it
 
-		// cache these two
-		tmpScope = *sc;
-		didSwapData = se->didSwapData;
+		if (!tmpScope.active || tmpScope.data == NULL || tmpScope.volume == 0 || tmpScope.length == 0)
+			continue;
 
-		samplesToScan = tmpScope.delta >> 16;
+		int32_t samplesToScan = tmpScope.delta >> SCOPE_FRAC_BITS; // amount of integer samples getting skipped every frame
 		if (samplesToScan <= 0)
 			continue;
 
-		if (samplesToScan > 512) // don't waste cycles on reading a ton of samples
+		// shouldn't happen (low period 113 -> samplesToScan=490), but let's not waste cycles if it does
+		if (samplesToScan > 512)
 			samplesToScan = 512;
 
-		volume = song->channels[i].n_volume;
+		int32_t pos = tmpScope.pos;
+		int32_t length = tmpScope.length;
+		const int8_t *data = tmpScope.data;
 
-		if (se->active && tmpScope.data != NULL && volume != 0 && tmpScope.length > 0)
+		int32_t runningAmplitude = 0;
+		for (int32_t x = 0; x < samplesToScan; x++)
 		{
-			smpPeak = 0;
-			readPos = tmpScope.pos;
+			int16_t amplitude = 0;
+			if (data != NULL)
+				amplitude = data[pos] * tmpScope.volume;
 
-			if (tmpScope.loopFlag)
+			runningAmplitude += ABS(amplitude);
+
+			pos++;
+			if (pos >= length)
 			{
-				for (x = 0; x < samplesToScan; x += 2) // loop enabled
-				{
-					if (didSwapData)
-					{
-						if (readPos >= tmpScope.length)
-							readPos %= tmpScope.length; // s.data = loopStartPtr, wrap readPos to 0
-					}
-					else if (readPos >= tmpScope.length)
-					{
-						readPos = tmpScope.loopStart; // s.data = sampleStartPtr, wrap readPos to loop start
-					}
+				pos = 0;
 
-					smpDat = tmpScope.data[readPos] * volume;
-
-					smpDat = ABS(smpDat);
-					if (smpDat > smpPeak)
-						smpPeak = smpDat;
-
-					readPos += 2;
-				}
+				/* Read cycle done, temporarily update the display data/length variables
+				** before the scope thread does it.
+				*/
+				data = tmpScope.newData;
+				length = tmpScope.newLength;
 			}
-			else
-			{
-				for (x = 0; x < samplesToScan; x += 2) // no loop
-				{
-					if (readPos >= tmpScope.length)
-						break;
+		}
 
-					smpDat = tmpScope.data[readPos] * volume;
+		double dAvgAmplitude = runningAmplitude / (double)samplesToScan;
 
-					smpDat = ABS(smpDat);
-					if (smpDat > smpPeak)
-						smpPeak = smpDat;
+		dAvgAmplitude *= (96.0 / (128.0 * 64.0)); // normalize
 
-					readPos += 2;
-				}
-			}
+		int32_t vuHeight = (int32_t)dAvgAmplitude;
+		if (vuHeight > 48) // max VU-meter height
+			vuHeight = 48;
 
-			smpPeak = ((smpPeak * 48) + (1 << 12)) >> 13; // rounded
-			if (smpPeak > editor.realVuMeterVolumes[i])
-				editor.realVuMeterVolumes[i] = (int8_t)smpPeak;
-		}
+		if ((int8_t)vuHeight > editor.realVuMeterVolumes[i])
+			editor.realVuMeterVolumes[i] = (int8_t)vuHeight;
 	}
 }
 
 void drawScopes(void)
 {
-	bool didSwapData;
-	int16_t scopeData, volume;
-	int32_t i, x, y, readPos;
-	uint32_t *dstPtr, *scopePtr, scopePixel;
-	scopeChannel_t tmpScope, *sc;
-	scopeChannelExt_t *se;
+	int16_t scopeData;
+	int32_t i, x, y;
+	uint32_t *dstPtr, *scopeDrawPtr;
+	volatile scope_t *sc;
+	scope_t tmpScope;
 
-	scopesReading = true;
-	if (ui.visualizerMode == VISUAL_QUADRASCOPE)
+	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++)
 	{
-		// --- QUADRASCOPE ---
+		tmpScope = *sc; // cache it
 
-		scopePtr = &video.frameBuffer[(71 * SCREEN_W) + 128];
-		for (i = 0; i < AMIGA_VOICES; i++)
+		// render scope
+		if (tmpScope.active && tmpScope.data != NULL && tmpScope.volume != 0 && tmpScope.length > 0)
 		{
-			sc = &scope[i];
-			se = &scopeExt[i];
+			// scope is active
 
-			// cache these two
-			tmpScope = *sc;
-			didSwapData = se->didSwapData;
+			sc->emptyScopeDrawn = false;
 
-			volume = -song->channels[i].n_volume; // invert volume
-
-			// render scope
-			if (se->active && tmpScope.data != NULL && volume != 0 && tmpScope.length > 0)
+			// fill scope background
+			dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
+			for (y = 0; y < SCOPE_HEIGHT; y++)
 			{
-				// scope is active
+				for (x = 0; x < SCOPE_WIDTH; x++)
+					dstPtr[x] = bgColor;
 
-				se->emptyScopeDrawn = false;
+				dstPtr += SCREEN_W;
+			}
 
-				// draw scope background
+			// render scope data
 
-				dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
-				scopePixel = video.palette[PAL_BACKGRD]; // this palette can change
+			int32_t pos = tmpScope.pos;
+			int32_t length = tmpScope.length;
+			const int16_t volume = -(tmpScope.volume << 7);
+			const int8_t *data = tmpScope.data;
 
-				for (y = 0; y < SCOPE_HEIGHT; y++)
-				{
-					for (x = 0; x < SCOPE_WIDTH; x++)
-						dstPtr[x] = scopePixel;
+			for (x = 0; x < SCOPE_WIDTH; x++)
+			{
+				scopeData = 0;
+				if (data != NULL)
+					scopeData = (data[pos] * volume) >> 16;
 
-					dstPtr += SCREEN_W;
-				}
+				scopeDrawPtr[(scopeData * SCREEN_W) + x] = fgColor;
 
-				// render scope data
-
-				scopePixel = video.palette[PAL_QADSCP];
-
-				readPos = tmpScope.pos;
-				if (tmpScope.loopFlag)
+				pos++;
+				if (pos >= length)
 				{
-					// loop enabled
+					pos = 0;
 
-					for (x = 0; x < SCOPE_WIDTH; x++)
-					{
-						if (didSwapData)
-						{
-							if (readPos >= tmpScope.length)
-								readPos %= tmpScope.length; // s.data = loopStartPtr, wrap readPos to 0
-						}
-						else if (readPos >= tmpScope.length)
-						{
-							readPos = tmpScope.loopStart; // s.data = sampleStartPtr, wrap readPos to loop start
-						}
-
-						scopeData = (tmpScope.data[readPos++] * volume) >> 9; // (-128..127)*(-64..0) / 2^9 = -15..16
-						scopePtr[(scopeData * SCREEN_W) + x] = scopePixel;
-					}
+					/* Read cycle done, temporarily update the display data/length variables
+					** before the scope thread does it.
+					*/
+					length = tmpScope.newLength;
+					data = tmpScope.newData;
 				}
-				else
-				{
-					// no loop
-
-					for (x = 0; x < SCOPE_WIDTH; x++)
-					{
-						if (readPos >= tmpScope.length)
-						{
-							scopePtr[x] = scopePixel; // end of data, draw center pixel
-						}
-						else
-						{
-							scopeData = (tmpScope.data[readPos++] * volume) >> 9; // (-128..127)*(-64..0) / 2^9 = -15..16
-							scopePtr[(scopeData * SCREEN_W) + x] = scopePixel;
-						}
-					}
-				}
 			}
-			else
-			{
-				// scope is inactive, draw empty scope once until it gets active again
+		}
+		else
+		{
+			// scope is inactive, draw empty scope once until it gets active again
 
-				if (!se->emptyScopeDrawn)
+			if (!sc->emptyScopeDrawn)
+			{
+				// fill scope background
+				dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
+				for (y = 0; y < SCOPE_HEIGHT; y++)
 				{
-					// draw scope background
-
-					dstPtr = &video.frameBuffer[(55 * SCREEN_W) + (128 + (i * (SCOPE_WIDTH + 8)))];
-					scopePixel = video.palette[PAL_BACKGRD];
-
-					for (y = 0; y < SCOPE_HEIGHT; y++)
-					{
-						for (x = 0; x < SCOPE_WIDTH; x++)
-							dstPtr[x] = scopePixel;
-
-						dstPtr += SCREEN_W;
-					}
-
-					// draw line
-
-					scopePixel = video.palette[PAL_QADSCP];
 					for (x = 0; x < SCOPE_WIDTH; x++)
-						scopePtr[x] = scopePixel;
+						dstPtr[x] = bgColor;
 
-					se->emptyScopeDrawn = true;
+					dstPtr += SCREEN_W;
 				}
-			}
 
-			scopePtr += SCOPE_WIDTH+8;
+				// draw scope line
+				for (x = 0; x < SCOPE_WIDTH; x++)
+					scopeDrawPtr[x] = fgColor;
+
+				sc->emptyScopeDrawn = true;
+			}
 		}
+
+		scopeDrawPtr += SCOPE_WIDTH+8;
 	}
-	scopesReading = false;
+	scopesDisplayingFlag = false;
 }
 
 static int32_t SDLCALL scopeThreadFunc(void *ptr)
 {
-	int32_t time32;
-	uint32_t diff32;
-	uint64_t time64;
-
 	// this is needed for scope stability (confirmed)
 	SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);
 
@@ -348,16 +311,19 @@
 
 		updateScopes();
 
-		time64 = SDL_GetPerformanceCounter();
+		uint64_t time64 = SDL_GetPerformanceCounter();
 		if (time64 < timeNext64)
 		{
-			assert(timeNext64-time64 <= 0xFFFFFFFFULL);
-			diff32 = (uint32_t)(timeNext64 - time64);
+			time64 = timeNext64 - time64;
+			if (time64 > UINT32_MAX)
+				time64 = UINT32_MAX;
 
+			const uint32_t diff32 = (uint32_t)time64;
+
 			// convert to microseconds and round to integer
-			time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);
+			const int32_t time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);
 
-			// delay until we have reached next tick
+			// delay until we have reached the next frame
 			if (time32 > 0)
 				usleep(time32);
 		}
@@ -406,18 +372,23 @@
 
 void stopScope(int32_t ch)
 {
-	while (scopesReading);
-	memset(&scopeExt[ch], 0, sizeof (scopeChannelExt_t));
+	// wait for scopes to finish updating
+	while (scopesUpdatingFlag);
 
-	while (scopesReading);
-	memset(&scope[ch], 0, sizeof (scopeChannel_t));
+	scope[ch].active = false;
 
-	scope[ch].length = scopeExt[ch].newLength = 2;
-	while (scopesReading); // final wait to make sure scopes are all inactive
+	// wait for scope displaying to be done (safety)
+	while (scopesDisplayingFlag);
 }
 
 void stopAllScopes(void)
 {
+	// wait for scopes to finish updating
+	while (scopesUpdatingFlag);
+
 	for (int32_t i = 0; i < AMIGA_VOICES; i++)
-		stopScope(i);
+		scope[i].active = false;
+
+	// wait for scope displaying to be done (safety)
+	while (scopesDisplayingFlag);
 }
--- a/src/pt2_scopes.h
+++ b/src/pt2_scopes.h
@@ -3,24 +3,25 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include "pt2_header.h"
+#include "pt2_structs.h"
 
-typedef struct scopeChannel_t // internal scope state
+#define SCOPE_FRAC_BITS 16
+#define SCOPE_FRAC_SCALE (1UL << 16)
+#define SCOPE_FRAC_MASK (SCOPE_FRAC_SCALE-1)
+
+typedef struct scope_t
 {
 	const int8_t *data;
-	bool loopFlag;
-	int32_t length, pos, loopStart;
+	bool active, emptyScopeDrawn;
+	uint8_t volume;
+	int32_t length, pos;
 	uint32_t delta, posFrac;
-} scopeChannel_t;
 
-typedef struct scopeChannelExt // external scope state
-{
 	const int8_t *newData;
-	volatile bool active, didSwapData;
-	bool emptyScopeDrawn, newLoopFlag;
-	int32_t newLength, newLoopStart;
-} scopeChannelExt_t;
+	int32_t newLength;
+} scope_t;
 
-void setScopeDelta(int32_t ch, uint32_t delta);
+void scopeTrigger(int32_t ch, int32_t length);
 int32_t getSampleReadPos(int32_t ch, uint8_t smpNum);
 void updateScopes(void);
 void drawScopes(void);
@@ -28,5 +29,4 @@
 void stopScope(int32_t ch);
 void stopAllScopes(void);
 
-extern scopeChannel_t scope[AMIGA_VOICES];
-extern scopeChannelExt_t scopeExt[AMIGA_VOICES];
+extern scope_t scope[AMIGA_VOICES];
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -68,9 +68,10 @@
 typedef struct moduleChannel_t
 {
 	int8_t *n_start, *n_wavestart, *n_loopstart, n_chanindex, n_volume;
-	int8_t n_toneportdirec, n_vibratopos, n_tremolopos, n_pattpos, n_loopcount;
+	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;
+	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;
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -128,20 +128,19 @@
 {
 	// this routine almost never delays if we have 60Hz vsync, but it's still needed in some occasions
 
-	int32_t time32;
-	uint32_t diff32;
-	uint64_t time64;
-
-	time64 = SDL_GetPerformanceCounter();
+	uint64_t time64 = SDL_GetPerformanceCounter();
 	if (time64 < timeNext64)
 	{
-		assert(timeNext64-time64 <= 0xFFFFFFFFULL);
-		diff32 = (uint32_t)(timeNext64 - time64);
+		time64 = timeNext64 - time64;
+		if (time64 > UINT32_MAX)
+			time64 = UINT32_MAX;
 
+		const uint32_t diff32 = (uint32_t)time64;
+
 		// convert to microseconds and round to integer
-		time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);
+		const int32_t time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);
 
-		// delay until we have reached next tick
+		// delay until we have reached the next frame
 		if (time32 > 0)
 			usleep(time32);
 	}
@@ -1262,8 +1261,8 @@
 		dstPtr += SCREEN_W;
 	}
 
-	for (uint32_t i = 0; i < AMIGA_VOICES; i++)
-		scopeExt[i].emptyScopeDrawn = false;
+	for (int32_t i = 0; i < AMIGA_VOICES; i++)
+		scope[i].emptyScopeDrawn = false;
 }
 
 void renderSpectrumAnalyzerBg(void)