shithub: pt2-clone

ref: 63402619f6de96adeb1a45dff7a558d745448a1c
dir: /src/pt2_modloader.c/

View raw version
// for finding memory leaks in debug mode with Visual Studio 
#if defined _DEBUG && defined _MSC_VER
#include <crtdbg.h>
#endif

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include <ctype.h> // tolower()
#ifdef _WIN32
#include <io.h>
#else
#include <unistd.h>
#endif
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include "pt2_mouse.h"
#include "pt2_header.h"
#include "pt2_sampler.h"
#include "pt2_textout.h"
#include "pt2_audio.h"
#include "pt2_helpers.h"
#include "pt2_visuals.h"
#include "pt2_unicode.h"
#include "pt2_modloader.h"
#include "pt2_sampleloader.h"
#
typedef struct mem_t
{
	bool _eof;
	uint8_t *_ptr, *_base;
	uint32_t _cnt, _bufsiz;
} mem_t;

static bool oldAutoPlay;
static char oldFullPath[(PATH_MAX * 2) + 2];
static uint32_t oldFullPathLen;
static module_t *tempMod;

extern SDL_Window *window;

static mem_t *mopen(const uint8_t *src, uint32_t length);
static void mclose(mem_t **buf);
static int32_t mgetc(mem_t *buf);
static size_t mread(void *buffer, size_t size, size_t count, mem_t *buf);
static void mseek(mem_t *buf, int32_t offset, int32_t whence);
static uint8_t ppdecrunch(uint8_t *src, uint8_t *dst, uint8_t *offsetLens, uint32_t srcLen, uint32_t dstLen, uint8_t skipBits);

void showSongUnsavedAskBox(int8_t askScreenType)
{
	editor.ui.askScreenShown = true;
	editor.ui.askScreenType = askScreenType;

	pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
	setStatusMessage("SONG IS UNSAVED !", NO_CARRY);
	renderAskDialog();
}

bool modSave(char *fileName)
{
	int16_t tempPatternCount;
	int32_t i;
	uint32_t tempLoopLength, tempLoopStart, j, k;
	note_t tmp;
	FILE *fmodule;

	tempPatternCount = 0;

	fmodule = fopen(fileName, "wb");
	if (fmodule == NULL)
	{
		displayErrorMsg("FILE I/O ERROR !");
		return false;
	}

	for (i = 0; i < 20; i++)
		fputc(tolower(modEntry->head.moduleTitle[i]), fmodule);

	for (i = 0; i < MOD_SAMPLES; i++)
	{
		for (j = 0; j < 22; j++)
			fputc(tolower(modEntry->samples[i].text[j]), fmodule);

		fputc(modEntry->samples[i].length >> 9, fmodule);
		fputc(modEntry->samples[i].length >> 1, fmodule);
		fputc(modEntry->samples[i].fineTune & 0x0F, fmodule);
		fputc((modEntry->samples[i].volume > 64) ? 64 : modEntry->samples[i].volume, fmodule);

		tempLoopLength = modEntry->samples[i].loopLength;
		if (tempLoopLength < 2)
			tempLoopLength = 2;

		tempLoopStart = modEntry->samples[i].loopStart;
		if (tempLoopLength == 2)
			tempLoopStart = 0;

		fputc(tempLoopStart >> 9, fmodule);
		fputc(tempLoopStart >> 1, fmodule);
		fputc(tempLoopLength >> 9, fmodule);
		fputc(tempLoopLength >> 1, fmodule);
	}

	fputc(modEntry->head.orderCount & 0x00FF, fmodule);
	fputc(0x7F, fmodule); // ProTracker puts 0x7F at this place (restart pos/BPM in other trackers)

	for (i = 0; i < MOD_ORDERS; i++)
		fputc(modEntry->head.order[i] & 0xFF, fmodule);

	tempPatternCount = 0;
	for (i = 0; i < MOD_ORDERS; i++)
	{
		if (tempPatternCount < modEntry->head.order[i])
			tempPatternCount = modEntry->head.order[i];
	}

	if (++tempPatternCount > MAX_PATTERNS)
		  tempPatternCount = MAX_PATTERNS;

	fwrite((tempPatternCount <= 64) ? "M.K." : "M!K!", 1, 4, fmodule);

	for (i = 0; i < tempPatternCount; i++)
	{
		for (j = 0; j < MOD_ROWS; j++)
		{
			for (k = 0; k < AMIGA_VOICES; k++)
			{
				tmp = modEntry->patterns[i][(j * AMIGA_VOICES) + k];

				fputc((tmp.sample & 0xF0) | ((tmp.period >> 8) & 0x0F), fmodule);
				fputc(tmp.period & 0xFF, fmodule);
				fputc(((tmp.sample << 4) & 0xF0) | (tmp.command & 0x0F), fmodule);
				fputc(tmp.param, fmodule);
			}
		}
	}

	for (i = 0; i < MOD_SAMPLES; i++)
	{
		// Amiga ProTracker stuck "BEEP" sample fix
		if (modEntry->samples[i].length >= 2 && modEntry->samples[i].loopStart+modEntry->samples[i].loopLength == 2)
		{
			fputc(0, fmodule);
			fputc(0, fmodule);

			k = modEntry->samples[i].length;
			for (j = 2; j < k; j++)
				fputc(modEntry->sampleData[modEntry->samples[i].offset+j], fmodule);
		}
		else
		{
			fwrite(&modEntry->sampleData[MAX_SAMPLE_LEN * i], 1, modEntry->samples[i].length, fmodule);
		}
	}

	fclose(fmodule);

	displayMsg("MODULE SAVED !");
	setMsgPointer();

	editor.diskop.cached = false;
	if (editor.ui.diskOpScreenShown)
		editor.ui.updateDiskOpFileList = true;

	updateWindowTitle(MOD_NOT_MODIFIED);
	return true;
}

static int8_t checkModType(const char *buf)
{
	     if (!strncmp(buf, "M.K.", 4)) return FORMAT_MK;   // ProTracker v1.x, handled as ProTracker v2.x
	else if (!strncmp(buf, "M!K!", 4)) return FORMAT_MK2;  // ProTracker v2.x (if >64 patterns)
	else if (!strncmp(buf, "FLT4", 4)) return FORMAT_FLT4; // StarTrekker (4ch), handled as ProTracker v2.x
	else if (!strncmp(buf, "1CHN", 4)) return FORMAT_1CHN; // handled as 4ch
	else if (!strncmp(buf, "2CHN", 4)) return FORMAT_2CHN; // FastTracker II, handled as 4ch
	else if (!strncmp(buf, "3CHN", 4)) return FORMAT_3CHN; // handled as 4ch
	else if (!strncmp(buf, "4CHN", 4)) return FORMAT_4CHN; // rare type, not sure what tracker it comes from
	else if (!strncmp(buf, "N.T.", 4)) return FORMAT_MK;   // NoiseTracker 1.0, handled as ProTracker v2.x
	else if (!strncmp(buf, "M&K!", 4)) return FORMAT_FEST; // Special NoiseTracker format (used in music disks?)
	else if (!strncmp(buf, "FEST", 4)) return FORMAT_FEST; // Special NoiseTracker format (used in music disks?)
	else if (!strncmp(buf, "NSMS", 4)) return FORMAT_MK;   // OpenMPT Load_mod.cpp: "kingdomofpleasure.mod by bee hunter"
	else if (!strncmp(buf, "LARD", 4)) return FORMAT_MK;   // OpenMPT Load_mod.cpp: "judgement_day_gvine.mod by 4-mat"
	else if (!strncmp(buf, "PATT", 4)) return FORMAT_MK;   // OpenMPT Load_mod.cpp: "ProTracker 3.6"

	return FORMAT_UNKNOWN; // may be The Ultimate SoundTracker, 15 samples
}

// converts zeroes to spaces in a string, up until the last zero found
static void fixZeroesInString(char *str, uint32_t maxLength)
{
	int32_t i;

	for (i = maxLength-1; i >= 0; i--)
	{
		if (str[i] != '\0')
			break;
	}

	// convert zeroes to spaces
	if (i > 0)
	{
		for (int32_t j = 0; j < i; j++)
		{
			if (str[j] == '\0')
				str[j] = ' ';
		}
	}
}

module_t *modLoad(UNICHAR *fileName)
{
	bool mightBeSTK, lateSTKVerFlag, veryLateSTKVerFlag;
	char modSig[4], tmpChar;
	int8_t numSamples;
	uint8_t ppCrunchData[4], bytes[4], *ppBuffer;
	uint8_t *modBuffer, ch, row, pattern, channels;
	uint16_t ciaPeriod;
	int32_t i, loopStart, loopLength, loopOverflowVal;
	uint32_t j, PP20, ppPackLen, ppUnpackLen;
	FILE *fmodule;
	module_t *newModule;
	moduleSample_t *s;
	note_t *note;
	mem_t *mod;

	/* these flags are kinda dumb and inaccurate, but we
	** don't aim for excellent STK import anyway. */
	veryLateSTKVerFlag = false; // "DFJ SoundTracker III" nad later
	lateSTKVerFlag = false; // "TJC SoundTracker II" and later
	mightBeSTK = false;

	mod = NULL;
	ppBuffer = NULL;
	modBuffer = NULL;
	fmodule = NULL;
	newModule = NULL;

	newModule = (module_t *)calloc(1, sizeof (module_t));
	if (newModule == NULL)
	{
		statusOutOfMemory();
		goto modLoadError;
	}

	fmodule = UNICHAR_FOPEN(fileName, "rb");
	if (fmodule == NULL)
	{
		displayErrorMsg("FILE I/O ERROR !");
		goto modLoadError;
	}

	fseek(fmodule, 0, SEEK_END);
	newModule->head.moduleSize = ftell(fmodule);
	fseek(fmodule, 0, SEEK_SET);

	// check if mod is a powerpacker mod
	fread(&PP20, 4, 1, fmodule);
	if (PP20 == 0x30325850) // "PX20"
	{
		displayErrorMsg("ENCRYPTED PPACK !");
		goto modLoadError;
	}
	else if (PP20 == 0x30325050) // "PP20"
	{
		ppPackLen = newModule->head.moduleSize;
		if (ppPackLen & 3)
		{
			displayErrorMsg("POWERPACKER ERROR");
			goto modLoadError;
		}

		fseek(fmodule, ppPackLen - 4, SEEK_SET);

		ppCrunchData[0] = (uint8_t)fgetc(fmodule);
		ppCrunchData[1] = (uint8_t)fgetc(fmodule);
		ppCrunchData[2] = (uint8_t)fgetc(fmodule);
		ppCrunchData[3] = (uint8_t)fgetc(fmodule);

		ppUnpackLen = (ppCrunchData[0] << 16) | (ppCrunchData[1] << 8) | ppCrunchData[2];

		// smallest and biggest possible .MOD
		if (ppUnpackLen < 2108 || ppUnpackLen > 4195326)
		{
			displayErrorMsg("NOT A MOD FILE !");
			goto modLoadError;
		}

		ppBuffer = (uint8_t *)malloc(ppPackLen);
		if (ppBuffer == NULL)
		{
			statusOutOfMemory();
			goto modLoadError;
		}

		modBuffer = (uint8_t *)malloc(ppUnpackLen);
		if (modBuffer == NULL)
		{
			statusOutOfMemory();
			goto modLoadError;
		}

		fseek(fmodule, 0, SEEK_SET);
		fread(ppBuffer, 1, ppPackLen, fmodule);
		fclose(fmodule);
		ppdecrunch(ppBuffer + 8, modBuffer, ppBuffer + 4, ppPackLen - 12, ppUnpackLen, ppCrunchData[3]);
		free(ppBuffer);
		newModule->head.moduleSize = ppUnpackLen;
	}
	else
	{
		// smallest and biggest possible PT .MOD
		if (newModule->head.moduleSize < 2108 || newModule->head.moduleSize > 4195326)
		{
			displayErrorMsg("NOT A MOD FILE !");
			goto modLoadError;
		}

		modBuffer = (uint8_t *)malloc(newModule->head.moduleSize);
		if (modBuffer == NULL)
		{
			statusOutOfMemory();
			goto modLoadError;
		}

		fseek(fmodule, 0, SEEK_SET);
		fread(modBuffer, 1, newModule->head.moduleSize, fmodule);
		fclose(fmodule);
	}

	mod = mopen(modBuffer, newModule->head.moduleSize);
	if (mod == NULL)
	{
		displayErrorMsg("FILE I/O ERROR !");
		goto modLoadError;
	}

	// check module tag
	mseek(mod, 0x0438, SEEK_SET);
	mread(modSig, 1, 4, mod);

	newModule->head.format = checkModType(modSig);
	if (newModule->head.format == FORMAT_UNKNOWN)
		mightBeSTK = true;

	     if (newModule->head.format == FORMAT_1CHN) channels = 1;
	else if (newModule->head.format == FORMAT_2CHN) channels = 2;
	else if (newModule->head.format == FORMAT_3CHN) channels = 3;
	else channels = 4;

	mseek(mod, 0, SEEK_SET);

	mread(newModule->head.moduleTitle, 1, 20, mod);
	newModule->head.moduleTitle[20] = '\0';

	for (i = 0; i < 20; i++)
	{
		tmpChar = newModule->head.moduleTitle[i];
		if ((tmpChar < ' ' || tmpChar > '~') && tmpChar != '\0')
			tmpChar = ' ';

		newModule->head.moduleTitle[i] = (char)tolower(tmpChar);
	}

	fixZeroesInString(newModule->head.moduleTitle, 20);

	// read sample information
	for (i = 0; i < MOD_SAMPLES; i++)
	{
		s = &newModule->samples[i];

		if (mightBeSTK && i > 14)
		{
			s->loopLength = 2;
		}
		else
		{
			mread(s->text, 1, 22, mod);
			s->text[22] = '\0';

			for (j = 0; j < 22; j++)
			{
				tmpChar = s->text[j];
				if ((tmpChar < ' ' || tmpChar > '~') && tmpChar != '\0')
					tmpChar = ' ';

				s->text[j] = (char)tolower(tmpChar);
			}

			fixZeroesInString(s->text, 22);

			s->realLength = ((mgetc(mod) << 8) | mgetc(mod)) * 2;
			if (s->realLength > MAX_SAMPLE_LEN)
				s->length = MAX_SAMPLE_LEN;
			else
				s->length = (uint16_t)s->realLength;

			if (s->length > 9999)
				lateSTKVerFlag = true; // Only used if mightBeSTK is set

			if (newModule->head.format == FORMAT_FEST)
				s->fineTune = (uint8_t)((-mgetc(mod) & 0x1F) / 2); // One more bit of precision, + inverted
			else
				s->fineTune = (uint8_t)mgetc(mod) & 0x0F;

			s->volume = (int8_t)mgetc(mod);
			s->volume = CLAMP(s->volume, 0, 64);

			loopStart = ((mgetc(mod) << 8) | mgetc(mod)) * 2;
			loopLength = ((mgetc(mod) << 8) | mgetc(mod)) * 2;

			if (loopStart > MAX_SAMPLE_LEN || loopStart+loopLength > MAX_SAMPLE_LEN)
			{
				s->loopStart = 0;
				s->loopLength = 2;
			}
			else
			{
				s->loopStart = (uint16_t)loopStart;
				s->loopLength = (uint16_t)loopLength;
			}

			if (mightBeSTK)
				s->loopStart /= 2;

			if (s->loopLength < 2)
				s->loopLength = 2;

			// fix for poorly converted STK->PTMOD modules.
			if (!mightBeSTK && s->loopLength > 2 && s->loopStart+s->loopLength > s->length)
			{
				if ((s->loopStart/2) + s->loopLength <= s->length)
					s->loopStart /= 2;
			}

			if (mightBeSTK)
			{
				if (s->loopLength > 2 && s->loopStart < s->length)
				{
					s->tmpLoopStart = s->loopStart; // for sample data reading later on
					s->length -= s->loopStart;
					s->realLength -= s->loopStart;
					s->loopStart = 0;
				}

				// no finetune in STK/UST
				s->fineTune = 0;
			}

			// some modules are broken like this, adjust sample length if possible (this is ok if we have room)
			if (s->loopLength > 2 && s->loopStart+s->loopLength > s->length)
			{
				loopOverflowVal = (s->loopStart+s->loopLength) - s->length;
				if (s->length+loopOverflowVal <= MAX_SAMPLE_LEN)
				{
					s->length += loopOverflowVal; // this is safe, we're calloc()'ing 65535*(31+1) bytes
				}
				else
				{
					s->loopStart = 0;
					s->loopLength = 2;
				}
			}
		}
	}

	// STK 2.5 had loopStart in words, not bytes. Convert if late version STK.
	for (i = 0; i < 15; i++)
	{
		if (mightBeSTK && lateSTKVerFlag)
		{
			s = &newModule->samples[i];
			if (s->loopStart > 2)
			{
				s->length -= s->tmpLoopStart;
				s->tmpLoopStart *= 2;
			}
		}
	}

	newModule->head.orderCount = (uint8_t)mgetc(mod);

	// fixes beatwave.mod (129 orders) and other weird MODs
	if (newModule->head.orderCount > 128)
	{
		if (newModule->head.orderCount > 129)
		{
			displayErrorMsg("NOT A MOD FILE !");
			goto modLoadError;
		}

		newModule->head.orderCount = 128;
	}

	if (newModule->head.orderCount == 0)
	{
		displayErrorMsg("NOT A MOD FILE !");
		goto modLoadError;
	}

	newModule->head.restartPos = (uint8_t)mgetc(mod);
	if (mightBeSTK && (newModule->head.restartPos == 0 || newModule->head.restartPos > 220))
	{
		displayErrorMsg("NOT A MOD FILE !");
		goto modLoadError;
	}

	if (mightBeSTK)
	{
		/* If we're still here at this point and the mightBeSTK flag is set,
		** then it's definitely a proper The Ultimate SoundTracker (STK) module. */

		newModule->head.format = FORMAT_STK;

		if (newModule->head.restartPos != 120) // 120 is a special case and means 50Hz (125BPM)
		{
			if (newModule->head.restartPos > 239)
				newModule->head.restartPos = 239;

			// convert UST tempo to BPM

			ciaPeriod = (240 - newModule->head.restartPos) * 122;
			newModule->head.initBPM = (uint16_t)round(((double)CIA_PAL_CLK / ciaPeriod) * (125.0 / 50.0));
		}

		newModule->head.restartPos = 0;
	}

	for (i = 0; i < MOD_ORDERS; i++)
	{
		newModule->head.order[i] = (int16_t)mgetc(mod);
		if (newModule->head.order[i] > newModule->head.patternCount)
			newModule->head.patternCount = newModule->head.order[i];
	}

	if (++newModule->head.patternCount > MAX_PATTERNS)
	{
		displayErrorMsg("UNSUPPORTED MOD !");
		goto modLoadError;
	}

	if (newModule->head.format != FORMAT_STK) // The Ultimate SoundTracker MODs doesn't have this tag
		mseek(mod, 4, SEEK_CUR); // we already read/tested the tag earlier, skip it

	// init 100 patterns and load patternCount of patterns
	for (pattern = 0; pattern < MAX_PATTERNS; pattern++)
	{
		newModule->patterns[pattern] = (note_t *)calloc(MOD_ROWS * AMIGA_VOICES, sizeof (note_t));
		if (newModule->patterns[pattern] == NULL)
		{
			statusOutOfMemory();
			goto modLoadError;
		}
	}

	// load pattern data
	for (pattern = 0; pattern < newModule->head.patternCount; pattern++)
	{
		note = newModule->patterns[pattern];
		for (row = 0; row < MOD_ROWS; row++)
		{
			for (ch = 0; ch < channels; ch++)
			{
				mread(bytes, 1, 4, mod);

				note->period = ((bytes[0] & 0x0F) << 8) | bytes[1];
				note->sample = (bytes[0] & 0xF0) | (bytes[2] >> 4); // don't (!) clamp, the player checks for invalid samples
				note->command = bytes[2] & 0x0F;
				note->param = bytes[3];

				if (mightBeSTK)
				{
					if (note->command == 0xC || note->command == 0xD || note->command == 0xE)
					{
						// "TJC SoundTracker II" and later
						lateSTKVerFlag = true;
					}

					if (note->command == 0xF)
					{
						// "DFJ SoundTracker III" and later
						lateSTKVerFlag = true;
						veryLateSTKVerFlag = true;
					}
				}

				note++;
			}

			if (channels < 4)
				note += AMIGA_VOICES - channels;
		}
	}

	/* TODO: Find out if song is FORMAT_NT through heuristics
	** Only detected for FEST songs for now. */

	// pattern command conversion
	if (mightBeSTK || newModule->head.format == FORMAT_4CHN ||
		newModule->head.format == FORMAT_NT || newModule->head.format == FORMAT_FEST)
	{
		for (pattern = 0; pattern < newModule->head.patternCount; pattern++)
		{
			note = newModule->patterns[pattern];
			for (j = 0; j < MOD_ROWS*4; j++)
			{
				if (newModule->head.format == FORMAT_NT || newModule->head.format == FORMAT_FEST)
				{
					// any Dxx == D00 in N.T./FEST modules
					if (note->command == 0xD)
						note->param = 0;

					// effect F with param 0x00 does nothing in NT
					if (note->command == 0xF && note->param == 0)
						note->command = 0;
				}
				else if (mightBeSTK)
				{
					// convert STK effects to PT effects

					if (!lateSTKVerFlag)
					{
						// old SoundTracker 1.x commands

						if (note->command == 1)
						{
							// arpeggio
							note->command = 0;
						}
						else if (note->command == 2)
						{
							// pitch slide
							if (note->param & 0xF0)
							{
								// pitch slide down
								note->command = 2;
								note->param >>= 4;
							}
							else if (note->param & 0x0F)
							{
								// pitch slide up
								note->command = 1;
							}
						}
					}
					else
					{
						// "DFJ SoundTracker II" or later

						if (note->command == 0xD)
						{
							if (veryLateSTKVerFlag) // "DFJ SoundTracker III" or later
							{
								// pattern break w/ no param (param must be cleared to fix some songs)
								note->param = 0;
							}
							else
							{
								// volume slide
								note->command = 0xA;
							}
						}
					}
				}
				else if (newModule->head.format == FORMAT_4CHN) // 4CHN != PT MOD
				{
					// remove E8x (pan) commands as these are Karplus-Strong in ProTracker
					if (note->command == 0xE && (note->param >> 4) == 0x8)
					{
						note->command = 0;
						note->param = 0;
					}

					// effect F with param 0x00 does nothing in these 4CHN formats
					if (note->command == 0xF && note->param == 0)
					{
						note->command = 0;
						note->param = 0;
					}
				}

				note++;
			}
		}
	}

	for (i = 0; i < MOD_SAMPLES; i++)
		newModule->samples[i].offset = MAX_SAMPLE_LEN * i;

	newModule->sampleData = (int8_t *)calloc(MOD_SAMPLES + 2, MAX_SAMPLE_LEN); // +2 sample slots for overflow safety (Paula and scopes)
	if (newModule->sampleData == NULL)
	{
		statusOutOfMemory();
		goto modLoadError;
	}

	// load sample data
	numSamples = (newModule->head.format == FORMAT_STK) ? 15 : 31;
	for (i = 0; i < numSamples; i++)
	{
		s = &newModule->samples[i];
		if (mightBeSTK && (s->loopLength > 2 && s->loopLength < s->length))
			mseek(mod, s->tmpLoopStart, SEEK_CUR);

		mread(&newModule->sampleData[s->offset], 1, s->length, mod);
		if (s->realLength > s->length)
			mseek(mod, s->realLength - s->length, SEEK_CUR);

		// fix beeping samples
		if (s->length >= 2 && s->loopStart+s->loopLength <= 2)
		{
			newModule->sampleData[s->offset+0] = 0;
			newModule->sampleData[s->offset+1] = 0;
		}
	}

	mclose(&mod);
	free(modBuffer);

	for (i = 0; i < AMIGA_VOICES; i++)
		newModule->channels[i].n_chanindex = i;

	return newModule;

modLoadError:
	if (mod != NULL) mclose(&mod);
	if (modBuffer != NULL) free(modBuffer);
	if (ppBuffer != NULL) free(ppBuffer);

	if (newModule != NULL)
	{
		for (i = 0; i < MAX_PATTERNS; i++)
		{
			if (newModule->patterns[i] != NULL)
				free(newModule->patterns[i]);
		}

		free(newModule);
	}

	return NULL;
}

bool saveModule(bool checkIfFileExist, bool giveNewFreeFilename)
{
	char fileName[128], tmpBuffer[64];
	uint16_t i;
	struct stat statBuffer;

	memset(fileName, 0, sizeof (fileName));

	if (ptConfig.modDot)
	{
		// extension.filename
		if (*modEntry->head.moduleTitle == '\0')
		{
			strcat(fileName, "mod.untitled");
		}
		else
		{
			strcat(fileName, "mod.");
			for (i = 4; i < 20+4; i++)
			{
				fileName[i] = (char)tolower(modEntry->head.moduleTitle[i-4]);
				if (fileName[i] == '\0') break;
				sanitizeFilenameChar(&fileName[i]);
			}
		}
	}
	else
	{
		// filename.extension
		if (*modEntry->head.moduleTitle == '\0')
		{
			strcat(fileName, "untitled.mod");
		}
		else
		{
			for (i = 0; i < 20; i++)
			{
				fileName[i] = (char)tolower(modEntry->head.moduleTitle[i]);
				if (fileName[i] == '\0') break;
				sanitizeFilenameChar(&fileName[i]);
			}
			strcat(fileName, ".mod");
		}
	}

	if (giveNewFreeFilename && stat(fileName, &statBuffer) == 0)
	{
		for (uint16_t j = 1; j <= 9999; j++)
		{
			memset(fileName, 0, sizeof (fileName));
			if (ptConfig.modDot)
			{
				// extension.filename
				if (*modEntry->head.moduleTitle == '\0')
				{
					sprintf(fileName, "mod.untitled-%d", j);
				}
				else
				{
					for (i = 0; i < 20; i++)
					{
						tmpBuffer[i] = (char)tolower(modEntry->head.moduleTitle[i]);
						if (tmpBuffer[i] == '\0') break;
						sanitizeFilenameChar(&tmpBuffer[i]);
					}
					sprintf(fileName, "mod.%s-%d", tmpBuffer, j);
				}
			}
			else
			{
				// filename.extension
				if (*modEntry->head.moduleTitle == '\0')
				{
					sprintf(fileName, "untitled-%d.mod", j);
				}
				else
				{
					for (i = 0; i < 20; i++)
					{
						tmpBuffer[i] = (char)tolower(modEntry->head.moduleTitle[i]);
						if (tmpBuffer[i] == '\0') break;
						sanitizeFilenameChar(&tmpBuffer[i]);
					}
					sprintf(fileName, "%s-%d.mod", tmpBuffer, j);
				}
			}

			if (stat(fileName, &statBuffer) != 0)
				break;
		}
	}

	if (checkIfFileExist)
	{
		if (stat(fileName, &statBuffer) == 0)
		{
			editor.ui.askScreenShown = true;
			editor.ui.askScreenType = ASK_SAVEMOD_OVERWRITE;
			pointerSetMode(POINTER_MODE_MSG1, NO_CARRY);
			setStatusMessage("OVERWRITE FILE ?", NO_CARRY);
			renderAskDialog();
			return -1;
		}
	}

	if (editor.ui.askScreenShown)
	{
		editor.ui.answerNo = false;
		editor.ui.answerYes = false;
		editor.ui.askScreenShown = false;
	}

	return modSave(fileName);
}

static mem_t *mopen(const uint8_t *src, uint32_t length)
{
	mem_t *b;

	if (src == NULL || length == 0)
		return NULL;

	b = (mem_t *)malloc(sizeof (mem_t));
	if (b == NULL)
		return NULL;

	b->_base = (uint8_t *)src;
	b->_ptr = (uint8_t *)src;
	b->_cnt = length;
	b->_bufsiz = length;
	b->_eof = false;

	return b;
}

static void mclose(mem_t **buf)
{
	if (*buf != NULL)
	{
		free(*buf);
		*buf = NULL;
	}
}

static int32_t mgetc(mem_t *buf)
{
	int32_t b;

	if (buf == NULL || buf->_ptr == NULL || buf->_cnt <= 0)
		return 0;

	b = *buf->_ptr;

	buf->_cnt--;
	buf->_ptr++;

	if (buf->_cnt <= 0)
	{
		buf->_ptr = buf->_base + buf->_bufsiz;
		buf->_cnt = 0;
		buf->_eof = true;
	}

	return (int32_t)b;
}

static size_t mread(void *buffer, size_t size, size_t count, mem_t *buf)
{
	int32_t pcnt;
	size_t wrcnt;

	if (buf == NULL || buf->_ptr == NULL)
		return 0;

	wrcnt = size * count;
	if (size == 0 || buf->_eof)
		return 0;

	pcnt = (buf->_cnt > (uint32_t)wrcnt) ? (uint32_t)wrcnt : buf->_cnt;
	memcpy(buffer, buf->_ptr, pcnt);

	buf->_cnt -= pcnt;
	buf->_ptr += pcnt;

	if (buf->_cnt <= 0)
	{
		buf->_ptr = buf->_base + buf->_bufsiz;
		buf->_cnt = 0;
		buf->_eof = true;
	}

	return pcnt/size;
}

static void mseek(mem_t *buf, int32_t offset, int32_t whence)
{
	if (buf == NULL)
		return;

	if (buf->_base)
	{
		switch (whence)
		{
			case SEEK_SET: buf->_ptr  = buf->_base + offset; break;
			case SEEK_CUR: buf->_ptr += offset; break;
			case SEEK_END: buf->_ptr  = buf->_base + buf->_bufsiz + offset; break;
			default: break;
		}

		buf->_eof = false;
		if (buf->_ptr >= buf->_base+buf->_bufsiz)
		{
			buf->_ptr = buf->_base+buf->_bufsiz;
			buf->_eof = true;
		}

		buf->_cnt = (buf->_base+buf->_bufsiz) - buf->_ptr;
	}
}

/* Code taken from Heikki Orsila's amigadepack. Seems to have no license,
** so I'll assume it fits into wtfpl (wtfpl.net). Heikki should contact me
** if it shall not.
** Modified by 8bitbubsy */

#define PP_READ_BITS(nbits, var)            \
  bitCnt = (nbits);                         \
  while (bitsLeft < bitCnt) {               \
	if (bufSrc < src) return false;         \
	bitBuffer |= (*--bufSrc << bitsLeft);   \
	bitsLeft += 8;                          \
  }                                         \
  (var) = 0;                                \
  bitsLeft -= bitCnt;                       \
  while (bitCnt--) {                        \
	(var) = ((var) << 1) | (bitBuffer & 1); \
	bitBuffer >>= 1;                        \
  }                                         \

static uint8_t ppdecrunch(uint8_t *src, uint8_t *dst, uint8_t *offsetLens, uint32_t srcLen, uint32_t dstLen, uint8_t skipBits)
{
	uint8_t *bufSrc, *dstEnd, *out, bitsLeft, bitCnt;
	uint32_t x, todo, offBits, offset, written, bitBuffer;

	if (src == NULL || dst == NULL || offsetLens == NULL)
		return false;

	bitsLeft = 0;
	bitBuffer = 0;
	written = 0;
	bufSrc = src + srcLen;
	out = dst + dstLen;
	dstEnd = out;

	PP_READ_BITS(skipBits, x);
	while (written < dstLen)
	{
		PP_READ_BITS(1, x);
		if (x == 0)
		{
			todo = 1;

			do
			{
				PP_READ_BITS(2, x);
				todo += x;
			}
			while (x == 3);

			while (todo--)
			{
				PP_READ_BITS(8, x);

				if (out <= dst)
					return false;

				*--out = (uint8_t)x;
				written++;
			}

			if (written == dstLen)
				break;
		}

		PP_READ_BITS(2, x);

		offBits = offsetLens[x];
		todo = x + 2;

		if (x == 3)
		{
			PP_READ_BITS(1, x);
			if (x == 0) offBits = 7;

			PP_READ_BITS((uint8_t)offBits, offset);
			do
			{
				PP_READ_BITS(3, x);
				todo += x;
			}
			while (x == 7);
		}
		else
		{
			PP_READ_BITS((uint8_t)offBits, offset);
		}

		if (out+offset >= dstEnd)
			return false;

		while (todo--)
		{
			x = out[offset];

			if (out <= dst)
				return false;

			*--out = (uint8_t)x;
			written++;
		}
	}

	return true;
}

void setupNewMod(void)
{
	int8_t i;

	// setup GUI text pointers
	for (i = 0; i < MOD_SAMPLES; i++)
	{
		modEntry->samples[i].volumeDisp = &modEntry->samples[i].volume;
		modEntry->samples[i].lengthDisp = &modEntry->samples[i].length;
		modEntry->samples[i].loopStartDisp = &modEntry->samples[i].loopStart;
		modEntry->samples[i].loopLengthDisp = &modEntry->samples[i].loopLength;

		fillSampleRedoBuffer(i);
	}

	modSetPos(0, 0);
	modSetPattern(0); // set pattern to 00 instead of first order's pattern

	editor.currEditPatternDisp = &modEntry->currPattern;
	editor.currPosDisp = &modEntry->currOrder;
	editor.currPatternDisp = &modEntry->head.order[0];
	editor.currPosEdPattDisp = &modEntry->head.order[0];
	editor.currLengthDisp = &modEntry->head.orderCount;

	// calculate MOD size
	editor.ui.updateSongSize = true;

	editor.muted[0] = false;
	editor.muted[1] = false;
	editor.muted[2] = false;
	editor.muted[3] = false;

	editor.editMoveAdd = 1;
	editor.currSample = 0;
	editor.musicTime = 0;
	editor.modLoaded = true;
	editor.blockMarkFlag = false;
	editor.sampleZero = false;
	editor.keypadSampleOffset = 0;

	setLEDFilter(false); // real PT doesn't do this, but that's insane

	updateWindowTitle(MOD_NOT_MODIFIED);

	modSetSpeed(6);

	if (modEntry->head.initBPM > 0)
		modSetTempo(modEntry->head.initBPM);
	else
		modSetTempo(125);

	updateCurrSample();
	editor.samplePos = 0;
	updateSamplePos();
}

void loadModFromArg(char *arg)
{
	uint32_t filenameLen;
	UNICHAR *filenameU;

	editor.ui.introScreenShown = false;
	statusAllRight();

	filenameLen = (uint32_t)strlen(arg);

	filenameU = (UNICHAR *)calloc((filenameLen + 2), sizeof (UNICHAR));
	if (filenameU == NULL)
	{
		statusOutOfMemory();
		return;
	}

#ifdef _WIN32
	MultiByteToWideChar(CP_UTF8, 0, arg, -1, filenameU, filenameLen);
#else
	strcpy(filenameU, arg);
#endif

	tempMod = modLoad(filenameU);
	if (tempMod != NULL)
	{
		modEntry->moduleLoaded = false;
		modFree();
		modEntry = tempMod;
		setupNewMod();
		modEntry->moduleLoaded = true;
	}
	else
	{
		editor.errorMsgActive = true;
		editor.errorMsgBlock = true;
		editor.errorMsgCounter = 0;

		// status/error message is set in the mod loader
		setErrPointer();
	}

	free(filenameU);
}

static bool testExtension(char *ext, uint8_t extLen, char *fullPath)
{
	// checks for EXT.filename and filename.EXT
	char *fileName, begStr[8], endStr[8];
	uint32_t fileNameLen;

	extLen++; // add one to length (dot)

	fileName = strrchr(fullPath, DIR_DELIMITER);
	if (fileName != NULL)
		fileName++;
	else
		fileName = fullPath;

	fileNameLen = (uint32_t)strlen(fileName);
	if (fileNameLen >= extLen)
	{
		sprintf(begStr, "%s.", ext);
		if (!_strnicmp(begStr, fileName, extLen))
			return true;

		sprintf(endStr, ".%s", ext);
		if (!_strnicmp(endStr, fileName + (fileNameLen - extLen), extLen))
			return true;
	}

	return false;
}

void loadDroppedFile(char *fullPath, uint32_t fullPathLen, bool autoPlay, bool songModifiedCheck)
{
	bool isMod;
	char *fileName, *ansiName;
	uint8_t oldMode, oldPlayMode;
	UNICHAR *fullPathU;

	// don't allow drag n' drop if the tracker is busy
	if (editor.ui.pointerMode == POINTER_MODE_MSG1 ||
		editor.diskop.isFilling || editor.isWAVRendering ||
		editor.ui.samplerFiltersBoxShown || editor.ui.samplerVolBoxShown)
	{
		return;
	}

	ansiName = (char *)calloc(fullPathLen + 10, sizeof (char));
	if (ansiName == NULL)
	{
		statusOutOfMemory();
		return;
	}

	fullPathU = (UNICHAR *)calloc((fullPathLen + 2), sizeof (UNICHAR));
	if (fullPathU == NULL)
	{
		statusOutOfMemory();
		return;
	}

#ifdef _WIN32
	MultiByteToWideChar(CP_UTF8, 0, fullPath, -1, fullPathU, fullPathLen);
#else
	strcpy(fullPathU, fullPath);
#endif

	unicharToAnsi(ansiName, fullPathU, fullPathLen);

	// make a new pointer point to filename (strip path)
	fileName = strrchr(ansiName, DIR_DELIMITER);
	if (fileName != NULL)
		fileName++;
	else
		fileName = ansiName;

	// check if the file extension is a module (FIXME: check module by content instead..?)
	isMod = false;
	     if (testExtension("MOD", 3, fileName)) isMod = true;
	else if (testExtension("M15", 3, fileName)) isMod = true;
	else if (testExtension("STK", 3, fileName)) isMod = true;
	else if (testExtension("NST", 3, fileName)) isMod = true;
	else if (testExtension("UST", 3, fileName)) isMod = true;
	else if (testExtension("PP",  2, fileName)) isMod = true;
	else if (testExtension("NT",  2, fileName)) isMod = true;

	if (isMod)
	{
		if (songModifiedCheck && modEntry->modified)
		{
			free(ansiName);
			free(fullPathU);

			memcpy(oldFullPath, fullPath, fullPathLen);
			oldFullPath[fullPathLen+0] = 0;
			oldFullPath[fullPathLen+1] = 0;

			oldFullPathLen = fullPathLen;
			oldAutoPlay = autoPlay;

			// de-minimize window and set focus so that the user sees the message box
			SDL_RestoreWindow(window);
			SDL_RaiseWindow(window);

			showSongUnsavedAskBox(ASK_DISCARD_SONG_DRAGNDROP);
			return;
		}

		tempMod = modLoad(fullPathU);
		if (tempMod != NULL)
		{
			oldMode = editor.currMode;
			oldPlayMode = editor.playMode;

			modStop();
			modFree();

			modEntry = tempMod;
			setupNewMod();
			modEntry->moduleLoaded = true;

			statusAllRight();

			if (autoPlay)
			{
				// start normal playback
				editor.playMode = PLAY_MODE_NORMAL;
				modPlay(DONT_SET_PATTERN, 0, 0);
				editor.currMode = MODE_PLAY;
				pointerSetMode(POINTER_MODE_PLAY, DO_CARRY);
			}
			else if ((oldMode == MODE_PLAY) || (oldMode == MODE_RECORD))
			{
				// use last mode
				editor.playMode = oldPlayMode;
				if ((oldPlayMode == PLAY_MODE_PATTERN) || (oldMode == MODE_RECORD))
					modPlay(0, 0, 0);
				else
					modPlay(DONT_SET_PATTERN, 0, 0);
				editor.currMode = oldMode;

				if (oldMode == MODE_RECORD)
					pointerSetMode(POINTER_MODE_RECORD, DO_CARRY);
				else
					pointerSetMode(POINTER_MODE_PLAY, DO_CARRY);
			}
			else
			{
				// stop playback
				editor.playMode = PLAY_MODE_NORMAL;
				editor.currMode = MODE_IDLE;
				editor.songPlaying = false;
				pointerSetMode(POINTER_MODE_IDLE, DO_CARRY);
			}

			displayMainScreen();
		}
		else
		{
			editor.errorMsgActive = true;
			editor.errorMsgBlock = true;
			editor.errorMsgCounter = 0;
			setErrPointer(); // status/error message is set in the mod loader
		}
	}
	else
	{
		loadSample(fullPathU, fileName);
	}

	free(ansiName);
	free(fullPathU);
}

void loadDroppedFile2(void)
{
	loadDroppedFile(oldFullPath, oldFullPathLen, oldAutoPlay, false);
}

module_t *createNewMod(void)
{
	uint8_t i;
	module_t *newMod;

	newMod = (module_t *)calloc(1, sizeof (module_t));
	if (newMod == NULL)
		goto oom;

	for (i = 0; i < MAX_PATTERNS; i++)
	{
		newMod->patterns[i] = (note_t *)calloc(1, MOD_ROWS * sizeof (note_t) * AMIGA_VOICES);
		if (newMod->patterns[i] == NULL)
			goto oom;
	}

	newMod->sampleData = (int8_t *)calloc(MOD_SAMPLES + 2, MAX_SAMPLE_LEN); // +2 sample slots for overflow safety (Paula and scopes)
	if (newMod->sampleData == NULL)
		goto oom;

	newMod->head.orderCount = 1;
	newMod->head.patternCount = 1;

	for (i = 0; i < MOD_SAMPLES; i++)
	{
		newMod->samples[i].offset = MAX_SAMPLE_LEN * i;
		newMod->samples[i].loopLength = 2;

		// setup GUI text pointers
		newMod->samples[i].volumeDisp = &newMod->samples[i].volume;
		newMod->samples[i].lengthDisp = &newMod->samples[i].length;
		newMod->samples[i].loopStartDisp = &newMod->samples[i].loopStart;
		newMod->samples[i].loopLengthDisp = &newMod->samples[i].loopLength;
	}

	for (i = 0; i < AMIGA_VOICES; i++)
		newMod->channels[i].n_chanindex = i;

	// setup GUI text pointers
	editor.currEditPatternDisp = &newMod->currPattern;
	editor.currPosDisp = &newMod->currOrder;
	editor.currPatternDisp = &newMod->head.order[0];
	editor.currPosEdPattDisp = &newMod->head.order[0];
	editor.currLengthDisp = &newMod->head.orderCount;

	editor.ui.updateSongSize = true;
	return newMod;

oom:
	showErrorMsgBox("Out of memory!");
	return NULL;
}