shithub: ft²

ref: 89cb1fd4c15081a81c760389e6ff1f2f7cd5dd19
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 "scopes/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"


// 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 ptnNumRows[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;

static note_t tmpPattern[MAX_CHANNELS * MAX_PATT_LEN];

volatile pattMark_t pattMark; // globalized

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

	if (pattern[pattNum] == 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.
		**/

		pattern[pattNum] = (note_t *)calloc((MAX_PATT_LEN * TRACK_WIDTH) + 16, 1);
		if (pattern[pattNum] == NULL)
		{
			if (audioWasntLocked)
				unlockAudio();

			return false;
		}

		song.currNumRows = patternNumRows[pattNum];
	}

	if (audioWasntLocked)
		unlockAudio();

	return true;
}

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

	if (patternEmpty(pattNum))
	{
		if (pattern[pattNum] != NULL)
		{
			free(pattern[pattNum]);
			pattern[pattNum] = NULL;
		}
	}

	if (audioWasntLocked)
		unlockAudio();
}

uint8_t getMaxVisibleChannels(void)
{
	assert(config.ptnMaxChannels >= 0 && config.ptnMaxChannels <= 3);
	if (config.ptnShowVolColumn)
		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.numChannels - 1);
		if (ui.pattChanScrollShown)
			setScrollBarPos(SB_CHAN_SCROLL, song.numChannels, 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.numChannels-1)
	{
		cursor.ch = 0;
		if (ui.pattChanScrollShown)
			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.ptnShowVolColumn)
	{
		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.ptnShowVolColumn)
	{
		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];
		}
	}
}

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();
	drawSongLoopStart();
	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((void *)&pattMark, 0, sizeof (pattMark));

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

void checkMarkLimits(void)
{
	volatile int16_t markX1 = pattMark.markX1;
	volatile int16_t markX2 = pattMark.markX2;
	volatile int16_t markY1 = pattMark.markY1;
	volatile int16_t markY2 = pattMark.markY2;

	const int16_t limitY = patternNumRows[editor.editPattern];
	markY1 = CLAMP(markY1, 0, limitY);
	markY2 = CLAMP(markY2, 0, limitY);

	const int16_t limitX = (int16_t)(song.numChannels - 1);
	markX1 = CLAMP(markX1, 0, limitX);
	markX2 = CLAMP(markX2, 0, limitX);

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

	pattMark.markX1 = markX1;
	pattMark.markX2 = markX2;
	pattMark.markY1 = markY1;
	pattMark.markY2 = markY2;
}

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.numChannels)
		ch = (int8_t)(song.numChannels - 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.ptnStretch][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.ptnStretch ? 11 : 8;

	// test top/middle/bottom rows
	if (my < pattCoordsMouse->midRowY)
	{
		// top rows
		int16_t row = editor.row - (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.row;
	}
	else
	{
		// bottom rows
		int16_t row = (editor.row + 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.ptnStretch * 2) + ui.pattChanScrollShown;

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

		// clamp to pattern length
		const int16_t patternLen = patternNumRows[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.row > 0)
				setPos(-1, editor.row - 1, true);

			forceMarking = true;
			ui.updatePatternEditor = true;
		}
		else if (mouse.y > y2)
		{
			const int16_t numRows = patternNumRows[editor.editPattern];
			if (editor.row < numRows-1)
				setPos(-1, editor.row + 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();

	if (song.currNumRows > 0)
	{
		song.row = (song.row - 1 + song.currNumRows) % song.currNumRows;

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

	if (audioWasntLocked)
		unlockAudio();
}

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

	if (songPlaying)
	{
		song.tick = 2;
	}
	else if (song.currNumRows > 0)
	{
		song.row = (song.row + 1 + song.currNumRows) % song.currNumRows;
		editor.row = (uint8_t)song.row;
		ui.updatePatternEditor = true;
	}

	if (audioWasntLocked)
		unlockAudio();
}

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

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

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

	if (audioWasntLocked)
		unlockAudio();
}

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

	song.row += amount;
	if (song.row >= song.currNumRows)
		song.row = song.currNumRows - 1;

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

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

	}

	checkMarkLimits();
	rowOneUpWrap();
}

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

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

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

	checkMarkLimits();
	rowOneDownWrap();
}

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

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

	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 = row - 1;
		pattMark.markY2 = row;
	}

	checkMarkLimits();
	chanLeft();
}

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

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

	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 = row - 1;
		pattMark.markY2 = row;
	}

	checkMarkLimits();
	chanRight();
}

bool loadTrack(UNICHAR *filenameU)
{
	note_t loadBuff[MAX_PATT_LEN];
	xtHdr_t h;

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

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

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

	if (h.numRows > MAX_PATT_LEN)
		h.numRows = MAX_PATT_LEN;

	int16_t numRows = patternNumRows[editor.editPattern];
	if (numRows > h.numRows)
		numRows = h.numRows;

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

	if (!allocatePattern(editor.editPattern))
	{
		okBox(0, "System message", "Not enough memory!", NULL);
		goto trackLoadError;
	}

	lockMixerCallback();
	for (int32_t i = 0; i < numRows; i++)
	{
		note_t *p = &pattern[editor.editPattern][(i * MAX_CHANNELS) + cursor.ch];

		*p = loadBuff[i];

		// sanitize stuff (FT2 doesn't do this!)

		if (p->note > 97)
			p->note = 0;

		if (p->instr > 128)
			p->instr = 0;

		if (p->efx > 35)
		{
			p->efx = 0;
			p->efxData = 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)
{
	note_t saveBuff[MAX_PATT_LEN];
	xtHdr_t h;

	note_t *p = pattern[editor.editPattern];
	if (p == NULL)
	{
		okBox(0, "System message", "The current pattern is empty!", NULL);
		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?", NULL);
		return false;
	}

	h.version = 1;
	h.numRows = patternNumRows[editor.editPattern];

	for (int32_t i = 0; i < h.numRows; i++)
		saveBuff[i] = p[(i * MAX_CHANNELS) + cursor.ch];

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

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

	fclose(f);
	return true;
}

bool loadPattern(UNICHAR *filenameU)
{
	xpHdr_t h;

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

	if (!allocatePattern(editor.editPattern))
	{
		okBox(0, "System message", "Not enough memory!", NULL);
		goto loadPattError;
	}

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

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

	if (h.numRows > MAX_PATT_LEN)
		h.numRows = MAX_PATT_LEN;

	lockMixerCallback();

	note_t *p = pattern[editor.editPattern];
	if (fread(p, h.numRows * TRACK_WIDTH, 1, f) != 1)
	{
		unlockMixerCallback();
		okBox(0, "System message", "General I/O error during loading! Is the file in use?", NULL);
		goto loadPattError;
	}

	// sanitize data (FT2 doesn't do this!)
	for (int32_t row = 0; row < h.numRows; row++)
	{
		for (int32_t ch = 0; ch < MAX_CHANNELS; ch++)
		{
			p = &pattern[editor.editPattern][(row * MAX_CHANNELS) + ch];

			if (p->note > 97)
				p->note = 0;

			if (p->instr > 128)
				p->instr = 128;

			if (p->efx > 35)
			{
				p->efx = 0;
				p->efxData = 0;
			}
		}
	}

	// set new pattern length (FT2 doesn't do this, strange...)
	song.currNumRows = patternNumRows[editor.editPattern] = h.numRows;

	if (song.row >= song.currNumRows)
	{
		song.row = song.currNumRows-1;
		if (!songPlaying)
			editor.row = song.row;
	}

	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)
{
	xpHdr_t h;

	note_t *p = pattern[editor.editPattern];
	if (p == NULL)
	{
		okBox(0, "System message", "The current pattern is empty!", NULL);
		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?", NULL);
		return false;
	}

	h.version = 1;
	h.numRows = patternNumRows[editor.editPattern];
	
	if (fwrite(&h, 1, sizeof (h), f) != sizeof (h))
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?", NULL);
		return false;
	}

	if (fwrite(p, h.numRows * TRACK_WIDTH, 1, f) != 1)
	{
		fclose(f);
		okBox(0, "System message", "General I/O error during saving! Is the file in use?", NULL);
		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.numChannels > ui.numChannelsShown);
	if (ui.channelOffset >= song.numChannels-ui.numChannelsShown)
		ui.channelOffset = (uint8_t)(song.numChannels-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 chNr) // for ALT+q..i ALT+a..k
{
	if (ui.sampleEditorShown || ui.instEditorShown)
		return;

	chNr %= song.numChannels;
	if (cursor.ch == chNr)
		return;

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

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

	cursor.ch = chNr; // 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.songLength >= 255)
		return;

	lockMixerCallback();

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

	song.songLength++;

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

	unlockMixerCallback();
}

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

	lockMixerCallback();

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

	song.songLength--;
	if (song.songLoopStart >= song.songLength)
		song.songLoopStart = song.songLength - 1;

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

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

	unlockMixerCallback();
}

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

	lockMixerCallback();
	if (song.orders[song.songPos] < 255)
	{
		song.orders[song.songPos]++;
		song.pattNum = song.orders[song.songPos];

		song.currNumRows = patternNumRows[song.pattNum];
		if (song.row >= song.currNumRows)
		{
			song.row = song.currNumRows-1;
			if (!songPlaying)
				editor.row = song.row;
		}

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

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

		setSongModifiedFlag();
	}
	unlockMixerCallback();
}

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

	lockMixerCallback();
	if (song.orders[song.songPos] > 0)
	{
		song.orders[song.songPos]--;
		song.pattNum = song.orders[song.songPos];

		song.currNumRows = patternNumRows[song.pattNum];
		if (song.row >= song.currNumRows)
		{
			song.row = song.currNumRows-1;
			if (!songPlaying)
				editor.row = song.row;
		}

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

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

		setSongModifiedFlag();
	}
	unlockMixerCallback();
}

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

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

	song.songLength++;

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

	song.songLength--;
	if (song.songLoopStart >= song.songLength)
		song.songLoopStart = song.songLength - 1;

	if (song.songPos >= song.songLength)
	{
		song.songPos = song.songLength - 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.songLoopStart < song.songLength-1)
	{
		song.songLoopStart++;
		ui.updatePosSections = true;
		setSongModifiedFlag();
	}

	if (audioWasntLocked)
		unlockAudio();
}

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

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

	if (song.BPM < 255)
	{
		song.BPM++;
		setMixerBPM(song.BPM);

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

	if (song.BPM > 32)
	{
		song.BPM--;
		setMixerBPM(song.BPM);

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

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

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

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

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

	if (audioWasntLocked)
		unlockAudio();
}

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

	drawIDAdd();
}

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

	drawIDAdd();
}

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

	lockMixerCallback();
	song.numChannels += 2;

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

	if (ui.patternEditorShown)
		showPatternEditor();

	setSongModifiedFlag();
	unlockMixerCallback();
}

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

	lockMixerCallback();

	song.numChannels -= 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.pattNum < 255)
	{
		song.pattNum++;

		song.currNumRows = patternNumRows[song.pattNum];
		if (song.row >= song.currNumRows)
		{
			song.row = song.currNumRows-1;
			if (!songPlaying)
				editor.row = song.row;
		}

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

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

	if (audioWasntLocked)
		unlockAudio();
}

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

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

		song.currNumRows = patternNumRows[song.pattNum];
		if (song.row >= song.currNumRows)
		{
			song.row = song.currNumRows-1;
			if (!songPlaying)
				editor.row = song.row;
		}

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

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

	if (audioWasntLocked)
		unlockAudio();
}

void pbPattLenUp(void)
{
	const uint16_t numRows = patternNumRows[editor.editPattern];
	if (numRows >= MAX_PATT_LEN)
		return;

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();
	
	song.pattNum = editor.editPattern; // kludge
	setPatternLen(editor.editPattern, numRows+1);

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

	if (audioWasntLocked)
		unlockAudio();
}

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

	const bool audioWasntLocked = !audio.locked;
	if (audioWasntLocked)
		lockAudio();
	
	song.pattNum = editor.editPattern; // kludge
	setPatternLen(editor.editPattern, numRows-1);

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

	if (audioWasntLocked)
		unlockAudio();
}

void drawPosEdNums(int16_t songPos)
{
	if (songPos >= song.songLength)
		songPos = song.songLength - 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.orders[entry], color1);
		}
		else
		{
			pattTwoHexOut(8,  4 + (y * 8), (uint8_t)entry, color1);
			pattTwoHexOut(32, 4 + (y * 8), song.orders[entry], color1);
		}
	}

	assert(songPos < 256);

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

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

		if (ui.extended)
		{
			pattTwoHexOut(8,  33 + (y * 9), (uint8_t)entry, color1);
			pattTwoHexOut(32, 33 + (y * 9), song.orders[entry], color1);
		}
		else
		{
			pattTwoHexOut(8,  32 + (y * 8), (uint8_t)entry, color1);
			pattTwoHexOut(32, 32 + (y * 8), song.orders[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.songLength, 2);
}

void drawSongLoopStart(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.songLoopStart, 2);
}

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

	if (val > 255)
		val = 255;

	textOutFixed(145, 36, PAL_FORGRND, PAL_DESKTOP, dec3StrTab[val]);
}

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, patternNumRows[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.editRowSkip <= 16);
	textOutFixed(152, 64, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[editor.editRowSkip]);
}

void resetPlaybackTime(void)
{
	song.playbackSeconds = 0;
	song.playbackSecondsFrac = 0;

	last_TimeH = 0;
	last_TimeM = 0;
	last_TimeS = 0;
}

void drawPlaybackTime(void)
{
	if (songPlaying)
	{
		uint32_t seconds = song.playbackSeconds;

		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.songLength = 1;
	song.songLoopStart = 0; // FT2 doesn't do this!
	song.BPM = 125;
	song.speed = 6;
	song.songPos = 0;
	song.globalVolume = 64;

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

	// zero all pattern data and reset pattern lengths

	freeAllPatterns();
	for (int32_t i = 0; i < MAX_PATTERNS; i++)
		patternNumRows[i] = 64;
	song.currNumRows = patternNumRows[song.pattNum];

	resetMusic();
	setMixerBPM(song.BPM);

	editor.songPos = song.songPos;
	editor.editPattern = song.pattNum;
	editor.BPM = song.BPM;
	editor.speed = song.speed;
	editor.globalVolume = song.globalVolume;
	editor.tick = 1;

	resetPlaybackTime();

	if (!audio.linearPeriodsFlag)
		setLinearPeriods(true);

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

	setScrollBarPos(SB_POS_ED, 0, false);
	setScrollBarEnd(SB_POS_ED, (song.songLength - 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(3, "System request", "Total devastation of the...", NULL);

	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.numChannels > getMaxVisibleChannels();
	cursor.object = CURSOR_NOTE;
	cursor.ch = 0;
	setScrollBarPos(SB_CHAN_SCROLL, 0, true);
	ui.channelOffset = 0;
}

void shrinkPattern(void)
{
	pauseMusic();
	const volatile uint16_t curPattern = editor.editPattern;
	int16_t numRows = patternNumRows[editor.editPattern];
	resumeMusic();

	if (numRows <= 1)
	{
		okBox(0, "System message", "Pattern is too short to be shrunk!", NULL);
		return;
	}

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

	lockMixerCallback();

	note_t *p = pattern[curPattern];
	if (p != NULL)
	{
		for (int32_t i = 0; i < numRows / 2; i++)
		{
			for (int32_t j = 0; j < MAX_CHANNELS; j++)
				p[(i * MAX_CHANNELS) + j] = p[((i*2) * MAX_CHANNELS) + j];
		}
	}

	patternNumRows[curPattern] /= 2;
	numRows = patternNumRows[curPattern];

	if (song.pattNum == curPattern)
		song.currNumRows = numRows;

	song.row /= 2;
	if (song.row >= numRows)
		song.row = numRows-1;

	editor.row = song.row;

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

	unlockMixerCallback();
	setSongModifiedFlag();
}

void expandPattern(void)
{
	pauseMusic();
	const volatile uint16_t curPattern = editor.editPattern;
	int16_t numRows = patternNumRows[editor.editPattern];
	resumeMusic();

	if (numRows > MAX_PATT_LEN/2)
	{
		okBox(0, "System message", "Pattern is too long to be expanded!", NULL);
		return;
	}

	lockMixerCallback();

	note_t *p = pattern[curPattern];
	if (p != NULL)
	{
		memcpy(tmpPattern, p, numRows * TRACK_WIDTH);

		for (int32_t i = 0; i < numRows; i++)
		{
			for (int32_t j = 0; j < MAX_CHANNELS; j++)
				p[((i * 2) * MAX_CHANNELS) + j] = tmpPattern[(i * MAX_CHANNELS) + j];

			memset(&p[((i * 2) + 1) * MAX_CHANNELS], 0, TRACK_WIDTH);
		}
	}
	
	patternNumRows[curPattern] *= 2;
	numRows = patternNumRows[curPattern];

	if (song.pattNum == curPattern)
		song.currNumRows = numRows;

	song.row *= 2;
	if (song.row >= numRows)
		song.row = numRows-1;

	editor.row = song.row;

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

	unlockMixerCallback();
	setSongModifiedFlag();
}