shithub: ft2-clone

ref: 9217cd8b187d4626644151aa30c8ac6f88a6e563
dir: /src/ft2_pattern_ed.c/

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

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include "ft2_header.h"
#include "ft2_config.h"
#include "ft2_pattern_ed.h"
#include "ft2_gui.h"
#include "ft2_sample_ed.h"
#include "ft2_pattern_draw.h"
#include "ft2_inst_ed.h"
#include "ft2_scopes.h"
#include "ft2_diskop.h"
#include "ft2_audio.h"
#include "ft2_wav_renderer.h"
#include "ft2_mouse.h"
#include "ft2_video.h"
#include "ft2_tables.h"
#include "ft2_bmp.h"
#include "ft2_structs.h"

pattMark_t pattMark; // globalized

// for pattern marking w/ keyboard
static int8_t lastChMark;
static int16_t lastRowMark;

// for pattern marking w/ mouse
static int32_t lastMarkX1 = -1, lastMarkX2 = -1, lastMarkY1 = -1, lastMarkY2 = -1;

static const uint8_t ptnAntLine[8] = { 27, 25, 20, 19, 42, 40, 31, 30 };
static const uint8_t ptnLineSub[8] = { 13, 12,  9,  9, 20, 19, 15, 14 };
static const uint8_t iSwitchExtW[4] = { 40, 40, 40, 39 };
static const uint8_t iSwitchExtY[8] = { 2, 2, 2, 2, 19, 19, 19, 19 };
static const uint8_t iSwitchY[8] = { 2, 19, 36, 53, 73, 90, 107, 124 };
static const uint16_t iSwitchExtX[4] = { 221, 262, 303, 344 };

static int32_t lastMouseX, lastMouseY;
static int32_t last_TimeH, last_TimeM, last_TimeS;

bool allocatePattern(uint16_t nr) // for tracker use only, not in loader!
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (patt[nr] == NULL)
	{
		/* Original FT2 allocates only the amount of rows needed, but we don't
		** do that to avoid out of bondary row look-up between out-of-sync replayer
		** state and tracker state (yes it used to happen, rarely). We're not wasting
		** too much RAM for a modern computer anyway. Worst case: 256 allocated
		** patterns would be ~10MB.
		**/

		patt[nr] = (tonTyp *)calloc((MAX_PATT_LEN * TRACK_WIDTH) + 16, 1);
		if (patt[nr] == NULL)
		{
			if (audioWasntLocked)
				unlockAudio();

			return false;
		}

		song.pattLen = pattLens[nr];
	}

	if (audioWasntLocked)
		unlockAudio();

	return true;
}

void killPatternIfUnused(uint16_t nr) // for tracker use only, not in loader!
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (patternEmpty(nr))
	{
		if (patt[nr] != NULL)
		{
			free(patt[nr]);
			patt[nr] = NULL;
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

uint8_t getMaxVisibleChannels(void)
{
	assert(config.ptnMaxChannels >= 0 && config.ptnMaxChannels <= 3);
	if (config.ptnS3M)
		return maxVisibleChans1[config.ptnMaxChannels];
	else
		return maxVisibleChans2[config.ptnMaxChannels];
}

void updatePatternWidth(void)
{
	if (ui.numChannelsShown > ui.maxVisibleChannels)
		ui.numChannelsShown = ui.maxVisibleChannels;

	assert(ui.numChannelsShown >= 2 && ui.numChannelsShown <= 12);

	ui.patternChannelWidth = chanWidths[(ui.numChannelsShown / 2) - 1] + 3;
}

void updateAdvEdit(void)
{
	hexOutBg(92, 113, PAL_FORGRND, PAL_DESKTOP, editor.srcInstr, 2);
	hexOutBg(92, 126, PAL_FORGRND, PAL_DESKTOP, editor.curInstr, 2);
}

void setAdvEditCheckBoxes(void)
{
	checkBoxes[CB_ENABLE_MASKING].checked = editor.copyMaskEnable;
	checkBoxes[CB_COPY_MASK_0].checked = editor.copyMask[0];
	checkBoxes[CB_COPY_MASK_1].checked = editor.copyMask[1];
	checkBoxes[CB_COPY_MASK_2].checked = editor.copyMask[2];
	checkBoxes[CB_COPY_MASK_3].checked = editor.copyMask[3];
	checkBoxes[CB_COPY_MASK_4].checked = editor.copyMask[4];
	checkBoxes[CB_PASTE_MASK_0].checked = editor.pasteMask[0];
	checkBoxes[CB_PASTE_MASK_1].checked = editor.pasteMask[1];
	checkBoxes[CB_PASTE_MASK_2].checked = editor.pasteMask[2];
	checkBoxes[CB_PASTE_MASK_3].checked = editor.pasteMask[3];
	checkBoxes[CB_PASTE_MASK_4].checked = editor.pasteMask[4];
	checkBoxes[CB_TRANSP_MASK_0].checked = editor.transpMask[0];
	checkBoxes[CB_TRANSP_MASK_1].checked = editor.transpMask[1];
	checkBoxes[CB_TRANSP_MASK_2].checked = editor.transpMask[2];
	checkBoxes[CB_TRANSP_MASK_3].checked = editor.transpMask[3];
	checkBoxes[CB_TRANSP_MASK_4].checked = editor.transpMask[4];

	showCheckBox(CB_ENABLE_MASKING);
	showCheckBox(CB_COPY_MASK_0);
	showCheckBox(CB_COPY_MASK_1);
	showCheckBox(CB_COPY_MASK_2);
	showCheckBox(CB_COPY_MASK_3);
	showCheckBox(CB_COPY_MASK_4);
	showCheckBox(CB_PASTE_MASK_0);
	showCheckBox(CB_PASTE_MASK_1);
	showCheckBox(CB_PASTE_MASK_2);
	showCheckBox(CB_PASTE_MASK_3);
	showCheckBox(CB_PASTE_MASK_4);
	showCheckBox(CB_TRANSP_MASK_0);
	showCheckBox(CB_TRANSP_MASK_1);
	showCheckBox(CB_TRANSP_MASK_2);
	showCheckBox(CB_TRANSP_MASK_3);
	showCheckBox(CB_TRANSP_MASK_4);
}

void drawAdvEdit(void)
{
	drawFramework(  0,  92, 110,  17, FRAMEWORK_TYPE1);
	drawFramework(  0, 109, 110,  64, FRAMEWORK_TYPE1);
	drawFramework(110,  92, 124,  81, FRAMEWORK_TYPE1);
	drawFramework(234,  92,  19,  81, FRAMEWORK_TYPE1);
	drawFramework(253,  92,  19,  81, FRAMEWORK_TYPE1);
	drawFramework(272,  92,  19,  81, FRAMEWORK_TYPE1);

	textOutShadow(  4,  96, PAL_FORGRND, PAL_DSKTOP2, "Instr. remap:");
	textOutShadow(  4, 113, PAL_FORGRND, PAL_DSKTOP2, "Old number");
	textOutShadow(  4, 126, PAL_FORGRND, PAL_DSKTOP2, "New number");
	textOutShadow(129,  96, PAL_FORGRND, PAL_DSKTOP2, "Masking enable");
	textOutShadow(114, 109, PAL_FORGRND, PAL_DSKTOP2, "Note");
	textOutShadow(114, 122, PAL_FORGRND, PAL_DSKTOP2, "Instrument number");
	textOutShadow(114, 135, PAL_FORGRND, PAL_DSKTOP2, "Volume column");
	textOutShadow(114, 148, PAL_FORGRND, PAL_DSKTOP2, "Effect digit 1");
	textOutShadow(114, 161, PAL_FORGRND, PAL_DSKTOP2, "Effect digit 2,3");

	charOutShadow(239, 95, PAL_FORGRND, PAL_DSKTOP2, 'C');
	charOutShadow(258, 95, PAL_FORGRND, PAL_DSKTOP2, 'P');
	charOutShadow(277, 95, PAL_FORGRND, PAL_DSKTOP2, 'T');

	showPushButton(PB_REMAP_TRACK);
	showPushButton(PB_REMAP_PATTERN);
	showPushButton(PB_REMAP_SONG);
	showPushButton(PB_REMAP_BLOCK);

	setAdvEditCheckBoxes();

	updateAdvEdit();
}

void hideAdvEdit(void)
{
	ui.advEditShown = false;

	hidePushButton(PB_REMAP_TRACK);
	hidePushButton(PB_REMAP_PATTERN);
	hidePushButton(PB_REMAP_SONG);
	hidePushButton(PB_REMAP_BLOCK);

	hideCheckBox(CB_ENABLE_MASKING);
	hideCheckBox(CB_COPY_MASK_0);
	hideCheckBox(CB_COPY_MASK_1);
	hideCheckBox(CB_COPY_MASK_2);
	hideCheckBox(CB_COPY_MASK_3);
	hideCheckBox(CB_COPY_MASK_4);
	hideCheckBox(CB_PASTE_MASK_0);
	hideCheckBox(CB_PASTE_MASK_1);
	hideCheckBox(CB_PASTE_MASK_2);
	hideCheckBox(CB_PASTE_MASK_3);
	hideCheckBox(CB_PASTE_MASK_4);
	hideCheckBox(CB_TRANSP_MASK_0);
	hideCheckBox(CB_TRANSP_MASK_1);
	hideCheckBox(CB_TRANSP_MASK_2);
	hideCheckBox(CB_TRANSP_MASK_3);
	hideCheckBox(CB_TRANSP_MASK_4);

	ui.scopesShown = true;
	drawScopeFramework();
}

void showAdvEdit(void)
{
	if (ui.extended)
		exitPatternEditorExtended();

	hideTopScreen();
	showTopScreen(false);

	ui.advEditShown = true;
	ui.scopesShown  = false;
	drawAdvEdit();
}

void toggleAdvEdit(void)
{
	if (ui.advEditShown)
		hideAdvEdit();
	else
		showAdvEdit();
}

void drawTranspose(void)
{
	drawFramework(0,    92,  53,  16, FRAMEWORK_TYPE1);
	drawFramework(53,   92, 119,  16, FRAMEWORK_TYPE1);
	drawFramework(172,  92, 119,  16, FRAMEWORK_TYPE1);
	drawFramework(0,   108,  53,  65, FRAMEWORK_TYPE1);
	drawFramework(53,  108, 119,  65, FRAMEWORK_TYPE1);
	drawFramework(172, 108, 119,  65, FRAMEWORK_TYPE1);

	textOutShadow(4,    95, PAL_FORGRND, PAL_DSKTOP2, "Transp.");
	textOutShadow(58,   95, PAL_FORGRND, PAL_DSKTOP2, "Current instrument");
	textOutShadow(188,  95, PAL_FORGRND, PAL_DSKTOP2, "All instruments");
	textOutShadow(4,   114, PAL_FORGRND, PAL_DSKTOP2, "Track");
	textOutShadow(4,   129, PAL_FORGRND, PAL_DSKTOP2, "Pattern");
	textOutShadow(4,   144, PAL_FORGRND, PAL_DSKTOP2, "Song");
	textOutShadow(4,   159, PAL_FORGRND, PAL_DSKTOP2, "Block");

	showPushButton(PB_TRANSP_CUR_INS_TRK_UP);
	showPushButton(PB_TRANSP_CUR_INS_TRK_DN);
	showPushButton(PB_TRANSP_CUR_INS_TRK_12UP);
	showPushButton(PB_TRANSP_CUR_INS_TRK_12DN);
	showPushButton(PB_TRANSP_ALL_INS_TRK_UP);
	showPushButton(PB_TRANSP_ALL_INS_TRK_DN);
	showPushButton(PB_TRANSP_ALL_INS_TRK_12UP);
	showPushButton(PB_TRANSP_ALL_INS_TRK_12DN);
	showPushButton(PB_TRANSP_CUR_INS_PAT_UP);
	showPushButton(PB_TRANSP_CUR_INS_PAT_DN);
	showPushButton(PB_TRANSP_CUR_INS_PAT_12UP);
	showPushButton(PB_TRANSP_CUR_INS_PAT_12DN);
	showPushButton(PB_TRANSP_ALL_INS_PAT_UP);
	showPushButton(PB_TRANSP_ALL_INS_PAT_DN);
	showPushButton(PB_TRANSP_ALL_INS_PAT_12UP);
	showPushButton(PB_TRANSP_ALL_INS_PAT_12DN);
	showPushButton(PB_TRANSP_CUR_INS_SNG_UP);
	showPushButton(PB_TRANSP_CUR_INS_SNG_DN);
	showPushButton(PB_TRANSP_CUR_INS_SNG_12UP);
	showPushButton(PB_TRANSP_CUR_INS_SNG_12DN);
	showPushButton(PB_TRANSP_ALL_INS_SNG_UP);
	showPushButton(PB_TRANSP_ALL_INS_SNG_DN);
	showPushButton(PB_TRANSP_ALL_INS_SNG_12UP);
	showPushButton(PB_TRANSP_ALL_INS_SNG_12DN);
	showPushButton(PB_TRANSP_CUR_INS_BLK_UP);
	showPushButton(PB_TRANSP_CUR_INS_BLK_DN);
	showPushButton(PB_TRANSP_CUR_INS_BLK_12UP);
	showPushButton(PB_TRANSP_CUR_INS_BLK_12DN);
	showPushButton(PB_TRANSP_ALL_INS_BLK_UP);
	showPushButton(PB_TRANSP_ALL_INS_BLK_DN);
	showPushButton(PB_TRANSP_ALL_INS_BLK_12UP);
	showPushButton(PB_TRANSP_ALL_INS_BLK_12DN);
}

void showTranspose(void)
{
	if (ui.extended)
		exitPatternEditorExtended();

	hideTopScreen();
	showTopScreen(false);

	ui.transposeShown = true;
	ui.scopesShown = false;
	drawTranspose();
}

void hideTranspose(void)
{
	hidePushButton(PB_TRANSP_CUR_INS_TRK_UP);
	hidePushButton(PB_TRANSP_CUR_INS_TRK_DN);
	hidePushButton(PB_TRANSP_CUR_INS_TRK_12UP);
	hidePushButton(PB_TRANSP_CUR_INS_TRK_12DN);
	hidePushButton(PB_TRANSP_ALL_INS_TRK_UP);
	hidePushButton(PB_TRANSP_ALL_INS_TRK_DN);
	hidePushButton(PB_TRANSP_ALL_INS_TRK_12UP);
	hidePushButton(PB_TRANSP_ALL_INS_TRK_12DN);
	hidePushButton(PB_TRANSP_CUR_INS_PAT_UP);
	hidePushButton(PB_TRANSP_CUR_INS_PAT_DN);
	hidePushButton(PB_TRANSP_CUR_INS_PAT_12UP);
	hidePushButton(PB_TRANSP_CUR_INS_PAT_12DN);
	hidePushButton(PB_TRANSP_ALL_INS_PAT_UP);
	hidePushButton(PB_TRANSP_ALL_INS_PAT_DN);
	hidePushButton(PB_TRANSP_ALL_INS_PAT_12UP);
	hidePushButton(PB_TRANSP_ALL_INS_PAT_12DN);
	hidePushButton(PB_TRANSP_CUR_INS_SNG_UP);
	hidePushButton(PB_TRANSP_CUR_INS_SNG_DN);
	hidePushButton(PB_TRANSP_CUR_INS_SNG_12UP);
	hidePushButton(PB_TRANSP_CUR_INS_SNG_12DN);
	hidePushButton(PB_TRANSP_ALL_INS_SNG_UP);
	hidePushButton(PB_TRANSP_ALL_INS_SNG_DN);
	hidePushButton(PB_TRANSP_ALL_INS_SNG_12UP);
	hidePushButton(PB_TRANSP_ALL_INS_SNG_12DN);
	hidePushButton(PB_TRANSP_CUR_INS_BLK_UP);
	hidePushButton(PB_TRANSP_CUR_INS_BLK_DN);
	hidePushButton(PB_TRANSP_CUR_INS_BLK_12UP);
	hidePushButton(PB_TRANSP_CUR_INS_BLK_12DN);
	hidePushButton(PB_TRANSP_ALL_INS_BLK_UP);
	hidePushButton(PB_TRANSP_ALL_INS_BLK_DN);
	hidePushButton(PB_TRANSP_ALL_INS_BLK_12UP);
	hidePushButton(PB_TRANSP_ALL_INS_BLK_12DN);

	ui.transposeShown = false;
	ui.scopesShown = true;
	drawScopeFramework();
}

void toggleTranspose(void)
{
	if (ui.transposeShown)
		hideTranspose();
	else
		showTranspose();
}

// ----- PATTERN CURSOR FUNCTIONS -----

void cursorChannelLeft(void)
{
	cursor.object = CURSOR_EFX2;

	if (cursor.ch == 0)
	{
		cursor.ch = (uint8_t)(song.antChn - 1);
		if (ui.pattChanScrollShown)
		{
			scrollBars[SB_CHAN_SCROLL].oldPos = UINT32_MAX; // kludge
			setScrollBarPos(SB_CHAN_SCROLL, song.antChn, true);
		}
	}
	else
	{
		cursor.ch--;
		if (ui.pattChanScrollShown)
		{
			if (cursor.ch < ui.channelOffset)
				scrollBarScrollUp(SB_CHAN_SCROLL, 1);
		}
	}
}

void cursorChannelRight(void)
{
	cursor.object = CURSOR_NOTE;

	if (cursor.ch >= song.antChn-1)
	{
		cursor.ch = 0;
		if (ui.pattChanScrollShown)
		{
			scrollBars[SB_CHAN_SCROLL].oldPos = UINT32_MAX; // kludge
			setScrollBarPos(SB_CHAN_SCROLL, 0, true);
		}
	}
	else
	{
		cursor.ch++;
		if (ui.pattChanScrollShown && cursor.ch >= ui.channelOffset+ui.numChannelsShown)
			scrollBarScrollDown(SB_CHAN_SCROLL, 1);
	}
}

void cursorTabLeft(void)
{
	if (cursor.object == CURSOR_NOTE)
		cursorChannelLeft();

	cursor.object = CURSOR_NOTE;
	ui.updatePatternEditor = true;
}

void cursorTabRight(void)
{
	cursorChannelRight();
	cursor.object = CURSOR_NOTE;
	ui.updatePatternEditor = true;
}

void chanLeft(void)
{
	cursorChannelLeft();
	cursor.object = CURSOR_NOTE;
	ui.updatePatternEditor = true;
}

void chanRight(void)
{
	cursorChannelRight();
	cursor.object = CURSOR_NOTE;
	ui.updatePatternEditor = true;
}

void cursorLeft(void)
{
	cursor.object--;

	if (!config.ptnS3M)
	{
		while (cursor.object == CURSOR_VOL1 || cursor.object == CURSOR_VOL2)
			cursor.object--;
	}

	if (cursor.object == -1)
	{
		cursor.object = CURSOR_EFX2;
		cursorChannelLeft();
	}

	ui.updatePatternEditor = true;
}

void cursorRight(void)
{
	cursor.object++;

	if (!config.ptnS3M)
	{
		while (cursor.object == CURSOR_VOL1 || cursor.object == CURSOR_VOL2)
			cursor.object++;
	}

	if (cursor.object == 8)
	{
		cursor.object = CURSOR_NOTE;
		cursorChannelRight();
	}

	ui.updatePatternEditor = true;
}

void showPatternEditor(void)
{
	ui.patternEditorShown = true;
	updateChanNums();
	drawPatternBorders();
	ui.updatePatternEditor = true;
}

void hidePatternEditor(void)
{
	hideScrollBar(SB_CHAN_SCROLL);
	hidePushButton(PB_CHAN_SCROLL_LEFT);
	hidePushButton(PB_CHAN_SCROLL_RIGHT);

	ui.patternEditorShown = false;
}

static void updatePatternEditorGUI(void)
{
	uint16_t i;
	pushButton_t *p;
	textBox_t *t;

	if (ui.extended)
	{
		// extended pattern editor

		// instrument names
		t = &textBoxes[TB_INST1];
		for (i = 0; i < 8; i++, t++)
		{
			if (i < 4)
			{
				t->x = 406;
				t->y = 5 + (i * 11);
			}
			else
			{
				t->x = 529;
				t->y = 5 + ((i - 4) * 11);
			}

			t->w = 99;
			t->renderW = t->w - (t->tx * 2);
		}

		scrollBars[SB_POS_ED].h = 23;

		pushButtons[PB_POSED_POS_DOWN].y = 38;
		pushButtons[PB_POSED_PATT_UP].y = 20;
		pushButtons[PB_POSED_PATT_DOWN].y = 20;
		pushButtons[PB_POSED_DEL].y = 35;
		pushButtons[PB_SWAP_BANK].caption = "Swap B.";
		pushButtons[PB_SWAP_BANK].caption2 = NULL;
		pushButtons[PB_SWAP_BANK].x = 162;
		pushButtons[PB_SWAP_BANK].y = 35;
		pushButtons[PB_SWAP_BANK].w = 53;
		pushButtons[PB_SWAP_BANK].h = 16;
		pushButtons[PB_POSED_LEN_UP].x = 180;
		pushButtons[PB_POSED_LEN_UP].y = 3;
		pushButtons[PB_POSED_LEN_DOWN].x = 197;
		pushButtons[PB_POSED_LEN_DOWN].y = 3;
		pushButtons[PB_POSED_REP_UP].x = 180;
		pushButtons[PB_POSED_REP_UP].y = 17;
		pushButtons[PB_POSED_REP_DOWN].x = 197;
		pushButtons[PB_POSED_REP_DOWN].y = 17;
		pushButtons[PB_PATT_UP].x = 267;
		pushButtons[PB_PATT_UP].y = 37;
		pushButtons[PB_PATT_DOWN].x = 284;
		pushButtons[PB_PATT_DOWN].y = 37;
		pushButtons[PB_PATTLEN_UP].x = 348;
		pushButtons[PB_PATTLEN_UP].y = 37;
		pushButtons[PB_PATTLEN_DOWN].x = 365;
		pushButtons[PB_PATTLEN_DOWN].y = 37;

		// instrument switcher
		p = &pushButtons[PB_RANGE1];
		for (i = 0; i < 16; i++, p++)
		{
			p->w = iSwitchExtW[i & 3];
			p->x = iSwitchExtX[i & 3];
			p->y = iSwitchExtY[i & 7];
		}
	}
	else
	{
		// instrument names
		t = &textBoxes[TB_INST1];
		for (i = 0; i < 8; i++, t++)
		{
			t->y = 5 + (i * 11);
			t->x = 446;
			t->w = 140;
			t->renderW = t->w - (t->tx * 2);
		}

		// normal pattern editor

		scrollBars[SB_POS_ED].h = 21;

		pushButtons[PB_POSED_POS_DOWN].y = 36;
		pushButtons[PB_POSED_PATT_UP].y = 19;
		pushButtons[PB_POSED_PATT_DOWN].y = 19;
		pushButtons[PB_POSED_DEL].y = 33;
		pushButtons[PB_SWAP_BANK].caption = "Swap";
		pushButtons[PB_SWAP_BANK].caption2 = "Bank";
		pushButtons[PB_SWAP_BANK].x = 590;
		pushButtons[PB_SWAP_BANK].y = 144;
		pushButtons[PB_SWAP_BANK].w = 39;
		pushButtons[PB_SWAP_BANK].h = 27;
		pushButtons[PB_POSED_LEN_UP].x = 74;
		pushButtons[PB_POSED_LEN_UP].y = 50;
		pushButtons[PB_POSED_LEN_DOWN].x = 91;
		pushButtons[PB_POSED_LEN_DOWN].y = 50;
		pushButtons[PB_POSED_REP_UP].x = 74;
		pushButtons[PB_POSED_REP_UP].y = 62;
		pushButtons[PB_POSED_REP_DOWN].x = 91;
		pushButtons[PB_POSED_REP_DOWN].y = 62;
		pushButtons[PB_PATT_UP].x = 253;
		pushButtons[PB_PATT_UP].y = 34;
		pushButtons[PB_PATT_DOWN].x = 270;
		pushButtons[PB_PATT_DOWN].y = 34;
		pushButtons[PB_PATTLEN_UP].x = 253;
		pushButtons[PB_PATTLEN_UP].y = 48;
		pushButtons[PB_PATTLEN_DOWN].x = 270;
		pushButtons[PB_PATTLEN_DOWN].y = 48;

		// instrument switcher
		p = &pushButtons[PB_RANGE1];
		for (i = 0; i < 16; i++, p++)
		{
			p->w = 39;
			p->x = 590;
			p->y = iSwitchY[i & 7];
		}
	}

	// force update even if new values were to be the same as the old ones
	scrollBars[SB_POS_ED].oldEnd = UINT32_MAX;
	scrollBars[SB_POS_ED].oldPage = UINT32_MAX;
	scrollBars[SB_POS_ED].oldPos = UINT32_MAX;
}

void patternEditorExtended(void)
{
	// backup old screen flags
	ui._aboutScreenShown = ui.aboutScreenShown;
	ui._helpScreenShown = ui.helpScreenShown;
	ui._configScreenShown = ui.configScreenShown;
	ui._diskOpShown = ui.diskOpShown;
	ui._nibblesShown = ui.nibblesShown;
	ui._transposeShown = ui.transposeShown;
	ui._instEditorShown = ui.instEditorShown;
	ui._instEditorExtShown = ui.instEditorExtShown;
	ui._sampleEditorExtShown = ui.sampleEditorExtShown;
	ui._patternEditorShown = ui.patternEditorShown;
	ui._sampleEditorShown = ui.sampleEditorShown;
	ui._advEditShown= ui.advEditShown;
	ui._wavRendererShown = ui.wavRendererShown;
	ui._trimScreenShown = ui.trimScreenShown;

	hideTopScreen();
	hideSampleEditor();
	hideInstEditor();

	ui.extended = true;
	ui.patternEditorShown = true;
	updatePatternEditorGUI(); // change pattern editor layout (based on ui.extended flag)
	ui.updatePatternEditor = true; // redraw pattern editor

	drawFramework(0,    0, 112, 53, FRAMEWORK_TYPE1);
	drawFramework(112,  0, 106, 33, FRAMEWORK_TYPE1);
	drawFramework(112, 33, 106, 20, FRAMEWORK_TYPE1);
	drawFramework(218,  0, 168, 53, FRAMEWORK_TYPE1);

	// pos ed. stuff

	drawFramework(2,  2, 51, 20, FRAMEWORK_TYPE2);
	drawFramework(2, 31, 51, 20, FRAMEWORK_TYPE2);

	showScrollBar(SB_POS_ED);

	showPushButton(PB_POSED_POS_UP);
	showPushButton(PB_POSED_POS_DOWN);
	showPushButton(PB_POSED_INS);
	showPushButton(PB_POSED_PATT_UP);
	showPushButton(PB_POSED_PATT_DOWN);
	showPushButton(PB_POSED_DEL);
	showPushButton(PB_POSED_LEN_UP);
	showPushButton(PB_POSED_LEN_DOWN);
	showPushButton(PB_POSED_REP_UP);
	showPushButton(PB_POSED_REP_DOWN);
	showPushButton(PB_SWAP_BANK);
	showPushButton(PB_PATT_UP);
	showPushButton(PB_PATT_DOWN);
	showPushButton(PB_PATTLEN_UP);
	showPushButton(PB_PATTLEN_DOWN);

	showPushButton(PB_EXIT_EXT_PATT);

	textOutShadow(116,  5, PAL_FORGRND, PAL_DSKTOP2, "Sng.len.");
	textOutShadow(116, 19, PAL_FORGRND, PAL_DSKTOP2, "Repst.");
	textOutShadow(222, 39, PAL_FORGRND, PAL_DSKTOP2, "Ptn.");
	textOutShadow(305, 39, PAL_FORGRND, PAL_DSKTOP2, "Ln.");

	ui.instrSwitcherShown = true;
	showInstrumentSwitcher();

	drawSongLength();
	drawSongRepS();
	drawEditPattern(editor.editPattern);
	drawPatternLength(editor.editPattern);
	drawPosEdNums(editor.songPos);
	ui.updatePosSections = true;

	// kludge to fix scrollbar thumb when the scrollbar height changes during playback
	if (songPlaying)
		setScrollBarPos(SB_POS_ED, editor.songPos, false);
}

void exitPatternEditorExtended(void)
{
	ui.extended = false;
	updatePatternEditorGUI();
	hidePushButton(PB_EXIT_EXT_PATT);

	// set back top screen button maps

	// set back old screen flags
	ui.aboutScreenShown = ui._aboutScreenShown;
	ui.helpScreenShown = ui._helpScreenShown;
	ui.configScreenShown = ui._configScreenShown;
	ui.diskOpShown = ui._diskOpShown;
	ui.nibblesShown = ui._nibblesShown;
	ui.transposeShown = ui._transposeShown;
	ui.instEditorShown = ui._instEditorShown;
	ui.instEditorExtShown = ui._instEditorExtShown;
	ui.sampleEditorExtShown = ui._sampleEditorExtShown;
	ui.patternEditorShown = ui._patternEditorShown;
	ui.sampleEditorShown = ui._sampleEditorShown;
	ui.advEditShown = ui._advEditShown;
	ui.wavRendererShown = ui._wavRendererShown;
	ui.trimScreenShown = ui.trimScreenShown;

	showTopScreen(true);
	showBottomScreen();

	// kludge to fix scrollbar thumb when the scrollbar height changes during playback
	if (songPlaying)
		setScrollBarPos(SB_POS_ED, editor.songPos, false);
}

void togglePatternEditorExtended(void)
{
	if (ui.extended)
		exitPatternEditorExtended();
	else
		patternEditorExtended();
}

void clearPattMark(void)
{
	memset(&pattMark, 0, sizeof (pattMark));

	lastMarkX1 = -1;
	lastMarkX2 = -1;
	lastMarkY1 = -1;
	lastMarkY2 = -1;
}

void checkMarkLimits(void)
{
	const uint16_t limitY = pattLens[editor.editPattern];
	pattMark.markY1 = CLAMP(pattMark.markY1, 0, limitY);
	pattMark.markY2 = CLAMP(pattMark.markY2, 0, limitY);

	const uint16_t limitX = (uint16_t)(song.antChn - 1);
	pattMark.markX1 = CLAMP(pattMark.markX1, 0, limitX);
	pattMark.markX2 = CLAMP(pattMark.markX2, 0, limitX);

	// XXX: will probably never happen? FT2 has this in CheckMarkLimits() though...
	if (pattMark.markX1 > pattMark.markX2)
		pattMark.markX1 = pattMark.markX2;
}

static int8_t mouseXToCh(void) // used to get channel num from mouse x (for pattern marking)
{
	assert(ui.patternChannelWidth > 0);
	if (ui.patternChannelWidth == 0)
		return 0;

	int32_t mouseX = mouse.x - 29;
	mouseX = CLAMP(mouseX, 0, 573);

	const int8_t chEnd = (ui.channelOffset + ui.numChannelsShown) - 1;

	int8_t ch = ui.channelOffset + (int8_t)(mouseX / ui.patternChannelWidth);
	ch = CLAMP(ch, 0, chEnd);

	// in some setups there can be non-used channels to the right, do clamping
	if (ch >= song.antChn)
		ch = (int8_t)(song.antChn - 1);

	return ch;
}

static int16_t mouseYToRow(void) // used to get row num from mouse y (for pattern marking)
{
	const pattCoordsMouse_t *pattCoordsMouse = &pattCoordMouseTable[config.ptnUnpressed][ui.pattChanScrollShown][ui.extended];

	// clamp mouse y to boundaries
	const int16_t maxY = ui.pattChanScrollShown ? 382 : 396;
	const int16_t my = (int16_t)CLAMP(mouse.y, pattCoordsMouse->upperRowsY, maxY);

	const uint8_t charHeight = config.ptnUnpressed ? 11 : 8;

	// test top/middle/bottom rows
	if (my < pattCoordsMouse->midRowY)
	{
		// top rows
		int16_t row = editor.pattPos - (pattCoordsMouse->numUpperRows - ((my - pattCoordsMouse->upperRowsY) / charHeight));
		if (row < 0)
			row = 0;

		return row;
	}
	else if (my >= pattCoordsMouse->midRowY && my <= pattCoordsMouse->midRowY+10)
	{
		// current row (middle)
		return editor.pattPos;
	}
	else
	{
		// bottom rows
		int16_t row = (editor.pattPos + 1) + ((my - pattCoordsMouse->lowerRowsY) / charHeight);

		// prevent being able to mark the next unseen row on the bottom (in some configurations)
		const uint8_t mode = (ui.extended * 4) + (config.ptnUnpressed * 2) + ui.pattChanScrollShown;

		const int16_t maxRow = (ptnAntLine[mode] + (editor.pattPos - ptnLineSub[mode])) - 1;
		if (row > maxRow)
			row = maxRow;

		// clamp to pattern length
		const int16_t patternLen = pattLens[editor.editPattern];
		if (row >= patternLen)
			row = patternLen - 1;

		return row;
	}
}

void handlePatternDataMouseDown(bool mouseButtonHeld)
{
	int16_t y1, y2;

	// non-FT2 feature: Use right mouse button to remove pattern marking
	if (mouse.rightButtonPressed)
	{
		clearPattMark();
		ui.updatePatternEditor = true;
		return;
	}

	if (!mouseButtonHeld)
	{
		// we clicked inside the pattern data area for the first time, set initial vars

		mouse.lastUsedObjectType = OBJECT_PATTERNMARK;

		lastMouseX = mouse.x;
		lastMouseY = mouse.y;

		lastChMark = mouseXToCh();
		lastRowMark = mouseYToRow();

		pattMark.markX1 = lastChMark;
		pattMark.markX2 = lastChMark;
		pattMark.markY1 = lastRowMark;
		pattMark.markY2 = lastRowMark + 1;

		checkMarkLimits();

		ui.updatePatternEditor = true;
		return;
	}

	// we're holding down the mouse button inside the pattern data area

	bool forceMarking = songPlaying;

	// scroll left/right with mouse
	if (ui.pattChanScrollShown)
	{
		if (mouse.x < 29)
		{
			scrollBarScrollUp(SB_CHAN_SCROLL, 1);
			forceMarking = true;
		}
		else if (mouse.x > 604)
		{
			scrollBarScrollDown(SB_CHAN_SCROLL, 1);
			forceMarking = true;
		}
	}

	// mark channels
	if (forceMarking || lastMouseX != mouse.x)
	{
		lastMouseX = mouse.x;

		int8_t chTmp = mouseXToCh();
		if (chTmp < lastChMark)
		{
			pattMark.markX1 = chTmp;
			pattMark.markX2 = lastChMark;
		}
		else
		{
			pattMark.markX2 = chTmp;
			pattMark.markX1 = lastChMark;
		}

		if (lastMarkX1 != pattMark.markX1 || lastMarkX2 != pattMark.markX2)
		{
			checkMarkLimits();
			ui.updatePatternEditor = true;

			lastMarkX1 = pattMark.markX1;
			lastMarkX2 = pattMark.markX2;
		}
	}

	// scroll down/up with mouse (if song is not playing)
	if (!songPlaying)
	{
		y1 = ui.extended ? 56 : 176;
		y2 = ui.pattChanScrollShown ? 382 : 396;

		if (mouse.y < y1)
		{
			if (editor.pattPos > 0)
				setPos(-1, editor.pattPos - 1, true);

			forceMarking = true;
			ui.updatePatternEditor = true;
		}
		else if (mouse.y > y2)
		{
			const int16_t pattLen = pattLens[editor.editPattern];
			if (editor.pattPos < pattLen-1)
				setPos(-1, editor.pattPos + 1, true);

			forceMarking = true;
			ui.updatePatternEditor = true;
		}
	}

	// mark rows
	if (forceMarking || lastMouseY != mouse.y)
	{
		lastMouseY = mouse.y;

		const int16_t rowTmp = mouseYToRow();
		if (rowTmp < lastRowMark)
		{
			pattMark.markY1 = rowTmp;
			pattMark.markY2 = lastRowMark + 1;
		}
		else
		{
			pattMark.markY2 = rowTmp + 1;
			pattMark.markY1 = lastRowMark;
		}

		if (lastMarkY1 != pattMark.markY1 || lastMarkY2 != pattMark.markY2)
		{
			checkMarkLimits();
			ui.updatePatternEditor = true;

			lastMarkY1 = pattMark.markY1;
			lastMarkY2 = pattMark.markY2;
		}
	}
}

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

	song.pattPos = (song.pattPos - 1 + song.pattLen) % song.pattLen;
	if (!songPlaying)
	{
		editor.pattPos = (uint8_t)song.pattPos;
		ui.updatePatternEditor = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

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

	if (songPlaying)
	{
		song.timer = 2;
	}
	else
	{
		song.pattPos = (song.pattPos + 1 + song.pattLen) % song.pattLen;
		editor.pattPos = (uint8_t)song.pattPos;
		ui.updatePatternEditor = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

void rowUp(uint16_t amount)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	song.pattPos -= amount;
	if (song.pattPos < 0)
		song.pattPos = 0;

	if (!songPlaying)
	{
		editor.pattPos = (uint8_t)song.pattPos;
		ui.updatePatternEditor = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

void rowDown(uint16_t amount)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	song.pattPos += amount;
	if (song.pattPos >= song.pattLen)
		song.pattPos = song.pattLen - 1;

	if (!songPlaying)
	{
		editor.pattPos = (uint8_t)song.pattPos;
		ui.updatePatternEditor = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

void keybPattMarkUp(void)
{
	int8_t xPos = cursor.ch;
	int16_t pattPos = editor.pattPos;

	if (xPos != pattMark.markX1 && xPos != pattMark.markX2)
	{
		pattMark.markX1 = xPos;
		pattMark.markX2 = xPos;
		pattMark.markY1 = pattPos;
		pattMark.markY2 = pattPos + 1;
	}

	if (pattPos == pattMark.markY1-1)
	{
		pattMark.markY1 = pattPos;
	}
	else if (pattPos == pattMark.markY2)
	{
		pattMark.markY2 = pattPos - 1;
	}
	else if (pattPos != pattMark.markY1 && pattPos != pattMark.markY2)
	{
		pattMark.markX1 = xPos;
		pattMark.markX2 = xPos;
		pattMark.markY1 = pattPos;
		pattMark.markY2 = pattPos + 1;

	}

	checkMarkLimits();
	rowOneUpWrap();
}

void keybPattMarkDown(void)
{
	int8_t xPos = cursor.ch;
	int16_t pattPos = editor.pattPos;

	if (xPos != pattMark.markX1 && xPos != pattMark.markX2)
	{
		pattMark.markX1 = xPos;
		pattMark.markX2 = xPos;
		pattMark.markY1 = pattPos;
		pattMark.markY2 = pattPos + 1;
	}

	if (pattPos == pattMark.markY2)
	{
		pattMark.markY2 = pattPos + 1;
	}
	else if (pattPos == pattMark.markY1-1)
	{
		pattMark.markY1 = pattPos + 2;
	}
	else if (pattPos != pattMark.markY1 && pattPos != pattMark.markY2)
	{
		pattMark.markX1 = xPos;
		pattMark.markX2 = xPos;
		pattMark.markY1 = pattPos;
		pattMark.markY2 = pattPos + 1;
	}

	checkMarkLimits();
	rowOneDownWrap();
}

void keybPattMarkLeft(void)
{
	int8_t xPos = cursor.ch;
	int16_t pattPos = editor.pattPos;

	if (pattPos != pattMark.markY1-1 && pattPos != pattMark.markY2)
	{
		pattMark.markY1 = pattPos - 1;
		pattMark.markY2 = pattPos;
	}

	if (xPos == pattMark.markX1)
	{
		pattMark.markX1 = xPos - 1;
	}
	else if (xPos == pattMark.markX2)
	{
		pattMark.markX2 = xPos - 1;
	}
	else if (xPos != pattMark.markX1 && xPos != pattMark.markX2)
	{
		pattMark.markX1 = xPos - 1;
		pattMark.markX2 = xPos;
		pattMark.markY1 = pattPos - 1;
		pattMark.markY2 = pattPos;
	}

	checkMarkLimits();
	chanLeft();
}

void keybPattMarkRight(void)
{
	int8_t xPos = cursor.ch;
	int16_t pattPos = editor.pattPos;

	if (pattPos != pattMark.markY1-1 && pattPos != pattMark.markY2)
	{
		pattMark.markY1 = pattPos - 1;
		pattMark.markY2 = pattPos;
	}

	if (xPos == pattMark.markX2)
	{
		pattMark.markX2 = xPos + 1;
	}
	else if (xPos == pattMark.markX1)
	{
		pattMark.markX1 = xPos + 1;
	}
	else if (xPos != pattMark.markX1 && xPos != pattMark.markX2)
	{
		pattMark.markX1 = xPos;
		pattMark.markX2 = xPos + 1;
		pattMark.markY1 = pattPos - 1;
		pattMark.markY2 = pattPos;
	}

	checkMarkLimits();
	chanRight();
}

bool loadTrack(UNICHAR *filenameU)
{
	tonTyp loadBuff[MAX_PATT_LEN];
	trackHeaderType th;

	FILE *f = UNICHAR_FOPEN(filenameU, "rb");
	if (f == NULL)
	{
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		return false;
	}

	uint16_t nr = editor.editPattern;
	int16_t pattLen = pattLens[nr];

	if (fread(&th, 1, sizeof (th), f) != sizeof (th))
	{
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		goto trackLoadError;
	}

	if (th.ver != 1)
	{
		okBox(0, "System message", "Incompatible format version!");
		goto trackLoadError;
	}

	if (th.len > MAX_PATT_LEN)
		th.len = MAX_PATT_LEN;

	if (pattLen > th.len)
		pattLen = th.len;

	if (fread(loadBuff, pattLen * sizeof (tonTyp), 1, f) != 1)
	{
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		goto trackLoadError;
	}

	if (!allocatePattern(nr))
	{
		okBox(0, "System message", "Not enough memory!");
		goto trackLoadError;
	}

	tonTyp *pattPtr = patt[nr];

	lockMixerCallback();
	for (int32_t i = 0; i < pattLen; i++)
	{
		pattPtr = &patt[nr][(i * MAX_VOICES) + cursor.ch];
		*pattPtr = loadBuff[i];

		// non-FT2 security fix: remove overflown (illegal) stuff
		if (pattPtr->ton > 97)
			pattPtr->ton = 0;

		if (pattPtr->effTyp > 35)
		{
			pattPtr->effTyp = 0;
			pattPtr->eff = 0;
		}
	}
	unlockMixerCallback();

	fclose(f);

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

	diskOpSetFilename(DISKOP_ITEM_TRACK, filenameU);
	setSongModifiedFlag();

	return true;

trackLoadError:
	fclose(f);
	return false;
}

bool saveTrack(UNICHAR *filenameU)
{
	tonTyp saveBuff[MAX_PATT_LEN];
	trackHeaderType th;

	uint16_t nr = editor.editPattern;
	tonTyp *pattPtr = patt[nr];

	if (pattPtr == NULL)
	{
		okBox(0, "System message", "The current pattern is empty!");
		return false;
	}

	FILE *f = UNICHAR_FOPEN(filenameU, "wb");
	if (f == NULL)
	{
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	const int16_t pattLen = pattLens[nr];
	for (int32_t i = 0; i < pattLen; i++)
		saveBuff[i] = pattPtr[(i * MAX_VOICES) + cursor.ch];

	th.len = pattLen;
	th.ver = 1;

	if (fwrite(&th, sizeof (th), 1, f) !=  1)
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	if (fwrite(saveBuff, pattLen * sizeof (tonTyp), 1, f) != 1)
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	fclose(f);
	return true;
}

bool loadPattern(UNICHAR *filenameU)
{
	patternHeaderType th;

	FILE *f = UNICHAR_FOPEN(filenameU, "rb");
	if (f == NULL)
	{
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		return false;
	}

	uint16_t nr = editor.editPattern;

	if (!allocatePattern(nr))
	{
		okBox(0, "System message", "Not enough memory!");
		goto loadPattError;
	}

	tonTyp *pattPtr = patt[nr];
	uint16_t pattLen = pattLens[nr];

	if (fread(&th, 1, sizeof (th), f) != sizeof (th))
	{
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		goto loadPattError;
	}

	if (th.ver != 1)
	{
		okBox(0, "System message", "Incompatible format version!");
		goto loadPattError;
	}

	if (th.len > MAX_PATT_LEN)
		th.len = MAX_PATT_LEN;

	pattLen = th.len;

	lockMixerCallback();

	if (fread(pattPtr, pattLen * TRACK_WIDTH, 1, f) != 1)
	{
		unlockMixerCallback();
		okBox(0, "System message", "General I/O error during loading! Is the file in use?");
		goto loadPattError;
	}

	// non-FT2 security fix: remove overflown (illegal) stuff
	for (int32_t i = 0; i < pattLen; i++)
	{
		for (int32_t j = 0; j < MAX_VOICES; j++)
		{
			pattPtr = &patt[nr][(i * MAX_VOICES) + j];
			if (pattPtr->ton > 97)
				pattPtr->ton = 0;

			if (pattPtr->effTyp > 35)
			{
				pattPtr->effTyp = 0;
				pattPtr->eff = 0;
			}
		}
	}

	// set new pattern length (FT2 doesn't do this, strange...)
	pattLens[nr] = pattLen;
	song.pattLen = pattLen;
	if (song.pattPos >= pattLen)
	{
		song.pattPos = pattLen-1;
		if (!songPlaying)
			editor.pattPos = song.pattPos;
	}

	unlockMixerCallback();

	fclose(f);

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

	diskOpSetFilename(DISKOP_ITEM_PATTERN, filenameU);
	setSongModifiedFlag();

	return true;

loadPattError:
	fclose(f);
	return false;
}

bool savePattern(UNICHAR *filenameU)
{
	patternHeaderType th;

	uint16_t nr = editor.editPattern;
	tonTyp *pattPtr = patt[nr];

	if (pattPtr == NULL)
	{
		okBox(0, "System message", "The current pattern is empty!");
		return false;
	}

	FILE *f = UNICHAR_FOPEN(filenameU, "wb");
	if (f == NULL)
	{
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	uint16_t pattLen = pattLens[nr];

	th.len = pattLen;
	th.ver = 1;

	if (fwrite(&th, 1, sizeof (th), f) != sizeof (th))
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	if (fwrite(pattPtr, pattLen * TRACK_WIDTH, 1, f) != 1)
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?");
		return false;
	}

	fclose(f);
	return true;
}

void scrollChannelLeft(void)
{
	scrollBarScrollLeft(SB_CHAN_SCROLL, 1);
}

void scrollChannelRight(void)
{
	scrollBarScrollRight(SB_CHAN_SCROLL, 1);
}

void setChannelScrollPos(uint32_t pos)
{
	if (!ui.pattChanScrollShown)
	{
		ui.channelOffset = 0;
		return;
	}

	if (ui.channelOffset == (uint8_t)pos)
		return;

	ui.channelOffset = (uint8_t)pos;

	assert(song.antChn > ui.numChannelsShown);
	if (ui.channelOffset >= song.antChn-ui.numChannelsShown)
		ui.channelOffset = (uint8_t)(song.antChn-ui.numChannelsShown);

	if (cursor.ch >= ui.channelOffset+ui.numChannelsShown)
	{
		cursor.object = CURSOR_NOTE;
		cursor.ch = (ui.channelOffset + ui.numChannelsShown) - 1;
	}
	else if (cursor.ch < ui.channelOffset)
	{
		cursor.object = CURSOR_NOTE;
		cursor.ch = ui.channelOffset;
	}

	ui.updatePatternEditor = true;
}

void jumpToChannel(uint8_t channel) // for ALT+q..i ALT+a..k
{
	if (ui.sampleEditorShown || ui.instEditorShown)
		return;

	channel %= song.antChn;
	if (cursor.ch == channel)
		return;

	if (ui.pattChanScrollShown)
	{
		assert(song.antChn > ui.numChannelsShown);

		if (channel >= ui.channelOffset+ui.numChannelsShown)
			scrollBarScrollDown(SB_CHAN_SCROLL, (channel - (ui.channelOffset + ui.numChannelsShown)) + 1);
		else if (channel < ui.channelOffset)
			scrollBarScrollUp(SB_CHAN_SCROLL, ui.channelOffset - channel);
	}

	cursor.ch = channel; // set it here since scrollBarScrollX() changes it...
	ui.updatePatternEditor = true;
}

void sbPosEdPos(uint32_t pos)
{
	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();

	if (song.songPos != (int16_t)pos)
		setNewSongPos((int16_t)pos);

	if (audioWasntLocked)
		unlockAudio();
}

void pbPosEdPosUp(void)
{
	incSongPos();
}

void pbPosEdPosDown(void)
{
	decSongPos();
}

void pbPosEdIns(void)
{
	if (song.len >= 255)
		return;

	lockMixerCallback();

	const uint8_t oldPatt = song.songTab[song.songPos];
	for (uint16_t i = 0; i < 255-song.songPos; i++)
		song.songTab[255-i] = song.songTab[254-i];
	song.songTab[song.songPos] = oldPatt;

	song.len++;

	ui.updatePosSections = true;
	ui.updatePosEdScrollBar = true;
	setSongModifiedFlag();

	unlockMixerCallback();
}

void pbPosEdDel(void)
{
	if (song.len <= 1)
		return;

	lockMixerCallback();

	if (song.songPos < 254)
	{
		for (uint16_t i = 0; i < 254-song.songPos; i++)
			song.songTab[song.songPos+i] = song.songTab[song.songPos+1+i];
	}

	song.len--;
	if (song.repS >= song.len)
		song.repS = song.len - 1;

	if (song.songPos > song.len-1)
	{
		editor.songPos = song.songPos = song.len-1;
		setPos(song.songPos, -1, false);
	}

	ui.updatePosSections = true;
	ui.updatePosEdScrollBar = true;
	setSongModifiedFlag();

	unlockMixerCallback();
}

void pbPosEdPattUp(void)
{
	if (song.songTab[song.songPos] == 255)
		return;

	lockMixerCallback();
	if (song.songTab[song.songPos] < 255)
	{
		song.songTab[song.songPos]++;
		song.pattNr = song.songTab[song.songPos];

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

		if (!songPlaying)
			editor.editPattern = (uint8_t)song.pattNr;

		checkMarkLimits();
		ui.updatePatternEditor = true;
		ui.updatePosSections = true;

		setSongModifiedFlag();
	}
	unlockMixerCallback();
}

void pbPosEdPattDown(void)
{
	if (song.songTab[song.songPos] == 0)
		return;

	lockMixerCallback();
	if (song.songTab[song.songPos] > 0)
	{
		song.songTab[song.songPos]--;
		song.pattNr = song.songTab[song.songPos];

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

		if (!songPlaying)
			editor.editPattern = (uint8_t)song.pattNr;

		checkMarkLimits();
		ui.updatePatternEditor = true;
		ui.updatePosSections = true;

		setSongModifiedFlag();
	}
	unlockMixerCallback();
}

void pbPosEdLenUp(void)
{
	if (song.len >= 255)
		return;

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

	song.len++;

	ui.updatePosSections = true;
	ui.updatePosEdScrollBar = true;
	setSongModifiedFlag();

	if (audioWasntLocked)
		unlockAudio();
}

void pbPosEdLenDown(void)
{
	if (song.len <= 1)
		return;

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

	song.len--;

	if (song.repS >= song.len)
		song.repS = song.len - 1;

	if (song.songPos >= song.len)
	{
		song.songPos = song.len - 1;
		setPos(song.songPos, -1, false);
	}

	ui.updatePosSections = true;
	ui.updatePosEdScrollBar = true;
	setSongModifiedFlag();

	if (audioWasntLocked)
		unlockAudio();
}

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

	if (song.repS < song.len-1)
	{
		song.repS++;
		ui.updatePosSections = true;
		setSongModifiedFlag();
	}

	if (audioWasntLocked)
		unlockAudio();
}

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

	if (song.repS > 0)
	{
		song.repS--;
		ui.updatePosSections = true;
		setSongModifiedFlag();
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbBPMUp(void)
{
	if (song.speed == 255)
		return;

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

	if (song.speed < 255)
	{
		song.speed++;
		P_SetSpeed(song.speed);

		// if song is playing, the update is handled in the audio/video sync queue
		if (!songPlaying)
		{
			editor.speed = song.speed;
			drawSongBPM(song.speed);
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbBPMDown(void)
{
	if (song.speed == 32)
		return;

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

	if (song.speed > 32)
	{
		song.speed--;
		P_SetSpeed(song.speed);

		// if song is playing, the update is handled in the audio/video sync queue
		if (!songPlaying)
		{
			editor.speed = song.speed;
			drawSongBPM(editor.speed);
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbSpeedUp(void)
{
	if (song.tempo == 31)
		return;

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

	if (song.tempo < 31)
	{
		song.tempo++;

		// if song is playing, the update is handled in the audio/video sync queue
		if (!songPlaying)
		{
			editor.tempo = song.tempo;
			drawSongSpeed(editor.tempo);
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbSpeedDown(void)
{
	if (song.tempo == 0)
		return;

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

	if (song.tempo > 0)
	{
		song.tempo--;

		// if song is playing, the update is handled in the audio/video sync queue
		if (!songPlaying)
		{
			editor.tempo = song.tempo;
			drawSongSpeed(editor.tempo);
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbIncAdd(void)
{
	if (editor.ID_Add == 16)
		editor.ID_Add = 0;
	else
		editor.ID_Add++;

	drawIDAdd();
}

void pbDecAdd(void)
{
	if (editor.ID_Add == 0)
		editor.ID_Add = 16;
	else
		editor.ID_Add--;

	drawIDAdd();
}

void pbAddChan(void)
{
	if (song.antChn > 30)
		return;

	lockMixerCallback();
	song.antChn += 2;

	hideTopScreen();
	showTopLeftMainScreen(true);
	showTopRightMainScreen();

	if (ui.patternEditorShown)
		showPatternEditor();

	setSongModifiedFlag();
	unlockMixerCallback();
}

void pbSubChan(void)
{
	if (song.antChn < 4)
		return;

	lockMixerCallback();

	song.antChn -= 2;

	checkMarkLimits();

	hideTopScreen();
	showTopLeftMainScreen(true);
	showTopRightMainScreen();

	if (ui.patternEditorShown)
		showPatternEditor();

	setSongModifiedFlag();
	unlockMixerCallback();
}

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

	if (song.pattNr < 255)
	{
		song.pattNr++;

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

		if (!songPlaying)
			editor.editPattern = (uint8_t)song.pattNr;

		checkMarkLimits();
		ui.updatePatternEditor = true;
		ui.updatePosSections = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

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

	if (song.pattNr > 0)
	{
		song.pattNr--;

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

		if (!songPlaying)
			editor.editPattern = (uint8_t)song.pattNr;

		checkMarkLimits();
		ui.updatePatternEditor = true;
		ui.updatePosSections = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

void pbPattLenUp(void)
{
	const uint16_t pattLen = pattLens[editor.editPattern];
	if (pattLen >= 256)
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();
	
	setPatternLen(editor.editPattern, pattLen + 1);
	checkMarkLimits();

	ui.updatePatternEditor = true;
	ui.updatePosSections = true;
	setSongModifiedFlag();

	if (audioWasntLocked)
		unlockAudio();
}

void pbPattLenDown(void)
{
	const uint16_t pattLen = pattLens[editor.editPattern];
	if (pattLen <= 1)
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();
	
	setPatternLen(editor.editPattern, pattLen - 1);
	checkMarkLimits();

	ui.updatePatternEditor = true;
	ui.updatePosSections = true;
	setSongModifiedFlag();

	if (audioWasntLocked)
		unlockAudio();
}

void drawPosEdNums(int16_t songPos)
{
	if (songPos >= song.len)
		songPos = song.len - 1;

	// clear
	if (ui.extended)
	{
		clearRect(8,  4, 39, 16);
		fillRect(8, 23, 39, 7, PAL_DESKTOP);
		clearRect(8, 33, 39, 16);
	}
	else
	{
		clearRect(8,  4, 39, 15);
		fillRect(8, 22, 39, 7, PAL_DESKTOP);
		clearRect(8, 32, 39, 15);
	}

	const uint32_t color1 = video.palette[PAL_PATTEXT];
	const uint32_t color2 = video.palette[PAL_FORGRND];

	// top two
	for (int16_t y = 0; y < 2; y++)
	{
		int16_t entry = songPos - (2 - y);
		if (entry < 0)
			continue;

		assert(entry < 256);

		if (ui.extended)
		{
			pattTwoHexOut(8,  4 + (y * 9), (uint8_t)entry, color1);
			pattTwoHexOut(32, 4 + (y * 9), song.songTab[entry], color1);
		}
		else
		{
			pattTwoHexOut(8,  4 + (y * 8), (uint8_t)entry, color1);
			pattTwoHexOut(32, 4 + (y * 8), song.songTab[entry], color1);
		}
	}

	assert(songPos < 256);

	// middle
	if (ui.extended)
	{
		pattTwoHexOut(8,  23, (uint8_t)songPos, color2);
		pattTwoHexOut(32, 23, song.songTab[songPos], color2);
	}
	else
	{
		pattTwoHexOut(8,  22, (uint8_t)songPos, color2);
		pattTwoHexOut(32, 22, song.songTab[songPos], color2);
	}

	// bottom two
	for (int16_t y = 0; y < 2; y++)
	{
		int16_t entry = songPos + (1 + y);
		if (entry >= song.len)
			break;

		if (ui.extended)
		{
			pattTwoHexOut(8,  33 + (y * 9), (uint8_t)entry, color1);
			pattTwoHexOut(32, 33 + (y * 9), song.songTab[entry], color1);
		}
		else
		{
			pattTwoHexOut(8,  32 + (y * 8), (uint8_t)entry, color1);
			pattTwoHexOut(32, 32 + (y * 8), song.songTab[entry], color1);
		}
	}
}

void drawSongLength(void)
{
	int16_t x, y;

	if (ui.extended)
	{
		x = 165;
		y = 5;
	}
	else
	{
		x = 59;
		y = 52;
	}

	hexOutBg(x, y, PAL_FORGRND, PAL_DESKTOP, (uint8_t)song.len, 2);
}

void drawSongRepS(void)
{
	int16_t x, y;

	if (ui.extended)
	{
		x = 165;
		y = 19;
	}
	else
	{
		x = 59;
		y = 64;
	}

	hexOutBg(x, y, PAL_FORGRND, PAL_DESKTOP, (uint8_t)song.repS, 2);
}

void drawSongBPM(uint16_t val)
{
	char str[4];
	const char *strOut;
	
	if (ui.extended)
		return;

	if (val <= 255)
	{
		strOut = dec3StrTab[val];
	}
	else
	{
		if (val > MAX_BPM)
			val = MAX_BPM;

		assert(MAX_BPM == 999);
		str[0] = '0' + (char)(val / 100);
		str[1] = '0' + ((val / 10) % 10);
		str[2] = '0' + (val % 10);
		str[3] = 0;

		strOut = str;
	}

	textOutFixed(145, 36, PAL_FORGRND, PAL_DESKTOP, strOut);
}

void drawSongSpeed(uint16_t val)
{
	if (ui.extended)
		return;

	if (val > 99)
		val = 99;

	textOutFixed(152, 50, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[val]);
}

void drawEditPattern(uint16_t editPattern)
{
	int16_t x, y;

	if (ui.extended)
	{
		x = 252;
		y = 39;
	}
	else
	{
		x = 237;
		y = 36;
	}

	hexOutBg(x, y, PAL_FORGRND, PAL_DESKTOP, editPattern, 2);
}

void drawPatternLength(uint16_t editPattern)
{
	int16_t x, y;

	if (ui.extended)
	{
		x = 326;
		y = 39;
	}
	else
	{
		x = 230;
		y = 50;
	}

	hexOutBg(x, y, PAL_FORGRND, PAL_DESKTOP, pattLens[editPattern], 3);
}

void drawGlobalVol(uint16_t val)
{
	if (ui.extended)
		return;

	assert(val <= 64);
	textOutFixed(87, 80, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[val]);
}

void drawIDAdd(void)
{
	assert(editor.ID_Add <= 16);
	textOutFixed(152, 64, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[editor.ID_Add]);
}

void resetPlaybackTime(void)
{
	song.musicTime64 = 0;
	last_TimeH = 0;
	last_TimeM = 0;
	last_TimeS = 0;
}

void drawPlaybackTime(void)
{
	if (songPlaying)
	{
		const uint32_t ms1024 = song.musicTime64 >> 32; // milliseconds (scaled from 1000 to 1024)

		uint32_t seconds = ms1024 >> 10;
		last_TimeH = seconds / 3600; seconds -= last_TimeH * 3600;
		last_TimeM = seconds / 60;   seconds -= last_TimeM * 60;
		last_TimeS = seconds;
	}

	textOutFixed(235, 80, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[last_TimeH]);
	textOutFixed(255, 80, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[last_TimeM]);
	textOutFixed(275, 80, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[last_TimeS]);
}

void drawSongName(void)
{
	drawFramework(421, 155, 166, 18, FRAMEWORK_TYPE1);
	drawFramework(423, 157, 162, 14, FRAMEWORK_TYPE2);
	drawTextBox(TB_SONG_NAME);
}

void changeLogoType(uint8_t logoType)
{
	pushButtons[PB_LOGO].bitmapFlag = true;

	if (logoType == 0)
	{
		pushButtons[PB_LOGO].bitmapUnpressed = &bmp.ft2LogoBadges[(154 * 32) * 0];
		pushButtons[PB_LOGO].bitmapPressed = &bmp.ft2LogoBadges[(154 * 32) * 1];
	}
	else
	{
		pushButtons[PB_LOGO].bitmapUnpressed = &bmp.ft2LogoBadges[(154 * 32) * 2];
		pushButtons[PB_LOGO].bitmapPressed = &bmp.ft2LogoBadges[(154 * 32) * 3];
	}

	drawPushButton(PB_LOGO);
}

void changeBadgeType(uint8_t badgeType)
{
	pushButtons[PB_BADGE].bitmapFlag = true;

	if (badgeType == 0)
	{
		pushButtons[PB_BADGE].bitmapUnpressed = &bmp.ft2ByBadges[(25 * 32) * 0];
		pushButtons[PB_BADGE].bitmapPressed = &bmp.ft2ByBadges[(25 * 32) * 1];
	}
	else
	{
		pushButtons[PB_BADGE].bitmapUnpressed = &bmp.ft2ByBadges[(25 * 32) * 2];
		pushButtons[PB_BADGE].bitmapPressed = &bmp.ft2ByBadges[(25 * 32) * 3];
	}

	drawPushButton(PB_BADGE);
}

void updateInstrumentSwitcher(void)
{
	int16_t y;

	if (ui.aboutScreenShown || ui.configScreenShown || ui.helpScreenShown || ui.nibblesShown)
		return; // don't redraw instrument switcher when it's not shown!

	if (ui.extended) // extended pattern editor
	{
		//INSTRUMENTS

		clearRect(388, 5, 116, 43); // left box
		clearRect(511, 5, 116, 43); // right box

		// draw source instrument selection
		if (editor.srcInstr >= editor.instrBankOffset && editor.srcInstr <= editor.instrBankOffset+8)
		{
			y = 5 + ((editor.srcInstr - editor.instrBankOffset - 1) * 11);
			if (y >= 5 && y <= 82)
			{
				if (y <= 47)
					fillRect(388, y, 15, 10, PAL_BUTTONS); // left box
				else
					fillRect(511, y - 44, 15, 10, PAL_BUTTONS); // right box
			}
		}

		// draw destination instrument selection
		if (editor.curInstr >= editor.instrBankOffset && editor.curInstr <= editor.instrBankOffset+8)
		{
			y = 5 + ((editor.curInstr - editor.instrBankOffset - 1) * 11);
			y = 5 + ((editor.curInstr - editor.instrBankOffset - 1) * 11);
			if (y >= 5 && y <= 82)
			{
				if (y <= 47)
					fillRect(406, y, 98, 10, PAL_BUTTONS); // left box
				else
					fillRect(529, y - 44, 98, 10, PAL_BUTTONS); // right box
			}
		}

		// draw numbers and texts
		for (int16_t i = 0; i < 4; i++)
		{
			hexOut(388, 5 + (i * 11), PAL_FORGRND, 1 + editor.instrBankOffset + i, 2);
			hexOut(511, 5 + (i * 11), PAL_FORGRND, 5 + editor.instrBankOffset + i, 2);
			drawTextBox(TB_INST1 + i);
			drawTextBox(TB_INST5 + i);
		}
	}
	else // normal pattern editor
	{
		// INSTRUMENTS

		clearRect(424, 5,  15, 87); // src instrument
		clearRect(446, 5, 139, 87); // main instrument

		// draw source instrument selection
		if (editor.srcInstr >= editor.instrBankOffset && editor.srcInstr <= editor.instrBankOffset+8)
		{
			y = 5 + ((editor.srcInstr - editor.instrBankOffset - 1) * 11);
			if (y >= 5 && y <= 82)
				fillRect(424, y, 15, 10, PAL_BUTTONS);
		}

		// draw destination instrument selection
		if (editor.curInstr >= editor.instrBankOffset && editor.curInstr <= editor.instrBankOffset+8)
		{
			y = 5 + ((editor.curInstr - editor.instrBankOffset - 1) * 11);
			if (y >= 5 && y <= 82)
				fillRect(446, y, 139, 10, PAL_BUTTONS);
		}

		// draw numbers and texts
		for (int16_t i = 0; i < 8; i++)
		{
			hexOut(424, 5 + (i * 11), PAL_FORGRND, 1 + editor.instrBankOffset + i, 2);
			drawTextBox(TB_INST1 + i);
		}

		// SAMPLES

		clearRect(424, 99,  15, 54); // src sample
		clearRect(446, 99, 115, 54); // main sample

		// draw source sample selection
		if (editor.srcSmp >= editor.sampleBankOffset && editor.srcSmp <= editor.sampleBankOffset+4)
		{
			y = 99 + ((editor.srcSmp - editor.sampleBankOffset) * 11);
			if (y >= 36 && y <= 143)
				fillRect(424, y, 15, 10, PAL_BUTTONS);
		}

		// draw destination sample selection
		if (editor.curSmp >= editor.sampleBankOffset && editor.curSmp <= editor.sampleBankOffset+4)
		{
			y = 99 + ((editor.curSmp - editor.sampleBankOffset) * 11);
			if (y >= 36 && y <= 143)
				fillRect(446, y, 115, 10, PAL_BUTTONS);
		}

		// draw numbers and texts
		for (int16_t i = 0; i < 5; i++)
		{
			hexOut(424, 99 + (i * 11), PAL_FORGRND, editor.sampleBankOffset + i, 2);
			drawTextBox(TB_SAMP1 + i);
		}
	}
}

void showInstrumentSwitcher(void)
{
	if (!ui.instrSwitcherShown)
		return;

	for (uint16_t i = 0; i < 8; i++)
		showTextBox(TB_INST1 + i);

	if (ui.extended)
	{
		hidePushButton(PB_SAMPLE_LIST_UP);
		hidePushButton(PB_SAMPLE_LIST_DOWN);
		hideScrollBar(SB_SAMPLE_LIST);

		drawFramework(386,  0, 246,   3, FRAMEWORK_TYPE1);
		drawFramework(506,  3,   3,  47, FRAMEWORK_TYPE1);
		drawFramework(386, 50, 246,   3, FRAMEWORK_TYPE1);
		drawFramework(629,  3,   3,  47, FRAMEWORK_TYPE1);

		clearRect(386, 3, 120, 47);
		clearRect(509, 3, 120, 47);
	}
	else
	{
		drawFramework(421,   0, 166,   3, FRAMEWORK_TYPE1);
		drawFramework(442,   3,   3,  91, FRAMEWORK_TYPE1);
		drawFramework(421,  94, 166,   3, FRAMEWORK_TYPE1);
		drawFramework(442,  97,   3,  58, FRAMEWORK_TYPE1);
		drawFramework(563,  97,  24,  58, FRAMEWORK_TYPE1);
		drawFramework(587,   0,  45,  71, FRAMEWORK_TYPE1);
		drawFramework(587,  71,  45,  71, FRAMEWORK_TYPE1);
		drawFramework(587, 142,  45,  31, FRAMEWORK_TYPE1);

		fillRect(421,  3,  21, 91, PAL_BCKGRND);
		fillRect(445,  3, 142, 91, PAL_BCKGRND);
		fillRect(421, 97,  21, 58, PAL_BCKGRND);
		fillRect(445, 97, 118, 58, PAL_BCKGRND);

		showPushButton(PB_SAMPLE_LIST_UP);
		showPushButton(PB_SAMPLE_LIST_DOWN);
		showScrollBar(SB_SAMPLE_LIST);

		for (uint16_t i = 0; i < 5; i++)
			showTextBox(TB_SAMP1 + i);
	}

	updateInstrumentSwitcher();

	for (uint16_t i = 0; i < 8; i++)
		showPushButton(PB_RANGE1 + i + (editor.instrBankSwapped * 8));

	showPushButton(PB_SWAP_BANK);
}

void hideInstrumentSwitcher(void)
{
	for (uint16_t i = 0; i < 16; i++)
		hidePushButton(PB_RANGE1 + i);

	hidePushButton(PB_SWAP_BANK);
	hidePushButton(PB_SAMPLE_LIST_UP);
	hidePushButton(PB_SAMPLE_LIST_DOWN);
	hideScrollBar(SB_SAMPLE_LIST);

	for (uint16_t i = 0; i < 8; i++)
		hideTextBox(TB_INST1 + i);

	for (uint16_t i = 0; i < 5; i++)
		hideTextBox(TB_SAMP1 + i);
}

void pbSwapInstrBank(void)
{
	editor.instrBankSwapped ^= 1;

	if (editor.instrBankSwapped)
		editor.instrBankOffset += 8*8;
	else
		editor.instrBankOffset -= 8*8;

	updateTextBoxPointers();

	if (ui.instrSwitcherShown)
	{
		updateInstrumentSwitcher();
		for (uint16_t i = 0; i < 8; i++)
		{
			hidePushButton(PB_RANGE1 + i + (!editor.instrBankSwapped * 8));
			showPushButton(PB_RANGE1 + i + ( editor.instrBankSwapped * 8));
		}
	}
}

void pbSetInstrBank1(void)
{
	editor.instrBankOffset = 0 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank2(void)
{
	editor.instrBankOffset = 1 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank3(void)
{
	editor.instrBankOffset = 2 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank4(void)
{
	editor.instrBankOffset = 3 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank5(void)
{
	editor.instrBankOffset = 4 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank6(void)
{
	editor.instrBankOffset = 5 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank7(void)
{
	editor.instrBankOffset = 6 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank8(void)
{
	editor.instrBankOffset = 7 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank9(void)
{
	editor.instrBankOffset = 8 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank10(void)
{
	editor.instrBankOffset = 9 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank11(void)
{
	editor.instrBankOffset = 10 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank12(void)
{
	editor.instrBankOffset = 11 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank13(void)
{
	editor.instrBankOffset = 12 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank14(void)
{
	editor.instrBankOffset = 13 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank15(void)
{
	editor.instrBankOffset = 14 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void pbSetInstrBank16(void)
{
	editor.instrBankOffset = 15 * 8;

	updateTextBoxPointers();
	updateInstrumentSwitcher();
}

void setNewInstr(int16_t ins)
{ 
	if (ins <= MAX_INST)
	{
		editor.curInstr = (uint8_t)ins;
		updateTextBoxPointers();
		updateInstrumentSwitcher();
		updateNewInstrument();
	}
}

void sampleListScrollUp(void)
{
	scrollBarScrollUp(SB_SAMPLE_LIST, 1);
}

void sampleListScrollDown(void)
{
	scrollBarScrollDown(SB_SAMPLE_LIST, 1);
}

static void zapSong(void)
{
	lockMixerCallback();

	song.len = 1;
	song.repS = 0; // Bug: FT2 doesn't do this!
	song.speed = 125;
	song.tempo = 6;
	song.songPos = 0;
	song.globVol = 64;

	memset(song.name, 0, sizeof (song.name));
	memset(song.songTab, 0, sizeof (song.songTab));

	// zero all pattern data and reset pattern lengths

	freeAllPatterns();
	for (uint16_t i = 0; i < MAX_PATTERNS; i++)
		pattLens[i] = 64;
	song.pattLen = pattLens[song.pattNr];

	resetMusic();
	P_SetSpeed(song.speed);

	editor.songPos = song.songPos;
	editor.editPattern = song.pattNr;
	editor.speed = song.speed;
	editor.tempo = song.tempo;
	editor.globalVol = song.globVol;
	editor.timer = 1;

	resetPlaybackTime();

	if (!audio.linearFreqTable)
		setFrqTab(true);

	clearPattMark();
	resetWavRenderer();
	resetChannels();
	unlockMixerCallback();

	setScrollBarPos(SB_POS_ED, 0, false);
	setScrollBarEnd(SB_POS_ED, (song.len - 1) + 5);

	updateWindowTitle(true);
}

static void zapInstrs(void)
{
	lockMixerCallback();

	for (int16_t i = 1; i <= MAX_INST; i++)
	{
		freeInstr(i);
		memset(song.instrName[i], 0, 22+1);
	}

	updateNewInstrument();

	editor.currVolEnvPoint = 0;
	editor.currPanEnvPoint = 0;

	updateSampleEditorSample();

	if (ui.sampleEditorShown)
		updateSampleEditor();
	else if (ui.instEditorShown || ui.instEditorExtShown)
		updateInstEditor();

	unlockMixerCallback();
}

void pbZap(void)
{
	const int16_t choice = okBox(4, "System request", "Total devastation of the...");

	if (choice == 1) // zap all
	{
		zapSong();
		zapInstrs();
	}
	else if (choice == 2) // zap song
	{
		zapSong();
	}
	else if (choice == 3) // zap instruments
	{
		zapInstrs();
	}

	if (choice >= 1 && choice <= 3)
	{
		// redraw top screens
		hideTopScreen();
		showTopScreen(true);

		setSongModifiedFlag();
	}
}

void sbSmpBankPos(uint32_t pos)
{
	if (editor.sampleBankOffset != pos)
	{
		editor.sampleBankOffset = (uint8_t)pos;

		updateTextBoxPointers();
		updateInstrumentSwitcher();
	}
}

void pbToggleLogo(void)
{
	config.id_FastLogo ^= 1;
	changeLogoType(config.id_FastLogo);
}

void pbToggleBadge(void)
{
	config.id_TritonProd ^= 1;
	changeBadgeType(config.id_TritonProd);
}

void resetChannelOffset(void)
{
	ui.pattChanScrollShown = song.antChn > getMaxVisibleChannels();
	cursor.object = CURSOR_NOTE;
	cursor.ch = 0;
	setScrollBarPos(SB_CHAN_SCROLL, 0, true);
	ui.channelOffset = 0;
}

void shrinkPattern(void)
{
	if (okBox(2, "System request", "Shrink pattern?") != 1)
		return;

	uint16_t nr = editor.editPattern;

	int16_t pattLen = pattLens[nr];
	if (pattLen > 1)
	{
		lockMixerCallback();

		tonTyp *pattPtr = patt[nr];
		if (pattPtr != NULL)
		{
			for (int32_t i = 0; i < pattLen/2; i++)
			{
				for (int32_t j = 0; j < MAX_VOICES; j++)
					pattPtr[(i * MAX_VOICES) + j] = pattPtr[((i * 2) * MAX_VOICES) + j];
			}
		}

		pattLens[nr] >>= 1;

		if (song.pattNr == nr)
			song.pattLen = pattLens[nr];

		song.pattPos >>= 1;
		if (song.pattPos >= pattLens[nr])
			song.pattPos = pattLens[nr] - 1;

		editor.pattPos = song.pattPos;

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

		unlockMixerCallback();
		setSongModifiedFlag();
	}
}

void expandPattern(void)
{
	uint16_t nr = editor.editPattern;

	int16_t pattLen = pattLens[nr];
	if (pattLen > 128)
	{
		okBox(0, "System message", "Pattern is too long to be expanded.");
	}
	else
	{
		lockMixerCallback();

		if (patt[nr] != NULL)
		{
			tonTyp *tmpPtn = (tonTyp *)malloc((pattLen * 2) * TRACK_WIDTH);
			if (tmpPtn == NULL)
			{
				unlockMixerCallback();
				okBox(0, "System message", "Not enough memory!");
				return;
			}

			for (int32_t i = 0; i < pattLen; i++)
			{
				for (int32_t j = 0; j < MAX_VOICES; j++)
					tmpPtn[((i * 2) * MAX_VOICES) + j] = patt[nr][(i * MAX_VOICES) + j];

				memset(&tmpPtn[((i * 2) + 1) * MAX_VOICES], 0, TRACK_WIDTH);
			}

			free(patt[nr]);
			patt[nr] = tmpPtn;
		}

		pattLens[nr] *= 2;

		if (song.pattNr == nr)
			song.pattLen = pattLens[nr];

		song.pattPos *= 2;
		if (song.pattPos >= pattLens[nr])
			song.pattPos = pattLens[nr] - 1;

		editor.pattPos = song.pattPos;

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

		unlockMixerCallback();
		setSongModifiedFlag();
	}
}