shithub: ft²

ref: 0fe93d0068b6303b3367a825fe9012bc7dad0387
dir: /src/ft2_replayer.c/

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

#include <stdint.h>
#include <stdio.h>
#include <math.h>
#include "ft2_header.h"
#include "ft2_config.h"
#include "ft2_gui.h"
#include "ft2_video.h"
#include "ft2_pattern_ed.h"
#include "ft2_sample_ed.h"
#include "ft2_inst_ed.h"
#include "ft2_diskop.h"
#include "ft2_midi.h"
#include "ft2_scopes.h"
#include "ft2_mouse.h"
#include "ft2_sample_loader.h"
#include "ft2_tables.h"
#include "ft2_structs.h"
#include "mixer/ft2_cubic.h"

/* This is a mess, directly ported from the original FT2 code (with some modifications).
** You will experience a lot of headaches if you dig into it...
** If something looks to be off, it probably isn't!
*/

// non-FT2 precalced stuff
static double dPeriod2HzTab[65536], dLogTab[768], dHz2MixDeltaMul;
static uint32_t revMixDeltaTab[65536];
static bool bxxOverflow;
static tonTyp nilPatternLine[MAX_VOICES];

// globally accessed

int8_t playMode = 0;
bool songPlaying = false, audioPaused = false, musicPaused = false;
volatile bool replayerBusy = false;
const uint16_t *note2Period = NULL;
int16_t pattLens[MAX_PATTERNS];
stmTyp stm[MAX_VOICES];
songTyp song;
instrTyp *instr[132];
tonTyp *patt[MAX_PATTERNS];

void fixSongName(void) // removes spaces from right side of song name
{
	for (int16_t i = 20; i >= 0; i--)
	{
		if (song.name[i] == ' ')
			song.name[i] = '\0';
		else
			break;
	}
}

void fixSampleName(int16_t nr) // removes spaces from right side of ins/smp names
{
	int16_t i, j;
	sampleTyp *s;

	for (i = 21; i >= 0; i--)
	{
		if (song.instrName[nr][i] == ' ')
			song.instrName[nr][i] = '\0';
		else
			break;
	}

	if (instr[nr] != NULL)
	{
		for (i = 0; i < MAX_SMP_PER_INST; i++)
		{
			s = &instr[nr]->samp[i];
			for (j = 21; j >= 0; j--)
			{
				if (s->name[j] == ' ')
					s->name[j] = '\0';
				else
					break;
			}
			s->name[22] = '\0'; // just in case (for tracker, not present in sample header when saving)
		}
	}
}

void resetChannels(void)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	memset(stm, 0, sizeof (stm));
	for (int32_t i = 0; i < MAX_VOICES; i++)
	{
		stmTyp *ch = &stm[i];

		ch->instrSeg = instr[0];
		ch->status = IS_Vol;
		ch->oldPan = 128;
		ch->outPan = 128;
		ch->finalPan = 128;

		ch->stOff = !editor.chnMode[i]; // set channel mute flag from global mute flag
	}

	if (audioWasntLocked)
		unlockAudio();
}

void setSongModifiedFlag(void)
{
	song.isModified = true;
	editor.updateWindowTitle = true;
}

void removeSongModifiedFlag(void)
{
	song.isModified = false;
	editor.updateWindowTitle = true;
}

void tuneSample(sampleTyp *s, int32_t midCFreq)
{
	if (midCFreq <= 0)
	{
		s->fine = 0;
		s->relTon = 0;
		return;
	}

	double dFreq = log2(midCFreq / 8363.0) * (12.0 * 128.0);
	int32_t linearFreq = (int32_t)(dFreq + 0.5); // rounded
	s->fine = ((linearFreq + 128) & 255) - 128;

	int32_t relTon = (linearFreq - s->fine) >> 7;
	s->relTon = (int8_t)CLAMP(relTon, -48, 71);
}

void setPatternLen(uint16_t nr, int16_t len)
{
	assert(nr < MAX_PATTERNS);
	if ((len < 1 || len > MAX_PATT_LEN) || len == pattLens[nr])
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	pattLens[nr] = len;

	if (patt[nr] != NULL)
		killPatternIfUnused(nr);

	song.pattLen = pattLens[nr];
	if (song.pattPos >= song.pattLen)
	{
		song.pattPos = song.pattLen - 1;
		editor.pattPos = song.pattPos;
	}

	checkMarkLimits();

	if (audioWasntLocked)
		unlockAudio();

	ui.updatePatternEditor = true;
	ui.updatePosSections = true;
}

int16_t getUsedSamples(int16_t nr)
{
	int16_t i, j;
	instrTyp *ins;

	if (instr[nr] == NULL)
		return 0;

	ins = instr[nr];

	i = 16 - 1;
	while (i >= 0 && ins->samp[i].pek == NULL && ins->samp[i].name[0] == '\0')
		i--;

	/* Yes, 'i' can be -1 here, and will be set to at least 0
	** because of ins->ta values. Possibly an FT2 bug...
	*/
	for (j = 0; j < 96; j++)
	{
		if (ins->ta[j] > i)
			i = ins->ta[j];
	}

	return i+1;
}

int16_t getRealUsedSamples(int16_t nr)
{
	int8_t i;

	if (instr[nr] == NULL)
		return 0;

	i = 16 - 1;
	while (i >= 0 && instr[nr]->samp[i].pek == NULL)
		i--;

	return i+1;
}

static void calcPeriod2HzTable(void) // called every time "linear/amiga frequency" mode is changed
{
	dPeriod2HzTab[0] = 0.0; // in FT2, a period of 0 converts to 0Hz

	if (audio.linearFreqTable)
	{
		// linear periods
		for (int32_t i = 1; i < 65536; i++)
		{
			const uint16_t invPeriod = (12 * 192 * 4) - (uint16_t)i; // this intentionally 16-bit-underflows to be accurate to FT2
			const int32_t octave = invPeriod / 768;
			const int32_t period = invPeriod % 768;
			const int32_t invOct = (14 - octave) & 0x1F; // accurate to FT2

			dPeriod2HzTab[i] = dLogTab[period] / (1UL << invOct); // x = y / 2^invOct
		}
	}
	else
	{
		// Amiga periods
		for (int32_t i = 1; i < 65536; i++)
			dPeriod2HzTab[i] = (8363.0 * 1712.0) / i;
	}
}

/* Called every time "linear/amiga frequency" mode or audio frequency is changed.
**
** Used to replace a DIV with a MUL in the outside audio mixer loop. This can actually
** be beneficial if you are playing VERY tightly looped samples, and/or if the CPU has
** no DIV instruction (certain ARM CPUs, for instance).
**
** A bit hackish and extreme considering it's 65536*4 bytes, but that's My Game�
*/
void calcRevMixDeltaTable(void)
{
	for (int32_t i = 0; i < 65536; i++)
	{
		const uint16_t period = (uint16_t)i;
		const uint64_t delta = getMixerDelta(period);

		uint32_t revDelta = UINT32_MAX;
		if (delta != 0)
			revDelta = (uint32_t)((UINT64_MAX / delta) >> 16); // MUST be truncated, not rounded!

		revMixDeltaTab[i] = revDelta;
	}
}

void setFrqTab(bool linear)
{
	pauseAudio();

	audio.linearFreqTable = linear;

	if (audio.linearFreqTable)
		note2Period = linearPeriods;
	else
		note2Period = amigaPeriods;

	calcPeriod2HzTable();
	calcRevMixDeltaTable();

	resumeAudio();

	// update "frequency table" radiobutton, if it's shown
	if (ui.configScreenShown && editor.currConfigScreen == CONFIG_SCREEN_IO_DEVICES)
		setConfigIORadioButtonStates();
}

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)
{
	instrTyp *ins;

	if (!(ch->waveCtrl & 0x04)) ch->vibPos = 0;
	if (!(ch->waveCtrl & 0x40)) ch->tremPos = 0;

	ch->retrigCnt = 0;
	ch->tremorPos = 0;

	ch->envSustainActive = true;

	ins = ch->instrSeg;
	assert(ins != NULL);

	if (ins->envVTyp & 1)
	{
		ch->envVCnt = 65535;
		ch->envVPos = 0;
	}

	if (ins->envPTyp & 1)
	{
		ch->envPCnt = 65535;
		ch->envPPos = 0;
	}

	ch->fadeOutSpeed = ins->fadeOut; // 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;
		}
	}
}

void keyOff(stmTyp *ch)
{
	instrTyp *ins;

	ch->envSustainActive = false;

	ins = ch->instrSeg;
	assert(ins != NULL);

	if (!(ins->envPTyp & 1)) // yes, FT2 does this (!). Most likely a bug?
	{
		if (ch->envPCnt >= ins->envPP[ch->envPPos][0])
			ch->envPCnt = ins->envPP[ch->envPPos][0] - 1;
	}

	if (ins->envVTyp & 1)
	{
		if (ch->envVCnt >= 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;
	}
}

void calcReplayerLogTab(void)
{
	for (int32_t i = 0; i < 768; i++)
		dLogTab[i] = exp2(i / 768.0) * (8363.0 * 256.0);
}

void calcReplayRate(int32_t audioFreq)
{
	if (audioFreq == 0)
		return;

	dHz2MixDeltaMul = (double)MIXER_FRAC_SCALE / audioFreq;
	audio.quickVolRampSamples = (int32_t)((audioFreq / 200.0) + 0.5); // rounded
	audio.fRampQuickVolMul = 1.0f / audio.quickVolRampSamples;

	audio.dSamplesPerTickTab[0] = 0.0;
	audio.tickTimeLengthTab[0] = UINT64_MAX;
	audio.fRampTickMulTab[0] = 0.0f;

	for (int32_t i = MIN_BPM; i <= MAX_BPM; i++)
	{
		const double dBpmHz = i * (1.0 / 2.5); // i / 2.5
		const double dSamplesPerTick = audioFreq / dBpmHz;
		audio.dSamplesPerTickTab[i] = dSamplesPerTick;

		// BPM Hz -> tick length for performance counter (syncing visuals to audio)
		double dTimeInt;
		double dTimeFrac = modf(editor.dPerfFreq / dBpmHz, &dTimeInt);
		const int32_t timeInt = (int32_t)dTimeInt;

		dTimeFrac *= UINT32_MAX+1.0; // fractional part (scaled to 0..2^32-1)

		audio.tickTimeLengthTab[i] = ((uint64_t)timeInt << 32) | (uint32_t)dTimeFrac;

		// for calculating volume ramp length for tick-lenghted ramps
		const int32_t samplesPerTick = (int32_t)(dSamplesPerTick + 0.5); // this has to be rounded first
		audio.fRampTickMulTab[i] = 1.0f / samplesPerTick;
	}
}

double dPeriod2Hz(uint16_t period)
{
	return dPeriod2HzTab[period];
}

int64_t getMixerDelta(uint16_t period)
{
	/* Precision has been tested for most extreme case (Amiga period 1, 44100Hz),
	** and there is no precision loss using 64-bit double-precision here, even
	** though we can get very big numbers.
	*/
	return (int64_t)((dPeriod2Hz(period) * dHz2MixDeltaMul) + 0.5); // Hz -> rounded 32.32 fixed-point mixer delta
}

uint32_t getRevMixerDelta(uint16_t period)
{
	return revMixDeltaTab[period];
}

int32_t getPianoKey(uint16_t period, int32_t finetune, int32_t relativeNote) // for piano in Instr. Ed.
{
	finetune >>= 3; // FT2 does this in the replayer internally, so the actual range is -16..15

	const double dRelativeHz = dPeriod2Hz(period) * (1.0 / (8363.0 / 16.0));
	const double dNote = (log2(dRelativeHz) * 12.0) - (finetune * (1.0 / 16.0));

	const int32_t note = (int32_t)(dNote + 0.5) - relativeNote; // rounded

	// "note" is now the raw piano key number, unaffected by finetune and relativeNote
	return note;
}

static void startTone(uint8_t ton, uint8_t effTyp, uint8_t eff, stmTyp *ch)
{
	uint8_t smp;
	uint16_t tmpTon;
	sampleTyp *s;
	instrTyp *ins;

	if (ton == 97)
	{
		keyOff(ch);
		return;
	}

	// if we came from Rxy (retrig), we didn't check note (Ton) yet
	if (ton == 0)
	{
		ton = ch->tonNr;
		if (ton == 0)
			return; // if still no note, exit from routine
	}

	ch->tonNr = ton;

	assert(ch->instrNr <= 130);

	ins = instr[ch->instrNr];
	if (ins == NULL)
		ins = instr[0];

	ch->instrSeg = ins;
	ch->mute = ins->mute;

	if (ton > 96) // non-FT2 security (should never happen because I clamp in the patt. loader now)
		ton = 96;

	smp = ins->ta[ton-1] & 0xF;
	ch->sampleNr = smp;

	s = &ins->samp[smp];
	ch->smpPtr = s;
	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; // result is now -128..127
	else
		ch->fineTune = s->fine;

	if (ton != 0)
	{
		tmpTon = ((ton - 1) << 4) + (((ch->fineTune >> 3) + 16) & 0xFF);
		if (tmpTon < MAX_NOTES)
		{
			assert(note2Period != NULL);
			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;
	}
}

static void multiRetrig(stmTyp *ch)
{
	uint8_t cnt;
	int16_t vol;

	cnt = ch->retrigCnt + 1;
	if (cnt < ch->retrigSpeed)
	{
		ch->retrigCnt = cnt;
		return;
	}

	ch->retrigCnt = 0;

	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; // 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);
}

static void checkMoreEffects(stmTyp *ch) // called even if channel is muted
{
	int8_t envPos;
	bool envUpdate;
	uint8_t tmpEff;
	int16_t newEnvPos;
	uint16_t i;
	instrTyp *ins;

	ins = ch->instrSeg;
	assert(ins != NULL);

	// Bxx - position jump
	if (ch->effTyp == 11)
	{
		if (playMode != PLAYMODE_PATT && playMode != PLAYMODE_RECPATT)
		{
			if (ch->eff-1 < 0 || ch->eff-1 >= song.len)
				bxxOverflow = true; // non-FT2 security fix...
			else
				song.songPos = ch->eff - 1;
		}

		song.pBreakPos = 0;
		song.posJumpFlag = true;
	}

	// Dxx - pattern break
	else if (ch->effTyp == 13)
	{
		song.posJumpFlag = true;

		tmpEff = ((ch->eff >> 4) * 10) + (ch->eff & 0x0F);
		if (tmpEff <= 63)
			song.pBreakPos = tmpEff;
		else
			song.pBreakPos = 0;
	}

	// Exx - E effects
	else if (ch->effTyp == 14)
	{
		if (ch->stOff) // channel is muted
		{
			// E6x - pattern loop
			if ((ch->eff & 0xF0) == 0x60)
			{
				if (ch->eff == 0x60) // E60, empty param
				{
					ch->pattPos = song.pattPos & 0x00FF;
				}
				else if (ch->loopCnt == 0)
				{
					ch->loopCnt = ch->eff & 0x0F;

					song.pBreakPos = ch->pattPos;
					song.pBreakFlag = true;
				}
				else if (--ch->loopCnt > 0)
				{
					song.pBreakPos = ch->pattPos;
					song.pBreakFlag = true;
				}
			}

			// EEx - pattern delay
			else if ((ch->eff & 0xF0) == 0xE0)
			{
				if (song.pattDelTime2 == 0)
					song.pattDelTime = (ch->eff & 0x0F) + 1;
			}

			return;
		}

		// E1x - fine period slide up
		if ((ch->eff & 0xF0) == 0x10)
		{
			tmpEff = ch->eff & 0x0F;
			if (tmpEff == 0)
				tmpEff = ch->fPortaUpSpeed;

			ch->fPortaUpSpeed = tmpEff;

			ch->realPeriod -= tmpEff << 2;
			if ((int16_t)ch->realPeriod < 1)
				ch->realPeriod = 1;

			ch->outPeriod = ch->realPeriod;
			ch->status |= IS_Period;
		}

		// E2x - fine period slide down
		else if ((ch->eff & 0xF0) == 0x20)
		{
			tmpEff = ch->eff & 0x0F;
			if (tmpEff == 0)
				tmpEff = ch->fPortaDownSpeed;

			ch->fPortaDownSpeed = tmpEff;

			ch->realPeriod += tmpEff << 2;
			if ((int16_t)ch->realPeriod > 32000-1)
				ch->realPeriod = 32000-1;

			ch->outPeriod = ch->realPeriod;
			ch->status |= IS_Period;
		}

		// E3x - set glissando type
		else if ((ch->eff & 0xF0) == 0x30) ch->glissFunk = ch->eff & 0x0F;

		// E4x - set vibrato waveform
		else if ((ch->eff & 0xF0) == 0x40) ch->waveCtrl = (ch->waveCtrl & 0xF0) | (ch->eff & 0x0F);

		// E5x (set finetune) is handled in startTone()

		// E6x - pattern loop
		else if ((ch->eff & 0xF0) == 0x60)
		{
			if (ch->eff == 0x60) // E60, empty param
			{
				ch->pattPos = song.pattPos & 0xFF;
			}
			else if (ch->loopCnt == 0)
			{
				ch->loopCnt = ch->eff & 0x0F;

				song.pBreakPos = ch->pattPos;
				song.pBreakFlag = true;
			}
			else if (--ch->loopCnt > 0)
			{
				song.pBreakPos = ch->pattPos;
				song.pBreakFlag = true;
			}
		}

		// E7x - set tremolo waveform
		else if ((ch->eff & 0xF0) == 0x70) ch->waveCtrl = ((ch->eff & 0x0F) << 4) | (ch->waveCtrl & 0x0F);

		// EAx - fine volume slide up
		else if ((ch->eff & 0xF0) == 0xA0)
		{
			tmpEff = ch->eff & 0x0F;
			if (tmpEff == 0)
				tmpEff = ch->fVolSlideUpSpeed;

			ch->fVolSlideUpSpeed = tmpEff;

			ch->realVol += tmpEff;
			if (ch->realVol > 64)
				ch->realVol = 64;

			ch->outVol = ch->realVol;
			ch->status |= IS_Vol;
		}

		// EBx - fine volume slide down
		else if ((ch->eff & 0xF0) == 0xB0)
		{
			tmpEff = ch->eff & 0x0F;
			if (tmpEff == 0)
				tmpEff = ch->fVolSlideDownSpeed;

			ch->fVolSlideDownSpeed = tmpEff;

			ch->realVol -= tmpEff;
			if ((int8_t)ch->realVol < 0)
				ch->realVol = 0;

			ch->outVol = ch->realVol;
			ch->status |= IS_Vol;
		}

		// ECx - note cut
		else if ((ch->eff & 0xF0) == 0xC0)
		{
			if (ch->eff == 0xC0) // empty param
			{
				ch->realVol = 0;
				ch->outVol = 0;
				ch->status |= IS_Vol + IS_QuickVol;
			}
		}

		// EEx - pattern delay
		else if ((ch->eff & 0xF0) == 0xE0)
		{
			if (song.pattDelTime2 == 0)
				song.pattDelTime = (ch->eff & 0x0F) + 1;
		}
	}

	// Fxx - set speed/tempo
	else if (ch->effTyp == 15)
	{
		if (ch->eff >= 32)
		{
			song.speed = ch->eff;
			setSpeed(song.speed);
		}
		else
		{
			song.timer = song.tempo = ch->eff;
		}
	}

	// Gxx - set global volume
	else if (ch->effTyp == 16)
	{
		song.globVol = ch->eff;
		if (song.globVol > 64)
			song.globVol = 64;

		for (i = 0; i < song.antChn; i++) // update all voice volumes
			stm[i].status |= IS_Vol;
	}

	// Lxx - set vol and pan envelope position
	else if (ch->effTyp == 21)
	{
		// *** VOLUME ENVELOPE ***
		if (ins->envVTyp & 1)
		{
			ch->envVCnt = ch->eff - 1;

			envPos = 0;
			envUpdate = true;
			newEnvPos = ch->eff;

			if (ins->envVPAnt > 1)
			{
				envPos++;
				for (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) // probably an FT2 bug
		{
			ch->envPCnt = ch->eff - 1;

			envPos = 0;
			envUpdate = true;
			newEnvPos = ch->eff;

			if (ins->envPPAnt > 1)
			{
				envPos++;
				for (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;
		}
	}
}

static void checkEffects(stmTyp *ch)
{
	uint8_t tmpEff, tmpEffHi, volKol;

	/* This one is manipulated by vol column effects,
	** then used for multiretrig vol testing (FT2 quirk).
	*/
	volKol = ch->volKolVol;

	// *** VOLUME COLUMN EFFECTS (TICK 0) ***

	// set volume
	if (ch->volKolVol >= 0x10 && ch->volKolVol <= 0x50)
	{
		volKol -= 16;

		ch->outVol = volKol;
		ch->realVol = volKol;

		ch->status |= IS_Vol + IS_QuickVol;
	}

	// fine volume slide down
	else if ((ch->volKolVol & 0xF0) == 0x80)
	{
		volKol = ch->volKolVol & 0x0F;

		ch->realVol -= volKol;
		if ((int8_t)ch->realVol < 0)
			ch->realVol = 0;

		ch->outVol = ch->realVol;
		ch->status |= IS_Vol;
	}

	// fine volume slide up
	else if ((ch->volKolVol & 0xF0) == 0x90)
	{
		volKol = ch->volKolVol & 0x0F;

		ch->realVol += volKol;
		if (ch->realVol > 64)
			ch->realVol = 64;

		ch->outVol = ch->realVol;
		ch->status |= IS_Vol;
	}

	// set vibrato speed
	else if ((ch->volKolVol & 0xF0) == 0xA0)
	{
		volKol = (ch->volKolVol & 0x0F) << 2;
		ch->vibSpeed = volKol;
	}

	// set panning
	else if ((ch->volKolVol & 0xF0) == 0xC0)
	{
		volKol <<= 4;

		ch->outPan = volKol;
		ch->status |= IS_Pan;
	}

	// *** MAIN EFFECTS (TICK 0) ***

	if (ch->effTyp == 0 && ch->eff == 0) return; // no effect

	// Cxx - set volume
	if (ch->effTyp == 12)
	{
		ch->realVol = ch->eff;
		if (ch->realVol > 64)
			ch->realVol = 64;

		ch->outVol = ch->realVol;
		ch->status |= IS_Vol + IS_QuickVol;

		return;
	}

	// 8xx - set panning
	else if (ch->effTyp == 8)
	{
		ch->outPan = ch->eff;
		ch->status |= IS_Pan;

		return;
	}

	// Rxy - note multi retrigger
	else if (ch->effTyp == 27)
	{
		tmpEff = ch->eff & 0x0F;
		if (tmpEff == 0)
			tmpEff = ch->retrigSpeed;

		ch->retrigSpeed = tmpEff;

		tmpEffHi = ch->eff >> 4;
		if (tmpEffHi == 0)
			tmpEffHi = ch->retrigVol;

		ch->retrigVol = tmpEffHi;

		if (volKol == 0)
			multiRetrig(ch);

		return;
	}

	// X1x - extra fine period slide up
	else if (ch->effTyp == 33 && (ch->eff & 0xF0) == 0x10)
	{
		tmpEff = ch->eff & 0x0F;
		if (tmpEff == 0)
			tmpEff = ch->ePortaUpSpeed;

		ch->ePortaUpSpeed = tmpEff;

		ch->realPeriod -= tmpEff;
		if ((int16_t)ch->realPeriod < 1)
			ch->realPeriod = 1;

		ch->outPeriod = ch->realPeriod;
		ch->status |= IS_Period;

		return;
	}

	// X2x - extra fine period slide down
	else if (ch->effTyp == 33 && (ch->eff & 0xF0) == 0x20)
	{
		tmpEff = ch->eff & 0x0F;
		if (tmpEff == 0)
			tmpEff = ch->ePortaDownSpeed;

		ch->ePortaDownSpeed = tmpEff;

		ch->realPeriod += tmpEff;
		if ((int16_t)ch->realPeriod > 32000-1)
			ch->realPeriod = 32000-1;

		ch->outPeriod = ch->realPeriod;
		ch->status |= IS_Period;

		return;
	}

	checkMoreEffects(ch);
}

static void fixTonePorta(stmTyp *ch, const tonTyp *p, uint8_t inst)
{
	uint16_t portaTmp;

	if (p->ton > 0)
	{
		if (p->ton == 97)
		{
			keyOff(ch);
		}
		else
		{
			portaTmp = ((((p->ton - 1) + ch->relTonNr) & 0xFF) * 16) + (((ch->fineTune >> 3) + 16) & 0xFF);
			if (portaTmp < MAX_NOTES)
			{
				assert(note2Period != NULL);
				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)
{
	uint8_t inst;
	bool checkEfx;

	ch->volKolVol = p->vol;

	if (ch->effTyp == 0)
	{
		if (ch->eff > 0)
		{
			// we have an arpeggio running, set period back
			ch->outPeriod = ch->realPeriod;
			ch->status |= IS_Period;
		}
	}
	else
	{
		if (ch->effTyp == 4 || ch->effTyp == 6)
		{
			// we have a vibrato running
			if (p->effTyp != 4 && p->effTyp != 6)
			{
				// 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;

	if (ch->stOff)
	{
		checkMoreEffects(ch);
		return;
	}

	// 'inst' var is used for later if checks...
	inst = p->instr;
	if (inst > 0)
	{
		if (inst <= MAX_INST)
			ch->instrNr = inst;
		else
			inst = 0;
	}

	checkEfx = true;
	if (p->effTyp == 0x0E)
	{
		if (p->eff >= 0xD1 && p->eff <= 0xDF)
			return; // we have a note delay (ED1..EDF)
		else if (p->eff == 0x90)
			checkEfx = false;
	}

	if (checkEfx)
	{
		if ((ch->volKolVol & 0xF0) == 0xF0) // gxx
		{
			if ((ch->volKolVol & 0x0F) > 0)
				ch->portaSpeed = (ch->volKolVol & 0x0F) << 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 tmpPeriod, autoVibAmp, envVal;
	float fVol;
	instrTyp *ins;

	ins = ch->instrSeg;

	assert(ins != NULL);

	// *** FADEOUT ***
	if (!ch->envSustainActive)
	{
		ch->status |= IS_Vol;

		// 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;
				}
			}

			fVol  = song.globVol   * (1.0f / 64.0f);
			fVol *= ch->outVol     * (1.0f / 64.0f);
			fVol *= ch->fadeOutAmp * (1.0f / 32768.0f);
			fVol *= envVal         * (1.0f / 16384.0f);

			ch->status |= IS_Vol; // update vol every tick because vol envelope is enabled
		}
		else
		{
			fVol  = song.globVol   * (1.0f / 64.0f);
			fVol *= ch->outVol     * (1.0f / 64.0f);
			fVol *= ch->fadeOutAmp * (1.0f / 32768.0f);
		}

		if (fVol > 1.0f) // shouldn't happen, but just in case...
			fVol = 1.0f;

		ch->fFinalVol = fVol;
	}
	else
	{
		ch->fFinalVol = 0.0f;
	}

	// *** 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;
			}
		}

		const int32_t panTmp = 128 - ABS(ch->outPan - 128);
		const int32_t panEnv = (int32_t)envVal - (32*256); // -8192..7936
		const int32_t panAdd = (int32_t)roundf((panTmp * panEnv) * (1.0f / 8192.0f)); // -128..124
		ch->finalPan = (uint8_t)CLAMP(ch->outPan + panAdd, 0, 255);

		ch->status |= IS_Pan; // update pan every tick because pan envelope is enabled
	}
	else
	{
		ch->finalPan = ch->outPan;
	}

	// *** AUTO VIBRATO ***
#ifdef HAS_MIDI
	if (ch->midiVibDepth > 0 || ins->vibDepth > 0)
#else
	if (ins->vibDepth > 0)
#endif
	{
		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;
		}

#ifdef HAS_MIDI
		// non-FT2 hack to make modulation wheel work when auto vibrato rate is zero
		if (ch->midiVibDepth > 0 && ins->vibRate == 0)
			ins->vibRate = 0x20;

		autoVibAmp += ch->midiVibDepth;
#endif
		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;
		tmpPeriod = (autoVibVal * (int16_t)autoVibAmp) >> 16;

		tmpPeriod += ch->outPeriod;
		if (tmpPeriod > 32000-1)
			tmpPeriod = 0; // yes, FT2 does this (!)

#ifdef HAS_MIDI
		if (midi.enable)
			tmpPeriod -= ch->midiPitch;
#endif

		ch->finalPeriod = tmpPeriod;
		ch->status |= IS_Period;
	}
	else
	{
		ch->finalPeriod = ch->outPeriod;

#ifdef HAS_MIDI
		if (midi.enable)
		{
			ch->finalPeriod -= ch->midiPitch;
			ch->status |= IS_Period;
		}
#endif
	}
}

// for arpeggio and portamento (semitone-slide mode)
static uint16_t relocateTon(uint16_t period, uint8_t arpNote, stmTyp *ch)
{
	int32_t fineTune, loPeriod, hiPeriod, tmpPeriod, tableIndex;

	fineTune = ((ch->fineTune >> 3) + 16) << 1;
	hiPeriod = (8 * 12 * 16) * 2;
	loPeriod = 0;

	for (int32_t i = 0; i < 8; i++)
	{
		tmpPeriod = (((loPeriod + hiPeriod) >> 1) & 0xFFFFFFE0) + fineTune;

		tableIndex = (uint32_t)(tmpPeriod - 16) >> 1;
		tableIndex = CLAMP(tableIndex, 0, 1935); // 8bitbubsy: added security check

		if (period >= note2Period[tableIndex])
			hiPeriod = (tmpPeriod - fineTune) & 0xFFFFFFE0;
		else
			loPeriod = (tmpPeriod - fineTune) & 0xFFFFFFE0;
	}

	tmpPeriod = loPeriod + fineTune + (arpNote << 5);

	if (tmpPeriod < 0) // 8bitbubsy: added security check
		tmpPeriod = 0;

	if (tmpPeriod >= (8*12*16+15)*2-1) // FT2 bug: off-by-one edge case
		tmpPeriod = (8*12*16+15)*2;

	return note2Period[(uint32_t)tmpPeriod>>1];
}

static void tonePorta(stmTyp *ch)
{
	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) // semitone-slide flag
		ch->outPeriod = relocateTon(ch->realPeriod, 0, ch);
	else
		ch->outPeriod = ch->realPeriod;

	ch->status |= IS_Period;
}

static void volume(stmTyp *ch) // actually volume slide
{
	uint8_t tmpEff = ch->eff;
	if (tmpEff == 0)
		tmpEff = ch->volSlideSpeed;

	ch->volSlideSpeed = tmpEff;

	if ((tmpEff & 0xF0) == 0)
	{
		ch->realVol -= tmpEff;
		if ((int8_t)ch->realVol < 0)
			ch->realVol = 0;
	}
	else
	{
		tmpEff >>= 4;

		ch->realVol += tmpEff;
		if (ch->realVol > 64)
			ch->realVol = 64;
	}

	ch->outVol = ch->realVol;
	ch->status |= IS_Vol;
}

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; // logical shift (unsigned calc.), not arithmetic shift

	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 vibrato(stmTyp *ch)
{
	uint8_t tmp8;

	if (ch->eff > 0)
	{
		tmp8 = ch->eff & 0x0F;
		if (tmp8 > 0)
			ch->vibDepth = tmp8;

		tmp8 = (ch->eff & 0xF0) >> 2;
		if (tmp8 > 0)
			ch->vibSpeed = tmp8;
	}

	vibrato2(ch);
}

static void doEffects(stmTyp *ch)
{
	int8_t note;
	uint8_t tmp8, tmpEff, tremorData, tremorSign, tmpTrem;
	int16_t tremVol, tmp16;
	uint16_t i, tick;

	if (ch->stOff)
		return; // muted

	// *** VOLUME COLUMN EFFECTS (TICKS >0) ***

	// volume slide down
	if ((ch->volKolVol & 0xF0) == 0x60)
	{
		ch->realVol -= ch->volKolVol & 0x0F;
		if ((int8_t)ch->realVol < 0)
			ch->realVol = 0;

		ch->outVol = ch->realVol;
		ch->status |= IS_Vol;
	}

	// volume slide up
	else if ((ch->volKolVol & 0xF0) == 0x70)
	{
		ch->realVol += ch->volKolVol & 0x0F;
		if (ch->realVol > 64)
			ch->realVol = 64;

		ch->outVol = ch->realVol;
		ch->status |= IS_Vol;
	}

	// vibrato (+ set vibrato depth)
	else if ((ch->volKolVol & 0xF0) == 0xB0)
	{
		if (ch->volKolVol != 0xB0)
			ch->vibDepth = ch->volKolVol & 0x0F;

		vibrato2(ch);
	}

	// pan slide left
	else if ((ch->volKolVol & 0xF0) == 0xD0)
	{
		tmp16 = (int16_t)ch->outPan - (ch->volKolVol & 0x0F);
		if (tmp16 < 0 || (ch->volKolVol & 0x0F) == 0) // FT2 bug: param 0 = pan gets set to 0
			tmp16 = 0;

		ch->outPan = (uint8_t)tmp16;
		ch->status |= IS_Pan;
	}

	// pan slide right
	else if ((ch->volKolVol & 0xF0) == 0xE0)
	{
		tmp16 = (int16_t)ch->outPan + (ch->volKolVol & 0x0F);
		if (tmp16 > 255)
			tmp16 = 255;

		ch->outPan = (uint8_t)tmp16;
		ch->status |= IS_Pan;
	}

	// tone portamento
	else if ((ch->volKolVol & 0xF0) == 0xF0) tonePorta(ch);

	// *** MAIN EFFECTS (TICKS >0) ***

	if ((ch->eff == 0 && ch->effTyp == 0) || ch->effTyp >= 36) return; // no effect

	// 0xy - Arpeggio
	if (ch->effTyp == 0)
	{
		int16_t timer = song.timer;

		/* Non-FT2 protection for our extended 100-byte arp table.
		** (this shouldn't happen, but just in case)
		*/
		if (timer > 99)
			timer = 99;

		tick = arpTab[timer];
		if (tick == 0)
		{
			ch->outPeriod = ch->realPeriod;
		}
		else
		{
			if (tick == 1)
				note = ch->eff >> 4;
			else
				note = ch->eff & 0x0F; // tick 2

			ch->outPeriod = relocateTon(ch->realPeriod, note, ch);
		}

		ch->status |= IS_Period;
	}

	// 1xx - period slide up
	else if (ch->effTyp == 1)
	{
		tmpEff = ch->eff;
		if (tmpEff == 0)
			tmpEff = ch->portaUpSpeed;

		ch->portaUpSpeed = tmpEff;

		ch->realPeriod -= tmpEff << 2;
		if ((int16_t)ch->realPeriod < 1)
			ch->realPeriod = 1;

		ch->outPeriod = ch->realPeriod;
		ch->status |= IS_Period;
	}

	// 2xx - period slide down
	else if (ch->effTyp == 2)
	{
		tmpEff = ch->eff;
		if (tmpEff == 0)
			tmpEff = ch->portaDownSpeed;

		ch->portaDownSpeed = tmpEff;

		ch->realPeriod += tmpEff << 2;
		if ((int16_t)ch->realPeriod > 32000-1) // FT2 bug, should've been unsigned comparison
			ch->realPeriod = 32000-1;

		ch->outPeriod = ch->realPeriod;
		ch->status |= IS_Period;
	}

	// 3xx - tone portamento
	else if (ch->effTyp == 3) tonePorta(ch);

	// 4xy - vibrato
	else if (ch->effTyp == 4) vibrato(ch);

	// 5xy - tone portamento + volume slide
	else if (ch->effTyp == 5)
	{
		tonePorta(ch);
		volume(ch);
	}

	// 6xy - vibrato + volume slide
	else if (ch->effTyp == 6)
	{
		vibrato2(ch);
		volume(ch);
	}

	// 7xy - tremolo
	else if (ch->effTyp == 7)
	{
		tmpEff = ch->eff;
		if (tmpEff > 0)
		{
			tmp8 = tmpEff & 0x0F;
			if (tmp8 > 0)
				ch->tremDepth = tmp8;

			tmp8 = (tmpEff & 0xF0) >> 2;
			if (tmp8 > 0)
				ch->tremSpeed = tmp8;
		}

		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) // FT2 bug, should've been ch->tremPos
					tmpTrem = ~tmpTrem;
			}
			break;

			// 2/3: square
			default: tmpTrem = 255; break;
		}
		tmpTrem = (tmpTrem * ch->tremDepth) >> 6; // logical shift (unsigned calc.), not arithmetic shift

		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;
	}

	// Axy - volume slide
	else if (ch->effTyp == 10) volume(ch); // actually volume slide

	// Exy - E effects
	else if (ch->effTyp == 14)
	{
		// E9x - note retrigger
		if ((ch->eff & 0xF0) == 0x90)
		{
			if (ch->eff != 0x90) // E90 is handled in getNewNote()
			{
				if ((song.tempo-song.timer) % (ch->eff & 0x0F) == 0)
				{
					startTone(0, 0, 0, ch);
					retrigEnvelopeVibrato(ch);
				}
			}
		}

		// ECx - note cut
		else if ((ch->eff & 0xF0) == 0xC0)
		{
			if (((song.tempo-song.timer) & 0xFF) == (ch->eff & 0x0F))
			{
				ch->outVol = 0;
				ch->realVol = 0;
				ch->status |= IS_Vol + IS_QuickVol;
			}
		}

		// EDx - note delay
		else if ((ch->eff & 0xF0) == 0xD0)
		{
			if (((song.tempo-song.timer) & 0xFF) == (ch->eff & 0x0F))
			{
				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;
				}
			}
		}
	}

	// Hxy - global volume slide
	else if (ch->effTyp == 17)
	{
		tmpEff = ch->eff;
		if (tmpEff == 0)
			tmpEff = ch->globVolSlideSpeed;

		ch->globVolSlideSpeed = tmpEff;

		if ((tmpEff & 0xF0) == 0)
		{
			song.globVol -= tmpEff;
			if ((int8_t)song.globVol < 0)
				song.globVol = 0;
		}
		else
		{
			tmpEff >>= 4;

			song.globVol += tmpEff;
			if (song.globVol > 64)
				song.globVol = 64;
		}

		for (i = 0; i < song.antChn; i++) // update all voice volumes
			stm[i].status |= IS_Vol;
	}

	// Kxx - key off
	else if (ch->effTyp == 20)
	{
		if (((song.tempo-song.timer) & 31) == (ch->eff & 0x0F))
			keyOff(ch);
	}

	// Pxy - panning slide
	else if (ch->effTyp == 25)
	{
		tmpEff = ch->eff;
		if (tmpEff == 0)
			tmpEff = ch->panningSlideSpeed;

		ch->panningSlideSpeed = tmpEff;

		if ((tmpEff & 0xF0) == 0)
		{
			tmp16 = (int16_t)ch->outPan - tmpEff;
			if (tmp16 < 0)
				tmp16 = 0;
		}
		else
		{
			tmpEff >>= 4;

			tmp16 = (int16_t)ch->outPan + tmpEff;
			if (tmp16 > 255)
				tmp16 = 255;
		}
		
		ch->outPan = (uint8_t)tmp16;
		ch->status |= IS_Pan;
	}

	// Rxy - multi note retrig
	else if (ch->effTyp == 27) multiRetrig(ch);

	// Txy - tremor
	else if (ch->effTyp == 29)
	{
		tmpEff = ch->eff;
		if (tmpEff == 0)
			tmpEff = ch->tremorSave;

		ch->tremorSave = tmpEff;

		tremorSign = ch->tremorPos & 0x80;
		tremorData = ch->tremorPos & 0x7F;

		tremorData--;
		if ((int8_t)tremorData < 0)
		{
			if (tremorSign == 0x80)
			{
				tremorSign = 0x00;
				tremorData = tmpEff & 0x0F;
			}
			else
			{
				tremorSign = 0x80;
				tremorData = tmpEff >> 4;
			}
		}

		ch->tremorPos = tremorSign | tremorData;

		ch->outVol = (tremorSign == 0x80) ? ch->realVol : 0;
		ch->status |= IS_Vol + IS_QuickVol;
	}
}

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;

		if (playMode != PLAYMODE_PATT && playMode != PLAYMODE_RECPATT)
		{
			if (bxxOverflow)
			{
				song.songPos = 0;
				bxxOverflow = false;
			}
			else if (++song.songPos >= song.len)
			{
				editor.wavReachedEndFlag = true;
				song.songPos = song.repS;
			}

			assert(song.songPos <= 255);
			song.pattNr = song.songTab[song.songPos & 0xFF];
			song.pattLen = pattLens[song.pattNr & 0xFF];
		}
	}
}

void pauseMusic(void) // stops reading pattern data
{
	musicPaused = true;
	while (replayerBusy);
}

void resumeMusic(void) // starts reading pattern data
{
	musicPaused = false;
}


void tickReplayer(void) // periodically called from audio callback
{
	int32_t i;
	stmTyp *c;

	if (musicPaused || !songPlaying)
	{
		c = stm;
		for (i = 0; i < song.antChn; i++, c++)
			fixaEnvelopeVibrato(c);

		return;
	}

	// for song playback counter (hh:mm:ss)
	if (song.speed >= MIN_BPM && song.speed <= MAX_BPM)
		song.musicTime64 += musicTimeTab64[song.speed];

	bool tickZero = false;
	if (--song.timer == 0)
	{
		song.timer = song.tempo;
		tickZero = true;
	}
	
	// for visuals
	song.curReplayerTimer = (uint8_t)song.timer;
	song.curReplayerPattPos = (uint8_t)song.pattPos;
	song.curReplayerPattNr = (uint8_t)song.pattNr;
	song.curReplayerSongPos = (uint8_t)song.songPos;

	const bool readNewNote = tickZero && song.pattDelTime2 == 0;
	if (readNewNote)
	{
		const tonTyp *pattPtr = nilPatternLine;
		if (patt[song.pattNr] != NULL)
		{
			assert(song.pattNr  >= 0 && song.pattNr  < MAX_PATTERNS &&
			       song.pattPos >= 0 && song.pattPos < MAX_PATT_LEN);

			pattPtr = &patt[song.pattNr][song.pattPos * MAX_VOICES];
		}

		c = stm;
		for (i = 0; i < song.antChn; i++, c++, pattPtr++)
		{
			getNewNote(c, pattPtr);
			fixaEnvelopeVibrato(c);
		}
	}
	else
	{
		c = stm;
		for (i = 0; i < song.antChn; i++, c++)
		{
			doEffects(c);
			fixaEnvelopeVibrato(c);
		}
	}

	getNextPos();
}

void resetMusic(void)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	song.timer = 1;
	stopVoices();

	if (audioWasntLocked)
		unlockAudio();

	setPos(0, 0, false);

	if (!songPlaying)
	{
		setScrollBarEnd(SB_POS_ED, (song.len - 1) + 5);
		setScrollBarPos(SB_POS_ED, 0, false);
	}
}

void setPos(int16_t songPos, int16_t pattPos, bool resetTimer)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (songPos > -1)
	{
		song.songPos = songPos;
		if (song.len > 0 && song.songPos >= song.len)
			song.songPos = song.len - 1;

		song.pattNr = song.songTab[songPos];
		assert(song.pattNr < MAX_PATTERNS);
		song.pattLen = pattLens[song.pattNr];

		checkMarkLimits(); // non-FT2 safety
	}

	if (pattPos > -1)
	{
		song.pattPos = pattPos;
		if (song.pattPos >= song.pattLen)
			song.pattPos = song.pattLen - 1;
	}

	// if not playing, update local position variables
	if (!songPlaying)
	{
		if (pattPos > -1)
		{
			editor.pattPos = (uint8_t)pattPos;
			ui.updatePatternEditor = true;
		}

		if (songPos > -1)
		{
			editor.editPattern = (uint8_t)song.pattNr;
			editor.songPos = song.songPos;
			ui.updatePosSections = true;
		}
	}

	if (resetTimer)
		song.timer = 1;

	if (audioWasntLocked)
		unlockAudio();
}

void delta2Samp(int8_t *p, int32_t len, uint8_t typ)
{
	int8_t *p8, news8, olds8L, olds8R;
	int16_t *p16, news16, olds16L, olds16R, tmp16;
	int32_t i, tmp32;

	if (typ & 16) len /= 2; // 16-bit
	if (typ & 32) len /= 2; // stereo

	if (typ & 32)
	{
		if (typ & 16)
		{
			p16 = (int16_t *)p;

			olds16L = 0;
			olds16R = 0;

			for (i = 0; i < len; i++)
			{
				news16 = p16[i] + olds16L;
				p16[i] = news16;
				olds16L = news16;

				news16 = p16[len+i] + olds16R;
				p16[len+i] = news16;
				olds16R = news16;

				tmp32 = olds16L + olds16R;
				p16[i] = (int16_t)(tmp32 >> 1);
			}
		}
		else
		{
			p8 = (int8_t *)p;

			olds8L = 0;
			olds8R = 0;

			for (i = 0; i < len; i++)
			{
				news8 = p8[i] + olds8L;
				p8[i] = news8;
				olds8L = news8;

				news8 = p8[len+i] + olds8R;
				p8[len+i] = news8;
				olds8R = news8;

				tmp16 = olds8L + olds8R;
				p8[i] = (int8_t)(tmp16 >> 1);
			}
		}
	}
	else
	{
		if (typ & 16)
		{
			p16 = (int16_t *)p;

			olds16L = 0;
			for (i = 0; i < len; i++)
			{
				news16 = p16[i] + olds16L;
				p16[i] = news16;
				olds16L = news16;
			}
		}
		else
		{
			p8 = (int8_t *)p;

			olds8L = 0;
			for (i = 0; i < len; i++)
			{
				news8 = p8[i] + olds8L;
				p8[i] = news8;
				olds8L = news8;
			}
		}
	}
}

void samp2Delta(int8_t *p, int32_t len, uint8_t typ)
{
	int8_t *p8, news8, olds8;
	int16_t *p16, news16, olds16;
	int32_t i;

	if (typ & 16) len /= 2; // 16-bit

	if (typ & 16)
	{
		p16 = (int16_t *)p;

		news16 = 0;
		for (i = 0; i < len; i++)
		{
			olds16 = p16[i];
			p16[i] -= news16;
			news16 = olds16;
		}
	}
	else
	{
		p8 = (int8_t *)p;

		news8 = 0;
		for (i = 0; i < len; i++)
		{
			olds8 = p8[i];
			p8[i] -= news8;
			news8 = olds8;
		}
	}
}

bool allocateInstr(int16_t nr)
{
	if (instr[nr] != NULL)
		return false; // already allocated

	instrTyp *p = (instrTyp *)malloc(sizeof (instrTyp));
	if (p == NULL)
		return false;

	memset(p, 0, sizeof (instrTyp));

	for (int32_t i = 0; i < MAX_SMP_PER_INST; i++) // set standard sample pan/vol
	{
		p->samp[i].pan = 128;
		p->samp[i].vol = 64;
	}

	setStdEnvelope(p, 0, 3);

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	instr[nr] = p;

	if (audioWasntLocked)
		unlockAudio();

	return true;
}

void freeInstr(int32_t nr)
{
	if (instr[nr] == NULL)
		return; // not allocated

	pauseAudio(); // channel instrument pointers are now cleared

	for (int32_t i = 0; i < MAX_SMP_PER_INST; i++) // free sample data
	{
		sampleTyp *s = &instr[nr]->samp[i];
		if (s->origPek != NULL)
			free(s->origPek);
	}

	free(instr[nr]);
	instr[nr] = NULL;
	
	resumeAudio();
}

void freeAllInstr(void)
{
	pauseAudio(); // channel instrument pointers are now cleared
	for (int16_t i = 1; i <= MAX_INST; i++)
	{
		if (instr[i] != NULL)
		{
			for (int8_t j = 0; j < MAX_SMP_PER_INST; j++) // free sample data
			{
				sampleTyp *s = &instr[i]->samp[j];
				if (s->origPek != NULL)
					free(s->origPek);
			}

			free(instr[i]);
			instr[i] = NULL;
		}
	}
	resumeAudio();
}

void freeSample(int16_t nr, int16_t nr2)
{
	sampleTyp *s;

	if (instr[nr] == NULL)
		return; // instrument not allocated

	pauseAudio(); // voice sample pointers are now cleared

	s = &instr[nr]->samp[nr2];
	if (s->origPek != NULL)
		free(s->origPek);

	memset(s, 0, sizeof (sampleTyp));

	s->pan = 128;
	s->vol = 64;

	resumeAudio();
}

void freeAllPatterns(void)
{
	pauseAudio();
	for (uint16_t i = 0; i < MAX_PATTERNS; i++)
	{
		if (patt[i] != NULL)
		{
			free(patt[i]);
			patt[i] = NULL;
		}
	}
	resumeAudio();
}

void setStdEnvelope(instrTyp *ins, int16_t i, uint8_t typ)
{
	if (ins == NULL)
		return;

	pauseMusic();

	if (typ & 1)
	{
		memcpy(ins->envVP, config.stdEnvP[i][0], 2*2*12);
		ins->envVPAnt = (uint8_t)config.stdVolEnvAnt[i];
		ins->envVSust = (uint8_t)config.stdVolEnvSust[i];
		ins->envVRepS = (uint8_t)config.stdVolEnvRepS[i];
		ins->envVRepE = (uint8_t)config.stdVolEnvRepE[i];
		ins->fadeOut = config.stdFadeOut[i];
		ins->vibRate = (uint8_t)config.stdVibRate[i];
		ins->vibDepth = (uint8_t)config.stdVibDepth[i];
		ins->vibSweep = (uint8_t)config.stdVibSweep[i];
		ins->vibTyp = (uint8_t)config.stdVibTyp[i];
		ins->envVTyp = (uint8_t)config.stdVolEnvTyp[i];
	}

	if (typ & 2)
	{
		memcpy(ins->envPP, config.stdEnvP[i][1], 2*2*12);
		ins->envPPAnt = (uint8_t)config.stdPanEnvAnt[0];
		ins->envPSust = (uint8_t)config.stdPanEnvSust[0];
		ins->envPRepS = (uint8_t)config.stdPanEnvRepS[0];
		ins->envPRepE = (uint8_t)config.stdPanEnvRepE[0];
		ins->envPTyp  = (uint8_t)config.stdPanEnvTyp[0];
	}

	resumeMusic();
}

void setNoEnvelope(instrTyp *ins)
{
	if (ins == NULL)
		return;

	pauseMusic();

	memcpy(ins->envVP, config.stdEnvP[0][0], 2*2*12);
	ins->envVPAnt = (uint8_t)config.stdVolEnvAnt[0];
	ins->envVSust = (uint8_t)config.stdVolEnvSust[0];
	ins->envVRepS = (uint8_t)config.stdVolEnvRepS[0];
	ins->envVRepE = (uint8_t)config.stdVolEnvRepE[0];
	ins->envVTyp = 0;

	memcpy(ins->envPP, config.stdEnvP[0][1], 2*2*12);
	ins->envPPAnt = (uint8_t)config.stdPanEnvAnt[0];
	ins->envPSust = (uint8_t)config.stdPanEnvSust[0];
	ins->envPRepS = (uint8_t)config.stdPanEnvRepS[0];
	ins->envPRepE = (uint8_t)config.stdPanEnvRepE[0];
	ins->envPTyp = 0;

	ins->fadeOut = 0;
	ins->vibRate = 0;
	ins->vibDepth = 0;
	ins->vibSweep = 0;
	ins->vibTyp = 0;

	resumeMusic();
}

bool patternEmpty(uint16_t nr)
{
	uint8_t *scanPtr;
	uint32_t scanLen;

	if (patt[nr] == NULL)
		return true;

	scanPtr = (uint8_t *)patt[nr];
	scanLen = pattLens[nr] * TRACK_WIDTH;

	for (uint32_t i = 0; i < scanLen; i++)
	{
		if (scanPtr[i] != 0)
			return false;
	}

	return true;
}

void updateChanNums(void)
{
	uint8_t pageLen;

	assert(!(song.antChn & 1));

	pageLen = 8;
	if (config.ptnS3M)
	{
		     if (song.antChn == 2) pageLen = 4;
		else if (song.antChn == 4) pageLen = 4;
		else if (song.antChn == 6) pageLen = 6;
		else if (song.antChn >= 8) pageLen = 8;
	}
	else
	{
		     if (song.antChn ==  2) pageLen = 4;
		else if (song.antChn ==  4) pageLen = 4;
		else if (song.antChn ==  6) pageLen = 6;
		else if (song.antChn ==  8) pageLen = 8;
		else if (song.antChn == 10) pageLen = 10;
		else if (song.antChn >= 12) pageLen = 12;
	}

	ui.numChannelsShown = pageLen;
	if (song.antChn == 2)
		ui.numChannelsShown = 2;

	if (config.ptnMaxChannels == 0)
	{
		if (ui.numChannelsShown > 4)
			ui.numChannelsShown = 4;
	}
	else if (config.ptnMaxChannels == 1)
	{
		if (ui.numChannelsShown > 6)
			ui.numChannelsShown = 6;
	}
	else if (config.ptnMaxChannels == 2)
	{
		if (ui.numChannelsShown > 8)
			ui.numChannelsShown = 8;
	}
	else if (config.ptnMaxChannels == 3)
	{
		if (config.ptnS3M)
		{
			if (ui.numChannelsShown > 8)
				ui.numChannelsShown = 8;
		}
		else
		{
			if (ui.numChannelsShown > 12)
				ui.numChannelsShown = 12;
		}
	}

	ui.pattChanScrollShown = song.antChn > getMaxVisibleChannels();

	if (ui.patternEditorShown)
	{
		if (ui.channelOffset > song.antChn-ui.numChannelsShown)
			setScrollBarPos(SB_CHAN_SCROLL, song.antChn - ui.numChannelsShown, true);
	}

	if (ui.pattChanScrollShown)
	{
		if (ui.patternEditorShown)
		{
			showScrollBar(SB_CHAN_SCROLL);
			showPushButton(PB_CHAN_SCROLL_LEFT);
			showPushButton(PB_CHAN_SCROLL_RIGHT);
		}

		setScrollBarEnd(SB_CHAN_SCROLL, song.antChn);
		setScrollBarPageLength(SB_CHAN_SCROLL, ui.numChannelsShown);
	}
	else
	{
		hideScrollBar(SB_CHAN_SCROLL);
		hidePushButton(PB_CHAN_SCROLL_LEFT);
		hidePushButton(PB_CHAN_SCROLL_RIGHT);

		setScrollBarPos(SB_CHAN_SCROLL, 0, false);

		ui.channelOffset = 0;
	}

	if (cursor.ch >= ui.channelOffset+ui.numChannelsShown)
		cursor.ch = ui.channelOffset+ui.numChannelsShown - 1;
}

void conv8BitSample(int8_t *p, int32_t len, bool stereo)
{
	int8_t *p2, l, r;
	int16_t tmp16;
	int32_t i;

	if (stereo)
	{
		len /= 2;

		p2 = &p[len];
		for (i = 0; i < len; i++)
		{
			l = p[i]  - 128;
			r = p2[i] - 128;

			tmp16 = l + r;
			p[i] = (int8_t)(tmp16 >> 1);
		}
	}
	else
	{
		for (i = 0; i < len; i++)
			p[i] -= 128;
	}
}

void conv16BitSample(int8_t *p, int32_t len, bool stereo)
{
	int16_t *p16_1, *p16_2, l, r;
	int32_t i, tmp32;

	p16_1 = (int16_t *)p;

	len /= 2;

	if (stereo)
	{
		len /= 2;

		p16_2 = (int16_t *)&p[len * 2];
		for (i = 0; i < len; i++)
		{
			l = p16_1[i] - 32768;
			r = p16_2[i] - 32768;

			tmp32 = l + r;
			p16_1[i] = (int16_t)(tmp32 >> 1);
		}
	}
	else
	{
		for (i = 0; i < len; i++)
			p16_1[i] -= 32768;
	}
}

void closeReplayer(void)
{
	freeAllInstr();
	freeAllPatterns();

	// free reserved instruments

	if (instr[0] != NULL)
	{
		free(instr[0]);
		instr[0] = NULL;
	}

	if (instr[130] != NULL)
	{
		free(instr[130]);
		instr[130] = NULL;
	}

	if (instr[131] != NULL)
	{
		free(instr[131]);
		instr[131] = NULL;
	}
}

bool setupReplayer(void)
{
	int32_t i;

	for (i = 0; i < MAX_PATTERNS; i++)
		pattLens[i] = 64;

	playMode = PLAYMODE_IDLE;
	songPlaying = false;

	// unmute all channels (must be done before resetChannels() call)
	for (i = 0; i < MAX_VOICES; i++)
		editor.chnMode[i] = 1;

	resetChannels();

	song.len = 1;
	song.antChn = 8;

	editor.speed = song.speed = 125;
	editor.tempo = song.tempo = 6;
	editor.globalVol = song.globVol = 64;
	song.initialTempo = song.tempo;

	audio.linearFreqTable = true;
	note2Period = linearPeriods;

	calcCubicTable();
	calcPeriod2HzTable();
	calcRevMixDeltaTable();
	calcPanningTable();

	setPos(0, 0, true);

	if (!allocateInstr(0))
	{
		showErrorMsgBox("Not enough memory!");
		return false;
	}
	instr[0]->samp[0].vol = 0;

	if (!allocateInstr(130))
	{
		showErrorMsgBox("Not enough memory!");
		return false;
	}
	memset(instr[130], 0, sizeof (instrTyp));

	if (!allocateInstr(131)) // Instr. Ed. display instrument for unallocated/empty instruments
	{
		showErrorMsgBox("Not enough memory!");
		return false;
	}
	memset(instr[131], 0, sizeof (instrTyp));
	for (i = 0; i < 16; i++)
		instr[131]->samp[i].pan = 128;

	editor.tmpPattern = 65535; // pattern editor update/redraw kludge
	return true;
}

void startPlaying(int8_t mode, int16_t row)
{
	lockMixerCallback();

	assert(mode != PLAYMODE_IDLE && mode != PLAYMODE_EDIT);

	if (mode == PLAYMODE_PATT || mode == PLAYMODE_RECPATT)
		setPos(-1, row, true);
	else
		setPos(editor.songPos, row, true);

	playMode = mode;
	songPlaying = true;
	song.globVol = 64;
	song.pattDelTime2 = 0;
	song.pattDelTime = 0;

	resetPlaybackTime();

	// non-FT2 fix: If song speed was 0, set it back to initial speed on play
	if (song.tempo == 0)
		song.tempo = song.initialTempo;

	audio.dTickSampleCounter = 0.0; // zero tick sample counter so that it will instantly initiate a tick

	unlockMixerCallback();

	ui.updatePosSections = true;
	ui.updatePatternEditor = true;
}

void stopPlaying(void)
{
	uint8_t i;
	bool songWasPlaying;

	songWasPlaying = songPlaying;
	playMode = PLAYMODE_IDLE;
	songPlaying = false;

	if (config.killNotesOnStopPlay)
	{
		// safely kills all voices
		lockMixerCallback();
		unlockMixerCallback();

		// prevent getFrequenceValue() from calculating the rates forever
		for (i = 0; i < MAX_VOICES; i++)
			stm[i].outPeriod = 0;
	}
	else
	{
		for (i = 0; i < MAX_VOICES; i++)
			playTone(i, 0, 97, -1, 0, 0);
	}

	// if song was playing, update local pattPos (fixes certain glitches)
	if (songWasPlaying)
		editor.pattPos = song.pattPos;

#ifdef HAS_MIDI
	midi.currMIDIVibDepth = 0;
	midi.currMIDIPitch = 0;
#endif

	memset(editor.keyOnTab, 0, sizeof (editor.keyOnTab));

	ui.updatePosSections = true;
	ui.updatePatternEditor = true;

	// certain non-FT2 fixes
	song.timer = editor.timer = 1;
	song.globVol = editor.globalVol = 64;
	ui.drawGlobVolFlag = true;
}

// from keyboard/smp. ed.
void playTone(uint8_t stmm, uint8_t inst, uint8_t ton, int8_t vol, uint16_t midiVibDepth, uint16_t midiPitch)
{
	sampleTyp *s;
	stmTyp *ch;
	instrTyp *ins = instr[inst];

	if (ins == NULL)
		return;

	assert(stmm < MAX_VOICES && inst < MAX_INST && ton <= 97);
	ch = &stm[stmm];

	// FT2 bugfix: Don't play tone if certain requirements are not met
	if (ton != 97)
	{
		if (ton == 0 || ton > 96)
			return;

		s = &ins->samp[ins->ta[ton-1] & 0xF];

		int16_t newTon = (int16_t)ton + s->relTon;
		if (s->pek == NULL || s->len == 0 || newTon <= 0 || newTon >= 12*10)
			return;
	}
	// -------------------

	lockAudio();

	if (inst != 0 && ton != 97)
	{
		ch->tonTyp = (inst << 8) | (ch->tonTyp & 0xFF);
		ch->instrNr = inst;
	}

	ch->tonTyp = (ch->tonTyp & 0xFF00) | ton;
	ch->effTyp = 0;
	ch->eff = 0;

	startTone(ton, 0, 0, ch);

	if (ton != 97)
	{
		retrigVolume(ch);
		retrigEnvelopeVibrato(ch);

		if (vol != -1) // if jamming note keys, vol -1 = use sample's volume
		{
			ch->realVol = vol;
			ch->outVol = vol;
			ch->oldVol = vol;
		}
	}

	ch->midiVibDepth = midiVibDepth;
	ch->midiPitch = midiPitch;

	fixaEnvelopeVibrato(ch);

	unlockAudio();
}

// smp. ed.
void playSample(uint8_t stmm, uint8_t inst, uint8_t smpNr, uint8_t ton, uint16_t midiVibDepth, uint16_t midiPitch)
{
	uint8_t vol;
	stmTyp *ch;

	if (instr[inst] == NULL)
		return;

	// for sampling playback line in Smp. Ed.
	lastChInstr[stmm].instrNr = 255;
	lastChInstr[stmm].sampleNr = 255;
	editor.curPlayInstr = 255;
	editor.curPlaySmp = 255;

	assert(stmm < MAX_VOICES && inst < MAX_INST && smpNr < MAX_SMP_PER_INST && ton <= 97);
	ch = &stm[stmm];

	memcpy(&instr[130]->samp[0], &instr[inst]->samp[smpNr], sizeof (sampleTyp));

	vol = instr[inst]->samp[smpNr].vol;
	
	lockAudio();

	ch->instrNr = 130;
	ch->tonTyp = (ch->instrNr << 8) | ton;
	ch->effTyp = 0;

	startTone(ton, 0, 0, ch);

	if (ton != 97)
	{
		retrigVolume(ch);
		retrigEnvelopeVibrato(ch);

		ch->realVol = vol;
		ch->outVol = vol;
		ch->oldVol = vol;
	}

	ch->midiVibDepth = midiVibDepth;
	ch->midiPitch = midiPitch;

	fixaEnvelopeVibrato(ch);

	unlockAudio();

	while (ch->status & IS_NyTon); // wait for sample to latch in mixer

	// for sampling playback line in Smp. Ed.
	editor.curPlayInstr = editor.curInstr;
	editor.curPlaySmp = editor.curSmp;
}

// smp. ed.
void playRange(uint8_t stmm, uint8_t inst, uint8_t smpNr, uint8_t ton, uint16_t midiVibDepth, uint16_t midiPitch, int32_t offs, int32_t len)
{
	uint8_t vol;
	int32_t samplePlayOffset;
	stmTyp *ch;
	sampleTyp *s;

	if (instr[inst] == NULL)
		return;

	// for sampling playback line in Smp. Ed.
	lastChInstr[stmm].instrNr = 255;
	lastChInstr[stmm].sampleNr = 255;
	editor.curPlayInstr = 255;
	editor.curPlaySmp = 255;

	assert(stmm < MAX_VOICES && inst < MAX_INST && smpNr < MAX_SMP_PER_INST && ton <= 97);

	ch = &stm[stmm];
	s = &instr[130]->samp[0];

	memcpy(s, &instr[inst]->samp[smpNr], sizeof (sampleTyp));

	vol = instr[inst]->samp[smpNr].vol;

	if (s->typ & 16)
	{
		offs &= 0xFFFFFFFE;
		len &= 0xFFFFFFFE;
	}

	lockAudio();

	s->len = offs + len;
	s->repS = 0;
	s->repL = 0;
	s->typ &= 16; // only keep 8-bit/16-bit flag (disable loop)

	samplePlayOffset = offs;
	if (s->typ & 16)
		samplePlayOffset >>= 1;

	ch->instrNr = 130;
	ch->tonTyp = (ch->instrNr << 8) | ton;
	ch->effTyp = 0;

	startTone(ton, 0, 0, ch);

	ch->smpStartPos = samplePlayOffset;

	if (ton != 97)
	{
		retrigVolume(ch);
		retrigEnvelopeVibrato(ch);

		ch->realVol = vol;
		ch->outVol = vol;
		ch->oldVol = vol;
	}

	ch->midiVibDepth = midiVibDepth;
	ch->midiPitch = midiPitch;

	fixaEnvelopeVibrato(ch);

	unlockAudio();

	while (ch->status & IS_NyTon); // wait for sample to latch in mixer

	// for sampling playback line in Smp. Ed.
	editor.curPlayInstr = editor.curInstr;
	editor.curPlaySmp = editor.curSmp;
}

void stopVoices(void)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	for (int32_t i = 0; i < MAX_VOICES; i++)
	{
		stmTyp *ch = &stm[i];

		lastChInstr[i].sampleNr = 255;
		lastChInstr[i].instrNr = 255;

		ch->tonTyp = 0;
		ch->relTonNr = 0;
		ch->instrNr = 0;
		ch->instrSeg = instr[0]; // important: set instrument pointer to instr 0 (placeholder instrument)
		ch->status = IS_Vol;
		ch->realVol = 0;
		ch->outVol = 0;
		ch->oldVol = 0;
		ch->fFinalVol = 0.0f;
		ch->oldPan = 128;
		ch->outPan = 128;
		ch->finalPan = 128;
		ch->vibDepth = 0;
		ch->midiVibDepth = 0;
		ch->midiPitch = 0;
		ch->smpPtr = NULL;
		ch->portaDir = 0; // FT2 bugfix: weird 3xx behavior if not used with note

		stopVoice(i);
	}

	// for sampling playback line in Smp. Ed.
	editor.curPlayInstr = 255;
	editor.curPlaySmp = 255;

	stopAllScopes();
	resetAudioDither();
	resetCachedMixerVars();
	resetCachedScopeVars();

	// wait for scope thread to finish, so that we know pointers aren't deprecated
	while (editor.scopeThreadMutex);

	if (audioWasntLocked)
		unlockAudio();
}

void decSongPos(void)
{
	if (song.songPos == 0)
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (song.songPos > 0)
		setPos(song.songPos - 1, 0, true);

	if (audioWasntLocked)
		unlockAudio();
}

void incSongPos(void)
{
	if (song.songPos == song.len-1)
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (song.songPos < song.len-1)
		setPos(song.songPos + 1, 0, true);

	if (audioWasntLocked)
		unlockAudio();
}

void decCurIns(void)
{
	if (editor.curInstr <= 1)
		return;

	editor.curInstr--;
	if ((editor.curInstr > 0x40 && !editor.instrBankSwapped) || (editor.curInstr <= 0x40 && editor.instrBankSwapped))
		pbSwapInstrBank();

	editor.instrBankOffset = ((editor.curInstr - 1) / 8) * 8;
 
	updateTextBoxPointers();
	updateNewInstrument();

	if (ui.advEditShown)
		updateAdvEdit();
}

void incCurIns(void)
{
	if (editor.curInstr >= MAX_INST)
		return;

	editor.curInstr++;
	if ((editor.curInstr > 0x40 && !editor.instrBankSwapped) || (editor.curInstr <= 0x40 && editor.instrBankSwapped))
		pbSwapInstrBank();

	editor.instrBankOffset = ((editor.curInstr - 1) / 8) * 8;
 	if (editor.instrBankOffset > MAX_INST-8)
		editor.instrBankOffset = MAX_INST-8;

	updateTextBoxPointers();
	updateNewInstrument();

	if (ui.advEditShown)
		updateAdvEdit();
}

void decCurSmp(void)
{
	if (editor.curSmp == 0)
		return;

	editor.curSmp--;
	editor.sampleBankOffset = (editor.curSmp / 5) * 5;
	setScrollBarPos(SB_SAMPLE_LIST, editor.sampleBankOffset, true);

	updateTextBoxPointers();
	updateNewSample();
}

void incCurSmp(void)
{
	if (editor.curSmp >= MAX_SMP_PER_INST-1)
		return;

	editor.curSmp++;

	editor.sampleBankOffset = (editor.curSmp / 5) * 5;
	if (editor.sampleBankOffset > MAX_SMP_PER_INST-5)
		editor.sampleBankOffset = MAX_SMP_PER_INST-5;

	setScrollBarPos(SB_SAMPLE_LIST, editor.sampleBankOffset, true);

	updateTextBoxPointers();
	updateNewSample();
}

void pbPlaySong(void)
{
	startPlaying(PLAYMODE_SONG, 0);
}

void pbPlayPtn(void)
{
	startPlaying(PLAYMODE_PATT, 0);
}

void pbRecSng(void)
{
	startPlaying(PLAYMODE_RECSONG, 0);
}

void pbRecPtn(void)
{
	startPlaying(PLAYMODE_RECPATT, 0);
}

void setSyncedReplayerVars(void)
{
	uint8_t scopeUpdateStatus[MAX_VOICES];
	uint64_t frameTime64;

	pattSyncEntry = NULL;
	chSyncEntry = NULL;

	memset(scopeUpdateStatus, 0, sizeof (scopeUpdateStatus)); // this is needed

	frameTime64 = SDL_GetPerformanceCounter();

	// handle channel sync queue

	while (chQueueClearing);
	while (chQueueReadSize() > 0)
	{
		if (frameTime64 < getChQueueTimestamp())
			break; // we have no more stuff to render for now

		chSyncEntry = chQueuePeek();
		if (chSyncEntry == NULL)
			break;

		for (int32_t i = 0; i < song.antChn; i++)
			scopeUpdateStatus[i] |= chSyncEntry->channels[i].status; // yes, OR the status

		if (!chQueuePop())
			break;
	}

	/* Extra validation because of possible issues when the buffer is full
	** and positions are being reset, which is not entirely thread safe.
	*/
	if (chSyncEntry != NULL && chSyncEntry->timestamp == 0)
		chSyncEntry = NULL;

	// handle pattern sync queue

	while (pattQueueClearing);
	while (pattQueueReadSize() > 0)
	{
		if (frameTime64 < getPattQueueTimestamp())
			break; // we have no more stuff to render for now

		pattSyncEntry = pattQueuePeek();
		if (pattSyncEntry == NULL)
			break;

		if (!pattQueuePop())
			break;
	}

	/* Extra validation because of possible issues when the buffer is full
	** and positions are being reset, which is not entirely thread safe.
	*/
	if (pattSyncEntry != NULL && pattSyncEntry->timestamp == 0)
		pattSyncEntry = NULL;

	// do actual updates

	if (chSyncEntry != NULL)
	{
		handleScopesFromChQueue(chSyncEntry, scopeUpdateStatus);
		ui.drawReplayerPianoFlag = true;
	}

	if (!songPlaying || pattSyncEntry == NULL)
		return;

	// we have a new tick

	editor.timer = pattSyncEntry->timer;

	if (editor.speed != pattSyncEntry->speed)
	{
		editor.speed = pattSyncEntry->speed;
		ui.drawBPMFlag = true;
	}

	if (editor.tempo != pattSyncEntry->tempo)
	{
		editor.tempo = pattSyncEntry->tempo;
		ui.drawSpeedFlag = true;
	}

	if (editor.globalVol != pattSyncEntry->globalVol)
	{
		editor.globalVol = pattSyncEntry->globalVol;
		ui.drawGlobVolFlag = true;
	}

	if (editor.songPos != pattSyncEntry->songPos)
	{
		editor.songPos = pattSyncEntry->songPos;
		ui.drawPosEdFlag = true;
	}

	// somewhat of a kludge...
	if (editor.tmpPattern != pattSyncEntry->pattern || editor.pattPos != pattSyncEntry->patternPos)
	{
		// set pattern number
		editor.editPattern = editor.tmpPattern = pattSyncEntry->pattern;
		checkMarkLimits();
		ui.drawPattNumLenFlag = true;

		// set row
		editor.pattPos = (uint8_t)pattSyncEntry->patternPos;
		ui.updatePatternEditor = true;
	}
}