shithub: ft2play

Download patch

ref: 4ac52cd7304238d00090021d2db77e7fe8bdd6c1
parent: 54ff8f32a9588cf261bfea5d50ae2b90c69d7ffd
author: Olav Sørensen <[email protected]>
date: Tue Nov 24 14:13:49 EST 2020

Add files via upload

diff: cannot open b/audiodrivers/sdl//null: file does not exist: 'b/audiodrivers/sdl//null' diff: cannot open b/audiodrivers/winmm//null: file does not exist: 'b/audiodrivers/winmm//null' diff: cannot open b/audiodrivers//null: file does not exist: 'b/audiodrivers//null' diff: cannot open b/ft2play/src//null: file does not exist: 'b/ft2play/src//null' diff: cannot open b/ft2play/vs2019_project//null: file does not exist: 'b/ft2play/vs2019_project//null' diff: cannot open b/ft2play//null: file does not exist: 'b/ft2play//null'
--- /dev/null
+++ b/audiodrivers/how_to_write_drivers.txt
@@ -1,0 +1,30 @@
+---- How to write your own audio driver for ft2play ----
+
+1) Include the header "../../pmp_mix.h"
+
+2) Implement the following functions using your audio API of choice:
+
+  void lockMixer(void); // waits for the current mixing block to finish and disables further mixing
+  void unlockMixer(void); // enables mixing again
+  bool openMixer(int32_t mixingFrequency, int32_t mixingBufferSize); // 8000..96000, 256..8192 (true if ok, false if fail)
+  void closeMixer(void);
+
+3) When the audio API is requesting samples, make a call to mix_UpdateBuffer(), f.ex.:
+
+  mix_UpdateBuffer((int16_t *)stream, len / 4);
+  
+4) Make your own preprocessor define (f.ex. AUDIODRIVER_ALSA) and pass it to the compiler during compilation
+   (also remember to add the correct driver .c file to the compilation script)
+
+5) In "pmplay.h", insert your preprocessor define and include in the "AUDIO DRIVERS" #ifdef chain and
+   include your audio driver header in there.
+   
+NOTE:
+  lockMixer() should be implemented in a way where you wait until the mix_UpdateBuffer() call has finished (important),
+  then you block further calls to mix_UpdateBuffer() until the mixer is unlocked again.
+  You should not send zeroes to the audio device while it's locked, as the lock/unlock pairs are usually called within
+  a very short time frame anyway.
+  
+-------------------------------------------------------
+
+You can look at audiodrivers/sdl/sdldriver.c if you need some references...
--- /dev/null
+++ b/audiodrivers/sdl/sdldriver.c
@@ -1,0 +1,64 @@
+// SDL audio driver for ft2play
+
+#include <SDL2/SDL.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <string.h>
+#include "../../pmp_mix.h"
+
+static SDL_AudioDeviceID dev;
+
+static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
+{
+	mix_UpdateBuffer((int16_t *)stream, len / 4); // pmp_mix.c function
+	(void)userdata;
+}
+
+void lockMixer(void)
+{
+	if (dev != 0)
+		SDL_LockAudioDevice(dev);
+}
+
+void unlockMixer(void)
+{
+	if (dev != 0)
+		SDL_UnlockAudioDevice(dev);
+}
+
+bool openMixer(int32_t mixingFrequency, int32_t mixingBufferSize)
+{
+	SDL_AudioSpec want, have;
+
+	if (dev != 0)
+		return true;
+
+	if (SDL_Init(SDL_INIT_AUDIO) != 0)
+		return false;
+
+	memset(&want, 0, sizeof (want));
+	want.freq = mixingFrequency;
+	want.format = AUDIO_S16;
+	want.channels = 2;
+	want.samples = (uint16_t)mixingBufferSize;
+	want.callback = audioCallback;
+
+	dev = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
+	if (dev == 0)
+		return false;
+
+	SDL_PauseAudioDevice(dev, false);
+	return true;
+}
+
+void closeMixer(void)
+{
+	if (dev != 0)
+	{
+		SDL_PauseAudioDevice(dev, true);
+		SDL_CloseAudioDevice(dev);
+		dev = 0;
+	}
+
+	SDL_Quit();
+}
--- /dev/null
+++ b/audiodrivers/sdl/sdldriver.h
@@ -1,0 +1,9 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+void lockMixer(void);
+void unlockMixer(void);
+bool openMixer(int32_t mixingFrequency, int32_t mixingBufferSize);
+void closeMixer(void);
--- /dev/null
+++ b/audiodrivers/winmm/winmm.c
@@ -1,0 +1,178 @@
+/* winmm audio driver for ft2play
+**
+** Warning: This might not be 100% thread-safe or lock-safe!
+*/
+
+#define WIN32_LEAN_AND_MEAN
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <mmsystem.h>
+#include "../../pmp_mix.h"
+
+#define MIX_BUF_NUM 4
+
+static volatile BOOL mixerOpened, mixerBusy, mixerLocked;
+static uint8_t currBuffer;
+static int16_t *audioBuffer[MIX_BUF_NUM];
+static int32_t bufferSize;
+static HANDLE hThread, hAudioSem;
+static WAVEHDR waveBlocks[MIX_BUF_NUM];
+static HWAVEOUT hWave;
+
+static DWORD WINAPI mixThread(LPVOID lpParam)
+{
+	SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_TIME_CRITICAL);
+	while (mixerOpened)
+	{
+		WAVEHDR *waveBlock = &waveBlocks[currBuffer];
+
+		if (!mixerLocked)
+		{
+			mixerBusy = true;
+			mix_UpdateBuffer((int16_t *)waveBlock->lpData, bufferSize); // pmp_mix.c function
+			mixerBusy = false;
+		}
+
+		waveOutWrite(hWave, waveBlock, sizeof (WAVEHDR));
+		currBuffer = (currBuffer + 1) % MIX_BUF_NUM;
+		WaitForSingleObject(hAudioSem, INFINITE); // wait for buffer fill request
+	}
+
+	return 0;
+
+	(void)lpParam;
+}
+
+static void CALLBACK waveProc(HWAVEOUT hWaveOut, UINT uMsg, DWORD_PTR dwInstance, DWORD_PTR dwParam1, DWORD_PTR dwParam2)
+{
+	if (uMsg == WOM_DONE)
+		ReleaseSemaphore(hAudioSem, 1, NULL);
+
+	(void)hWaveOut;
+	(void)uMsg;
+	(void)dwInstance;
+	(void)dwParam1;
+	(void)dwParam2;
+}
+
+void lockMixer(void)
+{
+	mixerLocked = true;
+	while (mixerBusy);
+}
+
+void unlockMixer(void)
+{
+	mixerBusy = false;
+	mixerLocked = false;
+}
+
+void closeMixer(void)
+{
+	mixerOpened = false;
+	mixerBusy = false;
+
+	if (hAudioSem != NULL)
+		ReleaseSemaphore(hAudioSem, 1, NULL);
+
+	if (hThread != NULL)
+	{
+		WaitForSingleObject(hThread, INFINITE);
+		CloseHandle(hThread);
+		hThread = NULL;
+	}
+
+	if (hAudioSem != NULL)
+	{
+		CloseHandle(hAudioSem);
+		hAudioSem = NULL;
+	}
+
+	if (hWave != NULL)
+	{
+		waveOutReset(hWave);
+
+		for (int32_t i = 0; i < MIX_BUF_NUM; i++)
+		{
+			if (waveBlocks[i].dwUser != 0xFFFF)
+				waveOutUnprepareHeader(hWave, &waveBlocks[i], sizeof (WAVEHDR));
+		}
+
+		waveOutClose(hWave);
+		hWave = NULL;
+	}
+
+	for (int32_t i = 0; i < MIX_BUF_NUM; i++)
+	{
+		if (audioBuffer[i] != NULL)
+		{
+			free(audioBuffer[i]);
+			audioBuffer[i] = NULL;
+		}
+	}
+}
+
+bool openMixer(int32_t mixingFrequency, int32_t mixingBufferSize)
+{
+	DWORD threadID;
+	WAVEFORMATEX wfx;
+
+	// don't unprepare headers on error
+	for (int32_t i = 0; i < MIX_BUF_NUM; i++)
+		waveBlocks[i].dwUser = 0xFFFF;
+
+	closeMixer();
+	bufferSize = mixingBufferSize;
+
+	ZeroMemory(&wfx, sizeof (wfx));
+	wfx.nSamplesPerSec = mixingFrequency;
+	wfx.wBitsPerSample = 16;
+	wfx.nChannels = 2;
+	wfx.wFormatTag = WAVE_FORMAT_PCM;
+	wfx.nBlockAlign = wfx.nChannels * (wfx.wBitsPerSample / 8);
+	wfx.nAvgBytesPerSec = wfx.nSamplesPerSec * wfx.nBlockAlign;
+
+	if (waveOutOpen(&hWave, WAVE_MAPPER, &wfx, (DWORD_PTR)&waveProc, 0, CALLBACK_FUNCTION) != MMSYSERR_NOERROR)
+		goto omError;
+
+	// create semaphore for buffer fill requests
+	hAudioSem = CreateSemaphore(NULL, MIX_BUF_NUM - 1, MIX_BUF_NUM, NULL);
+	if (hAudioSem == NULL)
+		goto omError;
+
+	// allocate WinMM mix buffers
+	for (int32_t i = 0; i < MIX_BUF_NUM; i++)
+	{
+		audioBuffer[i] = (int16_t *)calloc(mixingBufferSize, wfx.nBlockAlign);
+		if (audioBuffer[i] == NULL)
+			goto omError;
+	}
+
+	// initialize WinMM mix headers
+	memset(waveBlocks, 0, sizeof (waveBlocks));
+	for (int32_t i = 0; i < MIX_BUF_NUM; i++)
+	{
+		waveBlocks[i].lpData = (LPSTR)audioBuffer[i];
+		waveBlocks[i].dwBufferLength = mixingBufferSize * wfx.nBlockAlign;
+		waveBlocks[i].dwFlags = WHDR_DONE;
+
+		if (waveOutPrepareHeader(hWave, &waveBlocks[i], sizeof (WAVEHDR)) != MMSYSERR_NOERROR)
+			goto omError;
+	}
+
+	currBuffer = 0;
+	mixerOpened = true;
+
+	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)mixThread, NULL, 0, &threadID);
+	if (hThread == NULL)
+		goto omError;
+
+	return TRUE;
+
+omError:
+	closeMixer();
+	return FALSE;
+}
--- /dev/null
+++ b/audiodrivers/winmm/winmm.h
@@ -1,0 +1,9 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+void lockMixer(void);
+void unlockMixer(void);
+bool openMixer(int32_t mixingFrequency, int32_t mixingBufferSize);
+void closeMixer(void);
--- /dev/null
+++ b/ft2play/make-linux.sh
@@ -1,0 +1,10 @@
+#!/bin/bash
+
+rm release/other/ft2play &> /dev/null
+echo Compiling, please wait...
+
+gcc -DNDEBUG -DAUDIODRIVER_SDL ../audiodrivers/sdl/*.c ../*.c src/*.c -g0 -lSDL2 -lm -lpthread -Wshadow -Winit-self -Wall -Wno-maybe-uninitialized -Wno-missing-field-initializers -Wno-unused-result -Wno-strict-aliasing -Wextra -Wunused -Wunreachable-code -Wswitch-default -O3 -o release/other/ft2play
+
+rm ../*.o src/*.o &> /dev/null
+
+echo Done. The executable can be found in \'release/other\' if everything went well.
--- /dev/null
+++ b/ft2play/make-macos.sh
@@ -1,0 +1,17 @@
+#!/bin/bash
+
+arch=$(arch)
+if [ $arch == "ppc" ]; then
+    echo Sorry, PowerPC \(PPC\) is not supported...
+else
+    echo Compiling 64-bit binary, please wait...
+    
+    rm release/other/ft2play &> /dev/null
+    
+    clang -mmacosx-version-min=10.7 -arch x86_64 -mmmx -mfpmath=sse -msse2 -I/Library/Frameworks/SDL2.framework/Headers -F/Library/Frameworks -g0 -DNDEBUG -DAUDIODRIVER_SDL ../audiodrivers/sdl/*.c ../*.c src/*.c -O3 -lm -Winit-self -Wno-deprecated -Wextra -Wunused -mno-ms-bitfields -Wno-missing-field-initializers -Wswitch-default -framework SDL2 -framework Cocoa -lm -o release/other/ft2play
+    strip release/other/ft2play
+    install_name_tool -change @rpath/SDL2.framework/Versions/A/SDL2 @executable_path/../Frameworks/SDL2.framework/Versions/A/SDL2 release/other/ft2play
+    
+    rm ../*.o src/*.o &> /dev/null
+    echo Done. The executable can be found in \'release/other\' if everything went well.
+fi
--- /dev/null
+++ b/ft2play/src/ft2play.c
@@ -1,0 +1,324 @@
+/* Example program for interfacing with ft2play.
+**
+** Please excuse my disgusting platform-independant code here...
+*/
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include "../../pmplay.h"
+#include "posix.h"
+
+// defaults when not overriden by argument switches
+#define DEFAULT_MIX_FREQ 44100
+#define DEFAULT_MIX_BUFSIZE 1024
+#define DEFAULT_MIX_AMP 4
+#define DEFAULT_MASTERVOL 256
+#define DEFAULT_INTRP_FLAG true
+#define DEFAULT_VRAMP_FLAG true
+
+// set to true if you want ft2play to always render to WAV
+#define DEFAULT_WAVRENDER_MODE_FLAG false 
+
+// default settings
+static bool renderToWavFlag = DEFAULT_WAVRENDER_MODE_FLAG;
+static bool interpolation = DEFAULT_INTRP_FLAG;
+static bool volumeRamping = DEFAULT_VRAMP_FLAG;
+static int32_t mixingAmp = DEFAULT_MIX_AMP;
+static int32_t masterVolume = DEFAULT_MASTERVOL;
+static int32_t mixingFrequency = DEFAULT_MIX_FREQ;
+static int32_t mixingBufferSize = DEFAULT_MIX_BUFSIZE;
+
+// ----------------------------------------------------------
+
+static volatile bool programRunning;
+static char *filename, *WAVRenderFilename;
+
+static void showUsage(void);
+static void handleArguments(int argc, char *argv[]);
+static void readKeyboard(void);
+static int32_t renderToWav(void);
+
+// yuck!
+#ifdef _WIN32
+void wavRecordingThread(void *arg)
+#else
+void *wavRecordingThread(void *arg)
+#endif
+{
+	WAVDump_Record(WAVRenderFilename);
+#ifndef _WIN32
+	return NULL;
+#endif
+	(void)arg;
+}
+
+#ifndef _WIN32
+static void sigtermFunc(int32_t signum)
+{
+	programRunning = false; // unstuck main loop
+	WAVDump_Flag = false; // unstuck WAV render loop
+	(void)signum;
+}
+#endif
+
+int main(int argc, char *argv[])
+{
+	if (argc < 2)
+	{
+		showUsage();
+		return 1;
+	}
+
+	handleArguments(argc, argv);
+
+	// TODO: Return proper error codes to tell the user what went wrong...
+	if (!initMusic(mixingFrequency, mixingBufferSize, interpolation, volumeRamping))
+	{
+		printf("Error: Out of memory while setting up replayer!\n");
+		return 1;
+	}
+
+	// TODO: Return proper error codes to tell the user what went wrong...
+	if (!loadMusic(filename))
+	{
+		printf("Error: Couldn't load song!\n");
+		return 1;
+	}
+
+	// you only need to make a call to these if amp != 4 and/or mastervol != 256, which are the FT2 defaults
+	if (mixingAmp != 4) setAmp(mixingAmp);
+	if (masterVolume != 256) setMasterVol(masterVolume);
+
+	// trap sigterm on Linux/macOS (since we need to properly revert the terminal)
+#ifndef _WIN32
+	struct sigaction action;
+	memset(&action, 0, sizeof (struct sigaction));
+	action.sa_handler = sigtermFunc;
+	sigaction(SIGTERM, &action, NULL);
+#endif
+
+	if (renderToWavFlag)
+		return renderToWav();
+
+	if (!startMusic())
+	{
+		printf("Error: Couldn't start the audio system!\n");
+		freeMusic();
+		return 1;
+	}
+
+	startPlaying();
+
+	printf("Playing, press ESC to stop...\n");
+	printf("\n");
+	printf("Controls:\n");
+	printf("Esc=Quit   Space=Toggle Pause   Plus = inc. song pos   Minus = dec. song pos\n");
+	printf("\n");
+	printf("Mixing frequency: %dHz\n", realReplayRate);
+	printf("Linear interpolation: %s\n", interpolation ? "On" : "Off");
+	printf("Volume ramping: %s\n", volumeRamping ? "On" : "Off");
+	printf("Mixing amp: %d/32\n", boostLevel);
+	printf("Mixing volume: %d/256\n", masterVol);
+	printf("\n");
+	printf("Name: %s\n", song.name);
+	printf("Channels: %d/32\n", song.antChn);
+	printf("Instruments: %d/128\n", song.antInstrs);
+	printf("Song length: %d/255 (restart pos: %d)\n", song.len, song.repS);
+	printf("\n");
+
+	printf("- STATUS -\n");
+
+#ifndef _WIN32
+	modifyTerminal();
+#endif
+
+	programRunning = true;
+	while (programRunning)
+	{
+		readKeyboard();
+
+		printf(" Pos: %03d/%03d - Pattern: %03d - Row: %03d/%03d - Active voices: %02d/%02d %s\r",
+			song.songPos, song.len, song.pattNr, song.pattPos, song.pattLen,
+			getNumActiveVoices(), song.antChn, musicPaused ? "(PAUSED)" : "        ");
+		fflush(stdout);
+
+		Sleep(100);
+	}
+
+#ifndef _WIN32
+	revertTerminal();
+#endif
+
+	printf("\n");
+
+	stopMusic();
+	freeMusic();
+
+	printf("Playback stopped.\n");
+	return 0;
+}
+
+static void showUsage(void)
+{
+	printf("Usage:\n");
+	printf("  ft2play input_module [-f mixfreq] [-b buffersize] [-a amp] [-m mastervol]\n");
+	printf("  ft2play input_module [--no-intrp] [--no-vramp] [--render-to-wav]\n");
+	printf("\n");
+	printf("  Options:\n");
+	printf("    input_module     Specifies the module file to load (.XM/.MOD/.FT supported)\n");
+	printf("    -f mixreq        Specifies the mixing frequency (8000..96000)\n");
+	printf("    -b buffersize    Specifies the mixing buffer size (256..8192)\n");
+	printf("    -a amp           Specifies the mixing amplitude (1..32)\n");
+	printf("    -m mastervol     Specifies the mixing master volume (0..256). This setting\n");
+	printf("                     is ignored when rendering to WAV (always set to 256).\n");
+	printf("    --no-intrp       Disables linear interpolation\n");
+	printf("    --no-vramp       Disables volume ramping\n");
+	printf("    --render-to-wav  Renders song to WAV instead of playing it. The output\n");
+	printf("                     filename will be the input filename with .WAV added to the\n");
+	printf("                     end.\n");
+	printf("\n");
+	printf("Default settings:\n");
+	printf("  - Mixing frequency:         %d\n", DEFAULT_MIX_FREQ);
+	printf("  - Mixing buffer size:       %d\n", DEFAULT_MIX_BUFSIZE);
+	printf("  - Amp:                      %d\n", DEFAULT_MIX_AMP);
+	printf("  - Master volume:            %d\n", DEFAULT_MASTERVOL);
+	printf("  - Linear interpolation:     %s\n", DEFAULT_INTRP_FLAG ? "On" : "Off");
+	printf("  - Volume ramping:           %s\n", DEFAULT_VRAMP_FLAG ? "On" : "Off");
+	printf("  - WAV render mode:          %s\n", DEFAULT_WAVRENDER_MODE_FLAG ? "On" : "Off");
+	printf("\n");
+}
+
+static void handleArguments(int argc, char *argv[])
+{
+	filename = argv[1];
+	if (argc > 2) // parse arguments
+	{
+		for (int32_t i = 1; i < argc; i++)
+		{
+			if (!strcmp(argv[i], "-f") && i+1 < argc)
+			{
+				const int32_t num = atoi(argv[i+1]);
+				mixingFrequency = CLAMP(num, 8000, 96000);
+			}
+			else if (!strcmp(argv[i], "-b") && i+1 < argc)
+			{
+				const int32_t num = atoi(argv[i+1]);
+				mixingBufferSize = CLAMP(num, 256, 8192);
+			}
+			else if (!strcmp(argv[i], "-a") && i+1 < argc)
+			{
+				const int32_t num = atoi(argv[i+1]);
+				mixingAmp = CLAMP(num, 1, 32);
+			}
+			else if (!strcmp(argv[i], "-m") && i+1 < argc)
+			{
+				const int32_t num = atoi(argv[i+1]);
+				masterVolume = CLAMP(num, 0, 256);
+			}
+			else if (!strcmp(argv[i], "--no-intrp"))
+			{
+				interpolation = false;
+			}
+			else if (!strcmp(argv[i], "--no-vramp"))
+			{
+				volumeRamping = false;
+			}
+			else if (!strcmp(argv[i], "--render-to-wav"))
+			{
+				renderToWavFlag = true;
+			}
+		}
+	}
+}
+
+static void readKeyboard(void)
+{
+	if (_kbhit())
+	{
+		const int32_t key = _getch();
+		switch (key)
+		{
+			case 0x1B: // esc
+				programRunning = false;
+			break;
+
+			case 0x20: // space
+				toggleMusic();
+			break;
+
+			case 0x2B: // numpad +
+				song.globVol = 64;
+				stopVoices(); // prevent stuck notes
+				setPos(song.songPos + 1, 0);
+			break;
+
+			case 0x2D: // numpad -
+				song.globVol = 64;
+				stopVoices(); // prevent stuck notes
+				setPos(song.songPos - 1, 0);
+			break;
+			
+			default: break;
+		}
+	}
+}
+
+static int32_t renderToWav(void)
+{
+	const size_t filenameLen = strlen(filename);
+	WAVRenderFilename = (char *)malloc(filenameLen+5);
+
+	if (WAVRenderFilename == NULL)
+	{
+		printf("Error: Out of memory!\n");
+		freeMusic();
+		return 1;
+	}
+
+	strcpy(WAVRenderFilename, filename);
+	strcat(WAVRenderFilename, ".wav");
+
+	/* The WAV render loop also sets/listens/clears "WAVDump_Flag", but let's set it now
+	** since we're doing the render in a separate thread (to be able to force-abort it if
+	** the user is pressing a key).
+	**
+	** If you don't want to create a thread for the render, you don't have to
+	** set this flag, and you just call WAVDump_Record("output.wav") directly.
+	** Though, some songs will render forever (if they Bxx-jump to a previous order),
+	** thus having this in a thread is recommended so that you can force-abort it, if stuck.
+	*/
+	WAVDump_Flag = true;
+	if (!createSingleThread(wavRecordingThread))
+	{
+		printf("Error: Couldn't create WAV rendering thread!\n");
+		free(WAVRenderFilename);
+		freeMusic();
+		return 1;
+	}
+
+	printf("Rendering to WAV. If stuck forever, press any key to stop rendering...\n");
+
+#ifndef _WIN32
+	modifyTerminal();
+#endif
+	while (WAVDump_Flag)
+	{
+		Sleep(200);
+		if ( _kbhit())
+			WAVDump_Flag = false;
+	}
+#ifndef _WIN32
+	revertTerminal();
+#endif
+
+	closeSingleThread();
+
+	free(WAVRenderFilename);
+	freeMusic();
+
+	return 0;
+}
+
--- /dev/null
+++ b/ft2play/src/posix.c
@@ -1,0 +1,147 @@
+/* Hackish attempt at making certain routines portable.
+**
+** Warning: Do not use these for other projects! They are not safe for general use.
+*/
+
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+
+#ifdef _WIN32 
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h>
+
+static HANDLE hThread;
+
+bool createSingleThread(void (*threadFunc)(void *arg))
+{
+	DWORD dwThreadId;
+
+	if (hThread != NULL)
+		return false;
+
+	hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadFunc, NULL, 0, &dwThreadId);
+	if (hThread == NULL)
+		return false;
+
+	return true;
+}
+
+void closeSingleThread(void)
+{
+	if (hThread != NULL)
+	{
+		WaitForSingleObject(hThread, INFINITE);
+
+		CloseHandle(hThread);
+		hThread = NULL;
+
+	}
+}
+
+#else
+
+#include <unistd.h>
+#include <termios.h>
+#include <sys/ioctl.h>
+#include <pthread.h>
+
+static bool threadOpen;
+static pthread_t threadId;
+static int32_t lastChar = -1;
+static struct termios tOld, tNew;
+
+void Sleep(uint32_t ms)
+{
+	usleep(ms * 1000);
+}
+
+bool createSingleThread(void *(*threadFunc)(void *arg))
+{
+	if (threadOpen)
+		return false;
+
+	const int32_t err = pthread_create(&threadId, NULL, threadFunc, NULL);
+	if (err)
+	{
+		threadOpen = false;
+		return false;
+	}
+
+	threadOpen = true;
+	return true;
+}
+
+void closeSingleThread(void)
+{
+	if (threadOpen)
+	{
+		pthread_join(threadId, NULL);
+		threadOpen = false;
+	}
+}
+
+// the following routines were found on google, and were modified
+
+void modifyTerminal(void)
+{
+	tcgetattr(0, &tOld);
+
+	tNew = tOld;
+	tNew.c_lflag &= ~ICANON;
+	tNew.c_lflag &= ~ECHO;
+	tNew.c_lflag &= ~ISIG;
+	tNew.c_cc[VMIN] = 1;
+	tNew.c_cc[VTIME] = 0;
+	tcsetattr(0, TCSANOW, &tNew);
+
+	lastChar = -1;
+}
+
+void revertTerminal(void)
+{
+	tcsetattr(0, TCSANOW, &tOld);
+}
+
+bool _kbhit(void)
+{
+	uint8_t ch;
+
+	if (lastChar != -1)
+		return true;
+
+	tNew.c_cc[VMIN] = 0;
+	tcsetattr(0, TCSANOW, &tNew);
+
+	const int32_t nread = read(0, &ch, 1);
+	tNew.c_cc[VMIN] = 1;
+	tcsetattr(0, TCSANOW, &tNew);
+
+	if (nread == 1)
+	{
+		lastChar = ch;
+		return true;
+	}
+
+	return false;
+}
+
+int32_t _getch(void)
+{
+	char ch;
+
+	if (lastChar != -1)
+	{
+		ch = lastChar;
+		lastChar = -1;
+	}
+	else
+	{
+		read(0, &ch, 1);
+	}
+
+	return ch;
+}
+
+#endif
--- /dev/null
+++ b/ft2play/src/posix.h
@@ -1,0 +1,32 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+void closeSingleThread(void);
+
+#ifdef _WIN32 
+
+#define WIN32_LEAN_AND_MEAN
+#include <windows.h> // Sleep()
+#include <conio.h> // _kbhit(), _getch()
+
+bool createSingleThread(void (*threadFunc)(void *arg));
+
+#else
+
+bool createSingleThread(void *(*threadFunc)(void *arg));
+
+#include <signal.h>
+#include <stdio.h>
+#include <string.h>
+#include <unistd.h>
+#include <pthread.h>
+
+void Sleep(uint32_t ms);
+void modifyTerminal(void);
+void revertTerminal(void);
+bool _kbhit(void);
+int32_t _getch(void);
+
+#endif
--- /dev/null
+++ b/ft2play/vs2019_project/ft2play.sln
@@ -1,0 +1,25 @@
+
+Microsoft Visual Studio Solution File, Format Version 12.00
+# Visual Studio Version 16
+VisualStudioVersion = 16.0.30523.141
+MinimumVisualStudioVersion = 10.0.40219.1
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "ft2play", "ft2play.vcxproj", "{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}"
+EndProject
+Global
+	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Debug|Win32 = Debug|Win32
+		Release|Win32 = Release|Win32
+	EndGlobalSection
+	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}.Debug|Win32.ActiveCfg = Debug|Win32
+		{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}.Debug|Win32.Build.0 = Debug|Win32
+		{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}.Release|Win32.ActiveCfg = Release|Win32
+		{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}.Release|Win32.Build.0 = Release|Win32
+	EndGlobalSection
+	GlobalSection(SolutionProperties) = preSolution
+		HideSolutionNode = FALSE
+	EndGlobalSection
+	GlobalSection(ExtensibilityGlobals) = postSolution
+		SolutionGuid = {0302C100-588F-45AD-8EBB-D1D343CEC87B}
+	EndGlobalSection
+EndGlobal
--- /dev/null
+++ b/ft2play/vs2019_project/ft2play.vcxproj
@@ -1,0 +1,346 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup Label="ProjectConfigurations">
+    <ProjectConfiguration Include="Debug|Win32">
+      <Configuration>Debug</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Debug|x64">
+      <Configuration>Debug</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|Win32">
+      <Configuration>Release</Configuration>
+      <Platform>Win32</Platform>
+    </ProjectConfiguration>
+    <ProjectConfiguration Include="Release|x64">
+      <Configuration>Release</Configuration>
+      <Platform>x64</Platform>
+    </ProjectConfiguration>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\audiodrivers\winmm\winmm.c" />
+    <ClCompile Include="..\..\pmplay.c" />
+    <ClCompile Include="..\..\pmp_main.c" />
+    <ClCompile Include="..\..\pmp_mix.c" />
+    <ClCompile Include="..\..\snd_masm.c" />
+    <ClCompile Include="..\..\tables.c" />
+    <ClCompile Include="..\src\ft2play.c" />
+    <ClCompile Include="..\src\posix.c" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\audiodrivers\winmm\winmm.h" />
+    <ClInclude Include="..\..\pmplay.h" />
+    <ClInclude Include="..\..\pmp_main.h" />
+    <ClInclude Include="..\..\pmp_mix.h" />
+    <ClInclude Include="..\..\snd_masm.h" />
+    <ClInclude Include="..\..\tables.h" />
+    <ClInclude Include="..\src\posix.h" />
+  </ItemGroup>
+  <PropertyGroup Label="Globals">
+    <ProjectGuid>{91B645FD-82AF-44D4-9D0A-CD74BC0CAC3C}</ProjectGuid>
+    <ProjectName>ft2play</ProjectName>
+    <WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
+    <CharacterSet>Unicode</CharacterSet>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
+    <CharacterSet>MultiByte</CharacterSet>
+    <WholeProgramOptimization>true</WholeProgramOptimization>
+    <PlatformToolset>v142</PlatformToolset>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
+    <CharacterSet>Unicode</CharacterSet>
+    <PlatformToolset>v142</PlatformToolset>
+    <UseDebugLibraries>true</UseDebugLibraries>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
+    <CharacterSet>MultiByte</CharacterSet>
+    <PlatformToolset>v142</PlatformToolset>
+    <UseDebugLibraries>true</UseDebugLibraries>
+  </PropertyGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
+  <ImportGroup Label="ExtensionSettings">
+    <Import Project="$(VCTargetsPath)\BuildCustomizations\masm.props" />
+  </ImportGroup>
+  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC60.props" />
+  </ImportGroup>
+  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC60.props" />
+  </ImportGroup>
+  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC60.props" />
+  </ImportGroup>
+  <ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
+    <Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
+    <Import Project="$(VCTargetsPath)Microsoft.CPP.UpgradeFromVC60.props" />
+  </ImportGroup>
+  <PropertyGroup Label="UserMacros" />
+  <PropertyGroup>
+    <_ProjectFileVersion>10.0.40219.1</_ProjectFileVersion>
+    <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">false</LinkIncremental>
+    <LinkIncremental Condition="'$(Configuration)|$(Platform)'=='Release|x64'">false</LinkIncremental>
+    <CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">AllRules.ruleset</CodeAnalysisRuleSet>
+    <CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">AllRules.ruleset</CodeAnalysisRuleSet>
+    <CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" />
+    <CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
+    <CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" />
+    <CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" />
+    <CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">AllRules.ruleset</CodeAnalysisRuleSet>
+    <CodeAnalysisRuleSet Condition="'$(Configuration)|$(Platform)'=='Release|x64'">AllRules.ruleset</CodeAnalysisRuleSet>
+    <CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" />
+    <CodeAnalysisRules Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
+    <CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" />
+    <CodeAnalysisRuleAssemblies Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
+    <CustomBuildAfterTargets Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    </CustomBuildAfterTargets>
+    <CustomBuildAfterTargets Condition="'$(Configuration)|$(Platform)'=='Release|x64'" />
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <IncludePath>sdl\include;$(VCInstallDir)include;$(IncludePath)</IncludePath>
+    <LibraryPath>sdl\lib;$(VCInstallDir)lib;$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <LinkIncremental>true</LinkIncremental>
+  </PropertyGroup>
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <OutDir>..\release\win32\</OutDir>
+    <TargetName>ft2play</TargetName>
+    <IncludePath>sdl\include;$(VCInstallDir)include;$(IncludePath)</IncludePath>
+    <LibraryPath>sdl\lib;$(VCInstallDir)lib;$(LibraryPath)</LibraryPath>
+  </PropertyGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
+    <Midl>
+      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <MkTypLibCompatible>true</MkTypLibCompatible>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <TargetEnvironment>Win32</TargetEnvironment>
+      <TypeLibraryName>.\Debug\intro.tlb</TypeLibraryName>
+      <HeaderFileName>
+      </HeaderFileName>
+    </Midl>
+    <ClCompile>
+      <PreprocessorDefinitions>DEBUG;_DEBUG;WIN32;_CRT_SECURE_NO_WARNINGS;AUDIODRIVER_WINMM;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <WarningLevel>Level4</WarningLevel>
+      <EnableEnhancedInstructionSet>StreamingSIMDExtensions</EnableEnhancedInstructionSet>
+      <OmitFramePointers />
+      <TreatWChar_tAsBuiltInType>false</TreatWChar_tAsBuiltInType>
+    </ClCompile>
+    <ResourceCompile>
+      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <Culture>0x0409</Culture>
+    </ResourceCompile>
+    <Link>
+      <AdditionalDependencies>winmm.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <SubSystem>Console</SubSystem>
+      <LargeAddressAware>true</LargeAddressAware>
+      <ImageHasSafeExceptionHandlers />
+      <MinimumRequiredVersion>5.1</MinimumRequiredVersion>
+      <TreatLinkerWarningAsErrors>true</TreatLinkerWarningAsErrors>
+    </Link>
+    <Bscmake>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <OutputFile>.\Debug\intro.bsc</OutputFile>
+    </Bscmake>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
+    <Midl>
+      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <MkTypLibCompatible>true</MkTypLibCompatible>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <TypeLibraryName>.\Debug\intro.tlb</TypeLibraryName>
+      <HeaderFileName>
+      </HeaderFileName>
+    </Midl>
+    <ClCompile>
+      <PreprocessorDefinitions>_CRTDBG_MAP_ALLOC;DEBUG;_DEBUG;WIN32;_CRT_SECURE_NO_WARNINGS;_USE_MATH_DEFINES;HAVE_M_PI;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <WarningLevel>Level4</WarningLevel>
+      <ExceptionHandling>false</ExceptionHandling>
+      <RuntimeLibrary>MultiThreadedDebugDLL</RuntimeLibrary>
+      <DebugInformationFormat>ProgramDatabase</DebugInformationFormat>
+      <Optimization>Disabled</Optimization>
+      <BasicRuntimeChecks>EnableFastChecks</BasicRuntimeChecks>
+    </ClCompile>
+    <ResourceCompile>
+      <PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <Culture>0x0409</Culture>
+    </ResourceCompile>
+    <Link>
+      <AdditionalDependencies>winmm.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <SubSystem>Console</SubSystem>
+      <DataExecutionPrevention>false</DataExecutionPrevention>
+      <LargeAddressAware>true</LargeAddressAware>
+      <ImageHasSafeExceptionHandlers>
+      </ImageHasSafeExceptionHandlers>
+      <MinimumRequiredVersion>
+      </MinimumRequiredVersion>
+    </Link>
+    <Bscmake>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <OutputFile>.\Debug\intro.bsc</OutputFile>
+    </Bscmake>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
+    <Midl>
+      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <MkTypLibCompatible>true</MkTypLibCompatible>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <TargetEnvironment>Win32</TargetEnvironment>
+      <TypeLibraryName>.\Release\intro.tlb</TypeLibraryName>
+      <HeaderFileName>
+      </HeaderFileName>
+    </Midl>
+    <ClCompile>
+      <Optimization>MaxSpeed</Optimization>
+      <InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <PreprocessorDefinitions>NDEBUG;WIN32;_CRT_SECURE_NO_WARNINGS;AUDIODRIVER_WINMM;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <StringPooling>true</StringPooling>
+      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <WarningLevel>Level4</WarningLevel>
+      <CompileAsManaged>
+      </CompileAsManaged>
+      <DebugInformationFormat>None</DebugInformationFormat>
+      <EnableEnhancedInstructionSet>StreamingSIMDExtensions</EnableEnhancedInstructionSet>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <RuntimeTypeInfo>false</RuntimeTypeInfo>
+      <OmitFramePointers>true</OmitFramePointers>
+      <TreatWChar_tAsBuiltInType>false</TreatWChar_tAsBuiltInType>
+    </ClCompile>
+    <ResourceCompile>
+      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <Culture>0x0409</Culture>
+    </ResourceCompile>
+    <Link>
+      <AdditionalDependencies>winmm.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <MapFileName>
+      </MapFileName>
+      <SubSystem>Console</SubSystem>
+      <EntryPointSymbol>
+      </EntryPointSymbol>
+      <BaseAddress>
+      </BaseAddress>
+      <LinkTimeCodeGeneration>UseLinkTimeCodeGeneration</LinkTimeCodeGeneration>
+      <OptimizeReferences>true</OptimizeReferences>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <TreatLinkerWarningAsErrors>true</TreatLinkerWarningAsErrors>
+      <LargeAddressAware>true</LargeAddressAware>
+      <ImageHasSafeExceptionHandlers>false</ImageHasSafeExceptionHandlers>
+      <MinimumRequiredVersion>5.1</MinimumRequiredVersion>
+      <GenerateDebugInformation>false</GenerateDebugInformation>
+      <SetChecksum>true</SetChecksum>
+      <FixedBaseAddress>false</FixedBaseAddress>
+    </Link>
+    <Bscmake>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <OutputFile>.\Release\intro.bsc</OutputFile>
+    </Bscmake>
+    <CustomBuildStep>
+      <Command>
+      </Command>
+    </CustomBuildStep>
+    <PostBuildEvent>
+      <Command>
+      </Command>
+    </PostBuildEvent>
+    <ProjectReference />
+    <Manifest>
+      <OutputManifestFile>
+      </OutputManifestFile>
+    </Manifest>
+  </ItemDefinitionGroup>
+  <ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
+    <Midl>
+      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <MkTypLibCompatible>true</MkTypLibCompatible>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <TypeLibraryName>.\Release\intro.tlb</TypeLibraryName>
+      <HeaderFileName>
+      </HeaderFileName>
+    </Midl>
+    <ClCompile>
+      <Optimization>MaxSpeed</Optimization>
+      <InlineFunctionExpansion>AnySuitable</InlineFunctionExpansion>
+      <IntrinsicFunctions>true</IntrinsicFunctions>
+      <FavorSizeOrSpeed>Speed</FavorSizeOrSpeed>
+      <PreprocessorDefinitions>NDEBUG;WIN32;_CRT_SECURE_NO_WARNINGS;_USE_MATH_DEFINES;HAVE_M_PI;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <StringPooling>true</StringPooling>
+      <ExceptionHandling>false</ExceptionHandling>
+      <RuntimeLibrary>MultiThreaded</RuntimeLibrary>
+      <BufferSecurityCheck>false</BufferSecurityCheck>
+      <FunctionLevelLinking>true</FunctionLevelLinking>
+      <WarningLevel>Level4</WarningLevel>
+      <CompileAsManaged>
+      </CompileAsManaged>
+      <DebugInformationFormat>None</DebugInformationFormat>
+      <WholeProgramOptimization>true</WholeProgramOptimization>
+      <MultiProcessorCompilation>true</MultiProcessorCompilation>
+      <EnableParallelCodeGeneration>false</EnableParallelCodeGeneration>
+      <RuntimeTypeInfo>
+      </RuntimeTypeInfo>
+      <LanguageStandard>stdcpplatest</LanguageStandard>
+      <FloatingPointExceptions>false</FloatingPointExceptions>
+      <OmitFramePointers>true</OmitFramePointers>
+      <AssemblerOutput>All</AssemblerOutput>
+    </ClCompile>
+    <ResourceCompile>
+      <PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
+      <Culture>0x0409</Culture>
+    </ResourceCompile>
+    <Link>
+      <AdditionalDependencies>winmm.lib;libcmt.lib;libvcruntime.lib;libucrt.lib;%(AdditionalDependencies)</AdditionalDependencies>
+      <MapFileName>
+      </MapFileName>
+      <SubSystem>Console</SubSystem>
+      <EntryPointSymbol>
+      </EntryPointSymbol>
+      <BaseAddress>
+      </BaseAddress>
+      <DataExecutionPrevention>false</DataExecutionPrevention>
+      <LinkTimeCodeGeneration>UseLinkTimeCodeGeneration</LinkTimeCodeGeneration>
+      <OptimizeReferences>true</OptimizeReferences>
+      <EnableCOMDATFolding>true</EnableCOMDATFolding>
+      <TreatLinkerWarningAsErrors>true</TreatLinkerWarningAsErrors>
+      <GenerateDebugInformation>true</GenerateDebugInformation>
+      <LargeAddressAware>true</LargeAddressAware>
+      <ImageHasSafeExceptionHandlers>
+      </ImageHasSafeExceptionHandlers>
+      <MinimumRequiredVersion>
+      </MinimumRequiredVersion>
+    </Link>
+    <Bscmake>
+      <SuppressStartupBanner>true</SuppressStartupBanner>
+      <OutputFile>.\Release\intro.bsc</OutputFile>
+    </Bscmake>
+    <CustomBuildStep>
+      <Command>
+      </Command>
+    </CustomBuildStep>
+    <PostBuildEvent>
+      <Command>
+      </Command>
+    </PostBuildEvent>
+    <ProjectReference />
+    <Manifest>
+      <OutputManifestFile>
+      </OutputManifestFile>
+    </Manifest>
+  </ItemDefinitionGroup>
+  <Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
+  <ImportGroup Label="ExtensionTargets">
+    <Import Project="$(VCTargetsPath)\BuildCustomizations\masm.targets" />
+  </ImportGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+++ b/ft2play/vs2019_project/ft2play.vcxproj.filters
@@ -1,0 +1,57 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <ItemGroup>
+    <Filter Include="pmplay">
+      <UniqueIdentifier>{2cfd8b0c-481e-4730-981b-6b8213c6bfd4}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="pmplay\audiodrivers">
+      <UniqueIdentifier>{8b2b35ad-3eac-48ea-b6ce-20cc06e98a9c}</UniqueIdentifier>
+    </Filter>
+    <Filter Include="pmplay\audiodrivers\winmm">
+      <UniqueIdentifier>{431a929b-85e5-4c94-9647-b6217c5ed9d4}</UniqueIdentifier>
+    </Filter>
+  </ItemGroup>
+  <ItemGroup>
+    <ClCompile Include="..\..\pmp_main.c">
+      <Filter>pmplay</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\pmp_mix.c">
+      <Filter>pmplay</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\pmplay.c">
+      <Filter>pmplay</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\snd_masm.c">
+      <Filter>pmplay</Filter>
+    </ClCompile>
+    <ClCompile Include="..\..\tables.c">
+      <Filter>pmplay</Filter>
+    </ClCompile>
+    <ClCompile Include="..\src\ft2play.c" />
+    <ClCompile Include="..\..\audiodrivers\winmm\winmm.c">
+      <Filter>pmplay\audiodrivers\winmm</Filter>
+    </ClCompile>
+    <ClCompile Include="..\src\posix.c" />
+  </ItemGroup>
+  <ItemGroup>
+    <ClInclude Include="..\..\pmp_main.h">
+      <Filter>pmplay</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\pmp_mix.h">
+      <Filter>pmplay</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\pmplay.h">
+      <Filter>pmplay</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\snd_masm.h">
+      <Filter>pmplay</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\tables.h">
+      <Filter>pmplay</Filter>
+    </ClInclude>
+    <ClInclude Include="..\..\audiodrivers\winmm\winmm.h">
+      <Filter>pmplay\audiodrivers\winmm</Filter>
+    </ClInclude>
+    <ClInclude Include="..\src\posix.h" />
+  </ItemGroup>
+</Project>
\ No newline at end of file
--- /dev/null
+++ b/ft2play/vs2019_project/ft2play.vcxproj.user
@@ -1,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
+  <PropertyGroup />
+</Project>
\ No newline at end of file
--- /dev/null
+++ b/pmp_main.c
@@ -1,0 +1,1815 @@
+// XM replayer
+
+#include <stdio.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include "pmplay.h"
+#include "pmp_mix.h"
+#include "snd_masm.h"
+#include "tables.h"
+
+#define MAX_NOTES ((12 * 10 * 16) + 16)
+
+static tonTyp nilPatternLine[32]; // 8bb: used for non-allocated (empty) patterns
+
+typedef void (*volKolEfxRoutine)(stmTyp *ch);
+typedef void (*volKolEfxRoutine2)(stmTyp *ch, uint8_t *volKol);
+typedef void (*efxRoutine)(stmTyp *ch, uint8_t param);
+
+static void retrigVolume(stmTyp *ch)
+{
+	ch->realVol = ch->oldVol;
+	ch->outVol = ch->oldVol;
+	ch->outPan = ch->oldPan;
+	ch->status |= IS_Vol + IS_Pan + IS_QuickVol;
+}
+
+static void retrigEnvelopeVibrato(stmTyp *ch)
+{
+	if (!(ch->waveCtrl & 0x04)) ch->vibPos = 0;
+	if (!(ch->waveCtrl & 0x40)) ch->tremPos = 0;
+
+	ch->retrigCnt = 0;
+	ch->tremorPos = 0;
+
+	ch->envSustainActive = true;
+
+	instrTyp *ins = ch->instrSeg;
+
+	if (ins->envVTyp & 1)
+	{
+		ch->envVCnt = 65535;
+		ch->envVPos = 0;
+	}
+
+	if (ins->envPTyp & 1)
+	{
+		ch->envPCnt = 65535;
+		ch->envPPos = 0;
+	}
+
+	ch->fadeOutSpeed = ins->fadeOut; // 8bb: FT2 doesn't check if fadeout is more than 4095
+	ch->fadeOutAmp = 32768;
+
+	if (ins->vibDepth > 0)
+	{
+		ch->eVibPos = 0;
+
+		if (ins->vibSweep > 0)
+		{
+			ch->eVibAmp = 0;
+			ch->eVibSweep = (ins->vibDepth << 8) / ins->vibSweep;
+		}
+		else
+		{
+			ch->eVibAmp = ins->vibDepth << 8;
+			ch->eVibSweep = 0;
+		}
+	}
+}
+
+static void keyOff(stmTyp *ch)
+{
+	ch->envSustainActive = false;
+
+	instrTyp *ins = ch->instrSeg;
+
+	if (!(ins->envPTyp & 1)) // 8bb: yes, FT2 does this (!)
+	{
+		if (ch->envPCnt >= (uint16_t)ins->envPP[ch->envPPos][0])
+			ch->envPCnt = ins->envPP[ch->envPPos][0]-1;
+	}
+
+	if (ins->envVTyp & 1)
+	{
+		if (ch->envVCnt >= (uint16_t)ins->envVP[ch->envVPos][0])
+			ch->envVCnt = ins->envVP[ch->envVPos][0]-1;
+	}
+	else
+	{
+		ch->realVol = 0;
+		ch->outVol = 0;
+		ch->status |= IS_Vol + IS_QuickVol;
+	}
+}
+
+uint32_t getFrequenceValue(uint16_t period)
+{
+	uint32_t delta;
+
+	if (period == 0)
+		return 0;
+
+	if (linearFrqTab)
+	{
+		const uint16_t invPeriod = (12 * 192 * 4) - period; // 8bb: this intentionally overflows uint16_t to be accurate to FT2
+		const int32_t octave = (14 - (invPeriod / 768)) & 0x1F;
+
+		delta = (uint32_t)(((int64_t)logTab[invPeriod % 768] * frequenceMulFactor) >> 24);
+		delta >>= octave;
+	}
+	else
+	{
+		delta = frequenceDivFactor / period;
+	}
+
+	return delta;
+}
+
+static void startTone(uint8_t ton, uint8_t effTyp, uint8_t eff, stmTyp *ch)
+{
+	if (ton == 97)
+	{
+		keyOff(ch);
+		return;
+	}
+
+	// 8bb: if we came from Rxy (retrig), we didn't check note (Ton) yet
+	if (ton == 0)
+	{
+		ton = ch->tonNr;
+		if (ton == 0)
+			return; // 8bb: if still no note, return
+	}
+
+	ch->tonNr = ton;
+
+	instrTyp *ins = instr[ch->instrNr];
+	if (ins == NULL)
+		ins = instr[0];
+
+	ch->instrSeg = ins;
+	ch->mute = ins->mute;
+
+	uint8_t smp = ins->ta[ton-1] & 0xF; // 8bb: added safety-AND
+	ch->sampleNr = smp;
+
+	sampleTyp *s = &ins->samp[smp];
+	ch->relTonNr = s->relTon;
+
+	ton += ch->relTonNr;
+	if (ton >= 12*10)
+		return;
+
+	ch->oldVol = s->vol;
+	ch->oldPan = s->pan;
+
+	if (effTyp == 0x0E && (eff & 0xF0) == 0x50)
+		ch->fineTune = ((eff & 0x0F) << 4) - 128;
+	else
+		ch->fineTune = s->fine;
+
+	if (ton != 0)
+	{
+		const uint16_t tmpTon = ((ton - 1) << 4) + (((ch->fineTune >> 3) + 16) & 0xFF);
+		if (tmpTon < MAX_NOTES)
+			ch->outPeriod = ch->realPeriod = note2Period[tmpTon];
+	}
+
+	ch->status |= IS_Period + IS_Vol + IS_Pan + IS_NyTon + IS_QuickVol;
+
+	if (effTyp == 9)
+	{
+		if (eff)
+			ch->smpOffset = ch->eff;
+
+		ch->smpStartPos = ch->smpOffset << 8;
+	}
+	else
+	{
+		ch->smpStartPos = 0;
+	}
+
+	P_StartTone(s, ch->smpStartPos);
+}
+
+static void volume(stmTyp *ch, uint8_t param); // 8bb: actually volume slide
+static void vibrato2(stmTyp *ch);
+static void tonePorta(stmTyp *ch, uint8_t param);
+
+static void dummy(stmTyp *ch, uint8_t param)
+{
+	return;
+
+	(void)ch;
+	(void)param;
+}
+
+static void finePortaUp(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->fPortaUpSpeed;
+
+	ch->fPortaUpSpeed = param;
+
+	ch->realPeriod -= param << 2;
+	if ((int16_t)ch->realPeriod < 1)
+		ch->realPeriod = 1;
+
+	ch->outPeriod = ch->realPeriod;
+	ch->status |= IS_Period;
+}
+
+static void finePortaDown(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->fPortaDownSpeed;
+
+	ch->fPortaDownSpeed = param;
+
+	ch->realPeriod += param << 2;
+	if ((int16_t)ch->realPeriod > 32000-1)
+		ch->realPeriod = 32000-1;
+
+	ch->outPeriod = ch->realPeriod;
+	ch->status |= IS_Period;
+}
+
+static void setGlissCtrl(stmTyp *ch, uint8_t param)
+{
+	ch->glissFunk = param;
+}
+
+static void setVibratoCtrl(stmTyp *ch, uint8_t param)
+{
+	ch->waveCtrl = (ch->waveCtrl & 0xF0) | param;
+}
+
+static void jumpLoop(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+	{
+		ch->pattPos = song.pattPos & 0xFF;
+	}
+	else if (ch->loopCnt == 0)
+	{
+		ch->loopCnt = param;
+
+		song.pBreakPos = ch->pattPos;
+		song.pBreakFlag = true;
+	}
+	else if (--ch->loopCnt > 0)
+	{
+		song.pBreakPos = ch->pattPos;
+		song.pBreakFlag = true;
+	}
+}
+
+static void setTremoloCtrl(stmTyp *ch, uint8_t param)
+{
+	ch->waveCtrl = (param << 4) | (ch->waveCtrl & 0x0F);
+}
+
+static void volFineUp(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->fVolSlideUpSpeed;
+
+	ch->fVolSlideUpSpeed = param;
+
+	ch->realVol += param;
+	if (ch->realVol > 64)
+		ch->realVol = 64;
+
+	ch->outVol = ch->realVol;
+	ch->status |= IS_Vol;
+}
+
+static void volFineDown(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->fVolSlideDownSpeed;
+
+	ch->fVolSlideDownSpeed = param;
+
+	ch->realVol -= param;
+	if ((int8_t)ch->realVol < 0)
+		ch->realVol = 0;
+
+	ch->outVol = ch->realVol;
+	ch->status |= IS_Vol;
+}
+
+static void noteCut0(stmTyp *ch, uint8_t param)
+{
+	if (param == 0) // 8bb: only a parameter of zero is handled here
+	{
+		ch->realVol = 0;
+		ch->outVol = 0;
+		ch->status |= IS_Vol + IS_QuickVol;
+	}
+}
+
+static void pattDelay(stmTyp *ch, uint8_t param)
+{
+	if (song.pattDelTime2 == 0)
+		song.pattDelTime = param + 1;
+
+	(void)ch;
+}
+
+static const efxRoutine EJumpTab_TickZero[16] =
+{
+	dummy, // 0
+	finePortaUp, // 1
+	finePortaDown, // 2
+	setGlissCtrl, // 3
+	setVibratoCtrl, // 4
+	dummy, // 5
+	jumpLoop, // 6
+	setTremoloCtrl, // 7
+	dummy, // 8
+	dummy, // 9
+	volFineUp, // A
+	volFineDown, // B
+	noteCut0, // C
+	dummy, // D
+	pattDelay, // E
+	dummy // F
+};
+
+static void E_Effects_TickZero(stmTyp *ch, uint8_t param)
+{
+	EJumpTab_TickZero[param >> 4](ch, param & 0x0F);
+}
+
+static void posJump(stmTyp *ch, uint8_t param)
+{
+	song.songPos = (int16_t)param - 1;
+	song.pBreakPos = 0;
+	song.posJumpFlag = true;
+
+	(void)ch;
+}
+
+static void pattBreak(stmTyp *ch, uint8_t param)
+{
+	song.posJumpFlag = true;
+
+	param = ((param >> 4) * 10) + (param & 0x0F);
+	if (param <= 63)
+		song.pBreakPos = param;
+	else
+		song.pBreakPos = 0;
+
+	(void)ch;
+}
+
+static void setSpeed(stmTyp *ch, uint8_t param)
+{
+	if (param >= 32)
+	{
+		song.speed = param;
+		P_SetSpeed(song.speed);
+	}
+	else
+	{
+		song.timer = song.tempo = param;
+	}
+
+	(void)ch;
+}
+
+static void setGlobaVol(stmTyp *ch, uint8_t param)
+{
+	if (param > 64)
+		param = 64;
+
+	song.globVol = param;
+
+	stmTyp *c = stm;
+	for (int32_t i = 0; i < song.antChn; i++, c++) // 8bb: update all voice volumes
+		c->status |= IS_Vol;
+
+	(void)ch;
+}
+
+static void setEnvelopePos(stmTyp *ch, uint8_t param)
+{
+	int8_t envPos;
+	bool envUpdate;
+	int16_t newEnvPos;
+
+	instrTyp *ins = ch->instrSeg;
+
+	// *** VOLUME ENVELOPE ***
+	if (ins->envVTyp & 1)
+	{
+		ch->envVCnt = param - 1;
+
+		envPos = 0;
+		envUpdate = true;
+		newEnvPos = param;
+
+		if (ins->envVPAnt > 1)
+		{
+			envPos++;
+			for (int32_t i = 0; i < ins->envVPAnt-1; i++)
+			{
+				if (newEnvPos < ins->envVP[envPos][0])
+				{
+					envPos--;
+
+					newEnvPos -= ins->envVP[envPos][0];
+					if (newEnvPos == 0)
+					{
+						envUpdate = false;
+						break;
+					}
+
+					if (ins->envVP[envPos+1][0] <= ins->envVP[envPos][0])
+					{
+						envUpdate = true;
+						break;
+					}
+
+					ch->envVIPValue = ((ins->envVP[envPos+1][1] - ins->envVP[envPos][1]) & 0xFF) << 8;
+					ch->envVIPValue /= (ins->envVP[envPos+1][0] - ins->envVP[envPos][0]);
+
+					ch->envVAmp = (ch->envVIPValue * (newEnvPos - 1)) + ((ins->envVP[envPos][1] & 0xFF) << 8);
+
+					envPos++;
+
+					envUpdate = false;
+					break;
+				}
+
+				envPos++;
+			}
+
+			if (envUpdate)
+				envPos--;
+		}
+
+		if (envUpdate)
+		{
+			ch->envVIPValue = 0;
+			ch->envVAmp = (ins->envVP[envPos][1] & 0xFF) << 8;
+		}
+
+		if (envPos >= ins->envVPAnt)
+		{
+			envPos = ins->envVPAnt - 1;
+			if (envPos < 0)
+				envPos = 0;
+		}
+
+		ch->envVPos = envPos;
+	}
+
+	// *** PANNING ENVELOPE ***
+	if (ins->envVTyp & 2) // 8bb: probably an FT2 bug
+	{
+		ch->envPCnt = param - 1;
+
+		envPos = 0;
+		envUpdate = true;
+		newEnvPos = param;
+
+		if (ins->envPPAnt > 1)
+		{
+			envPos++;
+			for (int32_t i = 0; i < ins->envPPAnt-1; i++)
+			{
+				if (newEnvPos < ins->envPP[envPos][0])
+				{
+					envPos--;
+
+					newEnvPos -= ins->envPP[envPos][0];
+					if (newEnvPos == 0)
+					{
+						envUpdate = false;
+						break;
+					}
+
+					if (ins->envPP[envPos + 1][0] <= ins->envPP[envPos][0])
+					{
+						envUpdate = true;
+						break;
+					}
+
+					ch->envPIPValue = ((ins->envPP[envPos+1][1] - ins->envPP[envPos][1]) & 0xFF) << 8;
+					ch->envPIPValue /= (ins->envPP[envPos+1][0] - ins->envPP[envPos][0]);
+
+					ch->envPAmp = (ch->envPIPValue * (newEnvPos - 1)) + ((ins->envPP[envPos][1] & 0xFF) << 8);
+
+					envPos++;
+
+					envUpdate = false;
+					break;
+				}
+
+				envPos++;
+			}
+
+			if (envUpdate)
+				envPos--;
+		}
+
+		if (envUpdate)
+		{
+			ch->envPIPValue = 0;
+			ch->envPAmp = (ins->envPP[envPos][1] & 0xFF) << 8;
+		}
+
+		if (envPos >= ins->envPPAnt)
+		{
+			envPos = ins->envPPAnt - 1;
+			if (envPos < 0)
+				envPos = 0;
+		}
+
+		ch->envPPos = envPos;
+	}
+}
+
+/* -- tick-zero volume column effects --
+** 2nd parameter is used for a volume column quirk with the Rxy command (multiretrig)
+*/
+
+static void v_SetVibSpeed(stmTyp *ch, uint8_t *volKol)
+{
+	*volKol = (ch->volKolVol & 0x0F) << 2;
+	if (*volKol != 0)
+		ch->vibSpeed = *volKol;
+}
+
+static void v_Volume(stmTyp *ch, uint8_t *volKol)
+{
+	*volKol -= 16;
+	if (*volKol > 64) // 8bb: no idea why FT2 has this check...
+		*volKol = 64;
+
+	ch->outVol = ch->realVol = *volKol;
+	ch->status |= IS_Vol + IS_QuickVol;
+}
+
+static void v_FineSlideDown(stmTyp *ch, uint8_t *volKol)
+{
+	*volKol = (uint8_t)(0 - (ch->volKolVol & 0x0F)) + ch->realVol;
+	if ((int8_t)*volKol < 0)
+		*volKol = 0;
+
+	ch->outVol = ch->realVol = *volKol;
+	ch->status |= IS_Vol;
+}
+
+static void v_FineSlideUp(stmTyp *ch, uint8_t *volKol)
+{
+	*volKol = (ch->volKolVol & 0x0F) + ch->realVol;
+	if (*volKol > 64)
+		*volKol = 64;
+
+	ch->outVol = ch->realVol = *volKol;
+	ch->status |= IS_Vol;
+}
+
+static void v_SetPan(stmTyp *ch, uint8_t *volKol)
+{
+	*volKol <<= 4;
+
+	ch->outPan = *volKol;
+	ch->status |= IS_Pan;
+}
+
+// -- non-tick-zero volume column effects --
+
+static void v_SlideDown(stmTyp *ch)
+{
+	uint8_t newVol = (uint8_t)(0 - (ch->volKolVol & 0x0F)) + ch->realVol;
+	if ((int8_t)newVol < 0)
+		newVol = 0;
+
+	ch->outVol = ch->realVol = newVol;
+	ch->status |= IS_Vol;
+}
+
+static void v_SlideUp(stmTyp *ch)
+{
+	uint8_t newVol = (ch->volKolVol & 0x0F) + ch->realVol;
+	if (newVol > 64)
+		newVol = 64;
+
+	ch->outVol = ch->realVol = newVol;
+	ch->status |= IS_Vol;
+}
+
+static void v_Vibrato(stmTyp *ch)
+{
+	const uint8_t param = ch->volKolVol & 0xF;
+	if (param > 0)
+		ch->vibDepth = param;
+
+	vibrato2(ch);
+}
+
+static void v_PanSlideLeft(stmTyp *ch)
+{
+	uint16_t tmp16 = (uint8_t)(0 - (ch->volKolVol & 0x0F)) + ch->outPan;
+	if (tmp16 < 256) // 8bb: includes an FT2 bug: pan-slide-left of 0 = set pan to 0
+		tmp16 = 0;
+
+	ch->outPan = (uint8_t)tmp16;
+	ch->status |= IS_Pan;
+}
+
+static void v_PanSlideRight(stmTyp *ch)
+{
+	uint16_t tmp16 = (ch->volKolVol & 0x0F) + ch->outPan;
+	if (tmp16 > 255)
+		tmp16 = 255;
+
+	ch->outPan = (uint8_t)tmp16;
+	ch->status |= IS_Pan;
+}
+
+static void v_TonePorta(stmTyp *ch)
+{
+	tonePorta(ch, 0); // 8bb: the last parameter is actually not used in tonePorta()
+}
+
+static void v_dummy(stmTyp *ch)
+{
+	(void)ch;
+	return;
+}
+
+static void v_dummy2(stmTyp *ch, uint8_t *volKol)
+{
+	(void)ch;
+	(void)volKol;
+	return;
+}
+
+static const volKolEfxRoutine VJumpTab_TickNonZero[16] =
+{
+	v_dummy,        v_dummy,         v_dummy,  v_dummy,
+	v_dummy,        v_dummy,     v_SlideDown, v_SlideUp,
+	v_dummy,        v_dummy,         v_dummy, v_Vibrato,
+	v_dummy, v_PanSlideLeft, v_PanSlideRight, v_TonePorta
+};
+
+static const volKolEfxRoutine2 VJumpTab_TickZero[16] =
+{
+	       v_dummy2,      v_Volume,      v_Volume, v_Volume,
+	       v_Volume,      v_Volume,      v_dummy2, v_dummy2,
+	v_FineSlideDown, v_FineSlideUp, v_SetVibSpeed, v_dummy2,
+	       v_SetPan,      v_dummy2,      v_dummy2, v_dummy2
+};
+
+static void setPan(stmTyp *ch, uint8_t param)
+{
+	ch->outPan = param;
+	ch->status |= IS_Pan;
+}
+
+static void setVol(stmTyp *ch, uint8_t param)
+{
+	if (param > 64)
+		param = 64;
+
+	ch->outVol = ch->realVol = param;
+	ch->status |= IS_Vol + IS_QuickVol;
+}
+
+static void xFinePorta(stmTyp *ch, uint8_t param)
+{
+	const uint8_t type = param >> 4;
+	param &= 0x0F;
+
+	if (type == 0x1) // extra fine porta up
+	{
+		if (param == 0)
+			param = ch->ePortaUpSpeed;
+
+		ch->ePortaUpSpeed = param;
+
+		uint16_t newPeriod = ch->realPeriod;
+
+		newPeriod -= param;
+		if ((int16_t)newPeriod < 1)
+			newPeriod = 1;
+
+		ch->outPeriod = ch->realPeriod = newPeriod;
+		ch->status |= IS_Period;
+	}
+	else if (type == 0x2) // extra fine porta down
+	{
+		if (param == 0)
+			param = ch->ePortaDownSpeed;
+
+		ch->ePortaDownSpeed = param;
+
+		uint16_t newPeriod = ch->realPeriod;
+
+		newPeriod += param;
+		if ((int16_t)newPeriod > 32000-1)
+			newPeriod = 32000-1;
+
+		ch->outPeriod = ch->realPeriod = newPeriod;
+		ch->status |= IS_Period;
+	}
+}
+
+static void doMultiRetrig(stmTyp *ch, uint8_t param) // 8bb: "param" is never used (needed for efx jumptable structure)
+{
+	uint8_t cnt = ch->retrigCnt + 1;
+	if (cnt < ch->retrigSpeed)
+	{
+		ch->retrigCnt = cnt;
+		return;
+	}
+
+	ch->retrigCnt = 0;
+
+	int16_t vol = ch->realVol;
+	switch (ch->retrigVol)
+	{
+		case 0x1: vol -= 1; break;
+		case 0x2: vol -= 2; break;
+		case 0x3: vol -= 4; break;
+		case 0x4: vol -= 8; break;
+		case 0x5: vol -= 16; break;
+		case 0x6: vol = (vol >> 1) + (vol >> 3) + (vol >> 4); break;
+		case 0x7: vol >>= 1; break;
+		case 0x8: break; // 8bb: does not change the volume
+		case 0x9: vol += 1; break;
+		case 0xA: vol += 2; break;
+		case 0xB: vol += 4; break;
+		case 0xC: vol += 8; break;
+		case 0xD: vol += 16; break;
+		case 0xE: vol = (vol >> 1) + vol; break;
+		case 0xF: vol += vol; break;
+		default: break;
+	}
+	vol = CLAMP(vol, 0, 64);
+
+	ch->realVol = (uint8_t)vol;
+	ch->outVol = ch->realVol;
+
+	if (ch->volKolVol >= 0x10 && ch->volKolVol <= 0x50)
+	{
+		ch->outVol = ch->volKolVol - 0x10;
+		ch->realVol = ch->outVol;
+	}
+	else if (ch->volKolVol >= 0xC0 && ch->volKolVol <= 0xCF)
+	{
+		ch->outPan = (ch->volKolVol & 0x0F) << 4;
+	}
+
+	startTone(0, 0, 0, ch);
+
+	(void)param;
+}
+
+static void multiRetrig(stmTyp *ch, uint8_t param, uint8_t volumeColumnData)
+{
+	uint8_t tmpParam;
+
+	tmpParam = param & 0x0F;
+	if (tmpParam == 0)
+		tmpParam = ch->retrigSpeed;
+
+	ch->retrigSpeed = tmpParam;
+
+	tmpParam = param >> 4;
+	if (tmpParam == 0)
+		tmpParam = ch->retrigVol;
+
+	ch->retrigVol = tmpParam;
+
+	if (volumeColumnData == 0)
+		doMultiRetrig(ch, 0); // 8bb: the second parameter is never used (needed for efx jumptable structure)
+}
+
+static const efxRoutine JumpTab_TickZero[36] =
+{
+	dummy, // 0
+	dummy, // 1
+	dummy, // 2
+	dummy, // 3
+	dummy, // 4
+	dummy, // 5
+	dummy, // 6
+	dummy, // 7
+	setPan, // 8
+	dummy, // 9
+	dummy, // A
+	posJump, // B
+	setVol, // C
+	pattBreak, // D
+	E_Effects_TickZero, // E
+	setSpeed, // F
+	setGlobaVol, // G
+	dummy, // H
+	dummy, // I
+	dummy, // J
+	dummy, // K
+	setEnvelopePos, // L
+	dummy, // M
+	dummy, // N
+	dummy, // O
+	dummy, // P
+	dummy, // Q
+	dummy, // R
+	dummy, // S
+	dummy, // T
+	dummy, // U
+	dummy, // V
+	dummy, // W
+	xFinePorta, // X
+	dummy, // Y
+	dummy  // Z
+};
+
+static void checkEffects(stmTyp *ch) // tick0 effect handling
+{
+	// volume column effects
+	uint8_t newVolKol = ch->volKolVol; // 8bb: manipulated by vol. column effects, then used for multiretrig check (FT2 quirk)
+	VJumpTab_TickZero[ch->volKolVol >> 4](ch, &newVolKol);
+
+	// normal effects
+	const uint8_t param = ch->eff;
+	if ((ch->effTyp == 0 && param == 0) || ch->effTyp > 35)
+		return; // no effect
+
+	// 8bb: this one has to be done here instead of in the jumptable, as it needs the "newVolKol" parameter (FT2 quirk)
+	if (ch->effTyp == 27)
+	{
+		multiRetrig(ch, param, newVolKol);
+		return;
+	}
+
+	JumpTab_TickZero[ch->effTyp](ch, ch->eff);
+}
+
+static void fixTonePorta(stmTyp *ch, const tonTyp *p, uint8_t inst)
+{
+	if (p->ton > 0)
+	{
+		if (p->ton == 97)
+		{
+			keyOff(ch);
+		}
+		else
+		{
+			const uint16_t portaTmp = ((((p->ton - 1) + ch->relTonNr) & 0xFF) * 16) + (((ch->fineTune >> 3) + 16) & 0xFF);
+			if (portaTmp < MAX_NOTES)
+			{
+				ch->wantPeriod = note2Period[portaTmp];
+
+				if (ch->wantPeriod == ch->realPeriod)
+					ch->portaDir = 0;
+				else if (ch->wantPeriod > ch->realPeriod)
+					ch->portaDir = 1;
+				else
+					ch->portaDir = 2;
+			}
+		}
+	}
+
+	if (inst > 0)
+	{
+		retrigVolume(ch);
+		if (p->ton != 97)
+			retrigEnvelopeVibrato(ch);
+	}
+}
+
+static void getNewNote(stmTyp *ch, const tonTyp *p)
+{
+	ch->volKolVol = p->vol;
+
+	if (ch->effTyp == 0)
+	{
+		if (ch->eff > 0) // 8bb: we have an arpeggio running, set period back
+		{
+			ch->outPeriod = ch->realPeriod;
+			ch->status |= IS_Period;
+		}
+	}
+	else
+	{
+		// 8bb: if we have a vibrato on previous row (ch) that ends at current row (p), set period back
+		if ((ch->effTyp == 4 || ch->effTyp == 6) && (p->effTyp != 4 && p->effTyp != 6))
+		{
+			// 8bb: but it's ending at the next (this) row, so set period back
+			ch->outPeriod = ch->realPeriod;
+			ch->status |= IS_Period;
+		}
+	}
+
+	ch->effTyp = p->effTyp;
+	ch->eff = p->eff;
+	ch->tonTyp = (p->instr << 8) | p->ton;
+
+	// 8bb: 'inst' var is used for later if checks...
+	uint8_t inst = p->instr;
+	if (inst > 0)
+	{
+		if (inst <= 128)
+			ch->instrNr = inst;
+		else
+			inst = 0;
+	}
+
+	bool checkEfx = true;
+	if (p->effTyp == 0x0E)
+	{
+		if (p->eff >= 0xD1 && p->eff <= 0xDF)
+			return; // 8bb: we have a note delay (ED1..EDF)
+		else if (p->eff == 0x90)
+			checkEfx = false;
+	}
+
+	if (checkEfx)
+	{
+		if ((ch->volKolVol & 0xF0) == 0xF0) // gxx
+		{
+			const uint8_t volKolParam = ch->volKolVol & 0x0F;
+			if (volKolParam > 0)
+				ch->portaSpeed = volKolParam << 6;
+
+			fixTonePorta(ch, p, inst);
+			checkEffects(ch);
+			return;
+		}
+
+		if (p->effTyp == 3 || p->effTyp == 5) // 3xx or 5xx
+		{
+			if (p->effTyp != 5 && p->eff != 0)
+				ch->portaSpeed = p->eff << 2;
+
+			fixTonePorta(ch, p, inst);
+			checkEffects(ch);
+			return;
+		}
+
+		if (p->effTyp == 0x14 && p->eff == 0) // K00 (KeyOff - only handle tick 0 here)
+		{
+			keyOff(ch);
+
+			if (inst)
+				retrigVolume(ch);
+
+			checkEffects(ch);
+			return;
+		}
+
+		if (p->ton == 0)
+		{
+			if (inst > 0)
+			{
+				retrigVolume(ch);
+				retrigEnvelopeVibrato(ch);
+			}
+
+			checkEffects(ch);
+			return;
+		}
+	}
+
+	if (p->ton == 97)
+		keyOff(ch);
+	else
+		startTone(p->ton, p->effTyp, p->eff, ch);
+
+	if (inst > 0)
+	{
+		retrigVolume(ch);
+		if (p->ton != 97)
+			retrigEnvelopeVibrato(ch);
+	}
+
+	checkEffects(ch);
+}
+
+static void fixaEnvelopeVibrato(stmTyp *ch)
+{
+	bool envInterpolateFlag, envDidInterpolate;
+	uint8_t envPos;
+	int16_t autoVibVal;
+	uint16_t autoVibAmp, envVal;
+	uint32_t vol;
+
+	instrTyp *ins = ch->instrSeg;
+
+	// *** FADEOUT ***
+	if (!ch->envSustainActive)
+	{
+		ch->status |= IS_Vol;
+
+		// 8bb: unsigned clamp + reset
+		if (ch->fadeOutAmp >= ch->fadeOutSpeed)
+		{
+			ch->fadeOutAmp -= ch->fadeOutSpeed;
+		}
+		else
+		{
+			ch->fadeOutAmp = 0;
+			ch->fadeOutSpeed = 0;
+		}
+	}
+
+	if (!ch->mute)
+	{
+		// *** VOLUME ENVELOPE ***
+		envVal = 0;
+		if (ins->envVTyp & 1)
+		{
+			envDidInterpolate = false;
+			envPos = ch->envVPos;
+
+			if (++ch->envVCnt == ins->envVP[envPos][0])
+			{
+				ch->envVAmp = ins->envVP[envPos][1] << 8;
+
+				envPos++;
+				if (ins->envVTyp & 4)
+				{
+					envPos--;
+
+					if (envPos == ins->envVRepE)
+					{
+						if (!(ins->envVTyp & 2) || envPos != ins->envVSust || ch->envSustainActive)
+						{
+							envPos = ins->envVRepS;
+
+							ch->envVCnt = ins->envVP[envPos][0];
+							ch->envVAmp = ins->envVP[envPos][1] << 8;
+						}
+					}
+
+					envPos++;
+				}
+
+				if (envPos < ins->envVPAnt)
+				{
+					envInterpolateFlag = true;
+					if ((ins->envVTyp & 2) && ch->envSustainActive)
+					{
+						if (envPos-1 == ins->envVSust)
+						{
+							envPos--;
+							ch->envVIPValue = 0;
+							envInterpolateFlag = false;
+						}
+					}
+
+					if (envInterpolateFlag)
+					{
+						ch->envVPos = envPos;
+
+						ch->envVIPValue = 0;
+						if (ins->envVP[envPos][0] > ins->envVP[envPos-1][0])
+						{
+							ch->envVIPValue = (ins->envVP[envPos][1] - ins->envVP[envPos-1][1]) << 8;
+							ch->envVIPValue /= (ins->envVP[envPos][0] - ins->envVP[envPos-1][0]);
+
+							envVal = ch->envVAmp;
+							envDidInterpolate = true;
+						}
+					}
+				}
+				else
+				{
+					ch->envVIPValue = 0;
+				}
+			}
+
+			if (!envDidInterpolate)
+			{
+				ch->envVAmp += ch->envVIPValue;
+
+				envVal = ch->envVAmp;
+				if (envVal > 64*256)
+				{
+					if (envVal > 128*256)
+						envVal = 64*256;
+					else
+						envVal = 0;
+
+					ch->envVIPValue = 0;
+				}
+			}
+
+			envVal >>= 8;
+
+			vol = (envVal * ch->outVol * ch->fadeOutAmp) >> (16+2);
+			vol = (vol * song.globVol) >> 7;
+
+			ch->status |= IS_Vol; // 8bb: update vol every tick because vol envelope is enabled
+		}
+		else
+		{
+			vol = ((ch->outVol << 4) * ch->fadeOutAmp) >> 16;
+			vol = (vol * song.globVol) >> 7;
+		}
+
+		ch->finalVol = (uint16_t)vol; // 0..256
+	}
+	else
+	{
+		ch->finalVol = 0;
+	}
+
+	// *** PANNING ENVELOPE ***
+
+	envVal = 0;
+	if (ins->envPTyp & 1)
+	{
+		envDidInterpolate = false;
+		envPos = ch->envPPos;
+
+		if (++ch->envPCnt == ins->envPP[envPos][0])
+		{
+			ch->envPAmp = ins->envPP[envPos][1] << 8;
+
+			envPos++;
+			if (ins->envPTyp & 4)
+			{
+				envPos--;
+
+				if (envPos == ins->envPRepE)
+				{
+					if (!(ins->envPTyp & 2) || envPos != ins->envPSust || ch->envSustainActive)
+					{
+						envPos = ins->envPRepS;
+
+						ch->envPCnt = ins->envPP[envPos][0];
+						ch->envPAmp = ins->envPP[envPos][1] << 8;
+					}
+				}
+
+				envPos++;
+			}
+
+			if (envPos < ins->envPPAnt)
+			{
+				envInterpolateFlag = true;
+				if ((ins->envPTyp & 2) && ch->envSustainActive)
+				{
+					if (envPos-1 == ins->envPSust)
+					{
+						envPos--;
+						ch->envPIPValue = 0;
+						envInterpolateFlag = false;
+					}
+				}
+
+				if (envInterpolateFlag)
+				{
+					ch->envPPos = envPos;
+
+					ch->envPIPValue = 0;
+					if (ins->envPP[envPos][0] > ins->envPP[envPos-1][0])
+					{
+						ch->envPIPValue = (ins->envPP[envPos][1] - ins->envPP[envPos-1][1]) << 8;
+						ch->envPIPValue /= (ins->envPP[envPos][0] - ins->envPP[envPos-1][0]);
+
+						envVal = ch->envPAmp;
+						envDidInterpolate = true;
+					}
+				}
+			}
+			else
+			{
+				ch->envPIPValue = 0;
+			}
+		}
+
+		if (!envDidInterpolate)
+		{
+			ch->envPAmp += ch->envPIPValue;
+
+			envVal = ch->envPAmp;
+			if (envVal > 64*256)
+			{
+				if (envVal > 128*256)
+					envVal = 64*256;
+				else
+					envVal = 0;
+
+				ch->envPIPValue = 0;
+			}
+		}
+
+		int16_t panTmp = ch->outPan - 128;
+		if (panTmp > 0)
+			panTmp = 0 - panTmp;
+		panTmp += 128;
+
+		panTmp <<= 3;
+		envVal -= 32*256;
+
+		ch->finalPan = ch->outPan + (uint8_t)(((int16_t)envVal * panTmp) >> 16);
+		ch->status |= IS_Pan;
+	}
+	else
+	{
+		ch->finalPan = ch->outPan;
+	}
+
+	// *** AUTO VIBRATO ***
+	if (ins->vibDepth > 0)
+	{
+		if (ch->eVibSweep > 0)
+		{
+			autoVibAmp = ch->eVibSweep;
+			if (ch->envSustainActive)
+			{
+				autoVibAmp += ch->eVibAmp;
+				if ((autoVibAmp >> 8) > ins->vibDepth)
+				{
+					autoVibAmp = ins->vibDepth << 8;
+					ch->eVibSweep = 0;
+				}
+
+				ch->eVibAmp = autoVibAmp;
+			}
+		}
+		else
+		{
+			autoVibAmp = ch->eVibAmp;
+		}
+
+		ch->eVibPos += ins->vibRate;
+
+		     if (ins->vibTyp == 1) autoVibVal = (ch->eVibPos > 127) ? 64 : -64; // square
+		else if (ins->vibTyp == 2) autoVibVal = (((ch->eVibPos >> 1) + 64) & 127) - 64; // ramp up
+		else if (ins->vibTyp == 3) autoVibVal = ((-(ch->eVibPos >> 1) + 64) & 127) - 64; // ramp down
+		else autoVibVal = vibSineTab[ch->eVibPos]; // sine
+
+		autoVibVal <<= 2;
+		uint16_t tmpPeriod = (autoVibVal * (int16_t)autoVibAmp) >> 16;
+
+		tmpPeriod += ch->outPeriod;
+		if (tmpPeriod >= 32000)
+			tmpPeriod = 0; // 8bb: yes, FT2 does this (!)
+
+		ch->finalPeriod = tmpPeriod;
+		ch->status |= IS_Period;
+	}
+	else
+	{
+		ch->finalPeriod = ch->outPeriod;
+	}
+}
+
+// 8bb: for arpeggio and portamento (semitone-slide mode)
+static uint16_t relocateTon(uint16_t period, uint8_t arpNote, stmTyp *ch)
+{
+	int32_t tmpPeriod;
+
+	const int32_t fineTune = ((ch->fineTune >> 3) + 16) << 1;
+	int32_t hiPeriod = (8 * 12 * 16) * 2;
+	int32_t loPeriod = 0;
+
+	for (int32_t i = 0; i < 8; i++)
+	{
+		tmpPeriod = (((loPeriod + hiPeriod) >> 1) & 0xFFFFFFE0) + fineTune;
+
+		int32_t tableIndex = (uint32_t)(tmpPeriod - 16) >> 1;
+		tableIndex = CLAMP(tableIndex, 0, 1935); // 8bb: added security check
+
+		if (period >= note2Period[tableIndex])
+			hiPeriod = (tmpPeriod - fineTune) & 0xFFFFFFE0;
+		else
+			loPeriod = (tmpPeriod - fineTune) & 0xFFFFFFE0;
+	}
+
+	tmpPeriod = loPeriod + fineTune + (arpNote << 5);
+
+	if (tmpPeriod < 0) // 8bb: added security check
+		tmpPeriod = 0;
+
+	if (tmpPeriod >= (8*12*16+15)*2-1) // 8bb: FT2 bug, off-by-one edge case
+		tmpPeriod = (8*12*16+15)*2;
+
+	return note2Period[(uint32_t)tmpPeriod>>1];
+}
+
+static void vibrato2(stmTyp *ch)
+{
+	uint8_t tmpVib = (ch->vibPos >> 2) & 0x1F;
+
+	switch (ch->waveCtrl & 3)
+	{
+		// 0: sine
+		case 0: tmpVib = vibTab[tmpVib]; break;
+
+		// 1: ramp
+		case 1:
+		{
+			tmpVib <<= 3;
+			if ((int8_t)ch->vibPos < 0)
+				tmpVib = ~tmpVib;
+		}
+		break;
+
+		// 2/3: square
+		default: tmpVib = 255; break;
+	}
+
+	tmpVib = (tmpVib * ch->vibDepth) >> 5;
+
+	if ((int8_t)ch->vibPos < 0)
+		ch->outPeriod = ch->realPeriod - tmpVib;
+	else
+		ch->outPeriod = ch->realPeriod + tmpVib;
+
+	ch->status |= IS_Period;
+	ch->vibPos += ch->vibSpeed;
+}
+
+static void arp(stmTyp *ch, uint8_t param)
+{
+	const uint8_t tick = arpTab[song.timer & 0xFF]; // 8bb: non-FT2 protection (we have 248 extra overflow bytes in LUT, but not more!)
+	if (tick == 0)
+	{
+		ch->outPeriod = ch->realPeriod;
+	}
+	else
+	{
+		const uint8_t note = (tick == 1) ? (param >> 4) : (param & 0x0F);
+		ch->outPeriod = relocateTon(ch->realPeriod, note, ch);
+	}
+
+	ch->status |= IS_Period;
+}
+
+static void portaUp(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->portaUpSpeed;
+
+	ch->portaUpSpeed = param;
+
+	ch->realPeriod -= param << 2;
+	if ((int16_t)ch->realPeriod < 1)
+		ch->realPeriod = 1;
+
+	ch->outPeriod = ch->realPeriod;
+	ch->status |= IS_Period;
+}
+
+static void portaDown(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->portaDownSpeed;
+
+	ch->portaDownSpeed = param;
+
+	ch->realPeriod += param << 2;
+	if ((int16_t)ch->realPeriod > 32000-1) // 8bb: FT2 bug, should've been unsigned comparison!
+		ch->realPeriod = 32000-1;
+
+	ch->outPeriod = ch->realPeriod;
+	ch->status |= IS_Period;
+}
+
+static void tonePorta(stmTyp *ch, uint8_t param)
+{
+	if (ch->portaDir == 0)
+		return;
+
+	if (ch->portaDir > 1)
+	{
+		ch->realPeriod -= ch->portaSpeed;
+		if ((int16_t)ch->realPeriod <= (int16_t)ch->wantPeriod)
+		{
+			ch->portaDir = 1;
+			ch->realPeriod = ch->wantPeriod;
+		}
+	}
+	else
+	{
+		ch->realPeriod += ch->portaSpeed;
+		if (ch->realPeriod >= ch->wantPeriod)
+		{
+			ch->portaDir = 1;
+			ch->realPeriod = ch->wantPeriod;
+		}
+	}
+
+	if (ch->glissFunk) // 8bb: semitone-slide flag
+		ch->outPeriod = relocateTon(ch->realPeriod, 0, ch);
+	else
+		ch->outPeriod = ch->realPeriod;
+
+	ch->status |= IS_Period;
+
+	(void)param;
+}
+
+static void vibrato(stmTyp *ch, uint8_t param)
+{
+	uint8_t tmp8;
+
+	if (ch->eff > 0)
+	{
+		tmp8 = param & 0x0F;
+		if (tmp8 > 0)
+			ch->vibDepth = tmp8;
+
+		tmp8 = (param & 0xF0) >> 2;
+		if (tmp8 > 0)
+			ch->vibSpeed = tmp8;
+	}
+
+	vibrato2(ch);
+}
+
+static void tonePlusVol(stmTyp *ch, uint8_t param)
+{
+	tonePorta(ch, 0); // 8bb: the last parameter is actually not used in tonePorta()
+	volume(ch, param);
+
+	(void)param;
+}
+
+static void vibratoPlusVol(stmTyp *ch, uint8_t param)
+{
+	vibrato2(ch);
+	volume(ch, param);
+
+	(void)param;
+}
+
+static void tremolo(stmTyp *ch, uint8_t param)
+{
+	uint8_t tmp8;
+	int16_t tremVol;
+
+	const uint8_t tmpEff = param;
+	if (tmpEff > 0)
+	{
+		tmp8 = tmpEff & 0x0F;
+		if (tmp8 > 0)
+			ch->tremDepth = tmp8;
+
+		tmp8 = (tmpEff & 0xF0) >> 2;
+		if (tmp8 > 0)
+			ch->tremSpeed = tmp8;
+	}
+
+	uint8_t tmpTrem = (ch->tremPos >> 2) & 0x1F;
+	switch ((ch->waveCtrl >> 4) & 3)
+	{
+		// 0: sine
+		case 0: tmpTrem = vibTab[tmpTrem]; break;
+
+		// 1: ramp
+		case 1:
+		{
+			tmpTrem <<= 3;
+			if ((int8_t)ch->vibPos < 0) // 8bb: FT2 bug, should've been ch->tremPos
+				tmpTrem = ~tmpTrem;
+		}
+		break;
+
+		// 2/3: square
+		default: tmpTrem = 255; break;
+	}
+	tmpTrem = (tmpTrem * ch->tremDepth) >> 6;
+
+	if ((int8_t)ch->tremPos < 0)
+	{
+		tremVol = ch->realVol - tmpTrem;
+		if (tremVol < 0)
+			tremVol = 0;
+	}
+	else
+	{
+		tremVol = ch->realVol + tmpTrem;
+		if (tremVol > 64)
+			tremVol = 64;
+	}
+
+	ch->outVol = (uint8_t)tremVol;
+	ch->status |= IS_Vol;
+	ch->tremPos += ch->tremSpeed;
+}
+
+static void volume(stmTyp *ch, uint8_t param) // 8bb: actually volume slide
+{
+	if (param == 0)
+		param = ch->volSlideSpeed;
+
+	ch->volSlideSpeed = param;
+
+	uint8_t newVol = ch->realVol;
+	if ((param & 0xF0) == 0)
+	{
+		newVol -= param;
+		if ((int8_t)newVol < 0)
+			newVol = 0;
+	}
+	else
+	{
+		param >>= 4;
+
+		newVol += param;
+		if (newVol > 64)
+			newVol = 64;
+	}
+
+	ch->outVol = ch->realVol = newVol;
+	ch->status |= IS_Vol;
+}
+
+static void globalVolSlide(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->globVolSlideSpeed;
+
+	ch->globVolSlideSpeed = param;
+
+	uint8_t newVol = (uint8_t)song.globVol;
+	if ((param & 0xF0) == 0)
+	{
+		newVol -= param;
+		if ((int8_t)newVol < 0)
+			newVol = 0;
+	}
+	else
+	{
+		param >>= 4;
+
+		newVol += param;
+		if (newVol > 64)
+			newVol = 64;
+	}
+
+	song.globVol = newVol;
+
+	stmTyp *c = stm;
+	for (int32_t i = 0; i < song.antChn; i++, c++) // 8bb: update all voice volumes
+		c->status |= IS_Vol;
+}
+
+static void keyOffCmd(stmTyp *ch, uint8_t param)
+{
+	if ((uint8_t)(song.tempo-song.timer) == (param & 31))
+		keyOff(ch);
+}
+
+static void panningSlide(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->panningSlideSpeed;
+
+	ch->panningSlideSpeed = param;
+
+	int16_t newPan = (int16_t)ch->outPan;
+	if ((param & 0xF0) == 0)
+	{
+		newPan -= param;
+		if (newPan < 0)
+			newPan = 0;
+	}
+	else
+	{
+		param >>= 4;
+
+		newPan += param;
+		if (newPan > 255)
+			newPan = 255;
+	}
+
+	ch->outPan = (uint8_t)newPan;
+	ch->status |= IS_Pan;
+}
+
+static void tremor(stmTyp *ch, uint8_t param)
+{
+	if (param == 0)
+		param = ch->tremorSave;
+
+	ch->tremorSave = param;
+
+	uint8_t tremorSign = ch->tremorPos & 0x80;
+	uint8_t tremorData = ch->tremorPos & 0x7F;
+
+	tremorData--;
+	if ((int8_t)tremorData < 0)
+	{
+		if (tremorSign == 0x80)
+		{
+			tremorSign = 0x00;
+			tremorData = param & 0x0F;
+		}
+		else
+		{
+			tremorSign = 0x80;
+			tremorData = param >> 4;
+		}
+	}
+
+	ch->tremorPos = tremorSign | tremorData;
+	ch->outVol = (tremorSign == 0x80) ? ch->realVol : 0;
+	ch->status |= IS_Vol + IS_QuickVol;
+}
+
+static void retrigNote(stmTyp *ch, uint8_t param)
+{
+	if (param == 0) // 8bb: E9x with a param of zero is handled in getNewNote()
+		return;
+
+	if ((song.tempo-song.timer) % param == 0)
+	{
+		startTone(0, 0, 0, ch);
+		retrigEnvelopeVibrato(ch);
+	}
+}
+
+static void noteCut(stmTyp *ch, uint8_t param)
+{
+	if ((uint8_t)(song.tempo-song.timer) == param)
+	{
+		ch->outVol = ch->realVol = 0;
+		ch->status |= IS_Vol + IS_QuickVol;
+	}
+}
+
+static void noteDelay(stmTyp *ch, uint8_t param)
+{
+	if ((uint8_t)(song.tempo-song.timer) == param)
+	{
+		startTone(ch->tonTyp & 0xFF, 0, 0, ch);
+
+		if ((ch->tonTyp & 0xFF00) > 0)
+			retrigVolume(ch);
+
+		retrigEnvelopeVibrato(ch);
+
+		if (ch->volKolVol >= 0x10 && ch->volKolVol <= 0x50)
+		{
+			ch->outVol = ch->volKolVol - 16;
+			ch->realVol = ch->outVol;
+		}
+		else if (ch->volKolVol >= 0xC0 && ch->volKolVol <= 0xCF)
+		{
+			ch->outPan = (ch->volKolVol & 0x0F) << 4;
+		}
+	}
+}
+
+static const efxRoutine EJumpTab_TickNonZero[16] =
+{
+	dummy, // 0
+	dummy, // 1
+	dummy, // 2
+	dummy, // 3
+	dummy, // 4
+	dummy, // 5
+	dummy, // 6
+	dummy, // 7
+	dummy, // 8
+	retrigNote, // 9
+	dummy, // A
+	dummy, // B
+	noteCut, // C
+	noteDelay, // D
+	dummy, // E
+	dummy // F
+};
+
+static void E_Effects_TickNonZero(stmTyp *ch, uint8_t param)
+{
+	EJumpTab_TickNonZero[param >> 4](ch, param & 0xF);
+}
+
+static const efxRoutine JumpTab_TickNonZero[36] =
+{
+	arp, // 0
+	portaUp, // 1
+	portaDown, // 2
+	tonePorta, // 3
+	vibrato, // 4
+	tonePlusVol, // 5
+	vibratoPlusVol, // 6
+	tremolo, // 7
+	dummy, // 8
+	dummy, // 9
+	volume, // A
+	dummy, // B
+	dummy, // C
+	dummy, // D
+	E_Effects_TickNonZero, // E
+	dummy, // F
+	dummy, // G
+	globalVolSlide, // H
+	dummy, // I
+	dummy, // J
+	keyOffCmd, // K
+	dummy, // L
+	dummy, // M
+	dummy, // N
+	dummy, // O
+	panningSlide, // P
+	dummy, // Q
+	doMultiRetrig, // R
+	dummy, // S
+	tremor, // T
+	dummy, // U
+	dummy, // V
+	dummy, // W
+	dummy, // X
+	dummy, // Y
+	dummy  // Z
+};
+
+static void doEffects(stmTyp *ch) // tick>0 effect handling
+{
+	const uint8_t volKolEfx = ch->volKolVol >> 4;
+	if (volKolEfx > 0)
+		VJumpTab_TickNonZero[volKolEfx](ch);
+
+	if ((ch->eff == 0 && ch->effTyp == 0) || ch->effTyp > 35) return;
+	JumpTab_TickNonZero[ch->effTyp](ch, ch->eff);
+}
+
+static void getNextPos(void)
+{
+	if (song.timer != 1)
+		return;
+
+	song.pattPos++;
+
+	if (song.pattDelTime > 0)
+	{
+		song.pattDelTime2 = song.pattDelTime;
+		song.pattDelTime = 0;
+	}
+
+	if (song.pattDelTime2 > 0)
+	{
+		song.pattDelTime2--;
+		if (song.pattDelTime2 > 0)
+			song.pattPos--;
+	}
+
+	if (song.pBreakFlag)
+	{
+		song.pBreakFlag = false;
+		song.pattPos = song.pBreakPos;
+	}
+
+	if (song.pattPos >= song.pattLen || song.posJumpFlag)
+	{
+		song.pattPos = song.pBreakPos;
+		song.pBreakPos = 0;
+		song.posJumpFlag = false;
+
+		song.songPos++;
+		if (song.songPos >= song.len)
+			song.songPos = song.repS;
+
+		song.pattNr = song.songTab[(uint8_t)song.songPos];
+		song.pattLen = pattLens[(uint8_t)song.pattNr];
+	}
+}
+
+void mainPlayer(void)
+{
+	if (musicPaused)
+		return;
+
+	bool tickZero = false;
+
+	song.timer--;
+	if (song.timer == 0)
+	{
+		song.timer = song.tempo;
+		tickZero = true;
+	}
+
+	const bool readNewNote = tickZero && (song.pattDelTime2 == 0);
+	if (readNewNote)
+	{
+		const tonTyp *pattPtr = nilPatternLine;
+		if (patt[song.pattNr] != NULL)
+			pattPtr = &patt[song.pattNr][song.pattPos * song.antChn];
+
+		stmTyp *c = stm;
+		for (uint8_t i = 0; i < song.antChn; i++, c++, pattPtr++)
+		{
+			PMPTmpActiveChannel = i; // 8bb: for P_StartTone()
+			getNewNote(c, pattPtr);
+			fixaEnvelopeVibrato(c);
+		}
+	}
+	else
+	{
+		stmTyp *c = stm;
+		for (uint8_t i = 0; i < song.antChn; i++, c++)
+		{
+			PMPTmpActiveChannel = i; // 8bb: for P_StartTone()
+			doEffects(c);
+			fixaEnvelopeVibrato(c);
+		}
+	}
+
+	getNextPos();
+}
--- /dev/null
+++ b/pmp_main.h
@@ -1,0 +1,7 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+void mainPlayer(void);
+uint32_t getFrequenceValue(uint16_t period);
--- /dev/null
+++ b/pmp_mix.c
@@ -1,0 +1,387 @@
+#include <assert.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <string.h>
+#include <math.h>
+#include "pmplay.h"
+#include "pmp_main.h"
+#include "snd_masm.h"
+#include "tables.h"
+
+// fast 32-bit -> 16-bit clamp
+#define CLAMP16(i) if ((int16_t)(i) != i) i = 0x7FFF ^ (i >> 31)
+
+static bool dump_Flag;
+static int32_t oldReplayRate;
+
+// globalized
+int16_t chnReloc[32];
+int32_t *CDA_MixBuffer = NULL;
+CIType CI[32 * 2];
+// ------------
+
+static void mix_UpdateChannel(int32_t nr, WaveChannelInfoType *WCI);
+
+void P_SetSpeed(uint16_t bpm)
+{
+	// 8bb: added this
+	if (bpm == 0)
+		bpm = 125;
+
+	speedVal = ((realReplayRate + realReplayRate) + (realReplayRate >> 1)) / bpm; // 8bb: same as doing "((realReplayRate * 5) / 2) / bpm"
+}
+
+void P_StartTone(sampleTyp *s, int32_t smpStartPos)
+{
+	WaveChannelInfoType WCI;
+
+	WCI.SStartPos = smpStartPos;
+	WCI.SBase = s->pek;
+	WCI.SLen = s->len;
+	WCI.SRepS = s->repS;
+	WCI.SRepL = s->repL;
+	WCI.SType = s->typ;
+	WCI.Status = Status_StartTone+Status_StopTone;
+
+	mix_UpdateChannel(PMPTmpActiveChannel, &WCI);
+}
+
+// 8bb: added these two
+bool mix_Init(int32_t audioBufferSize)
+{
+	CDA_MixBuffer = (int32_t *)malloc(audioBufferSize * (2 * sizeof (int32_t)));
+	if (CDA_MixBuffer == NULL)
+		return false;
+
+	PMPLeft = 0;
+	return true;
+}
+
+void mix_Free(void)
+{
+	if (CDA_MixBuffer != NULL)
+	{
+		free(CDA_MixBuffer);
+		CDA_MixBuffer = NULL;
+	}
+}
+// --------------------
+
+static void updateVolume(CIType *v, int32_t volIPLen)
+{
+	const uint32_t vol = v->SVol * CDA_Amp;
+
+	v->SLVol1 = (vol * panningTab[256-v->SPan]) >> (32-28);
+	v->SRVol1 = (vol * panningTab[    v->SPan]) >> (32-28);
+
+	if (volumeRampingFlag)
+	{
+		v->SLVolIP = (v->SLVol1 - v->SLVol2) / volIPLen;
+		v->SRVolIP = (v->SRVol1 - v->SRVol2) / volIPLen;
+		v->SVolIPLen = volIPLen;
+	}
+}
+
+static void mix_UpdateChannel(int32_t nr, WaveChannelInfoType *WCI)
+{
+	CIType *v = &CI[chnReloc[nr]];
+	const uint8_t status = WCI->Status;
+
+	if (status & Status_StopTone)
+	{
+		if (volumeRampingFlag)
+		{
+			// 8bb: fade out current voice
+			v->SType |= SType_Fadeout;
+			v->SVol = 0;
+			updateVolume(v, quickVolSizeVal);
+
+			// 8bb: swap current voice with neighbor
+			chnReloc[nr] ^= 1;
+			v = &CI[chnReloc[nr]];
+		}
+
+		v->SType = SType_Off;
+	}
+
+	if (status & Status_SetPan)
+		v->SPan = (uint8_t)WCI->SPan;
+
+	if (status & Status_SetVol)
+	{
+		uint16_t vol = WCI->SVol;
+		if (vol > 0) vol--; // 8bb: 0..256 -> 0..255 ( FT2 does this to prevent mul overflow in updateVolume() )
+		v->SVol = (uint8_t)vol;
+	}
+
+	if (status & (Status_SetVol+Status_SetPan))
+		updateVolume(v, (status & Status_QuickVol) ? quickVolSizeVal : speedVal);
+
+	if (status & Status_SetFrq)
+		v->SFrq = WCI->SFrq;
+
+	if (status & Status_StartTone)
+	{
+		int32_t len;
+
+		uint8_t type = WCI->SType;
+		const bool sample16Bit = (type >> 4) & 1;
+
+		if (type & (SType_Fwd+SType_Rev))
+		{
+			int32_t repL = WCI->SRepL;
+			int32_t repS = WCI->SRepS;
+
+			if (sample16Bit)
+			{
+				repL >>= 1;
+				repS >>= 1;
+
+				v->SRevBase = (int16_t *)WCI->SBase + (repS+repS+repL);
+			}
+			else
+			{
+				v->SRevBase = (int8_t *)WCI->SBase + (repS+repS+repL);
+			}
+
+			v->SRepL = repL;
+			v->SRepS = repS;
+
+			len = repS + repL;
+		}
+		else
+		{
+			type &= ~(SType_Fwd+SType_Rev); // 8bb: keep loop flags only
+
+			len = WCI->SLen;
+			if (sample16Bit)
+				len >>= 1;
+
+			if (len == 0)
+				return;
+		}
+		
+		// 8bb: overflown 9xx (set sample offset), cut voice (voice got ended earlier in "if (status & Status_StopTone)")
+		if (WCI->SStartPos >= len)
+			return;
+
+		v->SLen = len;
+		v->SPos = WCI->SStartPos;
+		v->SPosDec = 0;
+		v->SBase = WCI->SBase;
+		v->SMixType = (sample16Bit * 4) + (volumeRampingFlag * 2) + interpolationFlag;
+		v->SType = type;
+	}
+}
+
+static void mix_UpdateChannelVolPanFrq(void)
+{
+	WaveChannelInfoType WCI;
+
+	stmTyp *ch = stm;
+	for (int32_t i = 0; i < song.antChn; i++, ch++)
+	{
+		uint8_t newStatus = 0;
+
+		const uint8_t status = ch->status;
+		ch->status = 0;
+
+		if (status == 0)
+			continue;
+
+		if (status & IS_Vol)
+		{
+			WCI.SVol = ch->finalVol;
+			newStatus |= Status_SetVol;
+		}
+
+		if (status & IS_QuickVol)
+			newStatus |= Status_QuickVol;
+
+		if (status & IS_Pan)
+		{
+			WCI.SPan = ch->finalPan;
+			newStatus |= Status_SetPan;
+		}
+
+		if (status & IS_Period)
+		{
+			WCI.SFrq = getFrequenceValue(ch->finalPeriod);
+			newStatus |= Status_SetFrq;
+		}
+
+		WCI.Status = newStatus;
+		mix_UpdateChannel(i, &WCI);
+	}
+}
+
+void mix_ClearChannels(void) // 8bb: rewritten to handle all voices instead of song.antChn
+{
+	lockMixer();
+
+	memset(CI, 0, sizeof (CI));
+
+	CIType *v = CI;
+	for (int16_t i = 0; i < 32*2; i++, v++)
+	{
+		v->SPan = 128;
+		v->SType = SType_Off;
+	}
+
+	for (int16_t i = 0; i < 32; i++)
+		chnReloc[i] = i+i;
+
+	unlockMixer();
+}
+
+static void mix_SaveIPVolumes(void)
+{
+	CIType *v = CI;
+	for (int32_t i = 0; i < song.antChn*2; i++, v++)
+	{
+		// 8bb: this cuts any active fade-out voices (volume ramping)
+		if (v->SType & SType_Fadeout)
+			v->SType = SType_Off;
+
+		v->SLVol2 = v->SLVol1;
+		v->SRVol2 = v->SRVol1;
+		v->SVolIPLen = 0;
+	}
+}
+
+void mix_UpdateBuffer(int16_t *buffer, int32_t numSamples)
+{
+	if (numSamples <= 0)
+		return;
+
+	if (musicPaused || WAVDump_Flag)
+	{
+		memset(buffer, 0, numSamples * (2 * sizeof (int16_t)));
+		return;
+	}
+
+	assert(CDA_MixBuffer != NULL);
+	memset(CDA_MixBuffer, 0, numSamples * (2 * sizeof (int32_t)));
+
+	int32_t c = 0;
+	int32_t a = numSamples;
+
+	while (a > 0)
+	{
+		if (PMPLeft == 0)
+		{
+			mix_SaveIPVolumes();
+			mainPlayer();
+			mix_UpdateChannelVolPanFrq();
+			PMPLeft = speedVal;
+		}
+
+		int32_t b = a;
+		if (b > PMPLeft)
+			b = PMPLeft;
+
+		// 8bb: fix for PMPMix32Proc() silent mix (vol=0)
+		if (b > 65535)
+			b = 65535;
+
+		CIType *v = CI;
+		for (int32_t i = 0; i < song.antChn*2; i++, v++)
+			PMPMix32Proc(v, b, c);
+
+		c += b;
+		a -= b;
+		PMPLeft -= b;
+	}
+
+	numSamples *= 2; // 8bb: stereo
+
+	/* 8bb: Done a bit differently since we don't use a
+	** Sound Blaster with its master volume setting.
+	** Instead we change the amplitude here.
+	*/
+
+	if (masterVol == 256) // 8bb: max master volume, no need to change amp
+	{
+		for (int32_t i = 0; i < numSamples; i++)
+		{
+			int32_t out32 = CDA_MixBuffer[i] >> 8;
+			CLAMP16(out32);
+			buffer[i] = (int16_t)out32;
+		}
+	}
+	else
+	{
+		for (int32_t i = 0; i < numSamples; i++)
+		{
+			int32_t out32 = CDA_MixBuffer[i] >> 8;
+			CLAMP16(out32);
+			out32 = (out32 * masterVol) >> 8;
+			buffer[i] = (int16_t)out32;
+		}
+	}
+}
+
+bool dump_Init(int32_t frq, int32_t amp, int16_t songPos)
+{
+	setPos(songPos, 0);
+
+	oldReplayRate = realReplayRate;
+
+	realReplayRate = frq;
+	updateReplayRate();
+	CDA_Amp = 8 * amp;
+
+	mix_ClearChannels();
+	stopVoices();
+	song.globVol = 64;
+	speedVal = (frq*5 / 2) / song.speed;
+	quickVolSizeVal = frq / 200;
+
+	dump_Flag = false;
+	return true;
+}
+
+void dump_Close(void)
+{
+	stopVoices();
+	realReplayRate = oldReplayRate;
+	updateReplayRate();
+}
+
+bool dump_EndOfTune(int32_t endSongPos)
+{
+	bool returnValue = (dump_Flag && song.pattPos == 0 && song.timer == 1) || (song.tempo == 0);
+
+	// 8bb: FT2 bugfix for EEx (pattern delay) on first row of a pattern
+	if (song.pattDelTime2 > 0)
+		returnValue = false;
+
+	if (song.songPos == endSongPos && song.pattPos == 0 && song.timer == 1)
+		dump_Flag = true;
+
+	return returnValue;
+}
+
+int32_t dump_GetFrame(int16_t *p) // 8bb: returns bytes mixed
+{
+	mix_SaveIPVolumes();
+	mainPlayer();
+	mix_UpdateChannelVolPanFrq();
+
+	memset(CDA_MixBuffer, 0, speedVal * (2 * sizeof (int32_t)));
+
+	CIType *v = CI;
+	for (int32_t i = 0; i < song.antChn*2; i++, v++)
+		PMPMix32Proc(v, speedVal, 0);
+
+	const int32_t numSamples = speedVal * 2; // 8bb: *2 for stereo
+	for (int32_t i = 0; i < numSamples; i++)
+	{
+		int32_t out32 = CDA_MixBuffer[i] >> 8;
+		CLAMP16(out32);
+		p[i] = (int16_t)out32;
+	}
+
+	return speedVal * (2 * sizeof (int16_t));
+}
--- /dev/null
+++ b/pmp_mix.h
@@ -1,0 +1,59 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pmplay.h"
+
+enum
+{
+	Status_SetVol = 1,
+	Status_SetPan = 2,
+	Status_SetFrq = 4,
+	Status_StartTone = 8,
+	Status_StopTone = 16,
+	Status_QuickVol = 32,
+
+	SType_Fwd = 1,
+	SType_Rev = 2,
+	SType_RevDir = 4,
+	SType_Off = 8,
+	SType_16 = 16,
+	SType_Fadeout = 32
+};
+
+typedef struct
+{
+	const void *SBase, *SRevBase;
+	uint8_t SType, SPan, SVol;
+	int32_t SLVol1, SRVol1, SLVol2, SRVol2, SLVolIP, SRVolIP, SVolIPLen;
+	int32_t SLen, SRepS, SRepL, SPos, SMixType;
+	uint32_t SPosDec, SFrq;
+} CIType;
+
+typedef struct
+{
+	const void *SBase;
+	uint8_t Status, SType;
+	int16_t SVol, SPan;
+	int32_t SFrq, SLen, SRepS, SRepL, SStartPos;
+} WaveChannelInfoType;
+
+extern int16_t chnReloc[32];
+extern int32_t *CDA_MixBuffer;
+extern CIType CI[32 * 2];
+
+void P_SetSpeed(uint16_t bpm);
+void P_StartTone(sampleTyp *s, int32_t smpStartPos);
+
+// 8bb: added these two
+bool mix_Init(int32_t audioBufferSize);
+void mix_Free(void);
+// -------------------
+
+void mix_ClearChannels(void);
+void mix_UpdateBuffer(int16_t *buffer, int32_t numSamples);
+
+bool dump_Init(int32_t frq, int32_t amp, int16_t songPos);
+void dump_Close(void);
+bool dump_EndOfTune(int32_t endSongPos);
+int32_t dump_GetFrame(int16_t *p);
--- /dev/null
+++ b/pmplay.c
@@ -1,0 +1,1406 @@
+/*
+** C-port of FT2.09's XM replayer, by 8bitbubsy nov. 2020
+**
+** Note: This is not the exact same code used in the FT2 clone!
+** This is a direct port meant to give bit-accurate results to
+** FT2.08/FT2.09 (non-GUS mode). It's very handy to use as a
+** reference if you are trying to make your own, accurate XM
+** player.
+*/
+
+#define INSTR_HEADER_SIZE 263
+
+#define DEFAULT_AMP 4
+#define DEFAULT_MASTER_VOL 256
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <stdint.h>
+#include <stdbool.h>
+#include <math.h>
+#include <assert.h>
+#include "pmplay.h"
+#include "pmp_mix.h"
+#include "snd_masm.h"
+#include "tables.h"
+
+#define SWAP16(value) \
+( \
+	(((uint16_t)((value) & 0x00FF)) << 8) | \
+	(((uint16_t)((value) & 0xFF00)) >> 8)   \
+)
+
+#ifdef _MSC_VER
+#pragma pack(push)
+#pragma pack(1)
+#endif
+typedef struct songHeaderTyp_t
+{
+	char sig[17], name[21], progName[20];
+	uint16_t ver;
+	int32_t headerSize;
+	uint16_t len, repS, antChn, antPtn, antInstrs, flags, defTempo, defSpeed;
+	uint8_t songTab[256];
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+songHeaderTyp;
+
+typedef struct modSampleTyp
+{
+	char name[22];
+	uint16_t len;
+	uint8_t fine, vol;
+	uint16_t repS, repL;
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+modSampleTyp;
+
+typedef struct songMOD31HeaderTyp
+{
+	char name[20];
+	modSampleTyp sample[31];
+	uint8_t len, repS, songTab[128];
+	char Sig[4];
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+songMOD31HeaderTyp;
+
+typedef struct songMOD15HeaderTyp
+{
+	char name[20];
+	modSampleTyp sample[15];
+	uint8_t len, repS, songTab[128];
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+songMOD15HeaderTyp;
+
+typedef struct sampleHeaderTyp_t
+{
+	int32_t len, repS, repL;
+	uint8_t vol;
+	int8_t fine;
+	uint8_t typ, pan;
+	int8_t relTon;
+	uint8_t skrap;
+	char name[22];
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+sampleHeaderTyp;
+
+typedef struct instrHeaderTyp_t
+{
+	int32_t instrSize;
+	char name[22];
+	uint8_t typ;
+	uint16_t antSamp;
+	int32_t sampleSize;
+	uint8_t ta[96];
+	int16_t envVP[12][2], envPP[12][2];
+	uint8_t envVPAnt, envPPAnt, envVSust, envVRepS, envVRepE, envPSust, envPRepS;
+	uint8_t envPRepE, envVTyp, envPTyp, vibTyp, vibSweep, vibDepth, vibRate;
+	uint16_t fadeOut;
+	uint8_t midiOn, midiChannel;
+	int16_t midiProgram, midiBend;
+	int8_t mute;
+	uint8_t reserved[15];
+	sampleHeaderTyp samp[32];
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+instrHeaderTyp;
+
+typedef struct patternHeaderTyp_t
+{
+	int32_t patternHeaderSize;
+	uint8_t typ;
+	uint16_t pattLen, dataLen;
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+patternHeaderTyp;
+#ifdef _MSC_VER
+#pragma pack(pop)
+#endif
+
+static int32_t soundBufferSize;
+
+// globalized
+volatile bool interpolationFlag, volumeRampingFlag, moduleLoaded, musicPaused, WAVDump_Flag;
+bool linearFrqTab;
+volatile const uint16_t *note2Period;
+uint16_t pattLens[256];
+int16_t PMPTmpActiveChannel, boostLevel = DEFAULT_AMP;
+int32_t masterVol = DEFAULT_MASTER_VOL, PMPLeft = 0;
+int32_t realReplayRate, quickVolSizeVal, speedVal;
+int32_t frequenceDivFactor, frequenceMulFactor;
+uint32_t CDA_Amp = 8*DEFAULT_AMP;
+tonTyp *patt[256];
+instrTyp *instr[1+128];
+songTyp song;
+stmTyp stm[32];
+// ------------------
+
+// 8bb: added these for loader
+typedef struct
+{
+	uint8_t *_ptr, *_base;
+	bool _eof;
+	size_t _cnt, _bufsiz;
+} MEMFILE;
+
+static MEMFILE *mopen(const uint8_t *src, uint32_t length);
+static void mclose(MEMFILE **buf);
+static size_t mread(void *buffer, size_t size, size_t count, MEMFILE *buf);
+static bool meof(MEMFILE *buf);
+static void mseek(MEMFILE *buf, int32_t offset, int32_t whence);
+static void mrewind(MEMFILE *buf);
+// --------------------------
+
+static void resetMusic(void);
+static void freeAllPatterns(void);
+static void setFrqTab(bool linear);
+
+static CIType *getVoice(int32_t ch) // 8bb: added this
+{
+	if (ch < 0 || ch > 31)
+		return NULL;
+
+	return &CI[chnReloc[ch]];
+}
+
+/***************************************************************************
+ *        ROUTINES FOR SAMPLE HANDLING ETC.                                *
+ ***************************************************************************/
+
+// 8bb: modifies wrapped sample after loop/end (for branchless mixer interpolation)
+static void fixSample(sampleTyp *s)
+{
+	if (s->pek == NULL)
+		return; // empty sample
+
+	const bool sample16Bit = (s->typ >> 4) & 1;
+	uint8_t loopType = s->typ & 3;
+	int16_t *ptr16 = (int16_t *)s->pek;
+	int32_t len = s->len;
+	int32_t loopStart = s->repS;
+	int32_t loopEnd = s->repS + s->repL;
+
+	if (sample16Bit)
+	{
+		len >>= 1;
+		loopStart >>= 1;
+		loopEnd >>= 1;
+	}
+
+	if (len < 1)
+		return;
+
+	/* 8bb:
+	** This is the exact bit test order of which FT2 handles
+	** the sample fix in SND_MASM.ASM.
+	**
+	** This order is important for rare cases where both the
+	** "forward" and "pingpong" loop bits are set at once.
+	**
+	** This means that if both flags are set, the mixer will
+	** play the sample with pingpong looping, but the sample fix
+	** is handled as if it was a forward loop. This results in
+	** the wrong interpolation tap sample being written after the
+	** loop end point.
+	*/
+
+	if (loopType & 1)
+	{
+		// forward loop
+		if (sample16Bit)
+			ptr16[loopEnd] = ptr16[loopStart];
+		else
+			s->pek[loopEnd] = s->pek[loopStart];
+
+		return;
+	}
+	else if (loopType & 2)
+	{
+		// pingpong loop
+		if (sample16Bit)
+			ptr16[loopEnd] = ptr16[loopEnd-1];
+		else
+			s->pek[loopEnd] = s->pek[loopEnd-1];
+	}
+	else
+	{
+		// no loop
+		if (sample16Bit)
+			ptr16[len] = 0;
+		else
+			s->pek[len] = 0;
+	}
+}
+
+static void checkSampleRepeat(int32_t nr, int32_t nr2)
+{
+	instrTyp *i = instr[nr];
+	if (i == NULL) return;
+	sampleTyp *s = &i->samp[nr2];
+
+	if (s->repS < 0) s->repS = 0;
+	if (s->repL < 0) s->repL = 0;
+	if (s->repS > s->len) s->repS = s->len;
+	if (s->repS+s->repL > s->len) s->repL = s->len - s->repS;
+}
+
+static void upDateInstrs(void)
+{
+	for (int32_t i = 0; i <= 128; i++)
+	{
+		instrTyp *ins = instr[i];
+		if (ins == NULL)
+			continue;
+
+		sampleTyp *s = ins->samp;
+		for (int32_t j = 0; j < 16; j++, s++)
+		{
+			checkSampleRepeat(i, j);
+			fixSample(s);
+
+			if (s->pek == NULL)
+			{
+				s->len = 0;
+				s->repS = 0;
+				s->repL = 0;
+			}
+		}
+	}
+}
+
+static bool patternEmpty(uint16_t nr)
+{
+	if (patt[nr] == NULL)
+		return true;
+
+	const uint8_t *scanPtr = (const uint8_t *)patt[nr];
+	const int32_t scanLen = pattLens[nr] * song.antChn * sizeof (tonTyp);
+
+	for (int32_t i = 0; i < scanLen; i++)
+	{
+		if (scanPtr[i] != 0)
+			return false;
+	}
+
+	return true;
+}
+
+static bool allocateInstr(uint16_t i)
+{
+	if (instr[i] != NULL)
+		return true;
+
+	instrTyp *p = (instrTyp *)calloc(1, sizeof (instrTyp));
+	if (p == NULL)
+		return false;
+
+	sampleTyp *s = p->samp;
+	for (int32_t j = 0; j < 16; j++, s++)
+	{
+		s->pan = 128;
+		s->vol = 64;
+	}
+
+	instr[i] = p;
+	return true;
+}
+
+static void freeInstr(uint16_t nr)
+{
+	if (nr > 128)
+		return;
+
+	instrTyp *ins = instr[nr];
+	if (ins == NULL)
+		return;
+
+	sampleTyp *s = ins->samp;
+	for (uint8_t i = 0; i < 16; i++, s++)
+	{
+		if (s->pek != NULL)
+			free(s->pek);
+	}
+
+	free(ins);
+	ins = NULL;
+}
+
+static void freeAllInstr(void)
+{
+	for (uint16_t i = 0; i <= 128; i++)
+		freeInstr(i);
+}
+
+static void freeAllPatterns(void) // 8bb: added this one, since it's handy
+{
+	for (int32_t i = 0; i < 256; i++)
+	{
+		if (patt[i] != NULL)
+		{
+			free(patt[i]);
+			patt[i] = NULL;
+		}
+
+		pattLens[i] = 64;
+	}
+}
+
+static void delta2Samp(int8_t *p, uint32_t len, bool sample16Bit)
+{
+	if (sample16Bit)
+	{
+		len >>= 1;
+	
+		int16_t *p16 = (int16_t *)p;
+
+		int16_t olds16 = 0;
+		for (uint32_t i = 0; i < len; i++)
+		{
+			const int16_t news16 = p16[i] + olds16;
+			p16[i] = news16;
+			olds16 = news16;
+		}
+	}
+	else
+	{
+		int8_t *p8 = (int8_t *)p;
+
+		int8_t olds8 = 0;
+		for (uint32_t i = 0; i < len; i++)
+		{
+			const int8_t news8 = p8[i] + olds8;
+			p8[i] = news8;
+			olds8 = news8;
+		}
+	}
+}
+
+static void unpackPatt(uint8_t *dst, uint16_t inn, uint16_t len, uint8_t antChn)
+{
+	if (dst == NULL)
+		return;
+
+	const uint8_t *src = dst + inn;
+	const int32_t srcEnd = len * (sizeof (tonTyp) * antChn);
+
+	int32_t srcIdx = 0;
+	for (int32_t i = 0; i < len; i++)
+	{
+		for (int32_t j = 0; j < antChn; j++)
+		{
+			if (srcIdx >= srcEnd)
+				return; // error!
+
+			const uint8_t note = *src++;
+			if (note & 0x80)
+			{
+				*dst++ = (note & 0x01) ? *src++ : 0;
+				*dst++ = (note & 0x02) ? *src++ : 0;
+				*dst++ = (note & 0x04) ? *src++ : 0;
+				*dst++ = (note & 0x08) ? *src++ : 0;
+				*dst++ = (note & 0x10) ? *src++ : 0;
+			}
+			else
+			{
+				*dst++ = note;
+				*dst++ = *src++;
+				*dst++ = *src++;
+				*dst++ = *src++;
+				*dst++ = *src++;
+			}
+
+			// 8bb: added this. If note is overflowing (>97), remove it (prevent LUT buffer overrun)
+			if (*(dst-5) > 97)
+				*(dst-5) = 0;
+
+			srcIdx += sizeof (tonTyp);
+		}
+	}
+}
+
+void freeMusic(void)
+{
+	stopMusic();
+	freeAllInstr();
+	freeAllPatterns();
+
+	song.tempo = 6;
+	song.speed = 125;
+	song.timer = 1;
+
+	setFrqTab(true);
+	resetMusic();
+}
+
+void stopVoices(void)
+{
+	lockMixer();
+
+	stmTyp *ch = stm;
+	for (uint8_t i = 0; i < 32; i++, ch++)
+	{
+		ch->tonTyp = 0;
+		ch->relTonNr = 0;
+		ch->instrNr = 0;
+		ch->instrSeg = instr[0]; // 8bb: placeholder instrument
+		ch->status = IS_Vol;
+
+		ch->realVol = 0;
+		ch->outVol = 0;
+		ch->oldVol = 0;
+		ch->finalVol = 0;
+		ch->oldPan = 128;
+		ch->outPan = 128;
+		ch->finalPan = 128;
+		ch->vibDepth = 0;
+	}
+
+	unlockMixer();
+}
+
+static void resetMusic(void)
+{
+	song.timer = 1;
+	stopVoices();
+	setPos(0, 0);
+}
+
+void setPos(int32_t pos, int32_t row) // -1 = don't change
+{
+	if (pos != -1)
+	{
+		song.songPos = (int16_t)pos;
+		if (song.len > 0 && song.songPos >= song.len)
+			song.songPos = song.len - 1;
+
+		song.pattNr = song.songTab[song.songPos];
+		song.pattLen = pattLens[song.pattNr];
+	}
+
+	if (row != -1)
+	{
+		song.pattPos = (int16_t)row;
+		if (song.pattPos >= song.pattLen)
+			song.pattPos = song.pattLen - 1;
+	}
+
+	song.timer = 1;
+}
+
+/***************************************************************************
+ *        MODULE LOADING ROUTINES                                          *
+ ***************************************************************************/
+
+static bool loadInstrHeader(MEMFILE *f, uint16_t i)
+{
+	instrHeaderTyp ih;
+
+	memset(&ih, 0, INSTR_HEADER_SIZE);
+	mread(&ih.instrSize, 4, 1, f);
+	if (ih.instrSize > INSTR_HEADER_SIZE) ih.instrSize = INSTR_HEADER_SIZE;
+	mread(ih.name, ih.instrSize-4, 1, f);
+
+	if (ih.antSamp > 16)
+		return false;
+
+	if (ih.antSamp > 0)
+	{
+		if (!allocateInstr(i))
+			return false;
+
+		instrTyp *ins = instr[i];
+
+		memcpy(ins->name, ih.name, 22);
+		ins->name[22] = '\0';
+
+		// 8bb: copy instrument header elements to our instrument struct
+		memcpy(ins->ta, ih.ta, 96);
+		memcpy(ins->envVP, ih.envVP, 12*2*sizeof(int16_t));
+		memcpy(ins->envPP, ih.envPP, 12*2*sizeof(int16_t));
+		ins->envVPAnt = ih.envVPAnt;
+		ins->envPPAnt = ih.envPPAnt;
+		ins->envVSust = ih.envVSust;
+		ins->envVRepS = ih.envVRepS;
+		ins->envVRepE = ih.envVRepE;
+		ins->envPSust = ih.envPSust;
+		ins->envPRepS = ih.envPRepS;
+		ins->envPRepE = ih.envPRepE;
+		ins->envVTyp = ih.envVTyp;
+		ins->envPTyp = ih.envPTyp;
+		ins->vibTyp = ih.vibTyp;
+		ins->vibSweep = ih.vibSweep;
+		ins->vibDepth = ih.vibDepth;
+		ins->vibRate = ih.vibRate;
+		ins->fadeOut = ih.fadeOut;
+		ins->mute = (ih.mute == 1) ? true : false; // 8bb: correct logic!
+		ins->antSamp = ih.antSamp;
+
+		if (mread(ih.samp, ih.antSamp * sizeof (sampleHeaderTyp), 1, f) != 1)
+			return false;
+
+		sampleTyp *s = instr[i]->samp;
+		sampleHeaderTyp *src = ih.samp;
+		for (int32_t j = 0; j < ih.antSamp; j++, s++, src++)
+		{
+			memcpy(s->name, src->name, 22);
+			s->name[22] = '\0';
+
+			s->len = src->len;
+			s->repS = src->repS;
+			s->repL = src->repL;
+			s->vol = src->vol;
+			s->fine = src->fine;
+			s->typ = src->typ;
+			s->pan = src->pan;
+			s->relTon = src->relTon;
+		}
+	}
+
+	return true;
+}
+
+static bool loadInstrSample(MEMFILE *f, uint16_t i)
+{
+	if (instr[i] == NULL)
+		return true; // empty instrument
+
+	sampleTyp *s = instr[i]->samp;
+	for (uint16_t j = 0; j < instr[i]->antSamp; j++, s++)
+	{
+		if (s->len > 0)
+		{
+			s->pek = (int8_t *)malloc(s->len+2); // 8bb: +2 for linear interpolation point fix
+			if (s->pek == NULL)
+				return false;
+
+			mread(s->pek, 1, s->len, f);
+			delta2Samp(s->pek, s->len, (s->typ >> 4) & 1);
+		}
+
+		checkSampleRepeat(i, j);
+	}
+
+	return true;
+}
+
+static bool loadPatterns(MEMFILE *f, uint16_t antPtn)
+{
+	uint8_t tmpLen;
+	patternHeaderTyp ph;
+
+	for (uint16_t i = 0; i < antPtn; i++)
+	{
+		mread(&ph.patternHeaderSize, 4, 1, f);
+		mread(&ph.typ, 1, 1, f);
+
+		ph.pattLen = 0;
+		if (song.ver == 0x0102)
+		{
+			mread(&tmpLen, 1, 1, f);
+			mread(&ph.dataLen, 2, 1, f);
+			ph.pattLen = (uint16_t)tmpLen + 1; // 8bb: +1 in v1.02
+
+			if (ph.patternHeaderSize > 8)
+				mseek(f, ph.patternHeaderSize - 8, SEEK_CUR);
+		}
+		else
+		{
+			mread(&ph.pattLen, 2, 1, f);
+			mread(&ph.dataLen, 2, 1, f);
+
+			if (ph.patternHeaderSize > 9)
+				mseek(f, ph.patternHeaderSize - 9, SEEK_CUR);
+		}
+
+		if (meof(f))
+		{
+			mclose(&f);
+			return false;
+		}
+
+		pattLens[i] = ph.pattLen;
+		if (ph.dataLen)
+		{
+			const uint16_t a = ph.pattLen * song.antChn * sizeof (tonTyp);
+
+			patt[i] = (tonTyp *)malloc(a);
+			if (patt[i] == NULL)
+				return false;
+
+			uint8_t *pattPtr = (uint8_t *)patt[i];
+
+			memset(pattPtr, 0, a);
+			mread(&pattPtr[a - ph.dataLen], 1, ph.dataLen, f);
+			unpackPatt(pattPtr, a - ph.dataLen, ph.pattLen, song.antChn);
+		}
+
+		if (patternEmpty(i))
+		{
+			if (patt[i] != NULL)
+			{
+				free(patt[i]);
+				patt[i] = NULL;
+			}
+
+			pattLens[i] = 64;
+		}
+	}
+
+	return true;
+}
+
+static bool loadMusicMOD(MEMFILE *f)
+{
+	uint8_t ha[sizeof (songMOD31HeaderTyp)];
+	songMOD31HeaderTyp *h_MOD31 = (songMOD31HeaderTyp *)ha;
+	songMOD15HeaderTyp *h_MOD15 = (songMOD15HeaderTyp *)ha;
+
+	mread(ha, sizeof (ha), 1, f);
+	if (meof(f)) goto loadError2;
+
+	memcpy(song.name, h_MOD31->name, 20);
+	song.name[20] = '\0';
+
+	uint8_t j = 0;
+	for (uint8_t i = 1; i <= 16; i++)
+	{
+		if (memcmp(h_MOD31->Sig, MODSig[i-1], 4) == 0)
+			j = i + i;
+	}
+
+	if (memcmp(h_MOD31->Sig, "M!K!", 4) == 0 || memcmp(h_MOD31->Sig, "FLT4", 4) == 0)
+		j = 4;
+
+	if (memcmp(h_MOD31->Sig, "OCTA", 4) == 0)
+		j = 8;
+
+	uint8_t typ;
+	if (j > 0)
+	{
+		typ = 1;
+		song.antChn = j;
+	}
+	else
+	{
+		typ = 2;
+		song.antChn = 4;
+	}
+
+	int16_t ai;
+	if (typ == 1)
+	{
+		mseek(f, sizeof (songMOD31HeaderTyp), SEEK_SET);
+		song.len = h_MOD31->len;
+		song.repS = h_MOD31->repS;
+		memcpy(song.songTab, h_MOD31->songTab, 128);
+		ai = 31;
+	}
+	else
+	{
+		mseek(f, sizeof (songMOD15HeaderTyp), SEEK_SET);
+		song.len = h_MOD15->len;
+		song.repS = h_MOD15->repS;
+		memcpy(song.songTab, h_MOD15->songTab, 128);
+		ai = 15;
+	}
+
+	song.antInstrs = ai; // 8bb: added this
+
+	if (meof(f)) goto loadError2;
+
+	int32_t b = 0;
+	for (int32_t a = 0; a < 128; a++)
+	{
+		if (song.songTab[a] > b)
+			b = song.songTab[a];
+	}
+
+	uint8_t pattBuf[32 * 4 * 64]; // 8bb: max pattern size (32 channels, 64 rows)
+	for (uint16_t a = 0; a <= b; a++)
+	{
+		patt[a] = (tonTyp *)calloc(song.antChn * 64, sizeof (tonTyp));
+		if (patt[a] == NULL)
+			goto loadError;
+
+		pattLens[a] = 64;
+		mread(pattBuf, 1, song.antChn * 4 * 64, f);
+		if (meof(f)) goto loadError;
+
+		// convert pattern
+		uint8_t *bytes = pattBuf;
+		tonTyp *ton = patt[a];
+		for (int32_t i = 0; i < 64 * song.antChn; i++, bytes += 4, ton++)
+		{
+			const uint16_t period = ((bytes[0] & 0x0F) << 8) | bytes[1];
+			for (uint8_t k = 0; k < 96; k++)
+			{
+				if (period >= amigaPeriod[k])
+				{
+					ton->ton = k+1;
+					break;
+				}
+			}
+
+			ton->instr = (bytes[0] & 0xF0) | (bytes[2] >> 4);
+			ton->effTyp = bytes[2] & 0x0F;
+			ton->eff = bytes[3];
+
+			switch (ton->effTyp)
+			{
+				case 0xC:
+				{
+					if (ton->eff > 64)
+						ton->eff = 64;
+				}
+				break;
+
+				case 0x1:
+				case 0x2:
+				{
+					if (ton->eff == 0)
+						ton->effTyp = 0;
+				}
+				break;
+
+				case 0x5:
+				{
+					if (ton->eff == 0)
+						ton->effTyp = 3;
+				}
+				break;
+
+				case 0x6:
+				{
+					if (ton->eff == 0)
+						ton->effTyp = 4;
+				}
+				break;
+
+				case 0xA:
+				{
+					if (ton->eff == 0)
+						ton->effTyp = 0;
+				}
+				break;
+
+				case 0xE:
+				{
+					const uint8_t effTyp = ton->effTyp >> 4;
+					const uint8_t eff = ton->effTyp & 15;
+
+					if (eff == 0 && (effTyp == 0x1 || effTyp == 0x2 || effTyp == 0xA || effTyp == 0xB))
+					{
+						ton->eff = 0;
+						ton->effTyp = 0;
+					}
+				}
+				break;
+				
+				default: break;
+			}
+		}
+
+		if (patternEmpty(a))
+		{
+			free(patt[a]);
+			patt[a] = NULL;
+			pattLens[a] = 64;
+		}
+	}
+
+	for (uint16_t a = 1; a <= ai; a++)
+	{
+		modSampleTyp *modSmp = &h_MOD31->sample[a-1];
+
+		uint32_t len = 2 * SWAP16(modSmp->len);
+		if (len == 0)
+			continue;
+
+		if (!allocateInstr(a))
+			goto loadError;
+
+		sampleTyp *xmSmp = &instr[a]->samp[0];
+
+		memcpy(xmSmp->name, modSmp->name, 22);
+		xmSmp->name[22] = '\0';
+
+		uint32_t repS = 2 * SWAP16(modSmp->repS);
+		uint32_t repL = 2 * SWAP16(modSmp->repL);
+
+		if (repL <= 2)
+		{
+			repS = 0;
+			repL = 0;
+		}
+
+		if (repS+repL > len)
+		{
+			if (repS >= len)
+			{
+				repS = 0;
+				repL = 0;
+			}
+			else
+			{
+				repL = len-repS;
+			}
+		}
+
+		xmSmp->typ = (repL > 2) ? 1 : 0;
+		xmSmp->len = len;
+		xmSmp->vol = (modSmp->vol <= 64) ? modSmp->vol : 64;
+		xmSmp->fine = 8 * ((2 * ((modSmp->fine & 15) ^ 8)) - 16);
+		xmSmp->repL = repL;
+		xmSmp->repS = repS;
+
+		xmSmp->pek = (int8_t *)malloc(len + 2);
+		if (xmSmp->pek == NULL)
+			goto loadError;
+
+		mread(xmSmp->pek, 1, len, f);
+	}
+
+	mclose(&f);
+
+	if (song.repS > song.len)
+		song.repS = 0;
+
+	resetMusic();
+	upDateInstrs();
+
+	moduleLoaded = true;
+	return true;
+loadError:
+	freeAllInstr();
+	freeAllPatterns();
+loadError2:
+	mclose(&f);
+	return false;
+}
+
+bool loadMusicFromData(const uint8_t *data, uint32_t dataLength) // .XM/.MOD/.FT
+{
+	uint16_t i;
+	songHeaderTyp h;
+	MEMFILE *f;
+
+	freeMusic();
+	setFrqTab(false);
+
+	// 8bb: instr 0 is a placeholder for empty instruments
+	allocateInstr(0);
+	instr[0]->samp[0].vol = 0;
+
+	moduleLoaded = false;
+
+	f = mopen(data, dataLength);
+	if (f == NULL) return false;
+
+	mread(&h, sizeof (h), 1, f);
+	if (meof(f)) goto loadError2;
+
+	if (memcmp(h.sig, "Extended Module: ", 17) != 0)
+	{
+		mrewind(f);
+		return loadMusicMOD(f);
+	}
+
+	if (h.ver < 0x0102 || h.ver > 0x104 || h.antChn < 2 || h.antChn > 32 || (h.antChn & 1) != 0 ||
+		h.antPtn > 256 || h.antInstrs > 128)
+	{
+		goto loadError2;
+	}
+
+	mseek(f, 60+h.headerSize, SEEK_SET);
+	if (meof(f)) goto loadError2;
+
+	memcpy(song.name, h.name, 20);
+	song.name[20] = '\0';
+
+	song.len = h.len;
+	song.repS = h.repS;
+	song.antChn = (uint8_t)h.antChn;
+	setFrqTab(h.flags & 1);
+	memcpy(song.songTab, h.songTab, 256);
+
+	song.antInstrs = h.antInstrs; // 8bb: added this
+	if (h.defSpeed == 0) h.defSpeed = 125; // 8bb: (BPM) FT2 doesn't do this, but we do it for safety
+	song.speed = h.defSpeed;
+	song.tempo = h.defTempo;
+	song.ver = h.ver;
+
+	// 8bb: bugfixes...
+	if (song.speed < 1) song.speed = 1;
+	if (song.tempo < 1) song.tempo = 1;
+	// ----------------
+
+	if (song.ver < 0x0104) // old FT2 XM format
+	{
+		for (i = 1; i <= h.antInstrs; i++)
+		{
+			if (!loadInstrHeader(f, i))
+				goto loadError;
+		}
+
+		if (!loadPatterns(f, h.antPtn))
+			goto loadError;
+
+		for (i = 1; i <= h.antInstrs; i++)
+		{
+			if (!loadInstrSample(f, i))
+				goto loadError;
+		}
+	}
+	else // latest FT2 XM format
+	{
+		if (!loadPatterns(f, h.antPtn))
+			goto loadError;
+
+		for (i = 1; i <= h.antInstrs; i++)
+		{
+			if (!loadInstrHeader(f, i)) goto loadError;
+			if (!loadInstrSample(f, i)) goto loadError;
+		}
+	}
+
+	mclose(&f);
+
+	if (song.repS > song.len)
+		song.repS = 0;
+
+	resetMusic();
+	upDateInstrs();
+
+	moduleLoaded = true;
+	return true;
+
+loadError:
+	freeAllInstr();
+	freeAllPatterns();
+loadError2:
+	mclose(&f);
+	return false;
+}
+
+bool loadMusic(const char *fileName) // .XM/.MOD/.FT
+{
+	FILE *f = fopen(fileName, "rb");
+	if (f == NULL)
+		return false;
+
+	fseek(f, 0, SEEK_END);
+	const uint32_t fileSize = (uint32_t)ftell(f);
+	rewind(f);
+
+	uint8_t *fileBuffer = (uint8_t *)malloc(fileSize);
+	if (fileBuffer == NULL)
+	{
+		fclose(f);
+		return false;
+	}
+
+	if (fread(fileBuffer, 1, fileSize, f) != fileSize)
+	{
+		free(fileBuffer);
+		fclose(f);
+		return false;
+	}
+
+	fclose(f);
+
+	if (!loadMusicFromData((const uint8_t *)fileBuffer, fileSize))
+	{
+		free(fileBuffer);
+		return false;
+	}
+
+	free(fileBuffer);
+	return true;
+}
+
+/***************************************************************************
+ *        PROCESS HANDLING                                                 *
+ ***************************************************************************/
+
+bool startMusic(void)
+{
+	if (!moduleLoaded || song.speed == 0)
+		return false;
+
+	mix_ClearChannels();
+	stopVoices();
+	song.globVol = 64;
+
+	speedVal = ((realReplayRate * 5) / 2) / song.speed;
+	quickVolSizeVal = realReplayRate / 200;
+
+	if (!mix_Init(soundBufferSize))
+		return false;
+
+	if (openMixer(realReplayRate, soundBufferSize))
+	{
+		musicPaused = false;
+		return true;
+	}
+
+	return false;
+}
+
+void stopMusic(void)
+{
+	pauseMusic();
+
+	closeMixer();
+	mix_Free();
+	song.globVol = 64;
+
+	resumeMusic();
+}
+
+void startPlaying(void)
+{
+	stopMusic();
+	song.pattDelTime = song.pattDelTime2 = 0; // 8bb: added these
+	setPos(0, 0);
+	startMusic();
+}
+
+void stopPlaying(void)
+{
+	stopMusic();
+	stopVoices();
+}
+
+void pauseMusic(void)
+{
+	musicPaused = true;
+}
+
+void resumeMusic(void)
+{
+	musicPaused = false;
+}
+
+// 8bb: added these three, handy
+void toggleMusic(void)
+{
+	musicPaused ^= 1;
+}
+
+void setInterpolation(bool on)
+{
+	interpolationFlag = on;
+	mix_ClearChannels();
+}
+
+void setVolumeRamping(bool on)
+{
+	volumeRampingFlag = on;
+	mix_ClearChannels();
+}
+
+/***************************************************************************
+ *        CONFIGURATION ROUTINES                                           *
+ ***************************************************************************/
+
+void setMasterVol(int32_t v) // 0..256
+{
+	masterVol = CLAMP(v, 0, 256);
+
+	stmTyp *ch = stm;
+	for (int32_t i = 0; i < 32; i++, ch++)
+		ch->status |= IS_Vol;
+}
+
+void setAmp(int32_t level) // 1..32
+{
+	boostLevel = (int16_t)CLAMP(level, 1, 32);
+	CDA_Amp = boostLevel * 8;
+}
+
+int32_t getMasterVol(void) // 8bb: added this
+{
+	return masterVol;
+}
+
+int32_t getAmp(void) // 8bb: added this
+{
+	return boostLevel;
+}
+
+uint8_t getNumActiveVoices(void) // 8bb: added this
+{
+	uint8_t activeVoices = 0;
+	for (int32_t i = 0; i < song.antChn; i++)
+	{
+		CIType *v = getVoice(i);
+		if (!(v->SType & SType_Off) && v->SVol > 0)
+			activeVoices++;
+	}
+
+	return activeVoices;
+}
+
+static void setFrqTab(bool linear)
+{
+	linearFrqTab = linear;
+	note2Period = linear ? linearPeriods : amigaPeriods;
+}
+
+void updateReplayRate(void)
+{
+	lockMixer();
+
+	// 8bb: bit-exact to FT2
+	frequenceDivFactor = (int32_t)round(65536.0*1712.0/realReplayRate*8363.0);
+	frequenceMulFactor = (int32_t)round(256.0*65536.0/realReplayRate*8363.0);
+
+	unlockMixer();
+}
+
+/***************************************************************************
+ *        INITIALIZATION ROUTINES                                          *
+ ***************************************************************************/
+
+bool initMusic(int32_t audioFrequency, int32_t audioBufferSize, bool interpolation, bool volumeRamping)
+{
+	closeMixer();
+	freeMusic();
+	memset(stm, 0, sizeof (stm));
+
+	realReplayRate = CLAMP(audioFrequency, 8000, 96000);
+	updateReplayRate();
+
+	soundBufferSize = audioBufferSize;
+	interpolationFlag = interpolation;
+	volumeRampingFlag = volumeRamping;
+
+	song.tempo = 6;
+	song.speed = 125;
+	setFrqTab(true);
+	resetMusic();
+
+	return true;
+}
+
+/***************************************************************************
+ *        WAV DUMPING ROUTINES                                             *
+ ***************************************************************************/
+
+static void WAV_WriteHeader(FILE *f, int32_t frq)
+{
+	uint16_t w;
+	uint32_t l;
+
+	// 12 bytes
+
+	const uint32_t RIFF = 0x46464952;
+	fwrite(&RIFF, 4, 1, f);
+	fseek(f, 4, SEEK_CUR);
+	const uint32_t WAVE = 0x45564157;
+	fwrite(&WAVE, 4, 1, f);
+
+	// 24 bytes
+
+	const uint32_t fmt = 0x20746D66;
+	fwrite(&fmt, 4, 1, f);
+	l = 16; fwrite(&l, 4, 1, f);
+	w = 1; fwrite(&w, 2, 1, f);
+	w = 2; fwrite(&w, 2, 1, f);
+	l = frq; fwrite(&l, 4, 1, f);
+	l = frq*2*2; fwrite(&l, 4, 1, f);
+	w = 2*2; fwrite(&w, 2, 1, f);
+	w = 8*2; fwrite(&w, 2, 1, f);
+
+	// 8 bytes
+
+	const uint32_t DATA = 0x61746164;
+	fwrite(&DATA, 4, 1, f);
+	fseek(f, 4, SEEK_CUR);
+}
+
+static void WAV_WriteEnd(FILE *f, uint32_t size)
+{
+	fseek(f, 4, SEEK_SET);
+	uint32_t l = size+4+24+8;
+	fwrite(&l, 4, 1, f);
+	fseek(f, 12+24+4, SEEK_SET);
+	fwrite(&size, 4, 1, f);
+}
+
+void WAVDump_Abort(void) // 8bb: added this
+{
+	WAVDump_Flag = false;
+}
+
+bool WAVDump_Record(const char *filenameOut)
+{
+	FILE *fil = fopen(filenameOut, "wb");
+	if (fil == NULL)
+	{
+		WAVDump_Flag = false;
+		return false;
+	}
+
+	const int32_t WDFrequency = realReplayRate;
+	const int32_t WDAmp = boostLevel;
+
+	const uint32_t maxSamplesPerTick = (WDFrequency*5 / 2) / 1; // 8bb: added this (min. BPM = 1, through hex editing)
+	int16_t *pBlock = (int16_t *)malloc(maxSamplesPerTick * (2 * sizeof (int16_t)));
+	if (pBlock == NULL)
+	{
+		fclose(fil);
+		WAVDump_Flag = false;
+		return false;
+	}
+
+	WAV_WriteHeader(fil, WDFrequency);
+
+	stopMusic();
+	mix_Init(maxSamplesPerTick);
+
+	uint16_t WDStartPos = 0;
+	uint16_t WDStopPos = song.len-1;
+
+	dump_Init(WDFrequency, WDAmp, WDStartPos);
+
+	uint32_t totSize = 0;
+
+	WAVDump_Flag = true;
+	while (!dump_EndOfTune(WDStopPos))
+	{
+		if (!WAVDump_Flag) // extra check so that external threads can force-abort render
+			break;
+
+		const uint32_t size = dump_GetFrame(pBlock);
+		fwrite(pBlock, 1, size, fil);
+		totSize += size;
+	}
+	WAVDump_Flag = false;
+
+	mix_Free();
+
+	WAV_WriteEnd(fil, totSize);
+	dump_Close();
+
+	stopMusic();
+	fclose(fil);
+
+	free(pBlock);
+
+	WAVDump_Flag = false;
+	return true;
+}
+
+/***************************************************************************
+ *        MEMORY READ ROUTINES (8bb: added these)                          *
+ ***************************************************************************/
+
+static MEMFILE *mopen(const uint8_t *src, uint32_t length)
+{
+	if (src == NULL || length == 0)
+		return NULL;
+
+	MEMFILE *b = (MEMFILE *)malloc(sizeof (MEMFILE));
+	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(MEMFILE **buf)
+{
+	if (*buf != NULL)
+	{
+		free(*buf);
+		*buf = NULL;
+	}
+}
+
+static size_t mread(void *buffer, size_t size, size_t count, MEMFILE *buf)
+{
+	if (buf == NULL || buf->_ptr == NULL)
+		return 0;
+
+	size_t wrcnt = size * count;
+	if (size == 0 || buf->_eof)
+		return 0;
+
+	int32_t pcnt = (buf->_cnt > wrcnt) ? (int32_t)wrcnt : (int32_t)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 bool meof(MEMFILE *buf)
+{
+	if (buf == NULL)
+		return true;
+
+	return buf->_eof;
+}
+
+static void mseek(MEMFILE *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;
+	}
+}
+
+static void mrewind(MEMFILE *buf)
+{
+	mseek(buf, 0, SEEK_SET);
+}
--- /dev/null
+++ b/pmplay.h
@@ -1,0 +1,130 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+// AUDIO DRIVERS
+#if defined AUDIODRIVER_SDL
+#include "audiodrivers/sdl/sdldriver.h"
+#elif defined AUDIODRIVER_WINMM
+#include "audiodrivers/winmm/winmm.h"
+#else
+// Read "audiodrivers/how_to_write_drivers.txt"
+#endif
+
+enum
+{
+	IS_Vol = 1,
+	IS_Period = 2,
+	IS_NyTon = 4,
+	IS_Pan = 8,
+	IS_QuickVol = 16
+};
+
+typedef struct songTyp_t
+{
+	char name[20+1];
+	uint8_t antChn, pattDelTime, pattDelTime2, pBreakPos, songTab[256];
+	bool pBreakFlag, posJumpFlag;
+	int16_t songPos, pattNr, pattPos, pattLen;
+	uint16_t len, repS, speed, tempo, globVol, timer, ver;
+
+	uint16_t antInstrs; // 8bb: added this
+} songTyp;
+
+typedef struct sampleTyp_t
+{
+	char name[22+1];
+	int32_t len, repS, repL;
+	uint8_t vol;
+	int8_t fine;
+	uint8_t typ, pan;
+	int8_t relTon;
+	int8_t *pek;
+} sampleTyp;
+
+typedef struct instrTyp_t
+{
+	char name[22+1];
+	uint8_t ta[96];
+	int16_t envVP[12][2], envPP[12][2];
+	uint8_t envVPAnt, envPPAnt;
+	uint8_t envVSust, envVRepS, envVRepE;
+	uint8_t envPSust, envPRepS, envPRepE;
+	uint8_t envVTyp, envPTyp;
+	uint8_t vibTyp, vibSweep, vibDepth, vibRate;
+	uint16_t fadeOut;
+	uint8_t mute;
+	int16_t antSamp;
+	sampleTyp samp[16];
+} instrTyp;
+
+typedef struct stmTyp_t
+{
+	volatile uint8_t status;
+	int8_t relTonNr, fineTune;
+	uint8_t sampleNr, instrNr, effTyp, eff, smpOffset, tremorSave, tremorPos;
+	uint8_t globVolSlideSpeed, panningSlideSpeed, mute, waveCtrl, portaDir;
+	uint8_t glissFunk, vibPos, tremPos, vibSpeed, vibDepth, tremSpeed, tremDepth;
+	uint8_t pattPos, loopCnt, volSlideSpeed, fVolSlideUpSpeed, fVolSlideDownSpeed;
+	uint8_t fPortaUpSpeed, fPortaDownSpeed, ePortaUpSpeed, ePortaDownSpeed;
+	uint8_t portaUpSpeed, portaDownSpeed, retrigSpeed, retrigCnt, retrigVol;
+	uint8_t volKolVol, tonNr, envPPos, eVibPos, envVPos, realVol, oldVol, outVol;
+	uint8_t oldPan, outPan, finalPan;
+	bool envSustainActive;
+	int16_t envVIPValue, envPIPValue;
+	uint16_t outPeriod, realPeriod, finalPeriod, finalVol, tonTyp, wantPeriod, portaSpeed;
+	uint16_t envVCnt, envVAmp, envPCnt, envPAmp, eVibAmp, eVibSweep;
+	uint16_t fadeOutAmp, fadeOutSpeed;
+	int32_t smpStartPos;
+	instrTyp *instrSeg;
+} stmTyp;
+
+typedef struct tonTyp_t
+{
+	uint8_t ton, instr, vol, effTyp, eff;
+} tonTyp;
+
+// globalized
+extern volatile bool interpolationFlag, volumeRampingFlag, moduleLoaded, musicPaused, WAVDump_Flag;
+extern bool linearFrqTab;
+extern volatile const uint16_t *note2Period;
+extern uint16_t pattLens[256];
+extern int16_t PMPTmpActiveChannel, boostLevel;
+extern int32_t masterVol, PMPLeft;
+extern int32_t realReplayRate, quickVolSizeVal, speedVal;
+extern int32_t frequenceDivFactor, frequenceMulFactor;
+extern uint32_t CDA_Amp;
+extern tonTyp *patt[256];
+extern instrTyp *instr[1+128];
+extern songTyp song;
+extern stmTyp stm[32];
+
+#define CLAMP(x, low, high) (((x) > (high)) ? (high) : (((x) < (low)) ? (low) : (x)))
+
+bool initMusic(int32_t audioFrequency, int32_t audioBufferSize, bool interpolation, bool volumeRamping);
+bool loadMusicFromData(const uint8_t *data, uint32_t dataLength); // .XM/.MOD/.FT
+bool loadMusic(const char *filename); // .XM/.MOD/.FT
+void freeMusic(void);
+bool startMusic(void);
+void stopMusic();
+void pauseMusic(void);
+void resumeMusic(void);
+void setMasterVol(int32_t v); // 0..256
+void setAmp(int32_t level); // 1..32
+void setPos(int32_t pos, int32_t row); // input of -1 = don't change
+void stopVoices(void);
+void updateReplayRate(void);
+void startPlaying(void);
+void stopPlaying(void);
+
+bool WAVDump_Record(const char *filenameOut);
+
+// 8bb: added these three, handy
+void WAVDump_Abort(void);
+int32_t getMasterVol(void);
+int32_t getAmp(void);
+uint8_t getNumActiveVoices(void);
+void toggleMusic(void);
+void setInterpolation(bool on);
+void setVolumeRamping(bool on);
--- /dev/null
+++ b/snd_masm.c
@@ -1,0 +1,676 @@
+#include <stdint.h>
+#include <stdbool.h>
+#include "snd_masm.h"
+#include "pmplay.h"
+
+/* 8bb: This is done in a slightly different way, but the result
+** is the same (bit-accurate to FT2.08/FT2.09 w/ SB16, and WAV-writer).
+**
+** Mixer macros are stored in snd_masm.h
+*/
+
+void PMPMix32Proc(CIType *v, int32_t numSamples, int32_t bufferPos) // 8bb: numSamples = 1..65535
+{
+	if (numSamples > 65535)
+		return;
+
+	if (v->SType & SType_Off)
+		return; // voice is not active
+
+	uint32_t volStatus = v->SLVol1 | v->SRVol1;
+	if (volumeRampingFlag)
+		volStatus |= v->SLVol2 | v->SRVol2;
+
+	if (volStatus == 0) // mix silence
+	{
+		const uint32_t addPos = (v->SFrq >> 16) * (uint32_t)numSamples;
+		uint32_t addFrac = (v->SFrq & 0xFFFF) * (uint32_t)numSamples;
+
+		addFrac += v->SPosDec >> 16;
+		int32_t realPos = v->SPos + addPos + (addFrac >> 16);
+		if (realPos >= v->SLen)
+		{
+			uint8_t SType = v->SType;
+			if (SType & (SType_Fwd+SType_Rev))
+			{
+				do
+				{
+					SType ^= SType_RevDir;
+					realPos -= v->SRepL;
+				}
+				while (realPos >= v->SLen);
+				v->SType = SType;
+			}
+			else
+			{
+				v->SType = SType_Off;
+				return;
+			}
+		}
+
+		v->SPosDec = (addFrac & 0xFFFF) << 16;
+		v->SPos = realPos;
+	}
+	else // normal mixing
+	{
+		bool mixInCenter;
+		if (volumeRampingFlag)
+			mixInCenter = (v->SLVol2 == v->SRVol2) && (v->SLVolIP == v->SRVolIP);
+		else
+			mixInCenter = v->SLVol1 == v->SRVol1;
+
+		mixRoutineTable[(mixInCenter * 8) + v->SMixType](v, numSamples, bufferPos);
+	}
+}
+
+static void mix8b(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_VOL
+	GET_MIXER_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT
+			MIX_8BIT
+			MIX_8BIT
+			MIX_8BIT
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bIntrp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_VOL
+	GET_MIXER_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_INTRP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_INTRP
+			MIX_8BIT_INTRP
+			MIX_8BIT_INTRP
+			MIX_8BIT_INTRP
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bRamp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT
+			VOL_RAMP
+			MIX_8BIT
+			VOL_RAMP
+			MIX_8BIT
+			VOL_RAMP
+			MIX_8BIT
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bRampIntrp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_INTRP
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_INTRP
+			VOL_RAMP
+			MIX_8BIT_INTRP
+			VOL_RAMP
+			MIX_8BIT_INTRP
+			VOL_RAMP
+			MIX_8BIT_INTRP
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16b(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_VOL
+	GET_MIXER_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT
+			MIX_16BIT
+			MIX_16BIT
+			MIX_16BIT
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bIntrp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_VOL
+	GET_MIXER_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_INTRP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_INTRP
+			MIX_16BIT_INTRP
+			MIX_16BIT_INTRP
+			MIX_16BIT_INTRP
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bRamp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT
+			VOL_RAMP
+			MIX_16BIT
+			VOL_RAMP
+			MIX_16BIT
+			VOL_RAMP
+			MIX_16BIT
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bRampIntrp(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_INTRP
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_INTRP
+			VOL_RAMP
+			MIX_16BIT_INTRP
+			VOL_RAMP
+			MIX_16BIT_INTRP
+			VOL_RAMP
+			MIX_16BIT_INTRP
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_VOL_CENTER
+	GET_MIXER_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_M
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_M
+			MIX_8BIT_M
+			MIX_8BIT_M
+			MIX_8BIT_M
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bIntrpCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_VOL_CENTER
+	GET_MIXER_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_INTRP_M
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_INTRP_M
+			MIX_8BIT_INTRP_M
+			MIX_8BIT_INTRP_M
+			MIX_8BIT_INTRP_M
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bRampCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_M
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_M
+			VOL_RAMP
+			MIX_8BIT_M
+			VOL_RAMP
+			MIX_8BIT_M
+			VOL_RAMP
+			MIX_8BIT_M
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix8bRampIntrpCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE8
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_8BIT_INTRP_M
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_8BIT_INTRP_M
+			VOL_RAMP
+			MIX_8BIT_INTRP_M
+			VOL_RAMP
+			MIX_8BIT_INTRP_M
+			VOL_RAMP
+			MIX_8BIT_INTRP_M
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+GET_VOL_CENTER
+	GET_MIXER_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_M
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_M
+			MIX_16BIT_M
+			MIX_16BIT_M
+			MIX_16BIT_M
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bIntrpCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_VOL_CENTER
+	GET_MIXER_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		CDA_BytesLeft -= samplesToMix;
+
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_INTRP_M
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_INTRP_M
+			MIX_16BIT_INTRP_M
+			MIX_16BIT_INTRP_M
+			MIX_16BIT_INTRP_M
+		}
+		HANDLE_POS_END
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bRampCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_M
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_M
+			VOL_RAMP
+			MIX_16BIT_M
+			VOL_RAMP
+			MIX_16BIT_M
+			VOL_RAMP
+			MIX_16BIT_M
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+static void mix16bRampIntrpCenter(CIType *v, uint32_t numSamples, uint32_t bufferPos)
+{
+	int32_t sample, sample2;
+
+	GET_MIXER_VARS
+	GET_RAMP_VARS
+	SET_BASE16
+
+	int32_t CDA_BytesLeft = numSamples;
+	while (CDA_BytesLeft > 0)
+	{
+		LIMIT_MIX_NUM
+		LIMIT_MIX_NUM_RAMP
+		CDA_BytesLeft -= samplesToMix;
+
+		GET_VOL_RAMP
+		HANDLE_POS_START
+		for (i = 0; i < (samplesToMix & 3); i++)
+		{
+			MIX_16BIT_INTRP_M
+			VOL_RAMP
+		}
+		samplesToMix >>= 2;
+		for (i = 0; i < samplesToMix; i++)
+		{
+			MIX_16BIT_INTRP_M
+			VOL_RAMP
+			MIX_16BIT_INTRP_M
+			VOL_RAMP
+			MIX_16BIT_INTRP_M
+			VOL_RAMP
+			MIX_16BIT_INTRP_M
+			VOL_RAMP
+		}
+		HANDLE_POS_END
+		SET_VOL_BACK
+	}
+
+	SET_BACK_MIXER_POS
+}
+
+mixRoutine mixRoutineTable[16] =
+{
+	(mixRoutine)mix8b,
+	(mixRoutine)mix8bIntrp,
+	(mixRoutine)mix8bRamp,
+	(mixRoutine)mix8bRampIntrp,
+	(mixRoutine)mix16b,
+	(mixRoutine)mix16bIntrp,
+	(mixRoutine)mix16bRamp,
+	(mixRoutine)mix16bRampIntrp,
+	(mixRoutine)mix8bCenter,
+	(mixRoutine)mix8bIntrpCenter,
+	(mixRoutine)mix8bRampCenter,
+	(mixRoutine)mix8bRampIntrpCenter,
+	(mixRoutine)mix16bCenter,
+	(mixRoutine)mix16bIntrpCenter,
+	(mixRoutine)mix16bRampCenter,
+	(mixRoutine)mix16bRampIntrpCenter
+};
--- /dev/null
+++ b/snd_masm.h
@@ -1,0 +1,227 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pmp_mix.h"
+
+#define GET_VOL \
+	const int32_t CDA_LVol = v->SLVol1; \
+	const int32_t CDA_RVol = v->SRVol1; \
+
+#define GET_VOL_CENTER \
+	const int32_t CDA_LVol = v->SLVol1; \
+
+#define GET_VOL_RAMP \
+	int32_t CDA_LVol = v->SLVol2; \
+	int32_t CDA_RVol = v->SRVol2; \
+
+#define SET_VOL_BACK \
+	v->SLVol2 = CDA_LVol; \
+	v->SRVol2 = CDA_RVol; \
+
+#define GET_MIXER_VARS \
+	int32_t *audioMix = CDA_MixBuffer + (bufferPos << 1); \
+	int32_t realPos = v->SPos; \
+	uint32_t pos = v->SPosDec; \
+	uint16_t CDA_MixBuffPos = (32768+96)-8; /* address of FT2 mix buffer minus mix sample size (used for quirky LERP) */ \
+
+#define GET_RAMP_VARS \
+	int32_t CDA_LVolIP = v->SLVolIP; \
+	int32_t CDA_RVolIP = v->SRVolIP; \
+
+#define SET_BASE8 \
+	const int8_t *CDA_LinearAdr = (int8_t *)v->SBase; \
+	const int8_t *CDA_LinAdrRev = (int8_t *)v->SRevBase; \
+	const int8_t *smpPtr = CDA_LinearAdr + realPos; \
+
+#define SET_BASE16 \
+	const int16_t *CDA_LinearAdr = (int16_t *)v->SBase; \
+	const int16_t *CDA_LinAdrRev = (int16_t *)v->SRevBase; \
+	const int16_t *smpPtr = CDA_LinearAdr + realPos; \
+
+#define INC_POS \
+	smpPtr += CDA_IPValH; \
+	smpPtr += (CDA_IPValL > (uint32_t)~pos); /* if pos would 32-bit overflow after CDA_IPValL add, add one to smpPtr (branchless) */ \
+	pos += CDA_IPValL; \
+
+#define SET_BACK_MIXER_POS \
+	v->SPosDec = pos & 0xFFFF0000; \
+	v->SPos = realPos; \
+
+#define VOL_RAMP \
+	CDA_LVol += CDA_LVolIP; \
+	CDA_RVol += CDA_RVolIP; \
+
+// stereo mixing without interpolation
+
+#define MIX_8BIT \
+	sample = (*smpPtr) << (28-8); \
+	*audioMix++ += ((int64_t)sample * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += ((int64_t)sample * (int32_t)CDA_RVol) >> 32; \
+	INC_POS \
+
+#define MIX_16BIT \
+	sample = (*smpPtr) << (28-16); \
+	*audioMix++ += ((int64_t)sample * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += ((int64_t)sample * (int32_t)CDA_RVol) >> 32; \
+	INC_POS \
+
+// center mixing without interpolation
+
+#define MIX_8BIT_M \
+	sample = (*smpPtr) << (28-8); \
+	sample = ((int64_t)sample * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += sample; \
+	*audioMix++ += sample; \
+	INC_POS \
+
+#define MIX_16BIT_M \
+	sample = (*smpPtr) << (28-16); \
+	sample = ((int64_t)sample * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += sample; \
+	*audioMix++ += sample; \
+	INC_POS \
+
+// linear interpolation with bit-accurate results to FT2.08/FT2.09
+#define LERP(s1, s2, f) \
+{ \
+	s2 -= s1; \
+	f >>= 1; \
+	s2 = ((int64_t)s2 * (int32_t)f) >> 32; \
+	f += f; \
+	s2 += s2; \
+	s2 += s1; \
+} \
+
+// stereo mixing w/ linear interpolation
+
+#define MIX_8BIT_INTRP \
+	sample = smpPtr[0] << 8; \
+	sample2 = smpPtr[1] << 8; \
+	LERP(sample, sample2, pos) \
+	sample2 <<= (28-16); \
+	*audioMix++ += ((int64_t)sample2 * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += ((int64_t)sample2 * (int32_t)CDA_RVol) >> 32; \
+	INC_POS \
+
+#define MIX_16BIT_INTRP \
+	sample = smpPtr[0]; \
+	sample2 = smpPtr[1]; \
+	LERP(sample, sample2, pos) \
+	sample2 <<= (28-16); \
+	*audioMix++ += ((int64_t)sample2 * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += ((int64_t)sample2 * (int32_t)CDA_RVol) >> 32; \
+	INC_POS \
+
+// center mixing w/ linear interpolation
+
+#define MIX_8BIT_INTRP_M \
+	sample = smpPtr[0] << 8; \
+	sample2 = smpPtr[1] << 8; \
+	LERP(sample, sample2, pos) \
+	sample2 <<= (28-16); \
+	sample = ((int64_t)sample2 * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += sample; \
+	*audioMix++ += sample; \
+	INC_POS \
+
+#define MIX_16BIT_INTRP_M \
+	sample = smpPtr[0]; \
+	sample2 = smpPtr[1]; \
+	LERP(sample, sample2, pos) \
+	sample2 <<= (28-16); \
+	sample = ((int64_t)sample2 * (int32_t)CDA_LVol) >> 32; \
+	*audioMix++ += sample; \
+	*audioMix++ += sample; \
+	INC_POS \
+
+// ------------------------
+
+#define LIMIT_MIX_NUM \
+	int32_t samplesToMix; \
+	int32_t SFrq = v->SFrq; \
+	int32_t i = (v->SLen - 1) - realPos; \
+	if (i > 65535) i = 65535; /* 8bb: added this to prevent 64-bit div (still bit-accurate mixing results) */ \
+	if (SFrq != 0) \
+	{ \
+		const uint32_t tmp32 = (i << 16) | ((0xFFFF0000 - pos) >> 16); \
+		samplesToMix = (tmp32 / (uint32_t)SFrq) + 1; \
+	} \
+	else \
+	{ \
+		samplesToMix = 65535; \
+	} \
+	\
+	if (samplesToMix > CDA_BytesLeft) \
+		samplesToMix = CDA_BytesLeft; \
+
+#define LIMIT_MIX_NUM_RAMP \
+	if (v->SVolIPLen == 0) \
+	{ \
+		CDA_LVolIP = 0; \
+		CDA_RVolIP = 0; \
+	} \
+	else \
+	{ \
+		if (samplesToMix > v->SVolIPLen) \
+			samplesToMix = v->SVolIPLen; \
+		\
+		v->SVolIPLen -= samplesToMix; \
+	} \
+
+#define HANDLE_POS_START \
+	const bool backwards = (v->SType & (SType_Rev+SType_RevDir)) == SType_Rev+SType_RevDir; \
+	if (backwards) \
+	{ \
+		SFrq = 0 - SFrq; \
+		realPos = ~realPos; \
+		smpPtr = CDA_LinAdrRev + realPos; \
+		pos ^= 0xFFFF0000; \
+	} \
+	else \
+	{ \
+		smpPtr = CDA_LinearAdr + realPos; \
+	} \
+	\
+	pos += CDA_MixBuffPos; \
+	const int32_t CDA_IPValH = (int32_t)SFrq >> 16; \
+	const uint32_t CDA_IPValL = ((uint32_t)(SFrq & 0xFFFF) << 16) + 8; /* 8 = mixer buffer increase (for LERP to be bit-accurate) */ \
+
+#define HANDLE_POS_END \
+	if (backwards) \
+	{ \
+		pos ^= 0xFFFF0000; \
+		realPos = ~(int32_t)(smpPtr - CDA_LinAdrRev); \
+	} \
+	else \
+	{ \
+		realPos = (int32_t)(smpPtr - CDA_LinearAdr); \
+	} \
+	CDA_MixBuffPos = pos & 0xFFFF; \
+	pos &= 0xFFFF0000; \
+	\
+	if (realPos >= v->SLen) \
+	{ \
+		uint8_t SType = v->SType; \
+		if (SType & (SType_Fwd+SType_Rev)) \
+		{ \
+			do \
+			{ \
+				realPos -= v->SRepL; \
+				SType ^= SType_RevDir; \
+			} \
+			while (realPos >= v->SLen); \
+			v->SType = SType; \
+		} \
+		else \
+		{ \
+			v->SType = SType_Off; \
+			return; \
+		} \
+	} \
+
+typedef void (*mixRoutine)(void *, int32_t, int32_t);
+
+extern mixRoutine mixRoutineTable[16];
+
+void PMPMix32Proc(CIType *v, int32_t numSamples, int32_t bufferPos); // 8bb: numSamples = 1..65535
--- /dev/null
+++ b/tables.c
@@ -1,0 +1,453 @@
+#include <stdint.h>
+
+// 8bb: bit-accurate FT2 tables (FT2.08/FT2.09)
+
+/*
+** for (int32_t i = 0; i < 257; i++)
+**     panningTab[i] = (int32_t)round(65536.0 * sqrt(i / 256.0));
+*/
+const uint32_t panningTab[257] =
+{
+	    0, 4096, 5793, 7094, 8192, 9159,10033,10837,11585,12288,12953,13585,14189,14768,15326,15864,
+	16384,16888,17378,17854,18318,18770,19212,19644,20066,20480,20886,21283,21674,22058,22435,22806,
+	23170,23530,23884,24232,24576,24915,25249,25580,25905,26227,26545,26859,27170,27477,27780,28081,
+	28378,28672,28963,29251,29537,29819,30099,30377,30652,30924,31194,31462,31727,31991,32252,32511,
+	32768,33023,33276,33527,33776,34024,34270,34514,34756,34996,35235,35472,35708,35942,36175,36406,
+	36636,36864,37091,37316,37540,37763,37985,38205,38424,38642,38858,39073,39287,39500,39712,39923,
+	40132,40341,40548,40755,40960,41164,41368,41570,41771,41972,42171,42369,42567,42763,42959,43154,
+	43348,43541,43733,43925,44115,44305,44494,44682,44869,45056,45242,45427,45611,45795,45977,46160,
+	46341,46522,46702,46881,47059,47237,47415,47591,47767,47942,48117,48291,48465,48637,48809,48981,
+	49152,49322,49492,49661,49830,49998,50166,50332,50499,50665,50830,50995,51159,51323,51486,51649,
+	51811,51972,52134,52294,52454,52614,52773,52932,53090,53248,53405,53562,53719,53874,54030,54185,
+	54340,54494,54647,54801,54954,55106,55258,55410,55561,55712,55862,56012,56162,56311,56459,56608,
+	56756,56903,57051,57198,57344,57490,57636,57781,57926,58071,58215,58359,58503,58646,58789,58931,
+	59073,59215,59357,59498,59639,59779,59919,60059,60199,60338,60477,60615,60753,60891,61029,61166,
+	61303,61440,61576,61712,61848,61984,62119,62254,62388,62523,62657,62790,62924,63057,63190,63323,
+	63455,63587,63719,63850,63982,64113,64243,64374,64504,64634,64763,64893,65022,65151,65279,65408,
+	65536
+};
+
+// 8bb: the last 17 values are off (but identical to FT2.08/FT2.09) because of a bug in how it calculates this table
+const uint16_t amigaPeriods[1936] =
+{
+	29024,28912,28800,28704,28608,28496,28384,28288,28192,28096,28000,27888,27776,27680,27584,27488,
+	27392,27296,27200,27104,27008,26912,26816,26720,26624,26528,26432,26336,26240,26144,26048,25952,
+	25856,25760,25664,25568,25472,25392,25312,25216,25120,25024,24928,24848,24768,24672,24576,24480,
+	24384,24304,24224,24144,24064,23968,23872,23792,23712,23632,23552,23456,23360,23280,23200,23120,
+	23040,22960,22880,22784,22688,22608,22528,22448,22368,22288,22208,22128,22048,21968,21888,21792,
+	21696,21648,21600,21520,21440,21360,21280,21200,21120,21040,20960,20896,20832,20752,20672,20576,
+	20480,20416,20352,20288,20224,20160,20096,20016,19936,19872,19808,19728,19648,19584,19520,19424,
+	19328,19280,19232,19168,19104,19024,18944,18880,18816,18752,18688,18624,18560,18480,18400,18320,
+	18240,18192,18144,18080,18016,17952,17888,17824,17760,17696,17632,17568,17504,17440,17376,17296,
+	17216,17168,17120,17072,17024,16960,16896,16832,16768,16704,16640,16576,16512,16464,16416,16336,
+	16256,16208,16160,16112,16064,16000,15936,15872,15808,15760,15712,15648,15584,15536,15488,15424,
+	15360,15312,15264,15216,15168,15104,15040,14992,14944,14880,14816,14768,14720,14672,14624,14568,
+	14512,14456,14400,14352,14304,14248,14192,14144,14096,14048,14000,13944,13888,13840,13792,13744,
+	13696,13648,13600,13552,13504,13456,13408,13360,13312,13264,13216,13168,13120,13072,13024,12976,
+	12928,12880,12832,12784,12736,12696,12656,12608,12560,12512,12464,12424,12384,12336,12288,12240,
+	12192,12152,12112,12072,12032,11984,11936,11896,11856,11816,11776,11728,11680,11640,11600,11560,
+	11520,11480,11440,11392,11344,11304,11264,11224,11184,11144,11104,11064,11024,10984,10944,10896,
+	10848,10824,10800,10760,10720,10680,10640,10600,10560,10520,10480,10448,10416,10376,10336,10288,
+	10240,10208,10176,10144,10112,10080,10048,10008, 9968, 9936, 9904, 9864, 9824, 9792, 9760, 9712,
+	 9664, 9640, 9616, 9584, 9552, 9512, 9472, 9440, 9408, 9376, 9344, 9312, 9280, 9240, 9200, 9160,
+	 9120, 9096, 9072, 9040, 9008, 8976, 8944, 8912, 8880, 8848, 8816, 8784, 8752, 8720, 8688, 8648,
+	 8608, 8584, 8560, 8536, 8512, 8480, 8448, 8416, 8384, 8352, 8320, 8288, 8256, 8232, 8208, 8168,
+	 8128, 8104, 8080, 8056, 8032, 8000, 7968, 7936, 7904, 7880, 7856, 7824, 7792, 7768, 7744, 7712,
+	 7680, 7656, 7632, 7608, 7584, 7552, 7520, 7496, 7472, 7440, 7408, 7384, 7360, 7336, 7312, 7284,
+	 7256, 7228, 7200, 7176, 7152, 7124, 7096, 7072, 7048, 7024, 7000, 6972, 6944, 6920, 6896, 6872,
+	 6848, 6824, 6800, 6776, 6752, 6728, 6704, 6680, 6656, 6632, 6608, 6584, 6560, 6536, 6512, 6488,
+	 6464, 6440, 6416, 6392, 6368, 6348, 6328, 6304, 6280, 6256, 6232, 6212, 6192, 6168, 6144, 6120,
+	 6096, 6076, 6056, 6036, 6016, 5992, 5968, 5948, 5928, 5908, 5888, 5864, 5840, 5820, 5800, 5780,
+	 5760, 5740, 5720, 5696, 5672, 5652, 5632, 5612, 5592, 5572, 5552, 5532, 5512, 5492, 5472, 5448,
+	 5424, 5412, 5400, 5380, 5360, 5340, 5320, 5300, 5280, 5260, 5240, 5224, 5208, 5188, 5168, 5144,
+	 5120, 5104, 5088, 5072, 5056, 5040, 5024, 5004, 4984, 4968, 4952, 4932, 4912, 4896, 4880, 4856,
+	 4832, 4820, 4808, 4792, 4776, 4756, 4736, 4720, 4704, 4688, 4672, 4656, 4640, 4620, 4600, 4580,
+	 4560, 4548, 4536, 4520, 4504, 4488, 4472, 4456, 4440, 4424, 4408, 4392, 4376, 4360, 4344, 4324,
+	 4304, 4292, 4280, 4268, 4256, 4240, 4224, 4208, 4192, 4176, 4160, 4144, 4128, 4116, 4104, 4084,
+	 4064, 4052, 4040, 4028, 4016, 4000, 3984, 3968, 3952, 3940, 3928, 3912, 3896, 3884, 3872, 3856,
+	 3840, 3828, 3816, 3804, 3792, 3776, 3760, 3748, 3736, 3720, 3704, 3692, 3680, 3668, 3656, 3642,
+	 3628, 3614, 3600, 3588, 3576, 3562, 3548, 3536, 3524, 3512, 3500, 3486, 3472, 3460, 3448, 3436,
+	 3424, 3412, 3400, 3388, 3376, 3364, 3352, 3340, 3328, 3316, 3304, 3292, 3280, 3268, 3256, 3244,
+	 3232, 3220, 3208, 3196, 3184, 3174, 3164, 3152, 3140, 3128, 3116, 3106, 3096, 3084, 3072, 3060,
+	 3048, 3038, 3028, 3018, 3008, 2996, 2984, 2974, 2964, 2954, 2944, 2932, 2920, 2910, 2900, 2890,
+	 2880, 2870, 2860, 2848, 2836, 2826, 2816, 2806, 2796, 2786, 2776, 2766, 2756, 2746, 2736, 2724,
+	 2712, 2706, 2700, 2690, 2680, 2670, 2660, 2650, 2640, 2630, 2620, 2612, 2604, 2594, 2584, 2572,
+	 2560, 2552, 2544, 2536, 2528, 2520, 2512, 2502, 2492, 2484, 2476, 2466, 2456, 2448, 2440, 2428,
+	 2416, 2410, 2404, 2396, 2388, 2378, 2368, 2360, 2352, 2344, 2336, 2328, 2320, 2310, 2300, 2290,
+	 2280, 2274, 2268, 2260, 2252, 2244, 2236, 2228, 2220, 2212, 2204, 2196, 2188, 2180, 2172, 2162,
+	 2152, 2146, 2140, 2134, 2128, 2120, 2112, 2104, 2096, 2088, 2080, 2072, 2064, 2058, 2052, 2042,
+	 2032, 2026, 2020, 2014, 2008, 2000, 1992, 1984, 1976, 1970, 1964, 1956, 1948, 1942, 1936, 1928,
+	 1920, 1914, 1908, 1902, 1896, 1888, 1880, 1874, 1868, 1860, 1852, 1846, 1840, 1834, 1828, 1821,
+	 1814, 1807, 1800, 1794, 1788, 1781, 1774, 1768, 1762, 1756, 1750, 1743, 1736, 1730, 1724, 1718,
+	 1712, 1706, 1700, 1694, 1688, 1682, 1676, 1670, 1664, 1658, 1652, 1646, 1640, 1634, 1628, 1622,
+	 1616, 1610, 1604, 1598, 1592, 1587, 1582, 1576, 1570, 1564, 1558, 1553, 1548, 1542, 1536, 1530,
+	 1524, 1519, 1514, 1509, 1504, 1498, 1492, 1487, 1482, 1477, 1472, 1466, 1460, 1455, 1450, 1445,
+	 1440, 1435, 1430, 1424, 1418, 1413, 1408, 1403, 1398, 1393, 1388, 1383, 1378, 1373, 1368, 1362,
+	 1356, 1353, 1350, 1345, 1340, 1335, 1330, 1325, 1320, 1315, 1310, 1306, 1302, 1297, 1292, 1286,
+	 1280, 1276, 1272, 1268, 1264, 1260, 1256, 1251, 1246, 1242, 1238, 1233, 1228, 1224, 1220, 1214,
+	 1208, 1205, 1202, 1198, 1194, 1189, 1184, 1180, 1176, 1172, 1168, 1164, 1160, 1155, 1150, 1145,
+	 1140, 1137, 1134, 1130, 1126, 1122, 1118, 1114, 1110, 1106, 1102, 1098, 1094, 1090, 1086, 1081,
+	 1076, 1073, 1070, 1067, 1064, 1060, 1056, 1052, 1048, 1044, 1040, 1036, 1032, 1029, 1026, 1021,
+	 1016, 1013, 1010, 1007, 1004, 1000,  996,  992,  988,  985,  982,  978,  974,  971,  968,  964,
+	  960,  957,  954,  951,  948,  944,  940,  937,  934,  930,  926,  923,  920,  917,  914,  910,
+	  907,  903,  900,  897,  894,  890,  887,  884,  881,  878,  875,  871,  868,  865,  862,  859,
+	  856,  853,  850,  847,  844,  841,  838,  835,  832,  829,  826,  823,  820,  817,  814,  811,
+	  808,  805,  802,  799,  796,  793,  791,  788,  785,  782,  779,  776,  774,  771,  768,  765,
+	  762,  759,  757,  754,  752,  749,  746,  743,  741,  738,  736,  733,  730,  727,  725,  722,
+	  720,  717,  715,  712,  709,  706,  704,  701,  699,  696,  694,  691,  689,  686,  684,  681,
+	  678,  676,  675,  672,  670,  667,  665,  662,  660,  657,  655,  653,  651,  648,  646,  643,
+	  640,  638,  636,  634,  632,  630,  628,  625,  623,  621,  619,  616,  614,  612,  610,  607,
+	  604,  602,  601,  599,  597,  594,  592,  590,  588,  586,  584,  582,  580,  577,  575,  572,
+	  570,  568,  567,  565,  563,  561,  559,  557,  555,  553,  551,  549,  547,  545,  543,  540,
+	  538,  536,  535,  533,  532,  530,  528,  526,  524,  522,  520,  518,  516,  514,  513,  510,
+	  508,  506,  505,  503,  502,  500,  498,  496,  494,  492,  491,  489,  487,  485,  484,  482,
+	  480,  478,  477,  475,  474,  472,  470,  468,  467,  465,  463,  461,  460,  458,  457,  455,
+	  453,  451,  450,  448,  447,  445,  443,  441,  440,  438,  437,  435,  434,  432,  431,  429,
+	  428,  426,  425,  423,  422,  420,  419,  417,  416,  414,  413,  411,  410,  408,  407,  405,
+	  404,  402,  401,  399,  398,  396,  395,  393,  392,  390,  389,  388,  387,  385,  384,  382,
+	  381,  379,  378,  377,  376,  374,  373,  371,  370,  369,  368,  366,  365,  363,  362,  361,
+	  360,  358,  357,  355,  354,  353,  352,  350,  349,  348,  347,  345,  344,  343,  342,  340,
+	  339,  338,  337,  336,  335,  333,  332,  331,  330,  328,  327,  326,  325,  324,  323,  321,
+	  320,  319,  318,  317,  316,  315,  314,  312,  311,  310,  309,  308,  307,  306,  305,  303,
+	  302,  301,  300,  299,  298,  297,  296,  295,  294,  293,  292,  291,  290,  288,  287,  286,
+	  285,  284,  283,  282,  281,  280,  279,  278,  277,  276,  275,  274,  273,  272,  271,  270,
+	  269,  268,  267,  266,  266,  265,  264,  263,  262,  261,  260,  259,  258,  257,  256,  255,
+	  254,  253,  252,  251,  251,  250,  249,  248,  247,  246,  245,  244,  243,  242,  242,  241,
+	  240,  239,  238,  237,  237,  236,  235,  234,  233,  232,  231,  230,  230,  229,  228,  227,
+	  227,  226,  225,  224,  223,  222,  222,  221,  220,  219,  219,  218,  217,  216,  215,  214,
+	  214,  213,  212,  211,  211,  210,  209,  208,  208,  207,  206,  205,  205,  204,  203,  202,
+	  202,  201,  200,  199,  199,  198,  198,  197,  196,  195,  195,  194,  193,  192,  192,  191,
+	  190,  189,  189,  188,  188,  187,  186,  185,  185,  184,  184,  183,  182,  181,  181,  180,
+	  180,  179,  179,  178,  177,  176,  176,  175,  175,  174,  173,  172,  172,  171,  171,  170,
+	  169,  169,  169,  168,  167,  166,  166,  165,  165,  164,  164,  163,  163,  162,  161,  160,
+	  160,  159,  159,  158,  158,  157,  157,  156,  156,  155,  155,  154,  153,  152,  152,  151,
+	  151,  150,  150,  149,  149,  148,  148,  147,  147,  146,  146,  145,  145,  144,  144,  143,
+	  142,  142,  142,  141,  141,  140,  140,  139,  139,  138,  138,  137,  137,  136,  136,  135,
+	  134,  134,  134,  133,  133,  132,  132,  131,  131,  130,  130,  129,  129,  128,  128,  127,
+	  127,  126,  126,  125,  125,  124,  124,  123,  123,  123,  123,  122,  122,  121,  121,  120,
+	  120,  119,  119,  118,  118,  117,  117,  117,  117,  116,  116,  115,  115,  114,  114,  113,
+	  113,  112,  112,  112,  112,  111,  111,  110,  110,  109,  109,  108,  108,  108,  108,  107,
+	  107,  106,  106,  105,  105,  105,  105,  104,  104,  103,  103,  102,  102,  102,  102,  101,
+	  101,  100,  100,   99,   99,   99,   99,   98,   98,   97,   97,   97,   97,   96,   96,   95,
+	   95,   95,   95,   94,   94,   93,   93,   93,   93,   92,   92,   91,   91,   91,   91,   90,
+	   90,   89,   89,   89,   89,   88,   88,   87,   87,   87,   87,   86,   86,   85,   85,   85,
+	   85,   84,   84,   84,   84,   83,   83,   82,   82,   82,   82,   81,   81,   81,   81,   80,
+	   80,   79,   79,   79,   79,   78,   78,   78,   78,   77,   77,   77,   77,   76,   76,   75,
+	   75,   75,   75,   75,   75,   74,   74,   73,   73,   73,   73,   72,   72,   72,   72,   71,
+	   71,   71,   71,   70,   70,   70,   70,   69,   69,   69,   69,   68,   68,   68,   68,   67,
+	   67,   67,   67,   66,   66,   66,   66,   65,   65,   65,   65,   64,   64,   64,   64,   63,
+	   63,   63,   63,   63,   63,   62,   62,   62,   62,   61,   61,   61,   61,   60,   60,   60,
+	   60,   60,   60,   59,   59,   59,   59,   58,   58,   58,   58,   57,   57,   57,   57,   57,
+	   57,   56,   56,   56,   56,   55,   55,   55,   55,   55,   55,   54,   54,   54,   54,   53,
+	   53,   53,   53,   53,   53,   52,   52,   52,   52,   52,   52,   51,   51,   51,   51,   50,
+	   50,   50,   50,   50,   50,   49,   49,   49,   49,   49,   49,   48,   48,   48,   48,   48,
+	   48,   47,   47,   47,   47,   47,   47,   46,   46,   46,   46,   46,   46,   45,   45,   45,
+	   45,   45,   45,   44,   44,   44,   44,   44,   44,   43,   43,   43,   43,   43,   43,   42,
+	   42,   42,   42,   42,   42,   42,   42,   41,   41,   41,   41,   41,   41,   40,   40,   40,
+	   40,   40,   40,   39,   39,   39,   39,   39,   39,   39,   39,   38,   38,   38,   38,   38,
+	   38,   38,   38,   37,   37,   37,   37,   37,   37,   36,   36,   36,   36,   36,   36,   36,
+	   36,   35,   35,   35,   35,   35,   35,   35,   35,   34,   34,   34,   34,   34,   34,   34,
+	   34,   33,   33,   33,   33,   33,   33,   33,   33,   32,   32,   32,   32,   32,   32,   32,
+	   32,   32,   32,   31,   31,   31,   31,   31,   31,   31,   31,   30,   30,   30,   30,   30,
+	   30,   30,   30,   30,   30,   29,   29,   29,   29,   29,   29,   29,   29,   29,   29,   22,
+	   16,    8,    0,   16,   32,   24,   16,    8,    0,   16,   32,   24,   16,    8,    0,    0
+};
+
+const uint16_t linearPeriods[1936] =
+{
+	7744,7740,7736,7732,7728,7724,7720,7716,7712,7708,7704,7700,7696,7692,7688,7684,
+	7680,7676,7672,7668,7664,7660,7656,7652,7648,7644,7640,7636,7632,7628,7624,7620,
+	7616,7612,7608,7604,7600,7596,7592,7588,7584,7580,7576,7572,7568,7564,7560,7556,
+	7552,7548,7544,7540,7536,7532,7528,7524,7520,7516,7512,7508,7504,7500,7496,7492,
+	7488,7484,7480,7476,7472,7468,7464,7460,7456,7452,7448,7444,7440,7436,7432,7428,
+	7424,7420,7416,7412,7408,7404,7400,7396,7392,7388,7384,7380,7376,7372,7368,7364,
+	7360,7356,7352,7348,7344,7340,7336,7332,7328,7324,7320,7316,7312,7308,7304,7300,
+	7296,7292,7288,7284,7280,7276,7272,7268,7264,7260,7256,7252,7248,7244,7240,7236,
+	7232,7228,7224,7220,7216,7212,7208,7204,7200,7196,7192,7188,7184,7180,7176,7172,
+	7168,7164,7160,7156,7152,7148,7144,7140,7136,7132,7128,7124,7120,7116,7112,7108,
+	7104,7100,7096,7092,7088,7084,7080,7076,7072,7068,7064,7060,7056,7052,7048,7044,
+	7040,7036,7032,7028,7024,7020,7016,7012,7008,7004,7000,6996,6992,6988,6984,6980,
+	6976,6972,6968,6964,6960,6956,6952,6948,6944,6940,6936,6932,6928,6924,6920,6916,
+	6912,6908,6904,6900,6896,6892,6888,6884,6880,6876,6872,6868,6864,6860,6856,6852,
+	6848,6844,6840,6836,6832,6828,6824,6820,6816,6812,6808,6804,6800,6796,6792,6788,
+	6784,6780,6776,6772,6768,6764,6760,6756,6752,6748,6744,6740,6736,6732,6728,6724,
+	6720,6716,6712,6708,6704,6700,6696,6692,6688,6684,6680,6676,6672,6668,6664,6660,
+	6656,6652,6648,6644,6640,6636,6632,6628,6624,6620,6616,6612,6608,6604,6600,6596,
+	6592,6588,6584,6580,6576,6572,6568,6564,6560,6556,6552,6548,6544,6540,6536,6532,
+	6528,6524,6520,6516,6512,6508,6504,6500,6496,6492,6488,6484,6480,6476,6472,6468,
+	6464,6460,6456,6452,6448,6444,6440,6436,6432,6428,6424,6420,6416,6412,6408,6404,
+	6400,6396,6392,6388,6384,6380,6376,6372,6368,6364,6360,6356,6352,6348,6344,6340,
+	6336,6332,6328,6324,6320,6316,6312,6308,6304,6300,6296,6292,6288,6284,6280,6276,
+	6272,6268,6264,6260,6256,6252,6248,6244,6240,6236,6232,6228,6224,6220,6216,6212,
+	6208,6204,6200,6196,6192,6188,6184,6180,6176,6172,6168,6164,6160,6156,6152,6148,
+	6144,6140,6136,6132,6128,6124,6120,6116,6112,6108,6104,6100,6096,6092,6088,6084,
+	6080,6076,6072,6068,6064,6060,6056,6052,6048,6044,6040,6036,6032,6028,6024,6020,
+	6016,6012,6008,6004,6000,5996,5992,5988,5984,5980,5976,5972,5968,5964,5960,5956,
+	5952,5948,5944,5940,5936,5932,5928,5924,5920,5916,5912,5908,5904,5900,5896,5892,
+	5888,5884,5880,5876,5872,5868,5864,5860,5856,5852,5848,5844,5840,5836,5832,5828,
+	5824,5820,5816,5812,5808,5804,5800,5796,5792,5788,5784,5780,5776,5772,5768,5764,
+	5760,5756,5752,5748,5744,5740,5736,5732,5728,5724,5720,5716,5712,5708,5704,5700,
+	5696,5692,5688,5684,5680,5676,5672,5668,5664,5660,5656,5652,5648,5644,5640,5636,
+	5632,5628,5624,5620,5616,5612,5608,5604,5600,5596,5592,5588,5584,5580,5576,5572,
+	5568,5564,5560,5556,5552,5548,5544,5540,5536,5532,5528,5524,5520,5516,5512,5508,
+	5504,5500,5496,5492,5488,5484,5480,5476,5472,5468,5464,5460,5456,5452,5448,5444,
+	5440,5436,5432,5428,5424,5420,5416,5412,5408,5404,5400,5396,5392,5388,5384,5380,
+	5376,5372,5368,5364,5360,5356,5352,5348,5344,5340,5336,5332,5328,5324,5320,5316,
+	5312,5308,5304,5300,5296,5292,5288,5284,5280,5276,5272,5268,5264,5260,5256,5252,
+	5248,5244,5240,5236,5232,5228,5224,5220,5216,5212,5208,5204,5200,5196,5192,5188,
+	5184,5180,5176,5172,5168,5164,5160,5156,5152,5148,5144,5140,5136,5132,5128,5124,
+	5120,5116,5112,5108,5104,5100,5096,5092,5088,5084,5080,5076,5072,5068,5064,5060,
+	5056,5052,5048,5044,5040,5036,5032,5028,5024,5020,5016,5012,5008,5004,5000,4996,
+	4992,4988,4984,4980,4976,4972,4968,4964,4960,4956,4952,4948,4944,4940,4936,4932,
+	4928,4924,4920,4916,4912,4908,4904,4900,4896,4892,4888,4884,4880,4876,4872,4868,
+	4864,4860,4856,4852,4848,4844,4840,4836,4832,4828,4824,4820,4816,4812,4808,4804,
+	4800,4796,4792,4788,4784,4780,4776,4772,4768,4764,4760,4756,4752,4748,4744,4740,
+	4736,4732,4728,4724,4720,4716,4712,4708,4704,4700,4696,4692,4688,4684,4680,4676,
+	4672,4668,4664,4660,4656,4652,4648,4644,4640,4636,4632,4628,4624,4620,4616,4612,
+	4608,4604,4600,4596,4592,4588,4584,4580,4576,4572,4568,4564,4560,4556,4552,4548,
+	4544,4540,4536,4532,4528,4524,4520,4516,4512,4508,4504,4500,4496,4492,4488,4484,
+	4480,4476,4472,4468,4464,4460,4456,4452,4448,4444,4440,4436,4432,4428,4424,4420,
+	4416,4412,4408,4404,4400,4396,4392,4388,4384,4380,4376,4372,4368,4364,4360,4356,
+	4352,4348,4344,4340,4336,4332,4328,4324,4320,4316,4312,4308,4304,4300,4296,4292,
+	4288,4284,4280,4276,4272,4268,4264,4260,4256,4252,4248,4244,4240,4236,4232,4228,
+	4224,4220,4216,4212,4208,4204,4200,4196,4192,4188,4184,4180,4176,4172,4168,4164,
+	4160,4156,4152,4148,4144,4140,4136,4132,4128,4124,4120,4116,4112,4108,4104,4100,
+	4096,4092,4088,4084,4080,4076,4072,4068,4064,4060,4056,4052,4048,4044,4040,4036,
+	4032,4028,4024,4020,4016,4012,4008,4004,4000,3996,3992,3988,3984,3980,3976,3972,
+	3968,3964,3960,3956,3952,3948,3944,3940,3936,3932,3928,3924,3920,3916,3912,3908,
+	3904,3900,3896,3892,3888,3884,3880,3876,3872,3868,3864,3860,3856,3852,3848,3844,
+	3840,3836,3832,3828,3824,3820,3816,3812,3808,3804,3800,3796,3792,3788,3784,3780,
+	3776,3772,3768,3764,3760,3756,3752,3748,3744,3740,3736,3732,3728,3724,3720,3716,
+	3712,3708,3704,3700,3696,3692,3688,3684,3680,3676,3672,3668,3664,3660,3656,3652,
+	3648,3644,3640,3636,3632,3628,3624,3620,3616,3612,3608,3604,3600,3596,3592,3588,
+	3584,3580,3576,3572,3568,3564,3560,3556,3552,3548,3544,3540,3536,3532,3528,3524,
+	3520,3516,3512,3508,3504,3500,3496,3492,3488,3484,3480,3476,3472,3468,3464,3460,
+	3456,3452,3448,3444,3440,3436,3432,3428,3424,3420,3416,3412,3408,3404,3400,3396,
+	3392,3388,3384,3380,3376,3372,3368,3364,3360,3356,3352,3348,3344,3340,3336,3332,
+	3328,3324,3320,3316,3312,3308,3304,3300,3296,3292,3288,3284,3280,3276,3272,3268,
+	3264,3260,3256,3252,3248,3244,3240,3236,3232,3228,3224,3220,3216,3212,3208,3204,
+	3200,3196,3192,3188,3184,3180,3176,3172,3168,3164,3160,3156,3152,3148,3144,3140,
+	3136,3132,3128,3124,3120,3116,3112,3108,3104,3100,3096,3092,3088,3084,3080,3076,
+	3072,3068,3064,3060,3056,3052,3048,3044,3040,3036,3032,3028,3024,3020,3016,3012,
+	3008,3004,3000,2996,2992,2988,2984,2980,2976,2972,2968,2964,2960,2956,2952,2948,
+	2944,2940,2936,2932,2928,2924,2920,2916,2912,2908,2904,2900,2896,2892,2888,2884,
+	2880,2876,2872,2868,2864,2860,2856,2852,2848,2844,2840,2836,2832,2828,2824,2820,
+	2816,2812,2808,2804,2800,2796,2792,2788,2784,2780,2776,2772,2768,2764,2760,2756,
+	2752,2748,2744,2740,2736,2732,2728,2724,2720,2716,2712,2708,2704,2700,2696,2692,
+	2688,2684,2680,2676,2672,2668,2664,2660,2656,2652,2648,2644,2640,2636,2632,2628,
+	2624,2620,2616,2612,2608,2604,2600,2596,2592,2588,2584,2580,2576,2572,2568,2564,
+	2560,2556,2552,2548,2544,2540,2536,2532,2528,2524,2520,2516,2512,2508,2504,2500,
+	2496,2492,2488,2484,2480,2476,2472,2468,2464,2460,2456,2452,2448,2444,2440,2436,
+	2432,2428,2424,2420,2416,2412,2408,2404,2400,2396,2392,2388,2384,2380,2376,2372,
+	2368,2364,2360,2356,2352,2348,2344,2340,2336,2332,2328,2324,2320,2316,2312,2308,
+	2304,2300,2296,2292,2288,2284,2280,2276,2272,2268,2264,2260,2256,2252,2248,2244,
+	2240,2236,2232,2228,2224,2220,2216,2212,2208,2204,2200,2196,2192,2188,2184,2180,
+	2176,2172,2168,2164,2160,2156,2152,2148,2144,2140,2136,2132,2128,2124,2120,2116,
+	2112,2108,2104,2100,2096,2092,2088,2084,2080,2076,2072,2068,2064,2060,2056,2052,
+	2048,2044,2040,2036,2032,2028,2024,2020,2016,2012,2008,2004,2000,1996,1992,1988,
+	1984,1980,1976,1972,1968,1964,1960,1956,1952,1948,1944,1940,1936,1932,1928,1924,
+	1920,1916,1912,1908,1904,1900,1896,1892,1888,1884,1880,1876,1872,1868,1864,1860,
+	1856,1852,1848,1844,1840,1836,1832,1828,1824,1820,1816,1812,1808,1804,1800,1796,
+	1792,1788,1784,1780,1776,1772,1768,1764,1760,1756,1752,1748,1744,1740,1736,1732,
+	1728,1724,1720,1716,1712,1708,1704,1700,1696,1692,1688,1684,1680,1676,1672,1668,
+	1664,1660,1656,1652,1648,1644,1640,1636,1632,1628,1624,1620,1616,1612,1608,1604,
+	1600,1596,1592,1588,1584,1580,1576,1572,1568,1564,1560,1556,1552,1548,1544,1540,
+	1536,1532,1528,1524,1520,1516,1512,1508,1504,1500,1496,1492,1488,1484,1480,1476,
+	1472,1468,1464,1460,1456,1452,1448,1444,1440,1436,1432,1428,1424,1420,1416,1412,
+	1408,1404,1400,1396,1392,1388,1384,1380,1376,1372,1368,1364,1360,1356,1352,1348,
+	1344,1340,1336,1332,1328,1324,1320,1316,1312,1308,1304,1300,1296,1292,1288,1284,
+	1280,1276,1272,1268,1264,1260,1256,1252,1248,1244,1240,1236,1232,1228,1224,1220,
+	1216,1212,1208,1204,1200,1196,1192,1188,1184,1180,1176,1172,1168,1164,1160,1156,
+	1152,1148,1144,1140,1136,1132,1128,1124,1120,1116,1112,1108,1104,1100,1096,1092,
+	1088,1084,1080,1076,1072,1068,1064,1060,1056,1052,1048,1044,1040,1036,1032,1028,
+	1024,1020,1016,1012,1008,1004,1000, 996, 992, 988, 984, 980, 976, 972, 968, 964,
+	 960, 956, 952, 948, 944, 940, 936, 932, 928, 924, 920, 916, 912, 908, 904, 900,
+	 896, 892, 888, 884, 880, 876, 872, 868, 864, 860, 856, 852, 848, 844, 840, 836,
+	 832, 828, 824, 820, 816, 812, 808, 804, 800, 796, 792, 788, 784, 780, 776, 772,
+	 768, 764, 760, 756, 752, 748, 744, 740, 736, 732, 728, 724, 720, 716, 712, 708,
+	 704, 700, 696, 692, 688, 684, 680, 676, 672, 668, 664, 660, 656, 652, 648, 644,
+	 640, 636, 632, 628, 624, 620, 616, 612, 608, 604, 600, 596, 592, 588, 584, 580,
+	 576, 572, 568, 564, 560, 556, 552, 548, 544, 540, 536, 532, 528, 524, 520, 516,
+	 512, 508, 504, 500, 496, 492, 488, 484, 480, 476, 472, 468, 464, 460, 456, 452,
+	 448, 444, 440, 436, 432, 428, 424, 420, 416, 412, 408, 404, 400, 396, 392, 388,
+	 384, 380, 376, 372, 368, 364, 360, 356, 352, 348, 344, 340, 336, 332, 328, 324,
+	 320, 316, 312, 308, 304, 300, 296, 292, 288, 284, 280, 276, 272, 268, 264, 260,
+	 256, 252, 248, 244, 240, 236, 232, 228, 224, 220, 216, 212, 208, 204, 200, 196,
+	 192, 188, 184, 180, 176, 172, 168, 164, 160, 156, 152, 148, 144, 140, 136, 132,
+	 128, 124, 120, 116, 112, 108, 104, 100,  96,  92,  88,  84,  80,  76,  72,  68,
+	  64,  60,  56,  52,  48,  44,  40,  36,  32,  28,  24,  20,  16,  12,   8,   4
+};
+
+/*
+** for (int32_t i = 0; i < 768; i++)
+**     logTab[i] = (int32_t)round(16777216.0 * exp2(i / 768.0));
+*/
+const int32_t logTab[768] =
+{
+	16777216,16792365,16807527,16822704,16837894,16853097,16868315,16883546,
+	16898791,16914049,16929322,16944608,16959908,16975222,16990549,17005891,
+	17021246,17036615,17051999,17067396,17082806,17098231,17113670,17129123,
+	17144589,17160070,17175564,17191073,17206595,17222132,17237683,17253247,
+	17268826,17284419,17300026,17315646,17331282,17346931,17362594,17378271,
+	17393963,17409669,17425389,17441123,17456871,17472634,17488410,17504202,
+	17520007,17535826,17551660,17567508,17583371,17599248,17615139,17631044,
+	17646964,17662898,17678847,17694810,17710787,17726779,17742785,17758806,
+	17774841,17790891,17806955,17823034,17839127,17855235,17871357,17887494,
+	17903645,17919811,17935992,17952187,17968397,17984621,18000860,18017114,
+	18033382,18049665,18065963,18082276,18098603,18114945,18131302,18147673,
+	18164060,18180461,18196877,18213307,18229753,18246213,18262689,18279179,
+	18295684,18312204,18328739,18345288,18361853,18378433,18395028,18411637,
+	18428262,18444902,18461556,18478226,18494911,18511611,18528325,18545056,
+	18561801,18578561,18595336,18612127,18628932,18645753,18662589,18679441,
+	18696307,18713189,18730086,18746998,18763925,18780868,18797826,18814800,
+	18831788,18848792,18865812,18882846,18899897,18916962,18934043,18951139,
+	18968251,18985378,19002521,19019679,19036853,19054042,19071247,19088467,
+	19105703,19122954,19140221,19157504,19174802,19192116,19209445,19226790,
+	19244151,19261527,19278919,19296327,19313750,19331190,19348645,19366115,
+	19383602,19401104,19418622,19436156,19453706,19471271,19488853,19506450,
+	19524063,19541692,19559337,19576998,19594675,19612368,19630077,19647802,
+	19665543,19683300,19701072,19718861,19736666,19754488,19772325,19790178,
+	19808047,19825933,19843835,19861752,19879686,19897637,19915603,19933586,
+	19951585,19969600,19987631,20005679,20023743,20041823,20059920,20078033,
+	20096162,20114308,20132470,20150648,20168843,20187054,20205282,20223526,
+	20241787,20260064,20278358,20296668,20314995,20333338,20351698,20370074,
+	20388467,20406877,20425303,20443746,20462206,20480682,20499175,20517684,
+	20536211,20554754,20573313,20591890,20610483,20629093,20647720,20666364,
+	20685025,20703702,20722396,20741107,20759835,20778580,20797342,20816121,
+	20834917,20853729,20872559,20891406,20910270,20929150,20948048,20966963,
+	20985895,21004844,21023810,21042794,21061794,21080812,21099846,21118898,
+	21137968,21157054,21176158,21195278,21214417,21233572,21252745,21271935,
+	21291142,21310367,21329609,21348868,21368145,21387439,21406751,21426080,
+	21445426,21464790,21484172,21503571,21522987,21542421,21561873,21581342,
+	21600829,21620333,21639855,21659395,21678952,21698527,21718119,21737729,
+	21757357,21777003,21796666,21816348,21836046,21855763,21875498,21895250,
+	21915020,21934808,21954614,21974438,21994279,22014139,22034016,22053912,
+	22073825,22093757,22113706,22133674,22153659,22173663,22193684,22213724,
+	22233781,22253857,22273951,22294063,22314194,22334342,22354509,22374693,
+	22394897,22415118,22435357,22455615,22475891,22496186,22516499,22536830,
+	22557179,22577547,22597933,22618338,22638761,22659202,22679662,22700141,
+	22720638,22741153,22761687,22782240,22802811,22823400,22844009,22864635,
+	22885281,22905945,22926628,22947329,22968049,22988788,23009546,23030322,
+	23051117,23071931,23092764,23113615,23134485,23155374,23176282,23197209,
+	23218155,23239120,23260103,23281106,23302127,23323168,23344227,23365306,
+	23386403,23407520,23428656,23449810,23470984,23492177,23513389,23534620,
+	23555871,23577140,23598429,23619737,23641065,23662411,23683777,23705162,
+	23726566,23747990,23769433,23790896,23812377,23833879,23855399,23876939,
+	23898499,23920078,23941676,23963294,23984932,24006589,24028265,24049962,
+	24071677,24093413,24115168,24136942,24158736,24180550,24202384,24224237,
+	24246111,24268003,24289916,24311848,24333801,24355773,24377765,24399776,
+	24421808,24443859,24465931,24488022,24510133,24532265,24554416,24576587,
+	24598778,24620990,24643221,24665472,24687744,24710036,24732347,24754679,
+	24777031,24799403,24821796,24844209,24866641,24889095,24911568,24934062,
+	24956576,24979110,25001665,25024240,25046835,25069451,25092088,25114744,
+	25137421,25160119,25182837,25205576,25228335,25251115,25273915,25296736,
+	25319578,25342440,25365322,25388226,25411150,25434095,25457060,25480047,
+	25503054,25526081,25549130,25572199,25595290,25618401,25641533,25664686,
+	25687859,25711054,25734270,25757506,25780764,25804042,25827342,25850662,
+	25874004,25897367,25920751,25944156,25967582,25991029,26014497,26037987,
+	26061498,26085030,26108583,26132158,26155754,26179371,26203009,26226669,
+	26250350,26274053,26297777,26321522,26345289,26369077,26392887,26416718,
+	26440571,26464445,26488341,26512259,26536198,26560158,26584141,26608145,
+	26632170,26656218,26680287,26704377,26728490,26752624,26776780,26800958,
+	26825158,26849380,26873623,26897888,26922176,26946485,26970816,26995169,
+	27019544,27043941,27068360,27092802,27117265,27141750,27166258,27190787,
+	27215339,27239913,27264509,27289127,27313768,27338430,27363116,27387823,
+	27412552,27437304,27462079,27486875,27511695,27536536,27561400,27586286,
+	27611195,27636126,27661080,27686057,27711056,27736077,27761121,27786188,
+	27811277,27836389,27861524,27886681,27911861,27937064,27962290,27987538,
+	28012809,28038103,28063420,28088760,28114122,28139508,28164916,28190347,
+	28215802,28241279,28266779,28292302,28317849,28343418,28369011,28394626,
+	28420265,28445927,28471612,28497320,28523052,28548806,28574584,28600385,
+	28626210,28652058,28677929,28703823,28729741,28755683,28781647,28807636,
+	28833647,28859682,28885741,28911823,28937929,28964058,28990211,29016388,
+	29042588,29068811,29095059,29121330,29147625,29173944,29200286,29226652,
+	29253042,29279456,29305894,29332355,29358841,29385350,29411883,29438441,
+	29465022,29491627,29518256,29544910,29571587,29598288,29625014,29651764,
+	29678538,29705336,29732158,29759004,29785875,29812770,29839689,29866633,
+	29893600,29920593,29947609,29974650,30001716,30028805,30055920,30083059,
+	30110222,30137410,30164622,30191859,30219120,30246407,30273717,30301053,
+	30328413,30355798,30383207,30410642,30438101,30465584,30493093,30520627,
+	30548185,30575768,30603377,30631010,30658668,30686351,30714059,30741792,
+	30769550,30797333,30825141,30852975,30880833,30908717,30936625,30964559,
+	30992519,31020503,31048513,31076548,31104608,31132694,31160805,31188941,
+	31217103,31245290,31273503,31301741,31330005,31358294,31386609,31414949,
+	31443315,31471707,31500124,31528567,31557035,31585529,31614049,31642595,
+	31671166,31699764,31728387,31757036,31785710,31814411,31843138,31871890,
+	31900669,31929473,31958304,31987160,32016043,32044951,32073886,32102847,
+	32131834,32160847,32189887,32218952,32248044,32277162,32306307,32335478,
+	32364675,32393898,32423148,32452424,32481727,32511056,32540412,32569794,
+	32599202,32628638,32658099,32687588,32717103,32746645,32776213,32805808,
+	32835430,32865078,32894754,32924456,32954184,32983940,33013723,33043532,
+	33073369,33103232,33133122,33163040,33192984,33222955,33252954,33282979,
+	33313032,33343112,33373219,33403353,33433514,33463703,33493919,33524162
+};
+
+const char *MODSig[16] =
+{
+	"2CHN","M.K.","6CHN","8CHN","10CH","12CH","14CH","16CH",
+	"18CH","20CH","22CH","24CH","26CH","28CH","30CH","32CH"
+};
+
+const uint16_t amigaPeriod[96] =
+{
+	6848,6464,6096,5760,5424,5120,4832,4560,4304,4064,3840,3624,
+	3424,3232,3048,2880,2712,2560,2416,2280,2152,2032,1920,1812,
+	1712,1616,1524,1440,1356,1280,1208,1140,1076,1016, 960, 906,
+	 856, 808, 762, 720, 678, 640, 604, 570, 538, 508, 480, 453,
+	 428, 404, 381, 360, 339, 320, 302, 285, 269, 254, 240, 226,
+	 214, 202, 190, 180, 170, 160, 151, 143, 135, 127, 120, 113,
+	 107, 101,  95,  90,  85,  80,  75,  71,  67,  63,  60,  56,
+	  53,  50,  47,  45,  42,  40,  37,  35,  33,  31,  30,  28,
+};
+
+const uint8_t vibTab[32] =
+{
+	  0, 24, 49, 74, 97,120,141,161,
+	180,197,212,224,235,244,250,253,
+	255,253,250,244,235,224,212,197,
+	180,161,141,120, 97, 74, 49, 24
+};
+
+const int8_t vibSineTab[256] =
+{
+	  0, -2, -3, -5, -6, -8, -9,-11,-12,-14,-16,-17,-19,-20,-22,-23,
+	-24,-26,-27,-29,-30,-32,-33,-34,-36,-37,-38,-39,-41,-42,-43,-44,
+	-45,-46,-47,-48,-49,-50,-51,-52,-53,-54,-55,-56,-56,-57,-58,-59,
+	-59,-60,-60,-61,-61,-62,-62,-62,-63,-63,-63,-64,-64,-64,-64,-64,
+	-64,-64,-64,-64,-64,-64,-63,-63,-63,-62,-62,-62,-61,-61,-60,-60,
+	-59,-59,-58,-57,-56,-56,-55,-54,-53,-52,-51,-50,-49,-48,-47,-46,
+	-45,-44,-43,-42,-41,-39,-38,-37,-36,-34,-33,-32,-30,-29,-27,-26,
+	-24,-23,-22,-20,-19,-17,-16,-14,-12,-11, -9, -8, -6, -5, -3, -2,
+	  0,  2,  3,  5,  6,  8,  9, 11, 12, 14, 16, 17, 19, 20, 22, 23,
+	 24, 26, 27, 29, 30, 32, 33, 34, 36, 37, 38, 39, 41, 42, 43, 44,
+	 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 56, 57, 58, 59,
+	 59, 60, 60, 61, 61, 62, 62, 62, 63, 63, 63, 64, 64, 64, 64, 64,
+	 64, 64, 64, 64, 64, 64, 63, 63, 63, 62, 62, 62, 61, 61, 60, 60,
+	 59, 59, 58, 57, 56, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46,
+	 45, 44, 43, 42, 41, 39, 38, 37, 36, 34, 33, 32, 30, 29, 27, 26,
+	 24, 23, 22, 20, 19, 17, 16, 14, 12, 11,  9,  8,  6,  5,  3,  2
+};
+
+const uint8_t arpTab[256] =
+{
+	0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,0,
+	
+	/* 8bb: the following are overflown bytes from FT2's binary, read by the buggy
+	** arpeggio routine. (values confirmed to be the same on FT2.08 and FT2.09)
+	*/
+	0x00,0x18,0x31,0x4A,0x61,0x78,0x8D,0xA1,0xB4,0xC5,0xD4,0xE0,0xEB,0xF4,0xFA,0xFD,
+	0xFF,0xFD,0xFA,0xF4,0xEB,0xE0,0xD4,0xC5,0xB4,0xA1,0x8D,0x78,0x61,0x4A,0x31,0x18,
+	0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x01,0x03,0x00,0x02,0x00,0x04,0x00,0x00,
+	0x00,0x05,0x06,0x00,0x00,0x07,0x00,0x01,0x00,0x02,0x00,0x03,0x04,0x05,0x00,0x00,
+	0x0B,0x00,0x0A,0x02,0x01,0x03,0x04,0x07,0x00,0x05,0x06,0x00,0x00,0x00,0x00,0x00,
+	0x03,0x00,0x00,0x00,0x00,0x00,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x79,0x02,0x00,0x00,0x8F,0x01,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x08,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0xFF,0xFF,0xFF,0xFF,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,
+	0x00,0x00,0x00,0x00,0x00,0x00,0x46,0x4F,0x52,0x4D,0x49,0x4C,0x42,0x4D,0x42,0x4D
+};
--- /dev/null
+++ b/tables.h
@@ -1,0 +1,13 @@
+#pragma once
+
+#include <stdint.h>
+
+extern const uint32_t panningTab[257];
+extern const uint16_t amigaPeriods[1936];
+extern const uint16_t linearPeriods[1936];
+extern const int32_t logTab[768];
+extern const char *MODSig[16];
+extern const uint16_t amigaPeriod[96];
+extern const uint8_t vibTab[32];
+extern const int8_t vibSineTab[256];
+extern const uint8_t arpTab[256];