shithub: pt2-clone

Download patch

ref: 5d1114967dc5dee345cc8823e3e53df2ff15a1c5
parent: 2b8c2daab5c67ce90aff2a65f330bd4511256428
author: Olav Sørensen <olav.sorensen@live.no>
date: Tue Jun 23 15:46:25 EDT 2020

Pushed v1.20 code

- Song BPM is now more accurate (it used to be slightly off for some BPMs).
  Please note that every Amiga's system clock is off by a very small fraction.
  This means that the true PT BPM (based on CIA) differs very slightly from
  machine to machine, so the PT2 clone's BPM will not precisely match a real
  Amiga.
- Added a delay when using left/right/delete/backspace keys in text edit mode.
  This makes it harder to do accidents!
- Bugfix: MOD2WAV didn't stop rendering even if a F00 pattern command was
  triggered.
- Bugfix: PAT2SMP would sometimes not render the very last song tick

--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -48,7 +48,7 @@
 static bool amigaPanFlag;
 static uint16_t ch1Pan, ch2Pan, ch3Pan, ch4Pan;
 static int32_t oldPeriod = -1, randSeed = INITIAL_DITHER_SEED;
-static uint32_t sampleCounter, audLatencyPerfValInt, audLatencyPerfValFrac;
+static uint32_t audLatencyPerfValInt, audLatencyPerfValFrac;
 static uint64_t tickTime64, tickTime64Frac;
 static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned, dOldVoiceDelta, dOldVoiceDeltaMul;
 static double dPrngStateL, dPrngStateR;
@@ -62,7 +62,6 @@
 
 // globalized
 audio_t audio;
-uint32_t samplesPerTick;
 paulaVoice_t paula[AMIGA_VOICES];
 
 bool intMusic(void); // defined in pt_modplayer.c
@@ -95,26 +94,16 @@
 	tickTimeLenFrac = timeLenFrac;
 }
 
-static uint16_t bpm2SmpsPerTick(int32_t bpm, double dAudioFreq)
-{
-	if (bpm == 0)
-		return 0;
-
-	const int32_t ciaVal = (int32_t)(1773447 / bpm); // yes, PT truncates here
-	const double dCiaHz = (double)CIA_PAL_CLK / ciaVal;
-
-	int32_t smpsPerTick = (int32_t)((dAudioFreq / dCiaHz) + 0.5); // rounded
-	return (uint16_t)smpsPerTick;
-}
-
 static void generateBpmTables(void)
 {
 	for (int32_t i = 32; i <= 255; i++)
 	{
-		audio.bpmTab[i-32] = bpm2SmpsPerTick(i, audio.outputRate);
-		audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, PAT2SMP_HI_FREQ); // PAT2SMP hi quality
-		audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, PAT2SMP_LO_FREQ); // PAT2SMP low quality
-		audio.bpmTabMod2Wav[i-32] = bpm2SmpsPerTick(i, MOD2WAV_FREQ); // MOD2WAV
+		const double dBpmHz = i / 2.5;
+
+		audio.bpmTab[i-32] = audio.outputRate / dBpmHz;
+		audio.bpmTab28kHz[i-32] = PAT2SMP_HI_FREQ / dBpmHz; // PAT2SMP hi quality
+		audio.bpmTab22kHz[i-32] = PAT2SMP_LO_FREQ / dBpmHz; // PAT2SMP low quality
+		audio.bpmTabMod2Wav[i-32] = MOD2WAV_FREQ / dBpmHz; // MOD2WAV
 	}
 }
 
@@ -1065,9 +1054,6 @@
 
 static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
 {
-	int16_t *streamOut;
-	uint32_t samplesLeft;
-
 	if (audio.forceMixerOff) // during MOD2WAV
 	{
 		memset(stream, 0, len);
@@ -1074,12 +1060,12 @@
 		return;
 	}
 
-	streamOut = (int16_t *)stream;
+	int16_t *streamOut = (int16_t *)stream;
 
-	samplesLeft = len >> 2;
+	int32_t samplesLeft = len >> 2;
 	while (samplesLeft > 0)
 	{
-		if (sampleCounter == 0)
+		if (audio.dTickSampleCounter <= 0.0)
 		{
 			// new replayer tick
 
@@ -1089,18 +1075,20 @@
 				fillVisualsSyncBuffer();
 			}
 
-			sampleCounter = samplesPerTick;
+			audio.dTickSampleCounter += audio.dSamplesPerTick;
 		}
 
-		uint32_t samplesTodo = sampleCounter;
-		if (samplesTodo > samplesLeft)
-			samplesTodo = samplesLeft;
+		const int32_t remainingTick = (int32_t)ceil(audio.dTickSampleCounter);
 
-		outputAudio(streamOut, samplesTodo);
-		streamOut += samplesTodo << 1;
+		int32_t samplesToMix = samplesLeft;
+		if (samplesToMix > remainingTick)
+			samplesToMix = remainingTick;
 
-		samplesLeft -= samplesTodo;
-		sampleCounter -= samplesTodo;
+		outputAudio(streamOut, samplesToMix);
+		streamOut += samplesToMix << 1;
+
+		samplesLeft -= samplesToMix;
+		audio.dTickSampleCounter -= samplesToMix;
 	}
 
 	(void)userdata;
@@ -1254,11 +1242,11 @@
 	** won't overflow.
 	*/
 
-	uint32_t maxSamplesToMix;
+	int32_t maxSamplesToMix;
 	if (MOD2WAV_FREQ > audio.outputRate)
-		maxSamplesToMix = audio.bpmTabMod2Wav[32-32]; // BPM 32
+		maxSamplesToMix = (int32_t)ceil(audio.bpmTabMod2Wav[32-32]); // BPM 32
 	else
-		maxSamplesToMix = audio.bpmTab[32-32]; // BPM 32
+		maxSamplesToMix = (int32_t)ceil(audio.bpmTab[32-32]); // BPM 32
 
 	dMixBufferLUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
 	dMixBufferRUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
@@ -1278,15 +1266,17 @@
 	filterFlags = config.a500LowPassFilter ? FILTER_A500 : 0;
 	calculateFilterCoeffs();
 
-	samplesPerTick = audio.bpmTab[125-32]; // BPM 125
-	sampleCounter = 0;
+	audio.dSamplesPerTick = audio.bpmTab[125-32]; // BPM 125
+	audio.dTickSampleCounter = 0;
 
 	calcAudioLatencyVars(audio.audioBufferSize, audio.outputRate);
-	for (int32_t i = 0; i < 256-32; i++)
+
+	audio.tickTimeLengthTab[0] = UINT64_MAX;
+	const double dMul = (editor.dPerfFreq / audio.outputRate) * (UINT32_MAX + 1.0);
+	for (int32_t i = 1; i < 256-32; i++)
 	{
 		// number of samples per tick -> tick length for performance counter (syncing visuals to audio)
-		const double dTickTimeLenMul = (editor.dPerfFreq / audio.outputRate) * (UINT32_MAX + 1.0);
-		audio.tickTimeLengthTab[i] = (uint64_t)(audio.bpmTab[i] * dTickTimeLenMul);
+		audio.tickTimeLengthTab[i] = (uint64_t)(audio.bpmTab[i] * dMul);
 	}
 
 	audio.resetSyncTickTimeFlag = true;
@@ -1314,16 +1304,6 @@
 		free(dMixBufferRUnaligned);
 		dMixBufferRUnaligned = NULL;
 	}
-}
-
-void mixerSetSamplesPerTick(uint32_t val)
-{
-	samplesPerTick = val;
-}
-
-void mixerClearSampleCounter(void)
-{
-	sampleCounter = 0;
 }
 
 void toggleAmigaPanMode(void)
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -18,10 +18,12 @@
 	volatile bool locked, isSampling;
 
 	bool forceMixerOff;
-	uint16_t bpmTab[256-32], bpmTab28kHz[256-32], bpmTab22kHz[256-32], bpmTabMod2Wav[256-32];
+	double bpmTab[256-32], bpmTab28kHz[256-32], bpmTab22kHz[256-32], bpmTabMod2Wav[256-32];
 	uint32_t outputRate, audioBufferSize;
 	double dPeriodToDeltaDiv;
 
+	double dSamplesPerTick, dTickSampleCounter;
+
 	// for audio sampling
 	bool rescanAudioDevicesSupported;
 
@@ -75,8 +77,6 @@
 void mixerKillVoice(int32_t ch);
 void turnOffVoices(void);
 void mixerCalcVoicePans(uint8_t stereoSeparation);
-void mixerSetSamplesPerTick(uint32_t val);
-void mixerClearSampleCounter(void);
 void outputAudio(int16_t *target, int32_t numSamples);
 
 extern audio_t audio; // pt2_audio.c
--- 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.19"
+#define PROG_VER_STR "1.20"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -30,6 +30,7 @@
 #include "pt2_config.h"
 #include "pt2_sampling.h"
 
+
 #if defined _WIN32 && !defined _DEBUG
 extern bool windowsKeyIsDown;
 extern HHOOK g_hKeyboardHook;
@@ -3198,11 +3199,18 @@
 		{
 			if (ui.editTextFlag)
 			{
-				if (keyb.repeatCounter >= 4)
+				if (keyb.delayCounter >= KEYB_REPEAT_DELAY)
 				{
-					keyb.repeatCounter = 0;
-					textCharPrevious();
+					if (keyb.repeatCounter >= 3)
+					{
+						keyb.repeatCounter = 0;
+						textCharPrevious();
+					}
 				}
+				else
+				{
+					keyb.delayCounter++;
+				}
 			}
 			else
 			{
@@ -3248,11 +3256,18 @@
 		{
 			if (ui.editTextFlag)
 			{
-				if (keyb.repeatCounter >= 4)
+				if (keyb.delayCounter >= KEYB_REPEAT_DELAY)
 				{
-					keyb.repeatCounter = 0;
-					textCharNext();
+					if (keyb.repeatCounter >= 3)
+					{
+						keyb.repeatCounter = 0;
+						textCharNext();
+					}
 				}
+				else
+				{
+					keyb.delayCounter++;
+				}
 			}
 			else
 			{
@@ -3406,11 +3421,19 @@
 			if (ui.editTextFlag)
 			{
 				// only repeat backspace while editing texts
-				if (keyb.repeatCounter >= 3)
+
+				if (keyb.delayCounter >= KEYB_REPEAT_DELAY)
 				{
-					keyb.repeatCounter = 0;
-					keyDownHandler(scancode, 0);
+					if (keyb.repeatCounter >= 3)
+					{
+						keyb.repeatCounter = 0;
+						keyDownHandler(scancode, 0);
+					}
 				}
+				else
+				{
+					keyb.delayCounter++;
+				}
 			}
 		}
 		break;
@@ -4202,7 +4225,7 @@
 					keyb.delayCounter = 0;
 
 				keyb.repeatKey = true;
-				keyb.delayKey = false;
+				keyb.delayKey = true;
 
 				updateTextObject(ui.editObject);
 			}
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -19,7 +19,6 @@
 
 void storeTempVariables(void); // pt_modplayer.c
 bool intMusic(void); // pt_modplayer.c
-extern uint32_t samplesPerTick; // pt_audio.c
 
 static volatile bool wavRenderingDone;
 static int16_t *mod2WavBuffer;
@@ -26,13 +25,12 @@
 
 static void calcMod2WavTotalRows(void);
 
-static uint32_t getAudioFrame(int16_t *outStream)
+static void renderSamples(int32_t samplesPerTick, int16_t *outStream)
 {
 	if (!intMusic())
 		wavRenderingDone = true;
 
 	outputAudio(outStream, samplesPerTick);
-	return samplesPerTick;
 }
 
 static int32_t SDLCALL mod2WavThreadFunc(void *ptr)
@@ -50,6 +48,8 @@
 	uint8_t loopCounter = 8;
 	uint32_t totalSampleCounter = 0;
 
+	double dTickSamples = audio.dSamplesPerTick;
+
 	bool renderDone = false;
 	while (!renderDone)
 	{
@@ -59,18 +59,21 @@
 		int16_t *ptr16 = mod2WavBuffer;
 		for (uint32_t i = 0; i < TICKS_PER_RENDER_CHUNK; i++)
 		{
-			if (!editor.isWAVRendering || wavRenderingDone || editor.abortMod2Wav)
+			if (!editor.isWAVRendering || wavRenderingDone || editor.abortMod2Wav || !editor.songPlaying)
 			{
 				renderDone = true;
 				break;
 			}
 
-			uint32_t tickSamples = getAudioFrame(ptr16) << 1; // *2 for stereo
+			int32_t tickSamples = (int32_t)dTickSamples;
+			renderSamples(tickSamples, ptr16);
+			
+			dTickSamples -= tickSamples; // keep fractional part
+			dTickSamples += audio.dSamplesPerTick;
 
+			tickSamples *= 2; // stereo
 			samplesInChunk += tickSamples;
 			totalSampleCounter += tickSamples;
-
-			// increase buffer pointer
 			ptr16 += tickSamples;
 
 			if (++loopCounter >= 8)
@@ -154,7 +157,7 @@
 		return false;
 	}
 
-	const uint32_t maxSamplesToMix = TICKS_PER_RENDER_CHUNK * audio.bpmTabMod2Wav[32-32]; // BPM 32, stereo
+	const int32_t maxSamplesToMix = (int32_t)ceil(TICKS_PER_RENDER_CHUNK * audio.bpmTabMod2Wav[32-32]); // BPM 32, stereo
 
 	mod2WavBuffer = (int16_t *)malloc(maxSamplesToMix * (2 * sizeof (int16_t)));
 	if (mod2WavBuffer == NULL)
--- a/src/pt2_module_loader.c
+++ b/src/pt2_module_loader.c
@@ -938,7 +938,7 @@
 
 	editor.editMoveAdd = 1;
 	editor.currSample = 0;
-	editor.musicTime = 0;
+	editor.musicTime64 = 0;
 	editor.modLoaded = true;
 	editor.blockMarkFlag = false;
 	editor.sampleZero = false;
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -14,7 +14,6 @@
 #include "pt2_textout.h"
 
 bool intMusic(void); // pt_modplayer.c
-extern uint32_t samplesPerTick; // pt_audio.c
 void storeTempVariables(void); // pt_modplayer.c
 
 void doPat2Smp(void)
@@ -36,8 +35,7 @@
 		return;
 	}
 
-	int8_t oldRow = editor.songPlaying ? 0 : song->currRow;
-	uint32_t oldSamplesPerTick = samplesPerTick;
+	const int8_t oldRow = editor.songPlaying ? 0 : song->currRow;
 
 	editor.isSMPRendering = true; // this must be set before restartSong()
 	storeTempVariables();
@@ -51,21 +49,28 @@
 	modSetTempo(song->currBPM, true);
 	editor.pat2SmpPos = 0;
 
+	double dTickSamples = audio.dSamplesPerTick;
+
 	editor.smpRenderingDone = false;
 	while (!editor.smpRenderingDone)
 	{
-		if (!intMusic())
+		const int32_t tickSamples = (int32_t)dTickSamples;
+
+		const bool ended = !intMusic() || !editor.songPlaying;
+		outputAudio(NULL, tickSamples);
+
+		dTickSamples -= tickSamples; // keep fractional part
+		dTickSamples += audio.dSamplesPerTick;
+
+		if (ended)
 			editor.smpRenderingDone = true;
 
-		outputAudio(NULL, samplesPerTick);
 	}
 	editor.isSMPRendering = false;
 	resetSong();
 
-	// set back old row and samplesPerTick
-	song->row = oldRow;
-	song->currRow = song->row;
-	mixerSetSamplesPerTick(oldSamplesPerTick);
+	// set back old row
+	song->currRow = song->row = oldRow;
 
 	normalize16bitSigned(editor.pat2SmpBuf, MIN(editor.pat2SmpPos, MAX_SAMPLE_LEN));
 
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -977,8 +977,8 @@
 	uint16_t *patt;
 	moduleChannel_t *c;
 
-	if (modBPM > 0)
-		editor.musicTime += (65536 / modBPM); // for playback counter
+	if (modBPM >= 32 && modBPM <= 255)
+		editor.musicTime64 += musicTimeTab64[modBPM-32]; // for playback counter
 
 	if (updateUIPositions)
 	{
@@ -1181,8 +1181,6 @@
 
 void modSetTempo(uint16_t bpm, bool doLockAudio)
 {
-	uint32_t smpsPerTick;
-
 	if (bpm < 32)
 		return;
 
@@ -1200,14 +1198,15 @@
 
 	bpm -= 32; // 32..255 -> 0..223
 
+	double dSamplesPerTick;
 	if (editor.isSMPRendering)
-		smpsPerTick = editor.pat2SmpHQ ? audio.bpmTab28kHz[bpm] : audio.bpmTab22kHz[bpm];
+		dSamplesPerTick = editor.pat2SmpHQ ? audio.bpmTab28kHz[bpm] : audio.bpmTab22kHz[bpm];
 	else if (editor.isWAVRendering)
-		smpsPerTick = audio.bpmTabMod2Wav[bpm];
+		dSamplesPerTick = audio.bpmTabMod2Wav[bpm];
 	else
-		smpsPerTick = audio.bpmTab[bpm];
+		dSamplesPerTick = audio.bpmTab[bpm];
 
-	mixerSetSamplesPerTick(smpsPerTick);
+	audio.dSamplesPerTick = dSamplesPerTick;
 
 	// calculate tick time length for audio/video sync timestamp
 	const uint64_t tickTimeLen64 = audio.tickTimeLengthTab[bpm];
@@ -1251,7 +1250,7 @@
 	if (!editor.stepPlayEnabled)
 		pointerSetMode(POINTER_MODE_PLAY, DO_CARRY);
 
-	mixerClearSampleCounter();
+	audio.dTickSampleCounter = 0.0; // zero tick sample counter so that it will instantly initiate a tick
 
 	song->currRow = song->row = startRow & 0x3F;
 	song->tick = song->speed;
@@ -1290,9 +1289,13 @@
 {
 	uint8_t oldPlayMode, oldMode;
 
+	const bool audioWasntLocked = !audio.locked;
+	if (audioWasntLocked)
+		lockAudio();
+
 	doStopIt(false);
 	turnOffVoices();
-	mixerClearSampleCounter();
+	audio.dTickSampleCounter = 0.0; // zero tick sample counter so that it will instantly initiate a tick
 
 	if (row != -1)
 	{
@@ -1347,8 +1350,11 @@
 	modHasBeenPlayed = false;
 	editor.songPlaying = true;
 	editor.didQuantize = false;
-	editor.musicTime = 0;
+	editor.musicTime64 = 0;
 
+	if (audioWasntLocked)
+		unlockAudio();
+
 	if (!editor.isSMPRendering && !editor.isWAVRendering)
 	{
 		ui.updateSongPos = true;
@@ -1381,7 +1387,7 @@
 	editor.f9Pos = 48;
 	editor.f10Pos = 63;
 
-	editor.musicTime = 0;
+	editor.musicTime64 = 0;
 
 	editor.metroFlag = false;
 	editor.currSample = 0;
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -178,7 +178,8 @@
 	uint16_t effectMacros[10], oldTempo, currPlayNote, vol1, vol2, lpCutOff, hpCutOff;
 	uint16_t smpRedoLoopStarts[MOD_SAMPLES], smpRedoLoopLengths[MOD_SAMPLES], smpRedoLengths[MOD_SAMPLES];
 	int32_t modulatePos, modulateOffset, markStartOfs, markEndOfs, pat2SmpPos;
-	uint32_t musicTime, vblankTimeLen, vblankTimeLenFrac;
+	uint32_t vblankTimeLen, vblankTimeLenFrac;
+	uint64_t musicTime64;
 	double dPerfFreq, dPerfFreqMulMicro;
 	note_t trackBuffer[MOD_ROWS], cmdsBuffer[MOD_ROWS], blockBuffer[MOD_ROWS];
 	note_t patternBuffer[MOD_ROWS * AMIGA_VOICES], undoBuffer[MOD_ROWS * AMIGA_VOICES];
--- a/src/pt2_tables.c
+++ b/src/pt2_tables.c
@@ -156,6 +156,52 @@
 	8739,9253,24625,12851,13365
 };
 
+/*
+** const double dBpmMs = 1000.0 / (bpm / 2.5);
+** x = (uint64_t)round((UINT32_MAX + 1.0) * dBpmMs);
+*/
+const uint64_t musicTimeTab64[256-32] =
+{
+	0x4e20000000,0x4bc1f07c1f,0x4987878788,0x476db6db6e,0x4571c71c72,0x43914c1bad,
+	0x41ca1af287,0x401a41a41a,0x3e80000000,0x3cf9c18f9c,0x3b86186186,0x3a23b88ee2,
+	0x38d1745d17,0x378e38e38e,0x36590b2164,0x3531057262,0x3415555555,0x330539782a,
+	0x3200000000,0x3105050505,0x3013b13b14,0x2f2b78c135,0x2e4bda12f7,0x2d745d1746,
+	0x2ca4924925,0x2bdc11f704,0x2b1a7b9612,0x2a5f75270d,0x29aaaaaaab,0x28fbcda3ac,
+	0x285294a529,0x27aebaebaf,0x2710000000,0x2676276276,0x25e0f83e10,0x25503d2263,
+	0x24c3c3c3c4,0x243b5cc0ed,0x23b6db6db7,0x233615a241,0x22b8e38e39,0x223f1f8fc8,
+	0x21c8a60dd6,0x2155555555,0x20e50d7943,0x2077b03532,0x200d20d20d,0x1fa5440cf6,
+	0x1f40000000,0x1edd3c0ca4,0x1e7ce0c7ce,0x1e1ed7e753,0x1dc30c30c3,0x1d69696969,
+	0x1d11dc4771,0x1cbc52640c,0x1c68ba2e8c,0x1c1702e05c,0x1bc71c71c7,0x1b78f78f79,
+	0x1b2c8590b2,0x1ae1b86e1c,0x1a9882b931,0x1a50d79436,0x1a0aaaaaab,0x19c5f02a3a,
+	0x19829cbc15,0x1940a57eb5,0x1900000000,0x18c0a237c3,0x1882828283,0x1845979c95,
+	0x1809d89d8a,0x17cf3cf3cf,0x1795bc609b,0x175d4ef40a,0x1725ed097b,0x16ef8f441c,
+	0x16ba2e8ba3,0x1685c4093a,0x1652492492,0x161fb78122,0x15ee08fb82,0x15bd37a6f5,
+	0x158d3dcb09,0x155e15e15e,0x152fba9387,0x150226b902,0x14d5555555,0x14a9419637,
+	0x147de6d1d6,0x1453408534,0x14294a5295,0x1400000000,0x13d75d75d7,0x13af5ebd7b,
+	0x1388000000,0x13613d84f6,0x133b13b13b,0x13157f05dd,0x12f07c1f08,0x12cc07b302,
+	0x12a81e9132,0x1284bda12f,0x1261e1e1e2,0x123f8868a4,0x121dae6077,0x11fc510935,
+	0x11db6db6db,0x11bb01d0cb,0x119b0ad120,0x117b864407,0x115c71c71c,0x113dcb08d4,
+	0x111f8fc7e4,0x1101bdd2b9,0x10e45306eb,0x10c74d50c0,0x10aaaaaaab,0x108e691cd2,
+	0x107286bca2,0x105701ac57,0x103bd81a99,0x1021084211,0x1006906907,0x0fec6ee105,
+	0x0fd2a2067b,0x0fb9284067,0x0fa0000000,0x0f8727c066,0x0f6e9e0652,0x0f56615fce,
+	0x0f3e7063e7,0x0f26c9b26d,0x0f0f6bf3aa,0x0ef855d825,0x0ee1861862,0x0ecafb74a4,
+	0x0eb4b4b4b5,0x0e9eb0a7ac,0x0e88ee23b9,0x0e736c05eb,0x0e5e293206,0x0e49249249,
+	0x0e345d1746,0x0e1fd1b7af,0x0e0b81702e,0x0df76b4338,0x0de38e38e4,0x0dcfe95ec3,
+	0x0dbc7bc7bc,0x0da9448be4,0x0d9642c859,0x0d83759f23,0x0d70dc370e,0x0d5e75bb8d,
+	0x0d4c415c99,0x0d3a3e4e90,0x0d286bca1b,0x0d16c90c10,0x0d05555555,0x0cf40feac7,
+	0x0ce2f8151d,0x0cd20d20d2,0x0cc14e5e0a,0x0cb0bb207d,0x0ca052bf5b,0x0c9014953a,
+	0x0c80000000,0x0c701460cc,0x0c60511be2,0x0c50b59897,0x0c41414141,0x0c31f3831f,
+	0x0c22cbce4b,0x0c13c995a4,0x0c04ec4ec5,0x0bf63371ea,0x0be79e79e8,0x0bd92ce418,
+	0x0bcade304d,0x0bbcb1e0c0,0x0baea77a05,0x0ba0be82fa,0x0b92f684be,0x0b854f0a9e,
+	0x0b77c7a20e,0x0b6a5fda98,0x0b5d1745d1,0x0b4fed7750,0x0b42e2049d,0x0b35f4852a,
+	0x0b29249249,0x0b1c71c71c,0x0b0fdbc091,0x0b03621d52,0x0af7047dc1,0x0aeac283ea,
+	0x0ade9bd37a,0x0ad29011bb,0x0ac69ee584,0x0abac7f736,0x0aaf0af0af,0x0aa3677d47,
+	0x0a97dd49c3,0x0a8c6c0452,0x0a81135c81,0x0a75d30337,0x0a6aaaaaab,0x0a5f9a0660,
+	0x0a54a0cb1c,0x0a49beaee1,0x0a3ef368eb,0x0a343eb1a2,0x0a29a0429a,0x0a1f17d68b,
+	0x0a14a5294a,0x0a0a47f7c6,0x0a00000000,0x09f5cd0105,0x09ebaebaec,0x09e1a4eecc,
+	0x09d7af5ebd,0x09cdcdcdce
+};
+
 // button tables taken from the ptplay project + modified
 
 const guiButton_t bAsk[] =
--- a/src/pt2_tables.h
+++ b/src/pt2_tables.h
@@ -17,6 +17,7 @@
 extern const uint8_t arpTickTable[32];
 extern const int16_t periodTable[(37*16)+15];
 extern int8_t pNoteTable[32];
+extern const uint64_t musicTimeTab64[256-32];
 
 // changable by config file
 extern uint16_t analyzerColors[36];
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -507,7 +507,7 @@
 void updateSongInfo2(void) // two middle rows of screen, always present
 {
 	char tempChar;
-	int32_t secs, MI_TimeM, MI_TimeS, x, i;
+	int32_t x, i;
 	moduleSample_t *currSample;
 
 	if (ui.updateStatusText)
@@ -601,13 +601,15 @@
 
 	// playback timer
 
-	secs = ((editor.musicTime >> 8) * 5) >> 9;
-	secs -= ((secs / 3600) * 3600);
+	const uint32_t milliseconds = editor.musicTime64 >> 32;
+	uint32_t seconds = milliseconds / 1000;
 
-	if (secs <= 5999) // below 99 minutes 59 seconds
+	seconds -= ((seconds / 3600) * 3600); // remove hours
+	if (seconds <= 5999) // below 99 minutes 59 seconds
 	{
-		MI_TimeM = secs / 60;
-		MI_TimeS = secs - (MI_TimeM * 60);
+		uint32_t MI_TimeM = seconds / 60;
+		seconds -= MI_TimeM * 60; // remove minutes
+		uint32_t MI_TimeS = seconds;
 
 		// xx:xx
 		printTwoDecimalsBg(272, 102, MI_TimeM, video.palette[PAL_GENTXT], video.palette[PAL_GENBKG]);