shithub: ft²

Download patch

ref: 1e5706a2419f55dc66900ecf6783a9a17172ee50
parent: 065a79216e2697245dc175423fe4a85f27106ede
author: Olav Sørensen <olav.sorensen@live.no>
date: Mon Jun 15 17:39:32 EDT 2020

Small scope/mixer delta calculation rework

Eliminated the need for a 512kB table, and also did some small code cleanup.

--- a/.gitignore
+++ b/.gitignore
@@ -18,3 +18,5 @@
 *.cod
 vs2019_project/ft2-clone/Debug/ft2-clone.vcxproj.FileListAbsolute.txt
 *.opendb
+*.db-shm
+*.db-wal
--- a/src/ft2_audio.c
+++ b/src/ft2_audio.c
@@ -18,14 +18,6 @@
 
 #define INITIAL_DITHER_SEED 0x12345000
 
-// globalized
-audio_t audio;
-pattSyncData_t *pattSyncEntry;
-chSyncData_t *chSyncEntry;
-chSync_t chSync;
-pattSync_t pattSync;
-volatile bool pattQueueClearing, chQueueClearing;
-
 static int8_t pmpCountDiv, pmpChannels = 2;
 static uint16_t smpBuffSize;
 static int32_t masterVol, oldAudioFreq, pmpLeft, randSeed = INITIAL_DITHER_SEED;
@@ -35,19 +27,29 @@
 static voice_t voice[MAX_VOICES * 2];
 static void (*sendAudSamplesFunc)(uint8_t *, uint32_t, uint8_t); // "send mixed samples" routines
 
-#if !defined __amd64__ && !defined _WIN64
 static int32_t oldPeriod;
+#if !defined __amd64__ && !defined _WIN64
 static uint32_t oldSFrq, oldSFrqRev;
+#else
+static uint64_t oldSFrq;
 #endif
 
-#if !defined __amd64__ && !defined _WIN64
+// globalized
+audio_t audio;
+pattSyncData_t *pattSyncEntry;
+chSyncData_t *chSyncEntry;
+chSync_t chSync;
+pattSync_t pattSync;
+volatile bool pattQueueClearing, chQueueClearing;
+
 void resetCachedMixerVars(void)
 {
 	oldPeriod = -1;
 	oldSFrq = 0;
+#if !defined __amd64__ && !defined _WIN64
 	oldSFrqRev = 0xFFFFFFFF;
-}
 #endif
+}
 
 void stopVoice(int32_t i)
 {
@@ -368,22 +370,23 @@
 
 		if (status & IS_Period)
 		{
-#if defined __amd64__ || defined _WIN64
-			v->SFrq = getFrequenceValue(ch->finalPeriod);
-#else
-			// use cached values to prevent a 32-bit divsion all the time
-			if (ch->finalPeriod != oldPeriod)
+			// use cached values if possible
+
+			const uint16_t period = ch->finalPeriod;
+			if (period != oldPeriod)
 			{
-				oldPeriod = ch->finalPeriod;
+				oldPeriod = period;
+				oldSFrq = getMixerDelta(period);
 
-				oldSFrq = getFrequenceValue(ch->finalPeriod);
-
+#if !defined __amd64__ && !defined _WIN64
 				oldSFrqRev = 0xFFFFFFFF;
 				if (oldSFrq != 0)
 					oldSFrqRev /= oldSFrq;
+#endif
 			}
 
 			v->SFrq = oldSFrq;
+#if !defined __amd64__ && !defined _WIN64
 			v->SFrqRev = oldSFrqRev;
 #endif
 		}
--- a/src/ft2_audio.h
+++ b/src/ft2_audio.h
@@ -54,7 +54,7 @@
 	uint32_t freq;
 	uint32_t audLatencyPerfValInt, audLatencyPerfValFrac, speedVal, musicTimeSpeedVal;
 	uint64_t tickTime64, tickTime64Frac, tickTimeLengthTab[MAX_BPM+1];
-	double dAudioLatencyMs, dSpeedValMul, dPianoDeltaMul;
+	double dAudioLatencyMs;
 	SDL_AudioDeviceID dev;
 	uint32_t wantFreq, haveFreq, wantSamples, haveSamples, wantChannels, haveChannels;
 } audio_t;
@@ -104,21 +104,7 @@
 	chSyncData_t data[SYNC_QUEUE_LEN + 1];
 } chSync_t;
 
-// in ft2_audio.c
-extern audio_t audio;
-extern pattSyncData_t *pattSyncEntry;
-extern chSyncData_t *chSyncEntry;
-extern chSync_t chSync;
-extern pattSync_t pattSync;
-
-extern volatile bool pattQueueClearing, chQueueClearing;
-
-#if !defined __amd64__ && !defined _WIN64
 void resetCachedMixerVars(void);
-#endif
-
-void calcAudioTables(void);
-
 int32_t pattQueueReadSize(void);
 int32_t pattQueueWriteSize(void);
 bool pattQueuePush(pattSyncData_t t);
@@ -153,3 +139,12 @@
 void mix_SaveIPVolumes(void);
 void mix_UpdateChannelVolPanFrq(void);
 uint32_t mixReplayerTickToBuffer(uint8_t *stream, uint8_t bitDepth);
+
+// in ft2_audio.c
+extern audio_t audio;
+extern pattSyncData_t *pattSyncEntry;
+extern chSyncData_t *chSyncEntry;
+extern chSync_t chSync;
+extern pattSync_t pattSync;
+
+extern volatile bool pattQueueClearing, chQueueClearing;
--- a/src/ft2_header.h
+++ b/src/ft2_header.h
@@ -12,7 +12,7 @@
 #endif
 #include "ft2_replayer.h"
 
-#define PROG_VER_STR "1.25"
+#define PROG_VER_STR "1.26"
 
 // do NOT change these! It will only mess things up...
 
--- a/src/ft2_main.c
+++ b/src/ft2_main.c
@@ -312,7 +312,7 @@
 
 	audio.linearFreqTable = true;
 
-	calcAudioTables();
+	calcReplayerLogTab();
 }
 
 static void cleanUpAndExit(void) // never call this inside the main loop!
--- a/src/ft2_replayer.c
+++ b/src/ft2_replayer.c
@@ -26,20 +26,10 @@
 ** If something looks to be off, it probably isn't!
 */
 
-/* Tables for pre-calculated stuff on run time and when changing freq. and/or linear/amiga mode.
-** FT2 obviously didn't have such big tables.
-*/
-
+// non-FT2 precalced stuff
 static uint32_t musicTimeTab[MAX_BPM+1];
-static uint64_t period2ScopeDeltaTab[65536];
-static double dLogTab[768], dLogTabMul[32], dAudioRateFactor;
+static double dPeriod2HzTab[65536], dLogTab[768], dAudioRateFactor;
 
-#if defined _WIN64 || defined __amd64__
-static uint64_t period2DeltaTab[65536];
-#else
-static uint32_t period2DeltaTab[65536];
-#endif
-
 static bool bxxOverflow;
 static tonTyp nilPatternLine;
 
@@ -141,8 +131,8 @@
 		return;
 	}
 
-	double dFreq = log2(midCFreq * (1.0 / 8363.0)) * (12.0 * 128.0);
-	int32_t linearFreq = (int32_t)(dFreq + 0.5);
+	double dFreq = log2(midCFreq / 8363.0) * (12.0 * 128.0);
+	int32_t linearFreq = (int32_t)(dFreq + 0.5); // rounded
 	s->fine = ((linearFreq + 128) & 255) - 128;
 
 	int32_t relTon = (linearFreq - s->fine) >> 7;
@@ -220,61 +210,29 @@
 	return i+1;
 }
 
-// called every time you change linear/amiga mode and mixing frequency
-static void calcPeriod2DeltaTables(void)
+static void calcPeriod2HzTab(void) // called every time you change linear/amiga period mode
 {
-	int32_t baseDelta;
-	uint32_t i;
+	dPeriod2HzTab[0] = 0.0; // in FT2, a period of 0 yields 0Hz
 
-	period2DeltaTab[0] = 0; // in FT2, a period of 0 is converted to a delta of 0
-
-	const double dScopeRateFactor = SCOPE_FRAC_SCALE / (double)SCOPE_HZ;
-
 	if (audio.linearFreqTable)
 	{
 		// linear periods
-		for (i = 1; i < 65536; i++)
+		for (int32_t i = 1; i < 65536; i++)
 		{
 			const uint16_t invPeriod = (12 * 192 * 4) - (uint16_t)i; // this intentionally overflows uint16_t to be accurate to FT2
 			const int32_t octave = invPeriod / 768;
 			const int32_t period = invPeriod % 768;
-			const int32_t shift = (14 - octave) & 0x1F; // 100% accurate to FT2!
+			const int32_t bitshift = (14 - octave) & 0x1F; // 100% accurate to FT2
 
-			const double dHz = dLogTab[period] * dLogTabMul[shift];
-
-#if defined _WIN64 || defined __amd64__
-			period2DeltaTab[i] = (uint64_t)((dHz * dAudioRateFactor) + 0.5);
-#else
-			period2DeltaTab[i] = (uint32_t)((dHz * dAudioRateFactor) + 0.5);
-#endif
-			period2ScopeDeltaTab[i] = (uint64_t)((dHz * dScopeRateFactor) + 0.5);
+			dPeriod2HzTab[i] = dLogTab[period] / (1UL << bitshift);
 		}
 	}
 	else
 	{
 		// Amiga periods
-		for (i = 1; i < 65536; i++)
-		{
-			double dHz = (8363.0 * 1712.0) / i;
-
-#if defined _WIN64 || defined __amd64__
-			period2DeltaTab[i] = (uint64_t)((dHz * dAudioRateFactor) + 0.5);
-#else
-			period2DeltaTab[i] = (uint32_t)((dHz * dAudioRateFactor) + 0.5);
-#endif
-			period2ScopeDeltaTab[i] = (uint64_t)((dHz * dScopeRateFactor) + 0.5);
-		}
+		for (int32_t i = 1; i < 65536; i++)
+			dPeriod2HzTab[i] = (8363.0 * 1712.0) / i;
 	}
-
-	// for piano in Instr. Ed.
-
-	// (this delta is small enough to fit in int32_t even with 32.32 deltas)
-	if (audio.linearFreqTable)
-		baseDelta = (int32_t)period2DeltaTab[7680];
-	else
-		baseDelta = (int32_t)period2DeltaTab[1712*16];
-
-	audio.dPianoDeltaMul = 1.0 / baseDelta;
 }
 
 void setFrqTab(bool linear)
@@ -288,7 +246,7 @@
 	else
 		note2Period = amigaPeriods;
 
-	calcPeriod2DeltaTables();
+	calcPeriod2HzTab();
 
 	resumeAudio();
 
@@ -302,7 +260,7 @@
 	ch->realVol = ch->oldVol;
 	ch->outVol = ch->oldVol;
 	ch->outPan = ch->oldPan;
-	ch->status |= (IS_Vol + IS_Pan + IS_QuickVol);
+	ch->status |= IS_Vol + IS_Pan + IS_QuickVol;
 }
 
 static void retrigEnvelopeVibrato(stmTyp *ch)
@@ -376,26 +334,18 @@
 	{
 		ch->realVol = 0;
 		ch->outVol = 0;
-		ch->status |= (IS_Vol + IS_QuickVol);
+		ch->status |= IS_Vol + IS_QuickVol;
 	}
 }
 
-void calcAudioTables(void)
+void calcReplayerLogTab(void)
 {
-	int32_t i;
-
-	for (i = 0; i < 768; i++)
+	for (int32_t i = 0; i < 768; i++)
 		dLogTab[i] = exp2(i / 768.0) * (8363.0 * 256.0);
-
-	dLogTabMul[0] = 1.0;
-	for (i = 1; i < 32; i++)
-		dLogTabMul[i] = exp2(-i);
 }
 
 void calcReplayRate(int32_t audioFreq)
 {
-	int32_t i;
-
 	if (audioFreq == 0)
 		return;
 
@@ -403,77 +353,66 @@
 
 	audio.quickVolSizeVal = audioFreq / 200; // FT2 truncates here
 	audio.rampQuickVolMul = (int32_t)(((UINT32_MAX + 1.0) / audio.quickVolSizeVal) + 0.5);
-	audio.dSpeedValMul = editor.dPerfFreq / audioFreq; // for audio/video sync
 
 	/* Calculate tables to prevent floating point operations on systems that
 	** might have a slow FPU. This is quite hackish and not really needed,
-	** but it doesn't take up THAT much RAM anyway.
+	** but it doesn't take up a lot of RAM, so why not.
 	*/
 
-	// calculate table used to count replayer time (displayed as hours/minutes/seconds)
-	const double dMul = (UINT32_MAX + 1.0) / audioFreq;
-
 	audio.speedValTab[0] = 0;
 	musicTimeTab[0] = UINT32_MAX;
-	audio.rampSpeedValMulTab[0] = UINT32_MAX;
-	audio.tickTimeLengthTab[0] = (uint64_t)UINT32_MAX << 32;
+	audio.tickTimeLengthTab[0] = UINT64_MAX;
+	audio.rampSpeedValMulTab[0] = INT32_MAX;
 
-	const double dTickTimeLenMul = audio.dSpeedValMul * (UINT32_MAX + 1.0);
-	for (i = 1; i <= MAX_BPM; i++)
-	{
-		int32_t samplesPerTick = (int32_t)(((audioFreq * 2.5) / i) + 0.5); // rounded
+	const double dMul1 = (UINT32_MAX + 1.0) / audioFreq;
+	const double dMul2 = (editor.dPerfFreq / audioFreq) * (UINT32_MAX + 1.0);
 
+	for (int32_t i = 1; i <= MAX_BPM; i++)
+	{
+		const int32_t samplesPerTick = (int32_t)(((audioFreq * 2.5) / i) + 0.5); // rounded
 		audio.speedValTab[i] = samplesPerTick;
 
 		// used for song playback counter (hh:mm:ss)
-		musicTimeTab[i] = (uint32_t)((samplesPerTick * dMul) + 0.5);
+		musicTimeTab[i] = (uint32_t)((samplesPerTick * dMul1) + 0.5);
 
 		// number of samples per tick -> tick length for performance counter (syncing visuals to audio)
-		audio.tickTimeLengthTab[i] = (uint64_t)(samplesPerTick * dTickTimeLenMul);
+		audio.tickTimeLengthTab[i] = (uint64_t)(samplesPerTick * dMul2);
 
 		// for calculating volume ramp length for "tick" ramps
 		audio.rampSpeedValMulTab[i] = (int32_t)(((UINT32_MAX + 1.0) / samplesPerTick) + 0.5);
 	}
+}
 
-	calcPeriod2DeltaTables();
+double period2Hz(uint16_t period)
+{
+	return dPeriod2HzTab[period];
 }
 
 #if defined _WIN64 || defined __amd64__
-uint64_t getFrequenceValue(uint16_t period)
+int64_t getMixerDelta(uint16_t period)
 {
-	return period2DeltaTab[period];
+	return (int64_t)((dPeriod2HzTab[period] * dAudioRateFactor) + 0.5); // Hz -> rounded fixed-point mixer delta
 }
 #else
-uint32_t getFrequenceValue(uint16_t period)
+int32_t getMixerDelta(uint16_t period)
 {
-	return period2DeltaTab[period];
+	return (int32_t)((dPeriod2HzTab[period] * dAudioRateFactor) + 0.5); // Hz -> rounded fixed-point mixer delta
 }
 #endif
 
 int32_t getPianoKey(uint16_t period, int32_t finetune, int32_t relativeNote) // for piano in Instr. Ed.
 {
-#if defined _WIN64 || defined __amd64__
-	uint64_t delta = period2DeltaTab[period];
-#else
-	uint32_t delta = period2DeltaTab[period];
-#endif
-
 	finetune >>= 3; // FT2 does this in the replayer internally, so the actual range is -16..15
 
-	const double dNote = (log2(delta * audio.dPianoDeltaMul) * 12.0) - (finetune * (1.0 / 16.0));
-	int32_t note = (int32_t)(dNote + 0.5);
+	const double dRelativeHz = dPeriod2HzTab[period] * (1.0 / (8363.0 / 16.0));
+	const double dNote = (log2(dRelativeHz) * 12.0) - (finetune * (1.0 / 16.0));
 
-	note -= relativeNote;
+	const int32_t note = (int32_t)(dNote + 0.5) - relativeNote; // rounded
 
-	// "note" is now the raw piano key number, unaffected by finetune/relativeNote
+	// "note" is now the raw piano key number, unaffected by finetune and relativeNote
 	return note;
 }
 
-uint64_t getScopeFrequenceValue(uint16_t period)
-{
-	return period2ScopeDeltaTab[period];
-}
-
 static void startTone(uint8_t ton, uint8_t effTyp, uint8_t eff, stmTyp *ch)
 {
 	uint8_t smp;
@@ -538,7 +477,7 @@
 		}
 	}
 
-	ch->status |= (IS_Period + IS_Vol + IS_Pan + IS_NyTon + IS_QuickVol);
+	ch->status |= IS_Period + IS_Vol + IS_Pan + IS_NyTon + IS_QuickVol;
 
 	if (effTyp == 9)
 	{
@@ -787,7 +726,7 @@
 			{
 				ch->realVol = 0;
 				ch->outVol = 0;
-				ch->status |= (IS_Vol + IS_QuickVol);
+				ch->status |= IS_Vol + IS_QuickVol;
 			}
 		}
 
@@ -978,7 +917,7 @@
 		ch->outVol = volKol;
 		ch->realVol = volKol;
 
-		ch->status |= (IS_Vol + IS_QuickVol);
+		ch->status |= IS_Vol + IS_QuickVol;
 	}
 
 	// fine volume slide down
@@ -1035,7 +974,7 @@
 			ch->realVol = 64;
 
 		ch->outVol = ch->realVol;
-		ch->status |= (IS_Vol + IS_QuickVol);
+		ch->status |= IS_Vol + IS_QuickVol;
 
 		return;
 	}
@@ -1939,7 +1878,7 @@
 			{
 				ch->outVol = 0;
 				ch->realVol = 0;
-				ch->status |= (IS_Vol + IS_QuickVol);
+				ch->status |= IS_Vol + IS_QuickVol;
 			}
 		}
 
@@ -2064,7 +2003,7 @@
 		ch->tremorPos = tremorSign | tremorData;
 
 		ch->outVol = (tremorSign == 0x80) ? ch->realVol : 0;
-		ch->status |= (IS_Vol + IS_QuickVol);
+		ch->status |= IS_Vol + IS_QuickVol;
 	}
 }
 
@@ -2779,6 +2718,7 @@
 
 	audio.linearFreqTable = true;
 	note2Period = linearPeriods;
+	calcPeriod2HzTab();
 
 	setPos(0, 0, true);
 
@@ -3114,9 +3054,8 @@
 
 	stopAllScopes();
 	resetAudioDither();
-#if !defined __amd64__ && !defined _WIN64
 	resetCachedMixerVars();
-#endif
+	resetCachedScopeVars();
 
 	// wait for scope thread to finish, so that we know pointers aren't deprecated
 	while (editor.scopeThreadMutex);
--- a/src/ft2_replayer.h
+++ b/src/ft2_replayer.h
@@ -237,18 +237,18 @@
 
 void fixSongName(void); // removes spaces from right side of song name
 void fixSampleName(int16_t nr); // removes spaces from right side of ins/smp names
-
 void calcReplayRate(int32_t rate);
 void tuneSample(sampleTyp *s, int32_t midCFreq);
 
+void calcReplayerLogTab(void);
+double period2Hz(uint16_t period);
 
 #if defined _WIN64 || defined __amd64__
-uint64_t getFrequenceValue(uint16_t period);
+int64_t getMixerDelta(uint16_t period);
 #else
-uint32_t getFrequenceValue(uint16_t period);
+int32_t getMixerDelta(uint16_t period);
 #endif
 
-uint64_t getScopeFrequenceValue(uint16_t period);
 int32_t getPianoKey(uint16_t period, int32_t finetune, int32_t relativeNote); // for piano in Instr. Ed.
 
 bool allocateInstr(int16_t nr);
--- a/src/ft2_scopedraw.c
+++ b/src/ft2_scopedraw.c
@@ -101,13 +101,13 @@
 
 #define SCOPE_UPDATE_DRAWPOS \
 	scopeDrawFrac += scopeDrawDelta; \
-	scopeDrawPos += scopeDrawFrac >> 16; \
-	scopeDrawFrac &= 0xFFFF; \
+	scopeDrawPos += scopeDrawFrac >> SCOPE_DRAW_FRAC_BITS; \
+	scopeDrawFrac &= SCOPE_DRAW_FRAC_MASK; \
 
 #define SCOPE_UPDATE_DRAWPOS_PINGPONG \
 	scopeDrawFrac += scopeDrawDelta; \
-	scopeDrawPos += (scopeDrawFrac >> 16) * drawPosDir; \
-	scopeDrawFrac &= 0xFFFF; \
+	scopeDrawPos += (scopeDrawFrac >> SCOPE_DRAW_FRAC_BITS) * drawPosDir; \
+	scopeDrawFrac &= SCOPE_DRAW_FRAC_MASK; \
 
 #define SCOPE_DRAW_SMP \
 	video.frameBuffer[((lineY - sample) * SCREEN_W) + x] = scopePixelColor;
--- a/src/ft2_scopedraw.h
+++ b/src/ft2_scopedraw.h
@@ -3,6 +3,6 @@
 #include <stdint.h>
 #include "ft2_scopes.h"
 
-typedef void (*scopeDrawRoutine)(scope_t *, uint32_t, uint32_t, uint32_t);
+typedef void (*scopeDrawRoutine)(const scope_t *, uint32_t, uint32_t, uint32_t);
 
 extern const scopeDrawRoutine scopeDrawRoutineTable[12]; // ft2_scopedraw.c
--- a/src/ft2_scopes.c
+++ b/src/ft2_scopes.c
@@ -41,8 +41,9 @@
 } scopeState_t;
 
 static volatile bool scopesUpdatingFlag, scopesDisplayingFlag;
-static uint32_t scopeTimeLen, scopeTimeLenFrac;
-static uint64_t timeNext64, timeNext64Frac;
+static int32_t oldPeriod;
+static uint32_t oldDFrq, scopeTimeLen, scopeTimeLenFrac;
+static uint64_t oldSFrq, timeNext64, timeNext64Frac;
 static volatile scope_t scope[MAX_VOICES];
 static SDL_Thread *scopeThread;
 static uint8_t *scopeMuteBMP_Ptrs[16];
@@ -49,6 +50,13 @@
 
 lastChInstr_t lastChInstr[MAX_VOICES]; // global
 
+void resetCachedScopeVars(void)
+{
+	oldPeriod = -1;
+	oldSFrq = 0;
+	oldDFrq = 0;
+}
+
 int32_t getSamplePosition(uint8_t ch)
 {
 	volatile bool active, sample16Bit;
@@ -202,7 +210,7 @@
 
 void refreshScopes(void)
 {
-	for (int16_t i = 0; i < MAX_VOICES; i++)
+	for (int32_t i = 0; i < MAX_VOICES; i++)
 		scope[i].wasCleared = false;
 }
 
@@ -292,7 +300,7 @@
 			if (mouse.x >= x && mouse.x < x+scopeLens[i])
 				break;
 
-			x += scopeLens[i] + 3;
+			x += scopeLens[i]+3;
 		}
 
 		if (i == chansPerRow)
@@ -309,7 +317,7 @@
 	return false;
 }
 
-static void scopeTrigger(int32_t ch, sampleTyp *s, int32_t playOffset)
+static void scopeTrigger(int32_t ch, const sampleTyp *s, int32_t playOffset)
 {
 	bool sampleIs16Bit;
 	uint8_t loopType;
@@ -387,15 +395,13 @@
 static void updateScopes(void)
 {
 	int32_t loopOverflowVal;
-	volatile scope_t *sc;
-	scope_t tempState;
 
 	scopesUpdatingFlag = true;
 
-	sc = scope;
+	volatile scope_t *sc = scope;
 	for (int32_t i = 0; i < song.antChn; i++, sc++)
 	{
-		tempState = *sc; // cache it
+		scope_t tempState = *sc; // cache it
 		if (!tempState.active)
 			continue; // scope is not active, no need
 
@@ -402,17 +408,15 @@
 		// scope position update
 
 		tempState.SPosDec += tempState.SFrq;
-
 		const uint32_t posAdd = tempState.SPosDec >> SCOPE_FRAC_BITS;
+		tempState.SPosDec &= SCOPE_FRAC_MASK;
+
 		if (tempState.backwards)
 			tempState.SPos -= posAdd;
 		else
 			tempState.SPos += posAdd;
 
-		tempState.SPosDec &= SCOPE_FRAC_MASK;
-
 		// handle loop wrapping or sample end
-
 		if (tempState.backwards && tempState.SPos < tempState.SRepS) // sampling backwards (definitely pingpong loop)
 		{
 			tempState.backwards = false; // change direction to forwards
@@ -460,9 +464,7 @@
 	const uint16_t *scopeLens;
 	uint16_t scopeXOffs, scopeYOffs, scopeDrawLen;
 	int32_t chansPerRow;
-	volatile scope_t *sc;
-	scope_t s;
-
+	
 	scopesDisplayingFlag = true;
 	chansPerRow = (uint32_t)song.antChn >> 1;
 
@@ -482,18 +484,16 @@
 		}
 
 		scopeDrawLen = scopeLens[i];
-
 		if (!editor.chnMode[i]) // scope muted (mute graphics blit()'ed elsewhere)
 		{
-			scopeXOffs += scopeDrawLen + 3; // align x to next scope
+			scopeXOffs += scopeDrawLen+3; // align x to next scope
 			continue;
 		}
 
-		s = scope[i]; // cache scope to lower thread race condition issues
+		const scope_t s = scope[i]; // cache scope to lower thread race condition issues
 		if (s.active && s.SVol > 0 && !audio.locked)
 		{
 			// scope is active
-
 			scope[i].wasCleared = false;
 
 			// clear scope background
@@ -506,8 +506,7 @@
 		else
 		{
 			// scope is inactive
-
-			sc = &scope[i];
+			volatile scope_t *sc = &scope[i];
 			if (!sc->wasCleared)
 			{
 				// clear scope background
@@ -528,7 +527,7 @@
 		if (config.multiRecChn[i])
 			blit(scopeXOffs + 1, scopeYOffs + 31, bmp.scopeRec, 13, 4);
 
-		scopeXOffs += scopeDrawLen + 3; // align x to next scope
+		scopeXOffs += scopeDrawLen+3; // align x to next scope
 	}
 
 	scopesDisplayingFlag = false;
@@ -537,7 +536,7 @@
 void drawScopeFramework(void)
 {
 	drawFramework(0, 92, 291, 81, FRAMEWORK_TYPE1);
-	for (uint8_t i = 0; i < song.antChn; i++)
+	for (int32_t i = 0; i < song.antChn; i++)
 		redrawScope(i);
 }
 
@@ -544,11 +543,9 @@
 void handleScopesFromChQueue(chSyncData_t *chSyncData, uint8_t *scopeUpdateStatus)
 {
 	uint8_t status;
-	syncedChannel_t *ch;
-	volatile scope_t *sc;
 
-	sc = scope;
-	ch = chSyncData->channels;
+	volatile scope_t *sc = scope;
+	syncedChannel_t *ch = chSyncData->channels;
 	for (int32_t i = 0; i < song.antChn; i++, sc++, ch++)
 	{
 		status = scopeUpdateStatus[i];
@@ -558,8 +555,23 @@
 
 		if (status & IS_Period)
 		{
-			sc->SFrq = getScopeFrequenceValue(ch->finalPeriod);
-			sc->DFrq = (uint32_t)(sc->SFrq >> (SCOPE_FRAC_BITS - 10)); // amount of samples to skip after drawing a pixel
+			// use cached values if possible
+
+			const uint16_t period = ch->finalPeriod;
+			if (period != oldPeriod)
+			{
+				oldPeriod = period;
+				const double dHz = period2Hz(period);
+
+				const double dScopeRateFactor = SCOPE_FRAC_SCALE / (double)SCOPE_HZ;
+				oldSFrq = (int64_t)((dHz * dScopeRateFactor) + 0.5); // Hz -> rounded fixed-point delta
+
+				const double dRelativeHz = dHz * (1.0 / (8363.0 / 2.0));
+				oldDFrq = (int32_t)((dRelativeHz * SCOPE_DRAW_FRAC_SCALE) + 0.5); // Hz -> rounded fixed-point draw delta
+			}
+
+			sc->SFrq = oldSFrq;
+			sc->DFrq = oldDFrq;
 		}
 
 		if (status & IS_NyTon)
--- a/src/ft2_scopes.h
+++ b/src/ft2_scopes.h
@@ -9,6 +9,12 @@
 #define SCOPE_FRAC_SCALE (1ULL << SCOPE_FRAC_BITS)
 #define SCOPE_FRAC_MASK (SCOPE_FRAC_SCALE-1)
 
+// just about max safe bits, don't mess with it!
+#define SCOPE_DRAW_FRAC_BITS 20
+#define SCOPE_DRAW_FRAC_SCALE (1UL << SCOPE_DRAW_FRAC_BITS)
+#define SCOPE_DRAW_FRAC_MASK (SCOPE_DRAW_FRAC_SCALE-1)
+
+void resetCachedScopeVars(void);
 int32_t getSamplePosition(uint8_t ch);
 void stopAllScopes(void);
 void refreshScopes(void);
--- a/vs2019_project/ft2-clone/ft2-clone.vcxproj.filters
+++ b/vs2019_project/ft2-clone/ft2-clone.vcxproj.filters
@@ -22,7 +22,6 @@
     <ClCompile Include="..\..\src\ft2_unicode.c" />
     <ClCompile Include="..\..\src\ft2_midi.c" />
     <ClCompile Include="..\..\src\ft2_wav_renderer.c" />
-    <ClCompile Include="..\..\src\ft2_about.c" />
     <ClCompile Include="..\..\src\ft2_trim.c" />
     <ClCompile Include="..\..\src\ft2_checkboxes.c" />
     <ClCompile Include="..\..\src\ft2_pushbuttons.c" />
@@ -77,6 +76,7 @@
       <Filter>graphics</Filter>
     </ClCompile>
     <ClCompile Include="..\..\src\ft2_structs.c" />
+    <ClCompile Include="..\..\src\ft2_about.c" />
   </ItemGroup>
   <ItemGroup>
     <ClInclude Include="..\..\src\ft2_audio.h">