shithub: pt2-clone

Download patch

ref: bc87dfb3f1cc5068df05eeb898c4eb3b8b48fa85
parent: 7442f4167c59bc041ba46fd0b0fee592a5de282f
author: Olav Sørensen <olav.sorensen@live.no>
date: Sun Apr 26 16:12:28 EDT 2020

Pushed v1.11 code

- The BLEP synthesis has been improved for slightly less resampling aliasing.
  aciddose (the writer of the BLEP implementation) is currently working on it
  to see if it can be further improved, but as for now this should in theory be
  a little bit better than the old one.
- Bugfix: Reset play mode to "Play song" when double-clicking files associated
  with the PT2 clone.
- Bugfix: The MOD2WAV buffer was too small and could cause nasty issues when
  rendering a song with low BPM values.
- MOD2WAV now renders the song at 96kHz, so that the user has more resolution
  before converting the WAV to the format/frequency of choice.
- Don't include high-pass filter and dithering in PAT2SMP
- Code cleanup

--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -30,6 +30,7 @@
 #include "pt2_textout.h"
 #include "pt2_visuals.h"
 #include "pt2_scopes.h"
+#include "pt2_mod2wav.h"
 
 #define INITIAL_DITHER_SEED 0x12345000
 
@@ -49,10 +50,10 @@
 
 static volatile int8_t filterFlags;
 static int8_t defStereoSep;
-static bool amigaPanFlag, wavRenderingDone;
+static bool amigaPanFlag;
 static uint16_t ch1Pan, ch2Pan, ch3Pan, ch4Pan;
-static int32_t oldPeriod, sampleCounter, randSeed = INITIAL_DITHER_SEED;
-static uint32_t oldScopeDelta;
+static int32_t oldPeriod, randSeed = INITIAL_DITHER_SEED;
+static uint32_t oldScopeDelta, sampleCounter;
 static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned, dOldVoiceDelta, dOldVoiceDeltaMul;
 static double dPrngStateL, dPrngStateR;
 static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES];
@@ -62,13 +63,10 @@
 static SDL_AudioDeviceID dev;
 
 // globalized
-int32_t samplesPerTick;
+uint32_t samplesPerTick;
 
 bool intMusic(void); // defined in pt_modplayer.c
-void storeTempVariables(void); // defined in pt_modplayer.c
 
-void calcMod2WavTotalRows(void);
-
 static uint16_t bpm2SmpsPerTick(uint32_t bpm, uint32_t audioFreq)
 {
 	if (bpm == 0)
@@ -77,7 +75,7 @@
 	const uint32_t ciaVal = (uint32_t)(1773447 / bpm); // yes, PT truncates here
 	const double dCiaHz = (double)CIA_PAL_CLK / ciaVal;
 
-	int32_t smpsPerTick = (int32_t)((audioFreq / dCiaHz) + 0.5);
+	int32_t smpsPerTick = (int32_t)((audioFreq / dCiaHz) + 0.5); // rounded
 	return (uint16_t)smpsPerTick;
 }
 
@@ -86,8 +84,9 @@
 	for (uint32_t i = 32; i <= 255; i++)
 	{
 		audio.bpmTab[i-32] = bpm2SmpsPerTick(i, audio.outputRate);
-		audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, 28836);
-		audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, 22168);
+		audio.bpmTab28kHz[i-32] = bpm2SmpsPerTick(i, 28836); // PAT2SMP hi quality
+		audio.bpmTab22kHz[i-32] = bpm2SmpsPerTick(i, 22168); // PAT2SMP low quality
+		audio.bpmTabMod2Wav[i-32] = bpm2SmpsPerTick(i, MOD2WAV_FREQ); // MOD2WAV
 	}
 }
 
@@ -404,9 +403,11 @@
 #if SCOPE_HZ != 64
 #error Scope Hz is not 64 (2^n), change rate calc. to use doubles+round in pt2_scope.c
 #endif
-		// if we are rendering pattern to sample (PAT2SMP), use different frequencies
+		// 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);
+		else if (editor.isWAVRendering)
+			dPeriodToDeltaDiv = PAULA_PAL_CLK / (double)MOD2WAV_FREQ;
 		else
 			dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv;
 
@@ -594,8 +595,8 @@
 				bVol->dLastValue = dVol;
 			}
 
-			if (bSmp->samplesLeft > 0) dSmp += blepRun(bSmp);
-			if (bVol->samplesLeft > 0) dVol += blepRun(bVol);
+			if (bSmp->samplesLeft > 0) dSmp = blepRun(bSmp, dSmp);
+			if (bVol->samplesLeft > 0) dVol = blepRun(bVol, dVol);
 
 			dSmp *= dVol;
 
@@ -665,8 +666,8 @@
 				bVol->dLastValue = dVol;
 			}
 
-			if (bSmp->samplesLeft > 0) dSmp += blepRun(bSmp);
-			if (bVol->samplesLeft > 0) dVol += blepRun(bVol);
+			if (bSmp->samplesLeft > 0) dSmp = blepRun(bSmp, dSmp);
+			if (bVol->samplesLeft > 0) dVol = blepRun(bVol, dVol);
 
 			dSmp *= dVol;
 
@@ -724,8 +725,8 @@
 	RCHighPassFilter(&filterHi, dOut, dOut);
 
 	// normalize and flip phase (A500/A1200 has an inverted audio signal)
-	dOut[0] *= -(INT16_MAX / AMIGA_VOICES);
-	dOut[1] *= -(INT16_MAX / AMIGA_VOICES);
+	dOut[0] *= (-INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= (-INT16_MAX / (double)AMIGA_VOICES);
 
 	// left channel - 1-bit triangular dithering (high-pass filtered)
 	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
@@ -761,8 +762,8 @@
 	RCHighPassFilter(&filterHi, dOut, dOut);
 
 	// normalize and flip phase (A500/A1200 has an inverted audio signal)
-	dOut[0] *= -(INT16_MAX / AMIGA_VOICES);
-	dOut[1] *= -(INT16_MAX / AMIGA_VOICES);
+	dOut[0] *= (-INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= (-INT16_MAX / (double)AMIGA_VOICES);
 
 	// left channel - 1-bit triangular dithering (high-pass filtered)
 	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
@@ -795,8 +796,8 @@
 	// process high-pass filter
 	RCHighPassFilter(&filterHi, dOut, dOut);
 
-	dOut[0] *= -(INT16_MAX / AMIGA_VOICES);
-	dOut[1] *= -(INT16_MAX / AMIGA_VOICES);
+	dOut[0] *= (-INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= (-INT16_MAX / (double)AMIGA_VOICES);
 
 	// left channel - 1-bit triangular dithering (high-pass filtered)
 	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
@@ -832,8 +833,8 @@
 	// process high-pass filter
 	RCHighPassFilter(&filterHi, dOut, dOut);
 
-	dOut[0] *= -(INT16_MAX / AMIGA_VOICES);
-	dOut[1] *= -(INT16_MAX / AMIGA_VOICES);
+	dOut[0] *= (-INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= (-INT16_MAX / (double)AMIGA_VOICES);
 
 	// left channel - 1-bit triangular dithering (high-pass filtered)
 	dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
@@ -852,28 +853,53 @@
 	out[1] = (int16_t)smp32;
 }
 
+// for PAT2SMP
+static inline void processMixedSamplesRaw(int32_t i, int16_t *out)
+{
+	int32_t smp32;
+	double dOut[2];
+
+	dOut[0] = dMixBufferL[i];
+	dOut[1] = dMixBufferR[i];
+
+	// normalize 
+	dOut[0] *= (INT16_MAX / (double)AMIGA_VOICES);
+	dOut[1] *= (INT16_MAX / (double)AMIGA_VOICES);
+
+	dOut[0] = (dOut[0] + dOut[1]) * 0.5; // mix to mono
+
+	smp32 = (int32_t)dOut[0];
+	CLAMP16(smp32);
+	out[0] = (int16_t)smp32;
+}
+
 void outputAudio(int16_t *target, int32_t numSamples)
 {
 	int16_t *outStream, out[2];
-	int32_t j;
+	int32_t i;
 
 	if (editor.isSMPRendering)
 	{
 		// render to sample (PAT2SMP)
 
+		int32_t samplesTodo = numSamples;
+		if (editor.pat2SmpPos+samplesTodo > MAX_SAMPLE_LEN)
+			samplesTodo = MAX_SAMPLE_LEN-editor.pat2SmpPos;
+
 		mixChannelsMultiStep(numSamples);
 
-		for (j = 0; j < numSamples; j++)
+		outStream = &editor.pat2SmpBuf[editor.pat2SmpPos];
+		for (i = 0; i < samplesTodo; i++)
 		{
-			processMixedSamplesA1200(j, out);
-			editor.pat2SmpBuf[editor.pat2SmpPos++] = (int16_t)((out[0] + out[1]) >> 1); // mix to mono
+			processMixedSamplesA1200(i, out);
+			outStream[i] = (out[0] + out[1]) >> 1;
+		}
 
-			if (editor.pat2SmpPos >= MAX_SAMPLE_LEN)
-			{
-				editor.smpRenderingDone = true;
-				updateWindowTitle(MOD_IS_MODIFIED);
-				break;
-			}
+		editor.pat2SmpPos += samplesTodo;
+		if (editor.pat2SmpPos >= MAX_SAMPLE_LEN)
+		{
+			editor.smpRenderingDone = true;
+			updateWindowTitle(MOD_IS_MODIFIED);
 		}
 	}
 	else
@@ -889,9 +915,9 @@
 
 			if (filterFlags & FILTER_LED_ENABLED)
 			{
-				for (j = 0; j < numSamples; j++)
+				for (i = 0; i < numSamples; i++)
 				{
-					processMixedSamplesA500LED(j, out);
+					processMixedSamplesA500LED(i, out);
 					*outStream++ = out[0];
 					*outStream++ = out[1];
 				}
@@ -898,9 +924,9 @@
 			}
 			else
 			{
-				for (j = 0; j < numSamples; j++)
+				for (i = 0; i < numSamples; i++)
 				{
-					processMixedSamplesA500(j, out);
+					processMixedSamplesA500(i, out);
 					*outStream++ = out[0];
 					*outStream++ = out[1];
 				}
@@ -912,9 +938,9 @@
 
 			if (filterFlags & FILTER_LED_ENABLED)
 			{
-				for (j = 0; j < numSamples; j++)
+				for (i = 0; i < numSamples; i++)
 				{
-					processMixedSamplesA1200LED(j, out);
+					processMixedSamplesA1200LED(i, out);
 					*outStream++ = out[0];
 					*outStream++ = out[1];
 				}
@@ -921,9 +947,9 @@
 			}
 			else
 			{
-				for (j = 0; j < numSamples; j++)
+				for (i = 0; i < numSamples; i++)
 				{
-					processMixedSamplesA1200(j, out);
+					processMixedSamplesA1200(i, out);
 					*outStream++ = out[0];
 					*outStream++ = out[1];
 				}
@@ -934,8 +960,8 @@
 
 static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
 {
-	int16_t *out;
-	int32_t sampleBlock, samplesTodo;
+	int16_t *streamOut;
+	uint32_t samplesLeft;
 
 	if (audio.forceMixerOff) // during MOD2WAV
 	{
@@ -943,27 +969,28 @@
 		return;
 	}
 
-	out = (int16_t *)stream;
+	streamOut = (int16_t *)stream;
 
-	sampleBlock = len >> 2;
-	while (sampleBlock)
+	samplesLeft = len >> 2;
+	while (samplesLeft > 0)
 	{
-		samplesTodo = (sampleBlock < sampleCounter) ? sampleBlock : sampleCounter;
-		if (samplesTodo > 0)
+		if (sampleCounter == 0)
 		{
-			outputAudio(out, samplesTodo);
-			out += (uint32_t)samplesTodo * 2;
-
-			sampleBlock -= samplesTodo;
-			sampleCounter -= samplesTodo;
-		}
-		else
-		{
 			if (editor.songPlaying)
 				intMusic();
 
 			sampleCounter = samplesPerTick;
 		}
+
+		uint32_t samplesTodo = sampleCounter;
+		if (samplesTodo > samplesLeft)
+			samplesTodo = samplesLeft;
+
+		outputAudio(streamOut, samplesTodo);
+		streamOut += samplesTodo << 1;
+
+		samplesLeft -= samplesTodo;
+		sampleCounter -= samplesTodo;
 	}
 
 	(void)userdata;
@@ -1081,13 +1108,22 @@
 	audio.dPeriodToDeltaDiv = (double)PAULA_PAL_CLK / audio.outputRate;
 
 	generateBpmTables();
-	const int32_t maxSamplesToMix = audio.bpmTab[0]; // BPM 32
 
+	/* If the audio output rate is lower than MOD2WAV_FREQ, we need
+	** to allocate slightly more space so that MOD2WAV rendering
+	** won't overflow.
+	*/
+
+	uint32_t maxSamplesToMix;
+	if (MOD2WAV_FREQ > audio.outputRate)
+		 maxSamplesToMix = audio.bpmTabMod2Wav[32-32]; // BPM 32
+	else
+		maxSamplesToMix = audio.bpmTab[32-32]; // BPM 32
+
 	dMixBufferLUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
 	dMixBufferRUnaligned = (double *)MALLOC_PAD(maxSamplesToMix * sizeof (double), 256);
-	editor.mod2WavBuffer = (int16_t *)malloc(maxSamplesToMix * sizeof (int16_t));
 
-	if (dMixBufferLUnaligned == NULL || dMixBufferRUnaligned == NULL || editor.mod2WavBuffer == NULL)
+	if (dMixBufferLUnaligned == NULL || dMixBufferRUnaligned == NULL)
 	{
 		showErrorMsgBox("Out of memory!");
 		return false;
@@ -1102,7 +1138,7 @@
 	filterFlags = config.a500LowPassFilter ? FILTER_A500 : 0;
 	calculateFilterCoeffs();
 
-	samplesPerTick = 0;
+	samplesPerTick = audio.bpmTab[125-32]; // BPM 125
 	sampleCounter = 0;
 
 	SDL_PauseAudioDevice(dev, false);
@@ -1129,15 +1165,9 @@
 		free(dMixBufferRUnaligned);
 		dMixBufferRUnaligned = NULL;
 	}
-
-	if (editor.mod2WavBuffer != NULL)
-	{
-		free(editor.mod2WavBuffer);
-		editor.mod2WavBuffer = NULL;
-	}
 }
 
-void mixerSetSamplesPerTick(int32_t val)
+void mixerSetSamplesPerTick(uint32_t val)
 {
 	samplesPerTick = val;
 }
@@ -1159,280 +1189,6 @@
 	{
 		mixerCalcVoicePans(100);
 		displayMsg("AMIGA PANNING ON");
-	}
-}
-
-// PAT2SMP RELATED STUFF
-
-uint32_t getAudioFrame(int16_t *outStream)
-{
-	int32_t smpCounter, samplesToMix;
-
-	if (!intMusic())
-		wavRenderingDone = true;
-
-	smpCounter = samplesPerTick;
-	while (smpCounter > 0)
-	{
-		samplesToMix = smpCounter;
-
-		outputAudio(outStream, samplesToMix);
-		outStream += (uint32_t)samplesToMix * 2;
-
-		smpCounter -= samplesToMix;
-	}
-
-	return (uint32_t)samplesPerTick * 2; // * 2 for stereo
-}
-
-static int32_t SDLCALL mod2WavThreadFunc(void *ptr)
-{
-	uint32_t size, totalSampleCounter, totalRiffChunkLen;
-	FILE *fOut;
-	wavHeader_t wavHeader;
-
-	fOut = (FILE *)ptr;
-	if (fOut == NULL)
-		return true;
-
-	// skip wav header place, render data first
-	fseek(fOut, sizeof (wavHeader_t), SEEK_SET);
-
-	wavRenderingDone = false;
-
-	totalSampleCounter = 0;
-	while (editor.isWAVRendering && !wavRenderingDone && !editor.abortMod2Wav)
-	{
-		size = getAudioFrame(editor.mod2WavBuffer);
-		if (size > 0)
-		{
-			fwrite(editor.mod2WavBuffer, sizeof (int16_t), size, fOut);
-			totalSampleCounter += size;
-		}
-
-		editor.ui.updateMod2WavDialog = true;
-	}
-
-	if (totalSampleCounter & 1)
-		fputc(0, fOut); // pad align byte
-
-	if ((ftell(fOut) - 8) > 0)
-		totalRiffChunkLen = ftell(fOut) - 8;
-	else
-		totalRiffChunkLen = 0;
-
-	editor.ui.mod2WavFinished = true;
-	editor.ui.updateMod2WavDialog = true;
-
-	// go back and fill the missing WAV header
-	fseek(fOut, 0, SEEK_SET);
-
-	wavHeader.chunkID = 0x46464952; // "RIFF"
-	wavHeader.chunkSize = totalRiffChunkLen;
-	wavHeader.format = 0x45564157; // "WAVE"
-	wavHeader.subchunk1ID = 0x20746D66; // "fmt "
-	wavHeader.subchunk1Size = 16;
-	wavHeader.audioFormat = 1;
-	wavHeader.numChannels = 2;
-	wavHeader.sampleRate = audio.outputRate;
-	wavHeader.bitsPerSample = 16;
-	wavHeader.byteRate = wavHeader.sampleRate * wavHeader.numChannels * (wavHeader.bitsPerSample / 8);
-	wavHeader.blockAlign = wavHeader.numChannels * (wavHeader.bitsPerSample / 8);
-	wavHeader.subchunk2ID = 0x61746164; // "data"
-	wavHeader.subchunk2Size = totalSampleCounter * (wavHeader.bitsPerSample / 8);
-
-	fwrite(&wavHeader, sizeof (wavHeader_t), 1, fOut);
-	fclose(fOut);
-
-	return true;
-}
-
-bool renderToWav(char *fileName, bool checkIfFileExist)
-{
-	FILE *fOut;
-	struct stat statBuffer;
-
-	if (checkIfFileExist)
-	{
-		if (stat(fileName, &statBuffer) == 0)
-		{
-			editor.ui.askScreenShown = true;
-			editor.ui.askScreenType = ASK_MOD2WAV_OVERWRITE;
-
-			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
-			setStatusMessage("OVERWRITE FILE?", NO_CARRY);
-
-			renderAskDialog();
-
-			return false;
-		}
-	}
-
-	if (editor.ui.askScreenShown)
-	{
-		editor.ui.askScreenShown = false;
-		editor.ui.answerNo = false;
-		editor.ui.answerYes = false;
-	}
-
-	fOut = fopen(fileName, "wb");
-	if (fOut == NULL)
-	{
-		displayErrorMsg("FILE I/O ERROR");
-		return false;
-	}
-
-	storeTempVariables();
-	calcMod2WavTotalRows();
-	restartSong();
-
-	editor.blockMarkFlag = false;
-
-	pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
-	setStatusMessage("RENDERING MOD...", NO_CARRY);
-
-	editor.ui.disableVisualizer = true;
-	editor.isWAVRendering = true;
-	renderMOD2WAVDialog();
-
-	editor.abortMod2Wav = false;
-
-	editor.mod2WavThread = SDL_CreateThread(mod2WavThreadFunc, NULL, fOut);
-	if (editor.mod2WavThread != NULL)
-	{
-		SDL_DetachThread(editor.mod2WavThread);
-	}
-	else
-	{
-		editor.ui.disableVisualizer = false;
-		editor.isWAVRendering = false;
-
-		displayErrorMsg("THREAD ERROR");
-
-		pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
-		statusAllRight();
-
-		return false;
-	}
-
-	return true;
-}
-
-// for MOD2WAV - ONLY used for a visual percentage counter, so accuracy is not important
-void calcMod2WavTotalRows(void)
-{
-	bool pBreakFlag, posJumpAssert, calcingRows;
-	int8_t n_pattpos[AMIGA_VOICES], n_loopcount[AMIGA_VOICES];
-	uint8_t modRow, pBreakPosition, ch, pos;
-	int16_t modOrder;
-	uint16_t modPattern;
-	note_t *note;
-
-	// for pattern loop
-	memset(n_pattpos, 0, sizeof (n_pattpos));
-	memset(n_loopcount, 0, sizeof (n_loopcount));
-
-	modEntry->rowsCounter = 0;
-	modEntry->rowsInTotal = 0;
-
-	modRow = 0;
-	modOrder = 0;
-	modPattern = modEntry->head.order[0];
-	pBreakPosition = 0;
-	posJumpAssert = false;
-	pBreakFlag = false;
-	calcingRows = true;
-
-	memset(editor.rowVisitTable, 0, MOD_ORDERS * MOD_ROWS);
-	while (calcingRows)
-	{
-		editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow] = true;
-
-		for (ch = 0; ch < AMIGA_VOICES; ch++)
-		{
-			note = &modEntry->patterns[modPattern][(modRow * AMIGA_VOICES) + ch];
-			if (note->command == 0x0B) // Bxx - Position Jump
-			{
-				modOrder = note->param - 1;
-				pBreakPosition = 0;
-				posJumpAssert = true;
-			}
-			else if (note->command == 0x0D) // Dxx - Pattern Break
-			{
-				pBreakPosition = (((note->param >> 4) * 10) + (note->param & 0x0F));
-				if (pBreakPosition > 63)
-					pBreakPosition = 0;
-
-				posJumpAssert = true;
-			}
-			else if (note->command == 0x0F && note->param == 0) // F00 - Set Speed 0 (stop)
-			{
-				calcingRows = false;
-				break;
-			}
-			else if (note->command == 0x0E && (note->param >> 4) == 0x06) // E6x - Pattern Loop
-			{
-				pos = note->param & 0x0F;
-				if (pos == 0)
-				{
-					n_pattpos[ch] = modRow;
-				}
-				else if (n_loopcount[ch] == 0)
-				{
-					n_loopcount[ch] = pos;
-
-					pBreakPosition = n_pattpos[ch];
-					pBreakFlag = true;
-
-					for (pos = pBreakPosition; pos <= modRow; pos++)
-						editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false;
-				}
-				else if (--n_loopcount[ch])
-				{
-					pBreakPosition = n_pattpos[ch];
-					pBreakFlag = true;
-
-					for (pos = pBreakPosition; pos <= modRow; pos++)
-						editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false;
-				}
-			}
-		}
-
-		modRow++;
-		modEntry->rowsInTotal++;
-
-		if (pBreakFlag)
-		{
-			modRow = pBreakPosition;
-			pBreakPosition = 0;
-			pBreakFlag = false;
-		}
-
-		if (modRow >= MOD_ROWS || posJumpAssert)
-		{
-			modRow = pBreakPosition;
-			pBreakPosition = 0;
-			posJumpAssert = false;
-
-			modOrder = (modOrder + 1) & 0x7F;
-			if (modOrder >= modEntry->head.orderCount)
-			{
-				modOrder = 0;
-				calcingRows = false;
-				break;
-			}
-
-			modPattern = modEntry->head.order[modOrder];
-			if (modPattern > MAX_PATTERNS-1)
-				modPattern = MAX_PATTERNS-1;
-		}
-
-		if (editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow])
-		{
-			// row has been visited before, we're now done!
-			calcingRows = false;
-			break;
-		}
 	}
 }
 
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -29,7 +29,6 @@
 void normalize8bitDoubleSigned(double *dSampleData, uint32_t sampleLength);
 void setLEDFilter(bool state);
 void toggleLEDFilter(void);
-bool renderToWav(char *fileName, bool checkIfFileExist);
 void toggleAmigaPanMode(void);
 void toggleA500Filters(void);
 void paulaStopDMA(uint8_t ch);
@@ -44,6 +43,6 @@
 void mixerKillVoice(uint8_t ch);
 void turnOffVoices(void);
 void mixerCalcVoicePans(uint8_t stereoSeparation);
-void mixerSetSamplesPerTick(int32_t val);
+void mixerSetSamplesPerTick(uint32_t val);
 void mixerClearSampleCounter(void);
 void outputAudio(int16_t *target, int32_t numSamples);
--- a/src/pt2_blep.c
+++ b/src/pt2_blep.c
@@ -1,4 +1,4 @@
-// These BLEP routines were coded by aciddose/adejr
+// these BLEP routines were coded by aciddose
 
 #include <stdint.h>
 #include "pt2_blep.h"
@@ -5,50 +5,101 @@
 #include "pt2_helpers.h"
 
 /* Why this table is not represented as readable floating-point numbers:
-** Accurate float (double) representation in string format requires at least 14 digits and normalized
+** Accurate double representation in string format requires at least 14 digits and normalized
 ** (scientific) notation, notwithstanding compiler issues with precision or rounding error.
-** Also, don't touch this table ever, just keep it exactly identical! */
+** Also, don't touch this table ever, just keep it exactly identical!
+*/
 
-// TODO: get a proper double-precision table. This one is converted from float.
-static const uint64_t dBlepData[48] =
+static const uint64_t minblepdata[] =
 {
-	0x3FEFFC3E20000000, 0x3FEFFAA900000000, 0x3FEFFAD460000000, 0x3FEFFA9C60000000,
-	0x3FEFF5B0A0000000, 0x3FEFE42A40000000, 0x3FEFB7F5C0000000, 0x3FEF599BE0000000,
-	0x3FEEA5E3C0000000, 0x3FED6E7080000000, 0x3FEB7F7960000000, 0x3FE8AB9E40000000,
-	0x3FE4DCA480000000, 0x3FE0251880000000, 0x3FD598FB80000000, 0x3FC53D0D60000000,
-	0x3F8383A520000000, 0xBFBC977CC0000000, 0xBFC755C080000000, 0xBFC91BDBA0000000,
-	0xBFC455AFC0000000, 0xBFB6461340000000, 0xBF7056C400000000, 0x3FB1028220000000,
-	0x3FBB5B7E60000000, 0x3FBC5903A0000000, 0x3FB55403E0000000, 0x3FA3CED340000000,
-	0xBF7822DAE0000000, 0xBFA2805D00000000, 0xBFA7140D20000000, 0xBFA18A7760000000,
-	0xBF87FF7180000000, 0x3F88CBFA40000000, 0x3F9D4AEC80000000, 0x3FA14A3AC0000000,
-	0x3F9D5C5AA0000000, 0x3F92558B40000000, 0x3F7C997EE0000000, 0x0000000000000000,
-	0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000,
-	0x0000000000000000, 0x0000000000000000, 0x0000000000000000, 0x0000000000000000
+	0x3FF000320C7E95A6,0x3FF00049BE220FD5,0x3FF0001B92A41ACA,0x3FEFFF4425AA9724,
+	0x3FEFFDABDF6CF05C,0x3FEFFB5AF233EF1A,0x3FEFF837E2AE85F3,0x3FEFF4217B80E938,
+	0x3FEFEEECEB4E0444,0x3FEFE863A8358B5F,0x3FEFE04126292670,0x3FEFD63072A0D592,
+	0x3FEFC9C9CD36F56F,0x3FEFBA90594BD8C3,0x3FEFA7F008BA9F13,0x3FEF913BE2A0E0E2,
+	0x3FEF75ACCB01A327,0x3FEF5460F06A4E8F,0x3FEF2C5C0389BD3C,0x3FEEFC8859BF6BCB,
+	0x3FEEC3B916FD8D19,0x3FEE80AD74F0AD16,0x3FEE32153552E2C7,0x3FEDD69643CB9778,
+	0x3FED6CD380FFA864,0x3FECF374A4D2961A,0x3FEC692F19B34E54,0x3FEBCCCFA695DD5C,
+	0x3FEB1D44B168764A,0x3FEA59A8D8E4527F,0x3FE9814D9B10A9A3,0x3FE893C5B62135F2,
+	0x3FE790EEEBF9DABD,0x3FE678FACDEE27FF,0x3FE54C763699791A,0x3FE40C4F1B1EB7A3,
+	0x3FE2B9D863D4E0F3,0x3FE156CB86586B0B,0x3FDFCA8F5005B828,0x3FDCCF9C3F455DAC,
+	0x3FD9C2787F20D06E,0x3FD6A984CAD0F3E5,0x3FD38BB0C452732E,0x3FD0705EC7135366,
+	0x3FCABE86754E238F,0x3FC4C0801A6E9A04,0x3FBDECF490C5EA17,0x3FB2DFFACE9CE44B,
+	0x3FA0EFD4449F4620,0xBF72F4A65E22806D,0xBFA3F872D761F927,0xBFB1D89F0FD31F7C,
+	0xBFB8B1EA652EC270,0xBFBE79B82A37C92D,0xBFC1931B697E685E,0xBFC359383D4C8ADA,
+	0xBFC48F3BFF81B06B,0xBFC537BBA8D6B15C,0xBFC557CEF2168326,0xBFC4F6F781B3347A,
+	0xBFC41EF872F0E009,0xBFC2DB9F119D54D3,0xBFC13A7E196CB44F,0xBFBE953A67843504,
+	0xBFBA383D9C597E74,0xBFB57FBD67AD55D6,0xBFB08E18234E5CB3,0xBFA70B06D699FFD1,
+	0xBF9A1CFB65370184,0xBF7B2CEB901D2067,0x3F86D5DE2C267C78,0x3F9C1D9EF73F384D,
+	0x3FA579C530950503,0x3FABD1E5FFF9B1D0,0x3FB07DCDC3A4FB5B,0x3FB2724A856EEC1B,
+	0x3FB3C1F7199FC822,0x3FB46D0979F5043B,0x3FB47831387E0110,0x3FB3EC4A58A3D527,
+	0x3FB2D5F45F8889B3,0x3FB145113E25B749,0x3FAE9860D18779BC,0x3FA9FFD5F5AB96EA,
+	0x3FA4EC6C4F47777E,0x3F9F16C5B2604C3A,0x3F9413D801124DB7,0x3F824F668CBB5BDF,
+	0xBF55B3FA2EE30D66,0xBF86541863B38183,0xBF94031BBBD551DE,0xBF9BAFC27DC5E769,
+	0xBFA102B3683C57EC,0xBFA3731E608CC6E4,0xBFA520C9F5B5DEBD,0xBFA609DC89BE6ECE,
+	0xBFA632B83BC5F52F,0xBFA5A58885841AD4,0xBFA471A5D2FF02F3,0xBFA2AAD5CD0377C7,
+	0xBFA0686FFE4B9B05,0xBF9B88DE413ACB69,0xBF95B4EF6D93F1C5,0xBF8F1B72860B27FA,
+	0xBF8296A865CDF612,0xBF691BEEDABE928B,0x3F65C04E6AF9D4F1,0x3F8035D8FFCDB0F8,
+	0x3F89BED23C431BE3,0x3F90E737811A1D21,0x3F941C2040BD7CB1,0x3F967046EC629A09,
+	0x3F97DE27ECE9ED89,0x3F98684DE31E7040,0x3F9818C4B07718FA,0x3F97005261F91F60,
+	0x3F95357FDD157646,0x3F92D37C696C572A,0x3F8FF1CFF2BEECB5,0x3F898D20C7A72AC4,
+	0x3F82BC5B3B0AE2DF,0x3F7784A1B8E9E667,0x3F637BB14081726B,0xBF4B2DACA70C60A9,
+	0xBF6EFB00AD083727,0xBF7A313758DC6AE9,0xBF819D6A99164BE0,0xBF8533F57533403B,
+	0xBF87CD120DB5D340,0xBF89638549CD25DE,0xBF89FB8B8D37B1BB,0xBF89A21163F9204E,
+	0xBF886BA8931297D4,0xBF8673477783D71E,0xBF83D8E1CB165DB8,0xBF80BFEA7216142A,
+	0xBF7A9B9BC2E40EBF,0xBF7350E806435A7E,0xBF67D35D3734AB5E,0xBF52ADE8FEAB8DB9,
+	0x3F415669446478E4,0x3F60C56A092AFB48,0x3F6B9F4334A4561F,0x3F724FB908FD87AA,
+	0x3F75CC56DFE382EA,0x3F783A0C23969A7B,0x3F799833C40C3B82,0x3F79F02721981BF3,
+	0x3F7954212AB35261,0x3F77DDE0C5FC15C9,0x3F75AD1C98FE0777,0x3F72E5DACC0849F2,
+	0x3F6F5D7E69DFDE1B,0x3F685EC2CA09E1FD,0x3F611D750E54DF3A,0x3F53C6E392A46D17,
+	0x3F37A046885F3365,0xBF3BB034D2EE45C2,0xBF5254267B04B482,0xBF5C0516F9CECDC6,
+	0xBF61E5736853564D,0xBF64C464B9CC47AB,0xBF669C1AEF258F56,0xBF67739985DD0E60,
+	0xBF675AFD6446395B,0xBF666A0C909B4F78,0xBF64BE9879A7A07B,0xBF627AC74B119DBD,
+	0xBF5F86B04069DC9B,0xBF597BE8F754AF5E,0xBF531F3EAAE9A1B1,0xBF496D3DE6AD7EA3,
+	0xBF3A05FFDE4670CF,0xBF06DF95C93A85CA,0x3F31EE2B2C6547AC,0x3F41E694A378C129,
+	0x3F4930BF840E23C9,0x3F4EBB5D05A0D47D,0x3F51404DA0539855,0x3F524698F56B3F33,
+	0x3F527EF85309E28F,0x3F51FE70FE2513DE,0x3F50DF1642009B74,0x3F4E7CDA93517CAE,
+	0x3F4A77AE24F9A533,0x3F45EE226AA69E10,0x3F411DB747374F52,0x3F387F39D229D97F,
+	0x3F2E1B3D39AF5F8B,0x3F18F557BB082715,0xBEFAC04896E68DDB,0xBF20F5BC77DF558A,
+	0xBF2C1B6DF3EE94A4,0xBF3254602A816876,0xBF354E90F6EAC26B,0xBF3709F2E5AF1624,
+	0xBF379FCCB331CE8E,0xBF37327192ADDAD3,0xBF35EA998A894237,0xBF33F4C4977B3489,
+	0xBF317EC5F68E887B,0xBF2D6B1F793EB773,0xBF2786A226B076D9,0xBF219BE6CEC2CA36,
+	0xBF17D7F36D2A3A18,0xBF0AAEC5BBAB42AB,0xBEF01818DC224040,0x3EEF2F6E21093846,
+	0x3F049D6E0060B71F,0x3F0E598CCAFABEFD,0x3F128BC14BE97261,0x3F148703BC70EF6A,
+	0x3F1545E1579CAA25,0x3F14F7DDF5F8D766,0x3F13D10FF9A1BE0C,0x3F1206D5738ECE3A,
+	0x3F0F99F6BF17C5D4,0x3F0AA6D7EA524E96,0x3F0588DDF740E1F4,0x3F0086FB6FEA9839,
+	0x3EF7B28F6D6F5EED,0x3EEEA300DCBAF74A,0x3EE03F904789777C,0x3EC1BFEB320501ED,
+	0xBEC310D8E585A031,0xBED6F55ECA7E151F,0xBEDFDAA5DACDD0B7,0xBEE26944F3CF6E90,
+	0xBEE346894453BD1F,0xBEE2E099305CD5A8,0xBEE190385A7EA8B2,0xBEDF4D5FA2FB6BA2,
+	0xBEDAD4F371257BA0,0xBED62A9CDEB0AB32,0xBED1A6DF97B88316,0xBECB100096894E58,
+	0xBEC3E8A76257D275,0xBEBBF6C29A5150C9,0xBEB296292998088E,0xBEA70A10498F0E5E,
+	0xBE99E52D02F887A1,0xBE88C17F4066D432,0xBE702A716CFF56CA,0x3E409F820F781F78,
+	0x3E643EA99B770FE7,0x3E67DE40CDE0A550,0x3E64F4D534A2335C,0x3E5F194536BDDF7A,
+	0x3E5425CEBE1FA40A,0x3E46D7B7CC631E73,0x3E364746B6582E54,0x3E21FC07B13031DE,
+	0x3E064C3D91CF7665,0x3DE224F901A0AFC7,0x3DA97D57859C74A4,0x0000000000000000,
+
+	// extra padding needed for interpolation
+	0x0000000000000000
 };
 
+const double *get_minblep_table(void) { return (const double *)minblepdata; }
+
 void blepAdd(blep_t *b, double dOffset, double dAmplitude)
 {
-	int8_t n;
-	int32_t i;
-	const double *dBlepSrc;
-	double f;
-
 	assert(dOffset >= 0.0 && dOffset < 1.0);
 
-	f = dOffset * BLEP_SP;
+	double f = dOffset * BLEP_SP;
 
-	i = (int32_t)f; // get integer part of f
-	dBlepSrc = (const double *)dBlepData + i + BLEP_OS;
+	int32_t i = (int32_t)f; // get integer part of f
+	const double *dBlepSrc = get_minblep_table() + i;
 	f -= i; // remove integer part from f
 
 	i = b->index;
-
-	n = BLEP_NS;
-	while (n--)
+	for (int32_t n = 0; n < BLEP_NS; n++)
 	{
 		b->dBuffer[i] += dAmplitude * LERP(dBlepSrc[0], dBlepSrc[1], f);
-		i = (i + 1) & BLEP_RNS;
 		dBlepSrc += BLEP_SP;
+
+		i = (i + 1) & BLEP_RNS;
 	}
 
 	b->samplesLeft = BLEP_NS;
@@ -55,20 +106,14 @@
 }
 
 /* 8bitbubsy: simplified, faster version of blepAdd for blep'ing voice volume.
-** Result is identical! (confirmed with binary comparison)
+** Result is identical! (confirmed with binary comparison w/ MOD2WAV)
 */
 void blepVolAdd(blep_t *b, double dAmplitude)
 {
-	int8_t n;
-	int32_t i;
-	const double *dBlepSrc;
+	const double *dBlepSrc = get_minblep_table();
 
-	dBlepSrc = (const double *)dBlepData + BLEP_OS;
-
-	i = b->index;
-
-	n = BLEP_NS;
-	while (n--)
+	int32_t i = b->index;
+	for (int32_t n = 0; n < BLEP_NS; n++)
 	{
 		b->dBuffer[i] += dAmplitude * (*dBlepSrc);
 		i = (i + 1) & BLEP_RNS;
@@ -78,15 +123,13 @@
 	b->samplesLeft = BLEP_NS;
 }
 
-double blepRun(blep_t *b)
+double blepRun(blep_t *b, double dInput)
 {
-	double fBlepOutput;
-
-	fBlepOutput = b->dBuffer[b->index];
+	double dBlepOutput = dInput + b->dBuffer[b->index];
 	b->dBuffer[b->index] = 0.0;
 
 	b->index = (b->index + 1) & BLEP_RNS;
 
 	b->samplesLeft--;
-	return fBlepOutput;
+	return dBlepOutput;
 }
--- a/src/pt2_blep.h
+++ b/src/pt2_blep.h
@@ -19,11 +19,11 @@
 ** the result of that is the filter cutoff is set at nyquist * (SP/OS), in this case nyquist/5.
 */
 
-#define BLEP_ZC 8
-#define BLEP_OS 5
-#define BLEP_SP 5
+#define BLEP_ZC 16
+#define BLEP_OS 16
+#define BLEP_SP 16
 #define BLEP_NS (BLEP_ZC * BLEP_OS / BLEP_SP)
-#define BLEP_RNS 7 // RNS = (2^ > NS) - 1
+#define BLEP_RNS 31 // RNS = (2^ > NS) - 1
 
 typedef struct blep_t
 {
@@ -33,4 +33,4 @@
 
 void blepAdd(blep_t *b, double dOffset, double dAmplitude);
 void blepVolAdd(blep_t *b, double dAmplitude);
-double blepRun(blep_t *b);
+double blepRun(blep_t *b, double dInput);
--- 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.10"
+#define PROG_VER_STR "1.11"
 
 #ifdef _WIN32
 #define DIR_DELIMITER '\\'
@@ -291,7 +291,7 @@
 {
 	volatile bool locked;
 	bool forceMixerOff;
-	uint16_t bpmTab[256-32], bpmTab28kHz[256-32], bpmTab22kHz[256-32];
+	uint16_t bpmTab[256-32], bpmTab28kHz[256-32], bpmTab22kHz[256-32], bpmTabMod2Wav[256-32];
 	uint32_t outputRate, audioBufferSize;
 	double dPeriodToDeltaDiv;
 } audio;
@@ -365,7 +365,7 @@
 	uint8_t blockFromPos, blockToPos, timingMode, f6Pos, f7Pos, f8Pos, f9Pos, f10Pos, keyOctave, pNoteFlag;
 	uint8_t tuningNote, resampleNote, initialTempo, initialSpeed, editMoveAdd;
 
-	int16_t *mod2WavBuffer, *pat2SmpBuf, modulateSpeed;
+	int16_t *pat2SmpBuf, modulateSpeed;
 	uint16_t metroSpeed, metroChannel, sampleVol, samplePos, chordLength;
 	uint16_t effectMacros[10], oldTempo, currPlayNote, vol1, vol2, lpCutOff, hpCutOff;
 	int32_t smpRedoLoopStarts[MOD_SAMPLES], smpRedoLoopLengths[MOD_SAMPLES], smpRedoLengths[MOD_SAMPLES];
--- /dev/null
+++ b/src/pt2_mod2wav.c
@@ -1,0 +1,320 @@
+// for finding memory leaks in debug mode with Visual Studio 
+#if defined _DEBUG && defined _MSC_VER
+#include <crtdbg.h>
+#endif
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_header.h"
+#include "pt2_audio.h"
+#include "pt2_mouse.h"
+#include "pt2_textout.h"
+#include "pt2_visuals.h"
+#include "pt2_mod2wav.h"
+
+#define TICKS_PER_RENDER_CHUNK 32
+
+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;
+
+static void calcMod2WavTotalRows(void);
+
+static uint32_t getAudioFrame(int16_t *outStream)
+{
+	if (!intMusic())
+		wavRenderingDone = true;
+
+	outputAudio(outStream, samplesPerTick);
+	return samplesPerTick;
+}
+
+static int32_t SDLCALL mod2WavThreadFunc(void *ptr)
+{
+	wavHeader_t wavHeader;
+
+	FILE *f = (FILE *)ptr;
+	assert(mod2WavBuffer != NULL && f != NULL);
+
+	// skip wav header place, render data first
+	fseek(f, sizeof (wavHeader_t), SEEK_SET);
+
+	wavRenderingDone = false;
+
+	uint8_t loopCounter = 8;
+	uint32_t totalSampleCounter = 0;
+
+	bool renderDone = false;
+	while (!renderDone)
+	{
+		uint32_t samplesInChunk = 0;
+
+		// render several ticks at once to prevent frequent disk I/O (can speed up the process)
+		int16_t *ptr16 = mod2WavBuffer;
+		for (uint32_t i = 0; i < TICKS_PER_RENDER_CHUNK; i++)
+		{
+			if (!editor.isWAVRendering || wavRenderingDone || editor.abortMod2Wav)
+			{
+				renderDone = true;
+				break;
+			}
+
+			uint32_t tickSamples = getAudioFrame(ptr16) << 1; // *2 for stereo
+
+			samplesInChunk += tickSamples;
+			totalSampleCounter += tickSamples;
+
+			// increase buffer pointer
+			ptr16 += tickSamples;
+
+			if (++loopCounter >= 8)
+			{
+				loopCounter = 0;
+				editor.ui.updateMod2WavDialog = true;
+			}
+		}
+
+		// write buffer to disk
+		if (samplesInChunk > 0)
+			fwrite(mod2WavBuffer, sizeof (int16_t), samplesInChunk, f);
+	}
+
+	free(mod2WavBuffer);
+
+	if (totalSampleCounter & 1)
+		fputc(0, f); // pad align byte
+
+	uint32_t totalRiffChunkLen = (uint32_t)ftell(f) - 8;
+
+	// go back and fill in WAV header
+	rewind(f);
+
+	wavHeader.chunkID = 0x46464952; // "RIFF"
+	wavHeader.chunkSize = totalRiffChunkLen;
+	wavHeader.format = 0x45564157; // "WAVE"
+	wavHeader.subchunk1ID = 0x20746D66; // "fmt "
+	wavHeader.subchunk1Size = 16;
+	wavHeader.audioFormat = 1;
+	wavHeader.numChannels = 2;
+	wavHeader.sampleRate = MOD2WAV_FREQ;
+	wavHeader.bitsPerSample = 16;
+	wavHeader.byteRate = (wavHeader.sampleRate * wavHeader.numChannels * wavHeader.bitsPerSample) / 8;
+	wavHeader.blockAlign = (wavHeader.numChannels * wavHeader.bitsPerSample) / 8;
+	wavHeader.subchunk2ID = 0x61746164; // "data"
+	wavHeader.subchunk2Size = totalSampleCounter * sizeof (int16_t);
+
+	// write main header
+	fwrite(&wavHeader, sizeof (wavHeader_t), 1, f);
+	fclose(f);
+
+	editor.ui.mod2WavFinished = true;
+	editor.ui.updateMod2WavDialog = true;
+
+	return true;
+}
+
+bool renderToWav(char *fileName, bool checkIfFileExist)
+{
+	FILE *fOut;
+	struct stat statBuffer;
+
+	if (checkIfFileExist)
+	{
+		if (stat(fileName, &statBuffer) == 0)
+		{
+			editor.ui.askScreenShown = true;
+			editor.ui.askScreenType = ASK_MOD2WAV_OVERWRITE;
+
+			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
+			setStatusMessage("OVERWRITE FILE?", NO_CARRY);
+
+			renderAskDialog();
+
+			return false;
+		}
+	}
+
+	if (editor.ui.askScreenShown)
+	{
+		editor.ui.askScreenShown = false;
+		editor.ui.answerNo = false;
+		editor.ui.answerYes = false;
+	}
+
+	fOut = fopen(fileName, "wb");
+	if (fOut == NULL)
+	{
+		displayErrorMsg("FILE I/O ERROR");
+		return false;
+	}
+
+	const uint32_t maxSamplesToMix = TICKS_PER_RENDER_CHUNK * audio.bpmTabMod2Wav[32-32]; // BPM 32, stereo
+
+	mod2WavBuffer = (int16_t *)malloc(maxSamplesToMix * (2 * sizeof (int16_t)));
+	if (mod2WavBuffer == NULL)
+	{
+		statusOutOfMemory();
+		return false;
+	}
+
+	storeTempVariables();
+	calcMod2WavTotalRows();
+	restartSong();
+
+	editor.blockMarkFlag = false;
+
+	pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
+	setStatusMessage("RENDERING MOD...", NO_CARRY);
+
+	editor.ui.disableVisualizer = true;
+	editor.isWAVRendering = true;
+	renderMOD2WAVDialog();
+
+	editor.abortMod2Wav = false;
+
+	modSetTempo(modEntry->currBPM); // update BPM with MOD2WAV audio output rate
+
+	editor.mod2WavThread = SDL_CreateThread(mod2WavThreadFunc, NULL, fOut);
+	if (editor.mod2WavThread != NULL)
+	{
+		SDL_DetachThread(editor.mod2WavThread);
+	}
+	else
+	{
+		free(mod2WavBuffer);
+
+		editor.ui.disableVisualizer = false;
+		editor.isWAVRendering = false;
+
+		displayErrorMsg("THREAD ERROR");
+
+		pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
+		statusAllRight();
+
+		return false;
+	}
+
+	return true;
+}
+
+// ONLY used for a visual percentage counter, so accuracy is not very important
+static void calcMod2WavTotalRows(void)
+{
+	bool pBreakFlag, posJumpAssert, calcingRows;
+	int8_t n_pattpos[AMIGA_VOICES], n_loopcount[AMIGA_VOICES];
+	uint8_t modRow, pBreakPosition, ch, pos;
+	int16_t modOrder;
+	uint16_t modPattern;
+	note_t *note;
+
+	// for pattern loop
+	memset(n_pattpos, 0, sizeof (n_pattpos));
+	memset(n_loopcount, 0, sizeof (n_loopcount));
+
+	modEntry->rowsCounter = 0;
+	modEntry->rowsInTotal = 0;
+
+	modRow = 0;
+	modOrder = 0;
+	modPattern = modEntry->head.order[0];
+	pBreakPosition = 0;
+	posJumpAssert = false;
+	pBreakFlag = false;
+	calcingRows = true;
+
+	memset(editor.rowVisitTable, 0, MOD_ORDERS * MOD_ROWS);
+	while (calcingRows)
+	{
+		editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow] = true;
+
+		for (ch = 0; ch < AMIGA_VOICES; ch++)
+		{
+			note = &modEntry->patterns[modPattern][(modRow * AMIGA_VOICES) + ch];
+			if (note->command == 0x0B) // Bxx - Position Jump
+			{
+				modOrder = note->param - 1;
+				pBreakPosition = 0;
+				posJumpAssert = true;
+			}
+			else if (note->command == 0x0D) // Dxx - Pattern Break
+			{
+				pBreakPosition = (((note->param >> 4) * 10) + (note->param & 0x0F));
+				if (pBreakPosition > 63)
+					pBreakPosition = 0;
+
+				posJumpAssert = true;
+			}
+			else if (note->command == 0x0F && note->param == 0) // F00 - Set Speed 0 (stop)
+			{
+				calcingRows = false;
+				break;
+			}
+			else if (note->command == 0x0E && (note->param >> 4) == 0x06) // E6x - Pattern Loop
+			{
+				pos = note->param & 0x0F;
+				if (pos == 0)
+				{
+					n_pattpos[ch] = modRow;
+				}
+				else if (n_loopcount[ch] == 0)
+				{
+					n_loopcount[ch] = pos;
+
+					pBreakPosition = n_pattpos[ch];
+					pBreakFlag = true;
+
+					for (pos = pBreakPosition; pos <= modRow; pos++)
+						editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false;
+				}
+				else if (--n_loopcount[ch])
+				{
+					pBreakPosition = n_pattpos[ch];
+					pBreakFlag = true;
+
+					for (pos = pBreakPosition; pos <= modRow; pos++)
+						editor.rowVisitTable[(modOrder * MOD_ROWS) + pos] = false;
+				}
+			}
+		}
+
+		modRow++;
+		modEntry->rowsInTotal++;
+
+		if (pBreakFlag)
+		{
+			modRow = pBreakPosition;
+			pBreakPosition = 0;
+			pBreakFlag = false;
+		}
+
+		if (modRow >= MOD_ROWS || posJumpAssert)
+		{
+			modRow = pBreakPosition;
+			pBreakPosition = 0;
+			posJumpAssert = false;
+
+			modOrder = (modOrder + 1) & 0x7F;
+			if (modOrder >= modEntry->head.orderCount)
+			{
+				modOrder = 0;
+				calcingRows = false;
+				break;
+			}
+
+			modPattern = modEntry->head.order[modOrder];
+			if (modPattern > MAX_PATTERNS-1)
+				modPattern = MAX_PATTERNS-1;
+		}
+
+		if (editor.rowVisitTable[(modOrder * MOD_ROWS) + modRow])
+		{
+			// row has been visited before, we're now done!
+			calcingRows = false;
+			break;
+		}
+	}
+}
--- /dev/null
+++ b/src/pt2_mod2wav.h
@@ -1,0 +1,7 @@
+#pragma once
+
+#include <stdbool.h>
+
+#define MOD2WAV_FREQ 96000
+
+bool renderToWav(char *fileName, bool checkIfFileExist);
--- a/src/pt2_modloader.c
+++ b/src/pt2_modloader.c
@@ -1349,17 +1349,19 @@
 
 			if (autoPlay)
 			{
-				// start normal playback
 				editor.playMode = PLAY_MODE_NORMAL;
-				modPlay(DONT_SET_PATTERN, 0, 0);
 				editor.currMode = MODE_PLAY;
+
+				// start normal playback
+				modPlay(DONT_SET_PATTERN, 0, 0);
+
 				pointerSetMode(POINTER_MODE_PLAY, DO_CARRY);
 			}
-			else if ((oldMode == MODE_PLAY) || (oldMode == MODE_RECORD))
+			else if (oldMode == MODE_PLAY || oldMode == MODE_RECORD)
 			{
 				// use last mode
 				editor.playMode = oldPlayMode;
-				if ((oldPlayMode == PLAY_MODE_PATTERN) || (oldMode == MODE_RECORD))
+				if (oldPlayMode == PLAY_MODE_PATTERN || oldMode == MODE_RECORD)
 					modPlay(0, 0, 0);
 				else
 					modPlay(DONT_SET_PATTERN, 0, 0);
--- a/src/pt2_modplayer.c
+++ b/src/pt2_modplayer.c
@@ -1174,7 +1174,7 @@
 
 void modSetTempo(uint16_t bpm)
 {
-	int16_t smpsPerTick;
+	uint32_t smpsPerTick;
 
 	if (bpm < 32)
 		return;
@@ -1190,6 +1190,8 @@
 
 	if (editor.isSMPRendering)
 		smpsPerTick = editor.pat2SmpHQ ? audio.bpmTab28kHz[bpm] : audio.bpmTab22kHz[bpm];
+	else if (editor.isWAVRendering)
+		smpsPerTick = audio.bpmTabMod2Wav[bpm];
 	else
 		smpsPerTick = audio.bpmTab[bpm];
 
--- /dev/null
+++ b/src/pt2_pat2smp.c
@@ -1,0 +1,102 @@
+// for finding memory leaks in debug mode with Visual Studio 
+#if defined _DEBUG && defined _MSC_VER
+#include <crtdbg.h>
+#endif
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_header.h"
+#include "pt2_helpers.h"
+#include "pt2_visuals.h"
+#include "pt2_mouse.h"
+#include "pt2_audio.h"
+#include "pt2_sampler.h"
+#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)
+{
+	moduleSample_t *s;
+
+	editor.ui.pat2SmpDialogShown = false;
+
+	editor.pat2SmpBuf = (int16_t *)malloc(MAX_SAMPLE_LEN * sizeof (int16_t));
+	if (editor.pat2SmpBuf == NULL)
+	{
+		statusOutOfMemory();
+		return;
+	}
+
+	int8_t oldRow = editor.songPlaying ? 0 : modEntry->currRow;
+	uint32_t oldSamplesPerTick = samplesPerTick;
+
+	editor.isSMPRendering = true; // this must be set before restartSong()
+	storeTempVariables();
+	restartSong();
+	modEntry->row = oldRow;
+	modEntry->currRow = modEntry->row;
+
+	editor.blockMarkFlag = false;
+	pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
+	setStatusMessage("RENDERING...", NO_CARRY);
+	modSetTempo(modEntry->currBPM);
+	editor.pat2SmpPos = 0;
+
+	editor.smpRenderingDone = false;
+	while (!editor.smpRenderingDone)
+	{
+		if (!intMusic())
+			editor.smpRenderingDone = true;
+
+		outputAudio(NULL, samplesPerTick);
+	}
+	editor.isSMPRendering = false;
+	resetSong();
+
+	// set back old row and samplesPerTick
+	modEntry->row = oldRow;
+	modEntry->currRow = modEntry->row;
+	mixerSetSamplesPerTick(oldSamplesPerTick);
+
+	normalize16bitSigned(editor.pat2SmpBuf, MIN(editor.pat2SmpPos, MAX_SAMPLE_LEN));
+
+	s = &modEntry->samples[editor.currSample];
+
+	// quantize to 8-bit
+	for (uint32_t i = 0; i < editor.pat2SmpPos; i++)
+		modEntry->sampleData[s->offset+i] = editor.pat2SmpBuf[i] >> 8;
+
+	// clear the rest of the sample
+	if (editor.pat2SmpPos < MAX_SAMPLE_LEN)
+		memset(&modEntry->sampleData[s->offset+editor.pat2SmpPos], 0, MAX_SAMPLE_LEN - editor.pat2SmpPos);
+
+	free(editor.pat2SmpBuf);
+
+	memset(s->text, 0, sizeof (s->text));
+	if (editor.pat2SmpHQ)
+	{
+		strcpy(s->text, "pat2smp (a-3 tune:+5)");
+		s->fineTune = 5;
+	}
+	else
+	{
+		strcpy(s->text, "pat2smp (f-3 tune:+1)");
+		s->fineTune = 1;
+	}
+
+	s->length = editor.pat2SmpPos;
+	s->volume = 64;
+	s->loopStart = 0;
+	s->loopLength = 2;
+
+	editor.samplePos = 0;
+	fixSampleBeep(s);
+	updateCurrSample();
+
+	pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
+	displayMsg("ROWS RENDERED!");
+	setMsgPointer();
+}
--- /dev/null
+++ b/src/pt2_pat2smp.h
@@ -1,0 +1,3 @@
+#pragma once
+
+void doPat2Smp(void);
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -44,6 +44,87 @@
 };
 
 void setLoopSprites(void);
+void fixSampleBeep(moduleSample_t *s);
+
+void upSample(void)
+{
+	moduleSample_t *s = &modEntry->samples[editor.currSample];
+
+	uint32_t newLength = (s->length >> 1) & 0xFFFE;
+	if (newLength < 2)
+		return;
+
+	turnOffVoices();
+
+	// upsample
+	int8_t *ptr8 = &modEntry->sampleData[s->offset];
+	for (uint32_t i = 0; i < newLength; i++)
+		ptr8[i] = ptr8[i << 1];
+
+	// clear junk after shrunk sample
+	if (newLength < MAX_SAMPLE_LEN)
+		memset(&ptr8[newLength], 0, MAX_SAMPLE_LEN - newLength);
+
+	s->length = (uint16_t)newLength;
+	s->loopStart = (s->loopStart >> 1) & 0xFFFE;
+	s->loopLength = (s->loopLength >> 1) & 0xFFFE;
+
+	if (s->loopLength < 2)
+	{
+		s->loopStart = 0;
+		s->loopLength = 2;
+	}
+
+	fixSampleBeep(s);
+	updateCurrSample();
+
+	editor.ui.updateSongSize = true;
+	updateWindowTitle(MOD_IS_MODIFIED);
+}
+
+void downSample(void)
+{
+	moduleSample_t *s = &modEntry->samples[editor.currSample];
+
+	uint32_t newLength = s->length << 1;
+	if (newLength > MAX_SAMPLE_LEN)
+		newLength = MAX_SAMPLE_LEN;
+
+	turnOffVoices();
+
+	// downsample
+
+	int8_t *ptr8 = &modEntry->sampleData[s->offset];
+	int8_t *ptr8_2 = ptr8 - 1;
+	for (int32_t i = s->length-1; i > 0; i--)
+	{
+		ptr8[i<<1] = ptr8[i];
+		ptr8_2[i<<1] = ptr8_2[i];
+	}
+
+	s->length = newLength;
+
+	if (s->loopLength > 2)
+	{
+		uint32_t loopStart = s->loopStart << 1;
+		uint32_t loopLength = s->loopLength << 1;
+
+		if (loopStart+loopLength > s->length)
+		{
+			loopStart = 0;
+			loopLength = 2;
+		}
+
+		s->loopStart = (uint16_t)loopStart;
+		s->loopLength = (uint16_t)loopLength;
+	}
+
+	fixSampleBeep(s);
+	updateCurrSample();
+
+	editor.ui.updateSongSize = true;
+	updateWindowTitle(MOD_IS_MODIFIED);
+}
 
 void createSampleMarkTable(void)
 {
--- a/src/pt2_sampler.h
+++ b/src/pt2_sampler.h
@@ -3,6 +3,8 @@
 #include <stdint.h>
 #include <stdbool.h>
 
+void downSample(void);
+void upSample(void);
 void createSampleMarkTable(void);
 int32_t smpPos2Scr(int32_t pos);
 int32_t scr2SmpPos(int32_t x);
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -35,6 +35,8 @@
 #include "pt2_helpers.h"
 #include "pt2_scopes.h"
 #include "pt2_edit.h"
+#include "pt2_pat2smp.h"
+#include "pt2_mod2wav.h"
 
 typedef struct sprite_t
 {
@@ -58,9 +60,7 @@
 	246, 270, 278, 286, 294, 302
 };
 
-bool intMusic(void); // pt_modplayer.c
-extern int32_t samplesPerTick; // pt_audio.c
-void storeTempVariables(void); // pt_modplayer.c
+
 void updateSongInfo1(void);
 void updateSongInfo2(void);
 void updateSampler(void);
@@ -1449,6 +1449,7 @@
 			}
 
 			editor.isWAVRendering = false;
+			modSetTempo(modEntry->currBPM); // update BPM with normal audio output rate
 			displayMainScreen();
 		}
 		else
@@ -1785,8 +1786,7 @@
 void handleAskYes(void)
 {
 	char fileName[20 + 4 + 1];
-	int8_t *tmpSmpBuffer, oldSample, oldRow;
-	int32_t j, newLength, oldSamplesPerTick, loopStart, loopLength;
+	int8_t oldSample;
 	uint32_t i;
 	moduleSample_t *s;
 
@@ -1816,90 +1816,7 @@
 		case ASK_PAT2SMP:
 		{
 			restoreStatusAndMousePointer();
-
-			editor.ui.pat2SmpDialogShown = false;
-
-			editor.pat2SmpBuf = (int16_t *)malloc(MAX_SAMPLE_LEN * sizeof (int16_t));
-			if (editor.pat2SmpBuf == NULL)
-			{
-				statusOutOfMemory();
-				return;
-			}
-
-			oldRow = editor.songPlaying ? 0 : modEntry->currRow;
-			oldSamplesPerTick = samplesPerTick;
-
-			editor.isSMPRendering = true; // this must be set before restartSong()
-			storeTempVariables();
-			restartSong();
-			modEntry->row = oldRow;
-			modEntry->currRow = modEntry->row;
-
-			editor.blockMarkFlag = false;
-			pointerSetMode(POINTER_MODE_MSG2, NO_CARRY);
-			setStatusMessage("RENDERING...", NO_CARRY);
-			modSetTempo(modEntry->currBPM);
-			editor.pat2SmpPos = 0;
-
-			editor.smpRenderingDone = false;
-			while (!editor.smpRenderingDone)
-			{
-				if (!intMusic())
-					editor.smpRenderingDone = true;
-
-				outputAudio(NULL, samplesPerTick);
-			}
-			editor.isSMPRendering = false;
-			resetSong();
-
-			// set back old row and samplesPerTick
-			modEntry->row = oldRow;
-			modEntry->currRow = modEntry->row;
-			mixerSetSamplesPerTick(oldSamplesPerTick);
-
-			// normalize 16-bit samples
-			normalize16bitSigned(editor.pat2SmpBuf, MIN(editor.pat2SmpPos, MAX_SAMPLE_LEN));
-
-			s = &modEntry->samples[editor.currSample];
-
-			// quantize to 8-bit
-			for (i = 0; i < editor.pat2SmpPos; i++)
-				modEntry->sampleData[s->offset+i] = editor.pat2SmpBuf[i] >> 8;
-
-			// clear the rest of the sample
-			if (editor.pat2SmpPos < MAX_SAMPLE_LEN)
-				memset(&modEntry->sampleData[s->offset+editor.pat2SmpPos], 0, MAX_SAMPLE_LEN - editor.pat2SmpPos);
-
-			// free temp mixing buffer
-			free(editor.pat2SmpBuf);
-
-			// zero out sample text
-			memset(s->text, 0, sizeof (s->text));
-
-			// set new sample text
-			if (editor.pat2SmpHQ)
-			{
-				strcpy(s->text, "pat2smp (a-3 tune:+5)");
-				s->fineTune = 5;
-			}
-			else
-			{
-				strcpy(s->text, "pat2smp (f-3 tune:+1)");
-				s->fineTune = 1;
-			}
-
-			// new sample attributes
-			s->length = editor.pat2SmpPos;
-			s->volume = 64;
-			s->loopStart = 0;
-			s->loopLength = 2;
-
-			pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
-			displayMsg("ROWS RENDERED!");
-			setMsgPointer();
-			editor.samplePos = 0;
-			fixSampleBeep(s);
-			updateCurrSample();
+			doPat2Smp();
 		}
 		break;
 
@@ -1961,48 +1878,7 @@
 		case ASK_UPSAMPLE:
 		{
 			restoreStatusAndMousePointer();
-
-			s = &modEntry->samples[editor.currSample];
-
-			tmpSmpBuffer = (int8_t *)malloc(s->length);
-			if (tmpSmpBuffer == NULL)
-			{
-				statusOutOfMemory();
-				return;
-			}
-
-			newLength = (s->length / 2) & 0xFFFE;
-			if (newLength < 2)
-				return;
-
-			turnOffVoices();
-
-			memcpy(tmpSmpBuffer, &modEntry->sampleData[s->offset], s->length);
-
-			// upsample
-			for (j = 0; j < newLength; j++)
-				modEntry->sampleData[s->offset + j] = tmpSmpBuffer[j * 2];
-
-			if (newLength < MAX_SAMPLE_LEN)
-				memset(&modEntry->sampleData[s->offset + newLength], 0, MAX_SAMPLE_LEN - newLength);
-
-			free(tmpSmpBuffer);
-
-			s->length = newLength;
-			s->loopStart = (s->loopStart / 2) & 0xFFFE;
-			s->loopLength = (s->loopLength / 2) & 0xFFFE;
-
-			if (s->loopLength < 2)
-			{
-				s->loopStart = 0;
-				s->loopLength = 2;
-			}
-
-			fixSampleBeep(s);
-			updateCurrSample();
-
-			editor.ui.updateSongSize = true;
-			updateWindowTitle(MOD_IS_MODIFIED);
+			upSample();
 		}
 		break;
 
@@ -2009,55 +1885,7 @@
 		case ASK_DOWNSAMPLE:
 		{
 			restoreStatusAndMousePointer();
-
-			s = &modEntry->samples[editor.currSample];
-
-			tmpSmpBuffer = (int8_t *)malloc(s->length);
-			if (tmpSmpBuffer == NULL)
-			{
-				statusOutOfMemory();
-				return;
-			}
-
-			newLength = s->length * 2;
-			if (newLength > MAX_SAMPLE_LEN)
-				newLength = MAX_SAMPLE_LEN;
-
-			turnOffVoices();
-
-			memcpy(tmpSmpBuffer, &modEntry->sampleData[s->offset], s->length);
-
-			// downsample
-			for (j = 0; j < newLength; j++)
-				modEntry->sampleData[s->offset+j] = tmpSmpBuffer[j >> 1];
-
-			if (newLength < MAX_SAMPLE_LEN)
-				memset(&modEntry->sampleData[s->offset+newLength], 0, MAX_SAMPLE_LEN - newLength);
-
-			free(tmpSmpBuffer);
-
-			s->length = newLength;
-
-			if (s->loopLength > 2)
-			{
-				loopStart = s->loopStart * 2;
-				loopLength = s->loopLength * 2;
-
-				if (loopStart+loopLength > s->length)
-				{
-					loopStart = 0;
-					loopLength = 2;
-				}
-
-				s->loopStart = (uint16_t)loopStart;
-				s->loopLength = (uint16_t)loopLength;
-			}
-
-			fixSampleBeep(s);
-			updateCurrSample();
-
-			editor.ui.updateSongSize = true;
-			updateWindowTitle(MOD_IS_MODIFIED);
+			downSample();
 		}
 		break;
 
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj
@@ -290,9 +290,11 @@
     <ClInclude Include="..\..\src\pt2_header.h" />
     <ClInclude Include="..\..\src\pt2_helpers.h" />
     <ClInclude Include="..\..\src\pt2_keyboard.h" />
+    <ClInclude Include="..\..\src\pt2_mod2wav.h" />
     <ClInclude Include="..\..\src\pt2_modloader.h" />
     <ClInclude Include="..\..\src\pt2_mouse.h" />
     <ClInclude Include="..\..\src\pt2_palette.h" />
+    <ClInclude Include="..\..\src\pt2_pat2smp.h" />
     <ClInclude Include="..\..\src\pt2_patternviewer.h" />
     <ClInclude Include="..\..\src\pt2_sampleloader.h" />
     <ClInclude Include="..\..\src\pt2_sampler.h" />
@@ -331,10 +333,12 @@
     <ClCompile Include="..\..\src\pt2_helpers.c" />
     <ClCompile Include="..\..\src\pt2_keyboard.c" />
     <ClCompile Include="..\..\src\pt2_main.c" />
+    <ClCompile Include="..\..\src\pt2_mod2wav.c" />
     <ClCompile Include="..\..\src\pt2_modloader.c" />
     <ClCompile Include="..\..\src\pt2_modplayer.c" />
     <ClCompile Include="..\..\src\pt2_mouse.c" />
     <ClCompile Include="..\..\src\pt2_palette.c" />
+    <ClCompile Include="..\..\src\pt2_pat2smp.c" />
     <ClCompile Include="..\..\src\pt2_patternviewer.c" />
     <ClCompile Include="..\..\src\pt2_sampleloader.c" />
     <ClCompile Include="..\..\src\pt2_sampler.c" />
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
@@ -66,6 +66,12 @@
     <ClInclude Include="..\..\src\pt2_visuals.h">
       <Filter>headers</Filter>
     </ClInclude>
+    <ClInclude Include="..\..\src\pt2_mod2wav.h">
+      <Filter>headers</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\src\pt2_pat2smp.h">
+      <Filter>headers</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="..\..\src\pt2_audio.c" />
@@ -142,6 +148,8 @@
     <ClCompile Include="..\..\src\gfx\pt2_gfx_yes_no_dialog.c">
       <Filter>gfx</Filter>
     </ClCompile>
+    <ClCompile Include="..\..\src\pt2_mod2wav.c" />
+    <ClCompile Include="..\..\src\pt2_pat2smp.c" />
   </ItemGroup>
   <ItemGroup>
     <ResourceCompile Include="..\..\src\pt2-clone.rc" />