shithub: pt2-clone

ref: 37069678c8a822cf43703ae627cdaa9768431e15
dir: /src/pt2_sampler.c/

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

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdbool.h>
#include "pt2_header.h"
#include "pt2_helpers.h"
#include "pt2_textout.h"
#include "pt2_audio.h"
#include "pt2_palette.h"
#include "pt2_tables.h"
#include "pt2_visuals.h"
#include "pt2_blep.h"
#include "pt2_mouse.h"
#include "pt2_scopes.h"

#define SAMPLE_AREA_Y_CENTER 169
#define SAMPLE_AREA_HEIGHT 64

typedef struct sampleMixer_t
{
	int32_t length, pos;
	uint32_t posFrac, delta;
} sampleMixer_t;

static int32_t samOffsetScaled;

static const int8_t tuneToneData[32] = // Tuning Tone (Sine Wave)
{
	   0,  25,  49,  71,  91, 106, 118, 126,
	 127, 126, 118, 106,  91,  71,  49,  25,
	   0, -25, -49, -71, -91,-106,-118,-126,
	-127,-126,-118,-106, -91, -71, -49, -25
};

extern uint32_t *pixelBuffer; // pt_main.c

void setLoopSprites(void);

static void updateSamOffset(void)
{
	if (editor.sampler.samDisplay == 0)
		samOffsetScaled = 0;
	else
		samOffsetScaled = (editor.sampler.samOffset * SAMPLE_AREA_WIDTH) / editor.sampler.samDisplay;
}

void fixSampleBeep(moduleSample_t *s)
{
	if (s->length >= 2 && s->loopStart+s->loopLength <= 2)
	{
		modEntry->sampleData[s->offset+0] = 0;
		modEntry->sampleData[s->offset+1] = 0;
	}
}

void updateSamplePos(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	if (editor.currSample >= 0 && editor.currSample <= 30)
	{
		s = &modEntry->samples[editor.currSample];
		if (editor.samplePos > s->length)
			editor.samplePos = s->length;

		if (editor.ui.editOpScreenShown && editor.ui.editOpScreen == 2)
			editor.ui.updatePosText = true;
	}
}

void fillSampleFilterUndoBuffer(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	if (editor.currSample >= 0 && editor.currSample <= 30)
	{
		s = &modEntry->samples[editor.currSample];
		memcpy(editor.tempSample, &modEntry->sampleData[s->offset], s->length);
	}
}

static void line(uint32_t *frameBuffer, int16_t line_x1, int16_t line_x2, int16_t line_y1, int16_t line_y2)
{
	int16_t d, x, y, ax, ay, sx, sy, dx, dy;

	assert(line_x1 >= 0 || line_x2 >= 0 || line_x1 < SCREEN_W || line_x2 < SCREEN_W);
	assert(line_y1 >= 0 || line_y2 >= 0 || line_y1 < SCREEN_H || line_y2 < SCREEN_H);

	dx = line_x2 - line_x1;
	ax = ABS(dx) * 2;
	sx = SGN(dx);
	dy = line_y2 - line_y1;
	ay = ABS(dy) * 2;
	sy = SGN(dy);
	x  = line_x1;
	y  = line_y1;

	if (ax > ay)
	{
		d = ay - ((uint16_t)ax / 2);
		while (true)
		{
			assert(y >= 0 || x >= 0 || y < SCREEN_H || x < SCREEN_W);

			frameBuffer[(y * SCREEN_W) + x] = palette[PAL_QADSCP];

			if (x == line_x2)
				break;

			if (d >= 0)
			{
				y += sy;
				d -= ax;
			}

			x += sx;
			d += ay;
		}
	}
	else
	{
		d = ax - ((uint16_t)ay / 2);
		while (true)
		{
			assert(y >= 0 || x >= 0 || y < SCREEN_H || x < SCREEN_W);

			frameBuffer[(y * SCREEN_W) + x] = palette[PAL_QADSCP];

			if (y == line_y2)
				break;

			if (d >= 0)
			{
				x += sx;
				d -= ay;
			}

			y += sy;
			d += ax;
		}
	}
}

static void setDragBar(void)
{
	int32_t pos32;
	uint32_t *dstPtr, pixel, bgPixel;
	double dPos;

	if (editor.sampler.samLength > 0 && editor.sampler.samDisplay != editor.sampler.samLength)
	{
		// update drag bar coordinates
		dPos = (editor.sampler.samOffset * 311.0) / editor.sampler.samLength;
		pos32 = (int32_t)(dPos + 0.5);
		editor.sampler.dragStart = 4 + (uint16_t)pos32;
		editor.sampler.dragStart = CLAMP(editor.sampler.dragStart, 4, 315);

		dPos = ((editor.sampler.samDisplay + editor.sampler.samOffset) * 311.0) / editor.sampler.samLength;
		pos32 = (int32_t)(dPos + 0.5);
		editor.sampler.dragEnd = 5 + (uint16_t)pos32;
		editor.sampler.dragEnd = CLAMP(editor.sampler.dragEnd, 5, 316);

		if (editor.sampler.dragStart > editor.sampler.dragEnd-1)
			editor.sampler.dragStart = editor.sampler.dragEnd-1;

		// draw drag bar

		dstPtr = &pixelBuffer[206 * SCREEN_W];
		pixel = palette[PAL_QADSCP];
		bgPixel = palette[PAL_BACKGRD];

		for (int32_t y = 0; y < 4; y++)
		{
			for (int32_t x = 4; x < 316; x++)
			{
				if (x >= editor.sampler.dragStart && x <= editor.sampler.dragEnd)
					dstPtr[x] = pixel; // drag bar
				else
					dstPtr[x] = bgPixel; // background
			}

			dstPtr += SCREEN_W;
		}
	}
	else
	{
		// clear drag bar background

		dstPtr = &pixelBuffer[(206 * SCREEN_W) + 4];
		pixel = palette[PAL_BACKGRD];

		for (int32_t y = 0; y < 4; y++)
		{
			for (int32_t x = 0; x < 312; x++)
				dstPtr[x] = pixel;

			dstPtr += SCREEN_W;
		}
	}
}

static int8_t getScaledSample(int32_t index)
{
	const int8_t *ptr8;

	if (editor.sampler.samLength <= 0 || index < 0 || index > editor.sampler.samLength)
		return 0;

	ptr8 = editor.sampler.samStart;
	if (ptr8 == NULL)
		return 0;

	return ptr8[index] >> 2;
}

int32_t smpPos2Scr(int32_t pos) // sample pos -> screen x pos
{
	double dPos;

	if (editor.sampler.samDisplay == 0)
		return 0;

	dPos = (pos * (double)SAMPLE_AREA_WIDTH) / editor.sampler.samDisplay;
	pos = (int32_t)(dPos + 0.5);
	pos -= samOffsetScaled;

	return pos;
}

int32_t scr2SmpPos(int32_t x) // screen x pos -> sample pos
{
	if (editor.sampler.samDisplay == 0)
		return 0;

	if (x < 0)
		x = 0;

	x += samOffsetScaled;
	x  = (x * editor.sampler.samDisplay) / SAMPLE_AREA_WIDTH;

	return x;
}

static void getSampleDataPeak(int8_t *smpPtr, int32_t numBytes, int16_t *outMin, int16_t *outMax)
{
	int8_t smp, smpMin, smpMax;

	smpMin = 127;
	smpMax = -128;

	for (int32_t i = 0; i < numBytes; i++)
	{
		smp = smpPtr[i];
		if (smp < smpMin) smpMin = smp;
		if (smp > smpMax) smpMax = smp;
	}

	*outMin = SAMPLE_AREA_Y_CENTER - (smpMin >> 2);
	*outMax = SAMPLE_AREA_Y_CENTER - (smpMax >> 2);
}

static void renderSampleData(void)
{
	int8_t *smpPtr;
	int16_t y1, y2, min, max, oldMin, oldMax;
	int32_t x, y, smpIdx, smpNum;
	uint32_t *dstPtr, pixel;
	moduleSample_t *s;

	s = &modEntry->samples[editor.currSample];

	// clear sample data background

	dstPtr = &pixelBuffer[(138 * SCREEN_W) + 3];
	pixel = palette[PAL_BACKGRD];

	for (y = 0; y < SAMPLE_VIEW_HEIGHT; y++)
	{
		for (x = 0; x < SAMPLE_AREA_WIDTH; x++)
			dstPtr[x] = pixel;

		dstPtr += SCREEN_W;
	}

	// display center line
	if (ptConfig.dottedCenterFlag)
		memset(&pixelBuffer[(SAMPLE_AREA_Y_CENTER * SCREEN_W) + 3], 0x373737, SAMPLE_AREA_WIDTH * sizeof (int32_t));

	// render sample data
	if (editor.sampler.samDisplay >= 0 && editor.sampler.samDisplay <= MAX_SAMPLE_LEN)
	{
		y1 = SAMPLE_AREA_Y_CENTER - getScaledSample(scr2SmpPos(0));

		if (editor.sampler.samDisplay <= SAMPLE_AREA_WIDTH)
		{
			// 1:1 or zoomed in
			for (x = 1; x < SAMPLE_AREA_WIDTH; x++)
			{
				y2 = SAMPLE_AREA_Y_CENTER - getScaledSample(scr2SmpPos(x));
				line(pixelBuffer, x + 2, x + 3, y1, y2);
				y1 = y2;
			}
		}
		else
		{
			// zoomed out

			oldMin = y1;
			oldMax = y1;

			smpPtr = &modEntry->sampleData[s->offset];
			for (x = 0; x < SAMPLE_AREA_WIDTH; x++)
			{
				smpIdx = scr2SmpPos(x);
				smpNum = scr2SmpPos(x+1) - smpIdx;

				// prevent look-up overflow (yes, this can happen near the end of the sample)
				if (smpIdx+smpNum > editor.sampler.samLength)
					smpNum = editor.sampler.samLength - smpNum;

				if (smpNum < 1)
					smpNum = 1;

				getSampleDataPeak(&smpPtr[smpIdx], smpNum, &min, &max);

				if (x > 0)
				{
					if (min > oldMax) line(pixelBuffer, x + 2, x + 3, oldMax, min);
					if (max < oldMin) line(pixelBuffer, x + 2, x + 3, oldMin, max);
				}

				line(pixelBuffer, x + 3, x + 3, max, min);

				oldMin = min;
				oldMax = max;
			}
		}
	}

	// render "sample display" text
	if (editor.sampler.samStart == editor.sampler.blankSample)
		printFiveDecimalsBg(pixelBuffer, 272, 214, 0, palette[PAL_GENTXT], palette[PAL_GENBKG]);
	else
		printFiveDecimalsBg(pixelBuffer, 272, 214, editor.sampler.samDisplay, palette[PAL_GENTXT], palette[PAL_GENBKG]);

	setDragBar();
	setLoopSprites();
}

void invertRange(void)
{
	int32_t x, y, rangeLen, dstPitch, start, end;
	uint32_t *dstPtr, pixel1, pixel2;

	if (editor.markStartOfs == -1)
		return; // no marking

	start = smpPos2Scr(editor.markStartOfs);
	end = smpPos2Scr(editor.markEndOfs);

	if (editor.sampler.samDisplay < editor.sampler.samLength && (start >= SAMPLE_AREA_WIDTH || end < 0))
		return; // range is outside of view (passed it by scrolling)

	start = CLAMP(start, 0, SAMPLE_AREA_WIDTH - 1);
	end = CLAMP(end, 0, SAMPLE_AREA_WIDTH - 1);

	rangeLen = (end + 1) - start;
	if (rangeLen < 1)
		rangeLen = 1;

	dstPtr = &pixelBuffer[(138 * SCREEN_W) + (3 + start)];
	dstPitch = SCREEN_W - rangeLen;
	pixel1 = palette[PAL_BACKGRD];
	pixel2 = palette[PAL_QADSCP];

	for (y = 0; y < 64; y++)
	{
		for (x = 0; x < rangeLen; x++)
		{
			// this is stupid...
			     if (*dstPtr == pixel1) *dstPtr = 0x666666;
			else if (*dstPtr == 0x666666) *dstPtr = pixel1;
			else if (*dstPtr == 0xCCCCCC) *dstPtr = pixel2;
			else if (*dstPtr == pixel2) *dstPtr = 0xCCCCCC;

			dstPtr++;
		}

		dstPtr += dstPitch;
	}
}

void displaySample(void)
{
	if (!editor.ui.samplerScreenShown)
		return;

	renderSampleData();
	if (editor.markStartOfs != -1)
		invertRange();

	editor.ui.update9xxPos = true;
}

void redrawSample(void)
{
	moduleSample_t *s;

	if (!editor.ui.samplerScreenShown)
		return;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	if (editor.currSample >= 0 && editor.currSample <= 30)
	{
		editor.markStartOfs = -1;

		editor.sampler.samOffset = 0;
		updateSamOffset();

		s = &modEntry->samples[editor.currSample];
		if (s->length > 0)
		{
			editor.sampler.samStart = &modEntry->sampleData[s->offset];
			editor.sampler.samDisplay = s->length;
			editor.sampler.samLength = s->length;
		}
		else
		{
			// "blank sample" template
			editor.sampler.samStart = editor.sampler.blankSample;
			editor.sampler.samLength = SAMPLE_AREA_WIDTH;
			editor.sampler.samDisplay = SAMPLE_AREA_WIDTH;
		}

		renderSampleData();
		updateSamplePos();

		editor.ui.update9xxPos = true;
		editor.ui.lastSampleOffset = 0x900;

		// for quadrascope
		editor.sampler.samDrawStart = s->offset;
		editor.sampler.samDrawEnd = s->offset + s->length;
	}
}

void highPassSample(int32_t cutOff)
{
	int32_t smp32, i, from, to;
	double *dSampleData, dBaseFreq, dCutOff, dIn[2], dOut[2];
	moduleSample_t *s;
	lossyIntegrator_t filterHi;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (cutOff == 0)
	{
		displayErrorMsg("CUTOFF CAN'T BE 0");
		return;
	}

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	from = 0;
	to = s->length;

	if (editor.markStartOfs != -1)
	{
		from = editor.markStartOfs;
		to = editor.markEndOfs;

		if (to > s->length)
			to = s->length;

		if (from == to)
		{
			from = 0;
			to = s->length;
		}
	}

	dSampleData = (double *)malloc(s->length * sizeof (double));
	if (dSampleData == NULL)
	{
		statusOutOfMemory();
		return;
	}

	fillSampleFilterUndoBuffer();

	// setup filter coefficients

	dBaseFreq = FILTERS_BASE_FREQ;

	dCutOff = (double)cutOff;
	if (dCutOff >= dBaseFreq/2.0)
	{
		dCutOff = dBaseFreq/2.0;
		editor.hpCutOff = (uint16_t)dCutOff;
	}

	calcCoeffLossyIntegrator(dBaseFreq, dCutOff, &filterHi);

	// copy over sample data to double buffer
	for (i = 0; i < s->length; i++)
		dSampleData[i] = modEntry->sampleData[s->offset+i];

	// filter forwards
	filterHi.dBuffer[0] = 0.0;
	if (to <= s->length)
	{
		for (i = from; i < to; i++)
		{
			dIn[0] = dSampleData[i];
			lossyIntegratorHighPass(&filterHi, dIn, dOut);
			dSampleData[i] = dOut[0];
		}
	}

	// filter backwards
	filterHi.dBuffer[0] = 0.0;
	if (to <= s->length)
	{
		for (i = to-1; i >= from; i--)
		{
			dIn[0] = dSampleData[i];
			lossyIntegratorHighPass(&filterHi, dIn, dOut);
			dSampleData[i] = dOut[0];
		}
	}

	if (editor.normalizeFiltersFlag)
		normalize8bitDoubleSigned(dSampleData, s->length);

	for (i = from; i < to; i++)
	{
		smp32 = (int32_t)dSampleData[i];
		CLAMP8(smp32);
		modEntry->sampleData[s->offset + i] = (int8_t)smp32;
	}

	free(dSampleData);

	fixSampleBeep(s);
	displaySample();
	updateWindowTitle(MOD_IS_MODIFIED);
}

void lowPassSample(int32_t cutOff)
{
	int32_t smp32, i, from, to;
	double *dSampleData, dBaseFreq, dCutOff, dIn[2], dOut[2];
	moduleSample_t *s;
	lossyIntegrator_t filterLo;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (cutOff == 0)
	{
		displayErrorMsg("CUTOFF CAN'T BE 0");
		return;
	}

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	from = 0;
	to = s->length;

	if (editor.markStartOfs != -1)
	{
		from = editor.markStartOfs;
		to = editor.markEndOfs;

		if (to > s->length)
			to = s->length;

		if (from == to)
		{
			from = 0;
			to = s->length;
		}
	}

	dSampleData = (double *)malloc(s->length * sizeof (double));
	if (dSampleData == NULL)
	{
		statusOutOfMemory();
		return;
	}

	fillSampleFilterUndoBuffer();

	// setup filter coefficients

	dBaseFreq = FILTERS_BASE_FREQ;

	dCutOff = (double)cutOff;
	if (dCutOff >= dBaseFreq/2.0)
	{
		dCutOff = dBaseFreq/2.0;
		editor.lpCutOff = (uint16_t)dCutOff;
	}

	calcCoeffLossyIntegrator(dBaseFreq, dCutOff, &filterLo);

	// copy over sample data to double buffer
	for (i = 0; i < s->length; i++)
		dSampleData[i] = modEntry->sampleData[s->offset+i];

	// filter forwards
	filterLo.dBuffer[0] = 0.0;
	if (to <= s->length)
	{
		for (i = from; i < to; i++)
		{
			dIn[0] = dSampleData[i];
			lossyIntegrator(&filterLo, dIn, dOut);
			dSampleData[i] = dOut[0];
		}
	}

	// filter backwards
	filterLo.dBuffer[0] = 0.0;
	if (to <= s->length)
	{
		for (i = to-1; i >= from; i--)
		{
			dIn[0] = dSampleData[i];
			lossyIntegrator(&filterLo, dIn, dOut);
			dSampleData[i] = dOut[0];
		}
	}

	if (editor.normalizeFiltersFlag)
		normalize8bitDoubleSigned(dSampleData, s->length);

	for (i = from; i < to; i++)
	{
		smp32 = (int32_t)dSampleData[i];
		CLAMP8(smp32);
		modEntry->sampleData[s->offset + i] = (int8_t)smp32;
	}

	free(dSampleData);

	fixSampleBeep(s);
	displaySample();
	updateWindowTitle(MOD_IS_MODIFIED);
}

void redoSampleData(int8_t sample)
{
	moduleSample_t *s;

	assert(sample >= 0 && sample <= 30);

	s = &modEntry->samples[sample];

	turnOffVoices();

	if (editor.smpRedoBuffer[sample] != NULL && editor.smpRedoLengths[sample] > 0)
	{
		memcpy(&modEntry->sampleData[s->offset], editor.smpRedoBuffer[sample], editor.smpRedoLengths[sample]);

		if (editor.smpRedoLengths[sample] < MAX_SAMPLE_LEN)
			memset(&modEntry->sampleData[s->offset + editor.smpRedoLengths[sample]], 0, MAX_SAMPLE_LEN - editor.smpRedoLengths[sample]);
	}
	else
	{
		memset(&modEntry->sampleData[s->offset], 0, MAX_SAMPLE_LEN);
	}

	s->fineTune = editor.smpRedoFinetunes[sample];
	s->volume = editor.smpRedoVolumes[sample];
	s->length = editor.smpRedoLengths[sample];
	s->loopStart = editor.smpRedoLoopStarts[sample];
	s->loopLength = (editor.smpRedoLoopLengths[sample] < 2) ? 2 : editor.smpRedoLoopLengths[sample];

	displayMsg("SAMPLE RESTORED !");

	editor.samplePos = 0;
	updateCurrSample();

	// this routine can be called while the sampler toolboxes are open, so redraw them
	if (editor.ui.samplerScreenShown)
	{
		if (editor.ui.samplerVolBoxShown)
			renderSamplerVolBox();
		else if (editor.ui.samplerFiltersBoxShown)
			renderSamplerFiltersBox();
	}
}

void fillSampleRedoBuffer(int8_t sample)
{
	moduleSample_t *s;

	assert(sample >= 0 && sample <= 30);

	s = &modEntry->samples[sample];

	if (editor.smpRedoBuffer[sample] != NULL)
	{
		free(editor.smpRedoBuffer[sample]);
		editor.smpRedoBuffer[sample] = NULL;
	}

	editor.smpRedoFinetunes[sample] = s->fineTune;
	editor.smpRedoVolumes[sample] = s->volume;
	editor.smpRedoLengths[sample] = s->length;
	editor.smpRedoLoopStarts[sample] = s->loopStart;
	editor.smpRedoLoopLengths[sample] = s->loopLength;

	if (s->length > 0)
	{
		editor.smpRedoBuffer[sample] = (int8_t *)malloc(s->length);
		if (editor.smpRedoBuffer[sample] != NULL)
			memcpy(editor.smpRedoBuffer[sample], &modEntry->sampleData[s->offset], s->length);
	}
}

bool allocSamplerVars(void)
{
	editor.sampler.copyBuf = (int8_t *)malloc(MAX_SAMPLE_LEN);
	editor.sampler.blankSample = (int8_t *)calloc(MAX_SAMPLE_LEN, 1);

	if (editor.sampler.copyBuf == NULL || editor.sampler.blankSample == NULL)
		return false;

	return true;
}

void deAllocSamplerVars(void)
{
	if (editor.sampler.copyBuf != NULL)
	{
		free(editor.sampler.copyBuf);
		editor.sampler.copyBuf = NULL;
	}

	if (editor.sampler.blankSample != NULL)
	{
		free(editor.sampler.blankSample);
		editor.sampler.blankSample = NULL;
	}

	for (uint8_t i = 0; i < MOD_SAMPLES; i++)
	{
		if (editor.smpRedoBuffer[i] != NULL)
		{
			free(editor.smpRedoBuffer[i]);
			editor.smpRedoBuffer[i] = NULL;
		}
	}
}

void samplerRemoveDcOffset(void)
{
	int8_t *smpDat;
	int32_t smp32, i, from, to, offset;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	smpDat = &modEntry->sampleData[s->offset];

	from = 0;
	to = s->length;

	if (editor.markStartOfs != -1)
	{
		from = editor.markStartOfs;
		to = editor.markEndOfs;

		if (to > s->length)
			to = s->length;

		if (from == to)
		{
			from = 0;
			to = s->length;
		}
	}

	if (to <= 0)
		return;

	// calculate offset value
	offset = 0;
	for (i = from; i < to; i++)
		offset += smpDat[i];
	offset /= to;

	// remove DC offset
	for (i = from; i < to; i++)
	{
		smp32 = smpDat[i] - offset;
		CLAMP8(smp32);
		smpDat[i] = (int8_t)smp32;
	}

	fixSampleBeep(s);
	displaySample();
	updateWindowTitle(MOD_IS_MODIFIED);
}

#define INTRP_QUADRATIC_TAPS 3
#define INTRP8_QUADRATIC(s1, s2, s3, f) /* output: -32768..32767 (+ spline overshoot) */ \
{ \
	int32_t s4, frac = (f) >> 1; \
	\
	s2 <<= 8; \
	s4 = ((s1 + s3) << (8 - 1)) - s2; \
	s4 = ((s4 * frac) >> 16) + s2; \
	s3 = (s1 + s3) << (8 - 1); \
	s1 <<= 8; \
	s3 = (s1 + s3) >> 1; \
	s1 += ((s4 - s3) * frac) >> 14; \
} \

#define INTRP_LINEAR_TAPS 2
#define INTRP8_LINEAR(s1, s2, f) /* output: -127..128 */ \
	s2 -= s1; \
	s2 *= (int32_t)(f); \
	s1 <<= 8; \
	s2 >>= (16 - 8); \
	s1 += s2; \
	s1 >>= 8; \

void mixChordSample(void)
{
	bool smpLoopFlag;
	char smpText[22 + 1];
	int8_t *smpData, sameNotes, smpVolume;
	uint8_t smpFinetune, finetune;
	int32_t channels, samples[INTRP_QUADRATIC_TAPS], *mixData, i, j, k, pos, smpLoopStart, smpLoopLength, smpEnd;
	sampleMixer_t mixCh[4], *v;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	assert(editor.tuningNote <= 35);

	if (editor.note1 == 36)
	{
		displayErrorMsg("NO BASENOTE!");
		return;
	}

	if (modEntry->samples[editor.currSample].length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	// check if all notes are the same (illegal)
	sameNotes = true;
	if ((editor.note2 != 36) && (editor.note2 != editor.note1)) sameNotes = false; else editor.note2 = 36;
	if ((editor.note3 != 36) && (editor.note3 != editor.note1)) sameNotes = false; else editor.note3 = 36;
	if ((editor.note4 != 36) && (editor.note4 != editor.note1)) sameNotes = false; else editor.note4 = 36;

	if (sameNotes)
	{
		displayErrorMsg("ONLY ONE NOTE!");
		return;
	}

	// sort the notes

	for (i = 0; i < 3; i++)
	{
		if (editor.note2 == 36)
		{
			editor.note2 = editor.note3;
			editor.note3 = editor.note4;
			editor.note4 = 36;
		}
	}

	for (i = 0; i < 3; i++)
	{
		if (editor.note3 == 36)
		{
			editor.note3 = editor.note4;
			editor.note4 = 36;
		}
	}

	// remove eventual note duplicates
	if (editor.note4 == editor.note3) editor.note4 = 36;
	if (editor.note4 == editor.note2) editor.note4 = 36;
	if (editor.note3 == editor.note2) editor.note3 = 36;

	editor.ui.updateNote1Text = true;
	editor.ui.updateNote2Text = true;
	editor.ui.updateNote3Text = true;
	editor.ui.updateNote4Text = true;

	// setup some variables

	smpLoopStart = modEntry->samples[editor.currSample].loopStart;
	smpLoopLength = modEntry->samples[editor.currSample].loopLength;
	smpLoopFlag = (smpLoopStart + smpLoopLength) > 2;
	smpEnd = smpLoopFlag ? (smpLoopStart + smpLoopLength) : modEntry->samples[editor.currSample].length;
	smpData = &modEntry->sampleData[modEntry->samples[editor.currSample].offset];

	if (editor.newOldFlag == 0)
	{
		// find a free sample slot for the new sample

		for (i = 0; i < MOD_SAMPLES; i++)
		{
			if (modEntry->samples[i].length == 0)
				break;
		}

		if (i == MOD_SAMPLES)
		{
			displayErrorMsg("NO EMPTY SAMPLE!");
			return;
		}

		smpFinetune = modEntry->samples[editor.currSample].fineTune;
		smpVolume = modEntry->samples[editor.currSample].volume;
		memcpy(smpText, modEntry->samples[editor.currSample].text, sizeof (smpText));

		s = &modEntry->samples[i];
		s->fineTune = smpFinetune;
		s->volume = smpVolume;

		memcpy(s->text, smpText, sizeof (smpText));
		editor.currSample = (int8_t)i;
	}
	else
	{
		// overwrite current sample
		s = &modEntry->samples[editor.currSample];
	}

	mixData = (int32_t *)calloc(MAX_SAMPLE_LEN, sizeof (int32_t));
	if (mixData == NULL)
	{
		statusOutOfMemory();
		return;
	}

	s->length = smpLoopFlag ? MAX_SAMPLE_LEN : editor.chordLength; // if sample loops, set max length
	s->loopLength = 2;
	s->loopStart = 0;
	s->text[21] = '!'; // chord sample indicator
	s->text[22] = '\0';

	memset(mixCh, 0, sizeof (mixCh));

	// setup mixing lengths and deltas

	finetune = s->fineTune & 0xF;
	channels = 0;

	if (editor.note1 < 36)
	{
		mixCh[0].delta = (periodTable[editor.tuningNote] << 16) / (periodTable[(finetune * 37) + editor.note1]);
		mixCh[0].length = (smpEnd * periodTable[(finetune * 37) + editor.note1]) / periodTable[editor.tuningNote];
		channels++;
	}

	if (editor.note2 < 36)
	{
		mixCh[1].delta = (periodTable[editor.tuningNote] << 16) / (periodTable[(finetune * 37) + editor.note2]);
		mixCh[1].length = (smpEnd * periodTable[(finetune * 37) + editor.note2]) / periodTable[editor.tuningNote];
		channels++;
	}

	if (editor.note3 < 36)
	{
		mixCh[2].delta = (periodTable[editor.tuningNote] << 16) / (periodTable[(finetune * 37) + editor.note3]);
		mixCh[2].length = (smpEnd * periodTable[(finetune * 37) + editor.note3]) / periodTable[editor.tuningNote];
		channels++;
	}

	if (editor.note4 < 36)
	{
		mixCh[3].delta = (periodTable[editor.tuningNote] << 16) / (periodTable[(finetune * 37) + editor.note4]);
		mixCh[3].length = (smpEnd * periodTable[(finetune * 37) + editor.note4]) / periodTable[editor.tuningNote];
		channels++;
	}

	// start mixing

	turnOffVoices();
	for (i = 0; i < channels; i++)
	{
		v = &mixCh[i];
		if (v->length <= 0)
			continue; // mix active channels only

		for (j = 0; j < MAX_SAMPLE_LEN; j++) // don't mix more than we can handle in a sample slot
		{
			// collect samples for interpolation
			for (k = 0; k < INTRP_QUADRATIC_TAPS; k++)
			{
				pos = v->pos + k;
				if (smpLoopFlag)
				{
					while (pos >= smpEnd)
						pos -= smpLoopLength;

					samples[k] = smpData[pos];
				}
				else
				{
					if (pos >= smpEnd)
						samples[k] = 0;
					else
						samples[k] = smpData[pos];
				}
			}

			INTRP8_QUADRATIC(samples[0], samples[1], samples[2], v->posFrac);
			mixData[j] += samples[0];

			v->posFrac += v->delta;
			if (v->posFrac > 0xFFFF)
			{
				v->pos += v->posFrac >> 16;
				v->posFrac &= 0xFFFF;

				if (smpLoopFlag)
				{
					while (v->pos >= smpEnd)
						v->pos -= smpLoopLength;
				}
			}
		}
	}

	normalize32bitSigned(mixData, s->length);

	// normalize gain and quantize to 8-bit
	for (i = 0; i < s->length; i++)
		modEntry->sampleData[s->offset + i] = (int8_t)(mixData[i] >> 24);

	if (s->length < MAX_SAMPLE_LEN)
		memset(&modEntry->sampleData[s->offset + s->length], 0, MAX_SAMPLE_LEN - s->length);

	// we're done

	free(mixData);

	editor.samplePos = 0;
	fixSampleBeep(s);
	updateCurrSample();

	updateWindowTitle(MOD_IS_MODIFIED);
}

void samplerResample(void)
{
	int8_t *readData, *writeData;
	int16_t refPeriod, newPeriod;
	int32_t samples[INTRP_LINEAR_TAPS], i, pos, readPos, writePos;
	int32_t readLength, writeLength, loopStart, loopLength;
	uint32_t posFrac, delta;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	assert(editor.tuningNote <= 35 && editor.resampleNote <= 35);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	// setup resampling variables
	readPos = 0;
	writePos = 0;
	writeData = &modEntry->sampleData[s->offset];
	refPeriod = periodTable[editor.tuningNote];
	newPeriod = periodTable[(37 * (s->fineTune & 0xF)) + editor.resampleNote];
	readLength = s->length;
	writeLength = (readLength * newPeriod) / refPeriod;

	if (readLength == writeLength)
		return; // no resampling needed

	// allocate memory for our sample duplicate
	readData = (int8_t *)malloc(s->length);
	if (readData == NULL)
	{
		statusOutOfMemory();
		return;
	}

	if (writeLength <= 0)
	{
		free(readData);
		displayErrorMsg("RESAMPLE ERROR !");
		return;
	}

	delta = ((uint32_t)readLength << 16) / (uint32_t)writeLength;
	assert(delta != 0);

	writeLength = writeLength & 0xFFFFFFFE;
	if (writeLength > MAX_SAMPLE_LEN)
		writeLength = MAX_SAMPLE_LEN;

	memcpy(readData, writeData, readLength);

	// resample

	posFrac = 0;

	turnOffVoices();
	while (writePos < writeLength)
	{
		// collect samples for interpolation
		for (i = 0; i < INTRP_LINEAR_TAPS; i++)
		{
			pos = readPos + i;
			if (pos >= readLength)
				samples[i] = 0;
			else
				samples[i] = readData[pos];
		}

		INTRP8_LINEAR(samples[0], samples[1], posFrac);
		writeData[writePos++] = (int8_t)samples[0];

		posFrac += delta;
		readPos += posFrac >> 16;
		posFrac &= 0xFFFF;
	}
	free(readData);

	// wipe non-used data in new sample
	if (writeLength < MAX_SAMPLE_LEN)
		memset(&writeData[writePos], 0, MAX_SAMPLE_LEN - writeLength);

	// update sample attributes
	s->length = writeLength;
	s->fineTune = 0;

	// scale loop points (and deactivate if overflowing)
	if ((s->loopStart + s->loopLength) > 2)
	{
		loopStart  = (int32_t)(((uint32_t)s->loopStart << 16) / delta) & 0xFFFFFFFE;
		loopLength = (int32_t)(((uint32_t)s->loopLength << 16) / delta) & 0xFFFFFFFE;

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

	fixSampleBeep(s);
	updateCurrSample();
	updateWindowTitle(MOD_IS_MODIFIED);
}

static uint8_t hexToInteger2(char *ptr)
{
	char lo, hi;

	/* This routine must ONLY be used on an address
	** where two bytes can be read. It will mess up
	** if the ASCII values are not '0 .. 'F' */

	hi = ptr[0];
	lo = ptr[1];

	// high nybble
	if (hi >= 'a')
		hi -= ' ';

	hi -= '0';
	if (hi > 9)
		hi -= 7;

	// low nybble
	if (lo >= 'a')
		lo -= ' ';

	lo -= '0';
	if (lo > 9)
		lo -= 7;

	return (hi << 4) | lo;
}

void doMix(void)
{
	int8_t *fromPtr1, *fromPtr2, *mixPtr;
	uint8_t smpFrom1, smpFrom2, smpTo;
	int16_t tmp16;
	int32_t i, mixLength;
	moduleSample_t *s1, *s2, *s3;

	smpFrom1 = hexToInteger2(&editor.mixText[4]);
	smpFrom2 = hexToInteger2(&editor.mixText[7]);
	smpTo = hexToInteger2(&editor.mixText[13]);

	if (smpFrom1 == 0 || smpFrom1 > 0x1F || smpFrom2 == 0 || smpFrom2 > 0x1F || smpTo == 0 || smpTo > 0x1F)
	{
		displayErrorMsg("NOT RANGE 01-1F !");
		return;
	}

	s1 = &modEntry->samples[--smpFrom1];
	s2 = &modEntry->samples[--smpFrom2];
	s3 = &modEntry->samples[--smpTo];

	if (s1->length == 0 || s2->length == 0)
	{
		displayErrorMsg("EMPTY SAMPLES !!!");
		return;
	}

	if (s1->length > s2->length)
	{
		fromPtr1 = &modEntry->sampleData[s1->offset];
		fromPtr2 = &modEntry->sampleData[s2->offset];
		mixLength = s1->length;
	}
	else
	{
		fromPtr1 = &modEntry->sampleData[s2->offset];
		fromPtr2 = &modEntry->sampleData[s1->offset];
		mixLength = s2->length;
	}

	mixPtr = (int8_t *)malloc(mixLength);
	if (mixPtr == NULL)
	{
		statusOutOfMemory();
		return;
	}

	turnOffVoices();
	if (mixLength <= MAX_SAMPLE_LEN)
	{
		for (i = 0; i < mixLength; i++)
		{
			tmp16 = (i < s2->length) ? (fromPtr1[i] + fromPtr2[i]) : fromPtr1[i];
			if (editor.halfClipFlag == 0)
				tmp16 >>= 1;

			CLAMP8(tmp16);
			mixPtr[i] = (int8_t)tmp16;
		}

		memcpy(&modEntry->sampleData[s3->offset], mixPtr, mixLength);
		if (mixLength < MAX_SAMPLE_LEN)
			memset(&modEntry->sampleData[s3->offset + mixLength], 0, MAX_SAMPLE_LEN - mixLength);
	}

	free(mixPtr);

	s3->length = mixLength;
	s3->volume = 64;
	s3->fineTune = 0;
	s3->loopStart = 0;
	s3->loopLength = 2;

	editor.currSample = smpTo;
	editor.samplePos = 0;

	fixSampleBeep(s3);
	updateCurrSample();
	updateWindowTitle(MOD_IS_MODIFIED);
}

// this is actually treble increase
void boostSample(int8_t sample, bool ignoreMark)
{
	int8_t *smpDat;
	int16_t tmp16_0, tmp16_1, tmp16_2;
	int32_t i, from, to;
	moduleSample_t *s;

	assert(sample >= 0 && sample <= 30);

	s = &modEntry->samples[sample];
	if (s->length == 0)
		return; // don't display warning/show warning pointer, it is done elsewhere

	smpDat = &modEntry->sampleData[s->offset];

	from = 0;
	to = s->length;

	if (!ignoreMark)
	{
		if (editor.markStartOfs != -1)
		{
			from = editor.markStartOfs;
			to = editor.markEndOfs;

			if (to > s->length)
				to = s->length;

			if (from == to)
			{
				from = 0;
				to = s->length;
			}
		}
	}

	tmp16_0 = 0;
	for (i = from; i < to; i++)
	{
		tmp16_1 = smpDat[i];
		tmp16_2 = tmp16_1;
		tmp16_1 -= tmp16_0;
		tmp16_0 = tmp16_2;
		tmp16_1 >>= 2;
		tmp16_2 += tmp16_1;

		CLAMP8(tmp16_2);
		smpDat[i] = (int8_t)tmp16_2;
	}

	fixSampleBeep(s);

	// don't redraw sample here, it is done elsewhere
}

// this is actually treble decrease
void filterSample(int8_t sample, bool ignoreMark)
{
	int8_t *smpDat;
	int16_t tmp16;
	int32_t i, from, to;
	moduleSample_t *s;

	assert(sample >= 0 && sample <= 30);

	s = &modEntry->samples[sample];
	if (s->length == 0)
		return; // don't display warning/show warning pointer, it is done elsewhere

	smpDat = &modEntry->sampleData[s->offset];

	from = 1;
	to = s->length;

	if (!ignoreMark)
	{
		if (editor.markStartOfs != -1)
		{
			from = editor.markStartOfs;
			to = editor.markEndOfs;

			if (to > s->length)
				to = s->length;

			if (from == to)
			{
				from = 0;
				to = s->length;
			}
		}
	}

	if (to < 1)
		return;
	to--;

	for (i = from; i < to; i++)
	{
		tmp16 = (smpDat[i+0] + smpDat[i+1]) >> 1;
		CLAMP8(tmp16);
		smpDat[i] = (int8_t)tmp16;
	}

	fixSampleBeep(s);
	// don't redraw sample here, it is done elsewhere
}

void toggleTuningTone(void)
{
	if (editor.currMode == MODE_PLAY || editor.currMode == MODE_RECORD)
		return;

	editor.tuningFlag ^= 1;
	if (editor.tuningFlag)
	{
		// turn tuning tone on

		editor.tuningChan = (editor.cursor.channel + 1) & 3;

		if (editor.tuningNote > 35)
			editor.tuningNote = 35;

		modEntry->channels[editor.tuningChan].n_volume = 64; // we need this for the scopes

		paulaSetPeriod(editor.tuningChan, periodTable[editor.tuningNote]);
		paulaSetVolume(editor.tuningChan, 64);
		paulaSetData(editor.tuningChan, tuneToneData);
		paulaSetLength(editor.tuningChan, sizeof (tuneToneData) / 2);
		paulaStartDMA(editor.tuningChan);

		// force loop flag on for scopes
		scopeExt[editor.tuningChan].newLoopFlag = scope[editor.tuningChan].loopFlag = true;
	}
	else
	{
		// turn tuning tone off
		mixerKillVoice(editor.tuningChan);
	}
}

void sampleMarkerToBeg(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		invertRange();
		editor.markStartOfs = -1;
		editor.samplePos = 0;
	}
	else
	{
		invertRange();
		if (input.keyb.shiftPressed && editor.markStartOfs != -1)
		{
			editor.markStartOfs = editor.sampler.samOffset;
		}
		else
		{
			editor.markStartOfs = editor.sampler.samOffset;
			editor.markEndOfs = editor.markStartOfs;
		}
		invertRange();

		editor.samplePos = (uint16_t)editor.markEndOfs;
	}

	updateSamplePos();
}

void sampleMarkerToCenter(void)
{
	int32_t middlePos;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		invertRange();
		editor.markStartOfs = -1;
		editor.samplePos = 0;
	}
	else
	{
		middlePos = editor.sampler.samOffset + (int32_t)((editor.sampler.samDisplay / 2.0) + 0.5);

		invertRange();
		if (input.keyb.shiftPressed && editor.markStartOfs != -1)
		{
			if (editor.markStartOfs < middlePos)
				editor.markEndOfs = middlePos;
			else if (editor.markEndOfs > middlePos)
				editor.markStartOfs = middlePos;
		}
		else
		{
			editor.markStartOfs = middlePos;
			editor.markEndOfs = editor.markStartOfs;
		}
		invertRange();

		editor.samplePos = (uint16_t)editor.markEndOfs;
	}

	updateSamplePos();
}

void sampleMarkerToEnd(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		invertRange();
		editor.markStartOfs = -1;
		editor.samplePos = 0;
	}
	else
	{
		invertRange();
		if (input.keyb.shiftPressed && editor.markStartOfs != -1)
		{
			editor.markEndOfs = s->length;
		}
		else
		{
			editor.markStartOfs = s->length;
			editor.markEndOfs = editor.markStartOfs;
		}
		invertRange();

		editor.samplePos = (uint16_t)editor.markEndOfs;
	}

	updateSamplePos();
}

void samplerSamCopy(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (editor.markStartOfs == -1)
	{
		displayErrorMsg("NO RANGE SELECTED");
		return;
	}

	if (editor.markEndOfs-editor.markStartOfs <= 0)
	{
		displayErrorMsg("SET LARGER RANGE");
		return;
	}

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	editor.sampler.copyBufSize = editor.markEndOfs - editor.markStartOfs;

	if ((int32_t)(editor.markStartOfs + editor.sampler.copyBufSize) > MAX_SAMPLE_LEN)
	{
		displayErrorMsg("COPY ERROR !");
		return;
	}

	memcpy(editor.sampler.copyBuf, &modEntry->sampleData[s->offset+editor.markStartOfs], editor.sampler.copyBufSize);
}

void samplerSamDelete(uint8_t cut)
{
	int8_t *tmpBuf;
	int32_t val32, sampleLength, copyLength, markEnd, markStart;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (editor.markStartOfs == -1)
	{
		displayErrorMsg("NO RANGE SELECTED");
		return;
	}

	if (editor.markEndOfs-editor.markStartOfs <= 0)
	{
		displayErrorMsg("SET LARGER RANGE");
		return;
	}

	if (cut)
		samplerSamCopy();

	s = &modEntry->samples[editor.currSample];

	sampleLength = s->length;
	if (sampleLength == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	turnOffVoices();

	// if whole sample is marked, wipe it
	if (editor.markEndOfs-editor.markStartOfs >= sampleLength)
	{
		memset(&modEntry->sampleData[s->offset], 0, MAX_SAMPLE_LEN);

		invertRange();
		editor.markStartOfs = -1;

		editor.sampler.samStart = editor.sampler.blankSample;
		editor.sampler.samDisplay = SAMPLE_AREA_WIDTH;
		editor.sampler.samLength = SAMPLE_AREA_WIDTH;

		s->length = 0;
		s->loopStart = 0;
		s->loopLength = 2;
		s->volume = 0;
		s->fineTune = 0;

		editor.samplePos = 0;
		updateCurrSample();

		updateWindowTitle(MOD_IS_MODIFIED);
		return;
	}

	markEnd = (editor.markEndOfs > sampleLength) ? sampleLength : editor.markEndOfs;
	markStart = editor.markStartOfs;

	copyLength = (editor.markStartOfs + sampleLength) - markEnd;
	if (copyLength < 2 || copyLength > MAX_SAMPLE_LEN)
	{
		displayErrorMsg("SAMPLE CUT FAIL !");
		return;
	}

	tmpBuf = (int8_t *)malloc(copyLength);
	if (tmpBuf == NULL)
	{
		statusOutOfMemory();
		return;
	}

	// copy start part
	memcpy(tmpBuf, &modEntry->sampleData[s->offset], editor.markStartOfs);

	// copy end part
	if (sampleLength-markEnd > 0)
		memcpy(&tmpBuf[editor.markStartOfs], &modEntry->sampleData[s->offset+markEnd], sampleLength - markEnd);

	// nuke sample data and copy over the result
	memcpy(&modEntry->sampleData[s->offset], tmpBuf, copyLength);

	if (copyLength < MAX_SAMPLE_LEN)
		memset(&modEntry->sampleData[s->offset+copyLength], 0, MAX_SAMPLE_LEN - copyLength);

	free(tmpBuf);

	editor.sampler.samLength = copyLength;
	if (editor.sampler.samOffset+editor.sampler.samDisplay >= editor.sampler.samLength)
	{
		if (editor.sampler.samDisplay < editor.sampler.samLength)
		{
			if (editor.sampler.samLength-editor.sampler.samDisplay < 0)
			{
				editor.sampler.samOffset = 0;
				editor.sampler.samDisplay = editor.sampler.samLength;
			}
			else
			{
				editor.sampler.samOffset = editor.sampler.samLength - editor.sampler.samDisplay;
			}
		}
		else
		{
			editor.sampler.samOffset = 0;
			editor.sampler.samDisplay = editor.sampler.samLength;
		}

		updateSamOffset();
	}

	if (s->loopLength > 2) // loop enabled?
	{
		if (markEnd > s->loopStart)
		{
			if (markStart < s->loopStart+s->loopLength)
			{
				// we cut data inside the loop, increase loop length
				val32 = (s->loopLength - (markEnd - markStart)) & 0xFFFFFFFE;
				if (val32 < 2)
					val32 = 2;

				s->loopLength = (uint16_t)val32;
			}

			// we cut data after the loop, don't modify loop points
		}
		else
		{
			// we cut data before the loop, adjust loop start point
			val32 = (s->loopStart - (markEnd - markStart)) & 0xFFFFFFFE;
			if (val32 < 0)
			{
				s->loopStart = 0;
				s->loopLength = 2;
			}
			else
			{
				s->loopStart = (uint16_t)val32;
			}
		}
	}

	s->length = copyLength & 0xFFFE;

	if (editor.sampler.samDisplay <= 2)
	{
		editor.sampler.samStart = editor.sampler.blankSample;
		editor.sampler.samLength = SAMPLE_AREA_WIDTH;
		editor.sampler.samDisplay = SAMPLE_AREA_WIDTH;
	}

	invertRange();
	if (editor.sampler.samDisplay == 0)
	{
		editor.markStartOfs = -1; // clear marking
	}
	else
	{
		if (editor.markStartOfs >= s->length)
			editor.markStartOfs = s->length - 1;

		editor.markEndOfs = editor.markStartOfs;
		invertRange();
	}

	editor.samplePos = editor.markStartOfs;
	fixSampleBeep(s);
	updateSamplePos();
	recalcChordLength();
	displaySample();

	editor.ui.updateCurrSampleLength = true;
	editor.ui.updateCurrSampleRepeat = true;
	editor.ui.updateCurrSampleReplen = true;
	editor.ui.updateSongSize = true;

	updateWindowTitle(MOD_IS_MODIFIED);
}

void samplerSamPaste(void)
{
	bool wasZooming;
	int8_t *tmpBuf;
	int32_t markStart;
	uint32_t readPos;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (editor.sampler.copyBuf == NULL || editor.sampler.copyBufSize == 0)
	{
		displayErrorMsg("BUFFER IS EMPTY");
		return;
	}

	s = &modEntry->samples[editor.currSample];
	if (s->length > 0 && editor.markStartOfs == -1)
	{
		displayErrorMsg("SET CURSOR POS");
		return;
	}

	markStart = editor.markStartOfs;
	if (s->length == 0)
		markStart = 0;

	if (s->length+editor.sampler.copyBufSize > MAX_SAMPLE_LEN)
	{
		displayErrorMsg("NOT ENOUGH ROOM");
		return;
	}

	tmpBuf = (int8_t *)malloc(MAX_SAMPLE_LEN);
	if (tmpBuf == NULL)
	{
		statusOutOfMemory();
		return;
	}

	readPos = 0;
	turnOffVoices();
	wasZooming = (editor.sampler.samDisplay != editor.sampler.samLength);

	// copy start part
	if (markStart > 0)
	{
		memcpy(&tmpBuf[readPos], &modEntry->sampleData[s->offset], markStart);
		readPos += markStart;
	}

	// copy buffer
	memcpy(&tmpBuf[readPos], editor.sampler.copyBuf, editor.sampler.copyBufSize);

	// copy end part
	if (markStart >= 0)
	{
		readPos += editor.sampler.copyBufSize;

		if (s->length-markStart > 0)
			memcpy(&tmpBuf[readPos], &modEntry->sampleData[s->offset+markStart], s->length - markStart);
	}

	s->length = (s->length + editor.sampler.copyBufSize) & 0xFFFFFFFE;
	if (s->length > MAX_SAMPLE_LEN)
		s->length = MAX_SAMPLE_LEN;

	editor.sampler.samLength = s->length;

	if (s->loopLength > 2) // loop enabled?
	{
		if (markStart > s->loopStart)
		{
			if (markStart < s->loopStart+s->loopLength)
			{
				// we pasted data inside the loop, increase loop length
				s->loopLength += editor.sampler.copyBufSize & 0xFFFFFFFE;
				if (s->loopStart+s->loopLength > s->length)
				{
					s->loopStart = 0;
					s->loopLength = 2;
				}
			}

			// we pasted data after the loop, don't modify loop points
		}
		else
		{
			// we pasted data before the loop, adjust loop start point
			s->loopStart = (s->loopStart + editor.sampler.copyBufSize) & 0xFFFFFFFE;
			if (s->loopStart+s->loopLength > s->length)
			{
				s->loopStart = 0;
				s->loopLength = 2;
			}
		}
	}

	memcpy(&modEntry->sampleData[s->offset], tmpBuf, s->length);
	if (s->length < MAX_SAMPLE_LEN)
		memset(&modEntry->sampleData[s->offset+s->length], 0, MAX_SAMPLE_LEN - s->length);

	free(tmpBuf);

	invertRange();
	editor.markStartOfs = -1;

	fixSampleBeep(s);
	updateSamplePos();
	recalcChordLength();

	if (wasZooming)
		displaySample();
	else
		redrawSample();

	editor.ui.updateCurrSampleLength = true;
	editor.ui.updateSongSize = true;

	updateWindowTitle(MOD_IS_MODIFIED);
}

static void playCurrSample(uint8_t chn, int32_t startOffset, int32_t endOffset, bool playWaveformFlag)
{
	moduleChannel_t *ch;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);
	assert(chn < AMIGA_VOICES);
	assert(editor.currPlayNote <= 35);

	s = &modEntry->samples[editor.currSample];
	ch = &modEntry->channels[chn];

	ch->n_samplenum = editor.currSample;
	ch->n_volume = s->volume;
	ch->n_period = periodTable[(37 * (s->fineTune & 0xF)) + editor.currPlayNote];
	
	if (playWaveformFlag)
	{
		ch->n_start = &modEntry->sampleData[s->offset];
		ch->n_length = (s->loopStart > 0) ? (uint32_t)(s->loopStart + s->loopLength) / 2 : s->length / 2;
		ch->n_loopstart = &modEntry->sampleData[s->offset + s->loopStart];
		ch->n_replen = s->loopLength / 2;
	}
	else
	{
		ch->n_start = &modEntry->sampleData[s->offset + startOffset];
		ch->n_length = (endOffset - startOffset) / 2;
		ch->n_loopstart = &modEntry->sampleData[s->offset];
		ch->n_replen = 1;
	}

	if (ch->n_length == 0)
		ch->n_length = 1;

	paulaSetVolume(chn, ch->n_volume);
	paulaSetPeriod(chn, ch->n_period);
	paulaSetData(chn, ch->n_start);
	paulaSetLength(chn, ch->n_length);

	if (!editor.muted[chn])
		paulaStartDMA(chn);
	else
		paulaStopDMA(chn);

	// these take effect after the current DMA cycle is done
	if (playWaveformFlag)
	{
		paulaSetData(chn, ch->n_loopstart);
		paulaSetLength(chn, ch->n_replen);
	}
	else
	{
		paulaSetData(chn, NULL);
		paulaSetLength(chn, 1);
	}

	updateSpectrumAnalyzer(ch->n_volume, ch->n_period);
}

void samplerPlayWaveform(void)
{
	playCurrSample(editor.cursor.channel, 0, 0, true);
}

void samplerPlayDisplay(void)
{
	int32_t start = editor.sampler.samOffset;
	int32_t end = editor.sampler.samOffset + editor.sampler.samDisplay;

	playCurrSample(editor.cursor.channel, start, end, false);
}

void samplerPlayRange(void)
{
	if (editor.markStartOfs == -1)
	{
		displayErrorMsg("NO RANGE SELECTED");
		return;
	}

	if (editor.markEndOfs-editor.markStartOfs < 2)
	{
		displayErrorMsg("SET LARGER RANGE");
		return;
	}

	playCurrSample(editor.cursor.channel, editor.markStartOfs, editor.markEndOfs, false);
}

void setLoopSprites(void)
{
	moduleSample_t *s;

	if (!editor.ui.samplerScreenShown)
	{
		hideSprite(SPRITE_LOOP_PIN_LEFT);
		hideSprite(SPRITE_LOOP_PIN_RIGHT);
		return;
	}

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->loopStart+s->loopLength > 2)
	{
		if (editor.sampler.samDisplay > 0)
		{
			editor.sampler.loopStartPos = smpPos2Scr(s->loopStart);
			if (editor.sampler.loopStartPos >= 0 && editor.sampler.loopStartPos <= SAMPLE_AREA_WIDTH)
				setSpritePos(SPRITE_LOOP_PIN_LEFT, editor.sampler.loopStartPos, 138);
			else
				hideSprite(SPRITE_LOOP_PIN_LEFT);

			editor.sampler.loopEndPos = smpPos2Scr(s->loopStart + s->loopLength);
			if (editor.sampler.loopEndPos >= 0 && editor.sampler.loopEndPos <= SAMPLE_AREA_WIDTH)
				setSpritePos(SPRITE_LOOP_PIN_RIGHT, editor.sampler.loopEndPos + 3, 138);
			else
				hideSprite(SPRITE_LOOP_PIN_RIGHT);
		}
	}
	else
	{
		editor.sampler.loopStartPos = 0;
		editor.sampler.loopEndPos = 0;

		hideSprite(SPRITE_LOOP_PIN_LEFT);
		hideSprite(SPRITE_LOOP_PIN_RIGHT);
	}

	textOutBg(pixelBuffer, 288, 225, (s->loopStart+s->loopLength > 2) ? "ON " : "OFF", palette[PAL_GENTXT], palette[PAL_GENBKG]);
}

void samplerShowAll(void)
{
	if (editor.sampler.samDisplay == editor.sampler.samLength)
		return; // don't attempt to show all if already showing all! }

	editor.sampler.samOffset = 0;
	editor.sampler.samDisplay = editor.sampler.samLength;

	updateSamOffset();
	displaySample();
}

static void samplerZoomIn(int32_t step, int16_t x)
{
	int32_t tmpDisplay, tmpOffset;

	if (modEntry->samples[editor.currSample].length == 0 || editor.sampler.samDisplay <= 2)
		return;

	if (step < 1)
		step = 1;

	tmpDisplay = editor.sampler.samDisplay - (step * 2);
	if (tmpDisplay < 2)
		tmpDisplay = 2;

	step += (((x - (SCREEN_W / 2)) * step) / (SCREEN_W / 2));

	tmpOffset = editor.sampler.samOffset + step;
	if (tmpOffset < 0)
		tmpOffset = 0;

	if (tmpOffset+tmpDisplay > editor.sampler.samLength)
		tmpOffset = editor.sampler.samLength-tmpDisplay;

	editor.sampler.samOffset = tmpOffset;
	editor.sampler.samDisplay = tmpDisplay;

	updateSamOffset();
	displaySample();
}

static void samplerZoomOut(int32_t step, int16_t x)
{
	int32_t tmpDisplay, tmpOffset;

	if (modEntry->samples[editor.currSample].length == 0 || editor.sampler.samDisplay == editor.sampler.samLength)
		return;

	if (step < 1)
		step = 1;

	tmpDisplay = editor.sampler.samDisplay + (step * 2);
	if (tmpDisplay > editor.sampler.samLength)
	{
		tmpOffset  = 0;
		tmpDisplay = editor.sampler.samLength;
	}
	else
	{
		step += (((x - (SCREEN_W / 2)) * step) / (SCREEN_W / 2));

		tmpOffset = editor.sampler.samOffset - step;
		if (tmpOffset < 0)
			tmpOffset = 0;

		if (tmpOffset+tmpDisplay > editor.sampler.samLength)
			tmpOffset = editor.sampler.samLength-tmpDisplay;
	}

	editor.sampler.samOffset = tmpOffset;
	editor.sampler.samDisplay = tmpDisplay;

	updateSamOffset();
	displaySample();
}

void samplerZoomInMouseWheel(void)
{
	int32_t step = (int32_t)((editor.sampler.samDisplay / 10.0f) + 0.5f);
	samplerZoomIn(step, input.mouse.x);
}

void samplerZoomOutMouseWheel(void)
{
	int32_t step = (int32_t)((editor.sampler.samDisplay / 10.0f) + 0.5f);
	samplerZoomOut(step, input.mouse.x);
}

void samplerZoomOut2x(void)
{
	int32_t step = (int32_t)((editor.sampler.samDisplay / 2.0f) + 0.5f);
	samplerZoomOut(step, SCREEN_W / 2);
}

void samplerRangeAll(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		invertRange();
		editor.markStartOfs = -1;
	}
	else
	{
		invertRange();
		editor.markStartOfs = editor.sampler.samOffset;
		editor.markEndOfs = editor.sampler.samOffset + editor.sampler.samDisplay;
		invertRange();
	}
}

void samplerShowRange(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length == 0)
	{
		displayErrorMsg("SAMPLE IS EMPTY");
		return;
	}

	if (editor.markStartOfs == -1)
	{
		displayErrorMsg("NO RANGE SELECTED");
		return;
	}

	if (editor.markEndOfs-editor.markStartOfs < 2)
	{
		displayErrorMsg("SET LARGER RANGE");
		return;
	}

	editor.sampler.samDisplay = editor.markEndOfs - editor.markStartOfs;
	editor.sampler.samOffset = editor.markStartOfs;

	if (editor.sampler.samDisplay+editor.sampler.samOffset > editor.sampler.samLength)
		editor.sampler.samOffset = editor.sampler.samLength-editor.sampler.samDisplay;

	updateSamOffset();

	invertRange();
	editor.markStartOfs = -1;

	displaySample();
}

void volBoxBarPressed(bool mouseButtonHeld)
{
	int32_t mouseX;

	if (input.mouse.y < 0 || input.mouse.x < 0 || input.mouse.y >= SCREEN_H || input.mouse.x >= SCREEN_W)
		return;

	if (!mouseButtonHeld)
	{
		if (input.mouse.x >= 72 && input.mouse.x <= 173)
		{
			if (input.mouse.y >= 154 && input.mouse.y <= 174) editor.ui.forceVolDrag = 1;
			if (input.mouse.y >= 165 && input.mouse.y <= 175) editor.ui.forceVolDrag = 2;
		}
	}
	else
	{
		if (editor.sampler.lastMouseX != input.mouse.x)
		{
			editor.sampler.lastMouseX = input.mouse.x;
			mouseX = CLAMP(editor.sampler.lastMouseX - 107, 0, 60);

			if (editor.ui.forceVolDrag == 1)
			{
				editor.vol1 = (int16_t)((mouseX * 200) / 60);
				editor.ui.updateVolFromText = true;
				showVolFromSlider();
			}
			else if (editor.ui.forceVolDrag == 2)
			{
				editor.vol2 = (int16_t)((mouseX * 200) / 60);
				editor.ui.updateVolToText = true;
				showVolToSlider();
			}
		}
	}
}

void samplerBarPressed(bool mouseButtonHeld)
{
	int32_t tmp32;

	if (input.mouse.y < 0 || input.mouse.x < 0 || input.mouse.y >= SCREEN_H || input.mouse.x >= SCREEN_W)
		return;

	if (!mouseButtonHeld)
	{
		if (input.mouse.x >= 4 && input.mouse.x <= 315)
		{
			if (input.mouse.x < editor.sampler.dragStart)
			{
				tmp32 = editor.sampler.samOffset - editor.sampler.samDisplay;
				if (tmp32 < 0)
					tmp32 = 0;

				if (tmp32 == editor.sampler.samOffset)
					return;

				editor.sampler.samOffset = tmp32;

				updateSamOffset();
				displaySample();
				return;
			}

			if (input.mouse.x > editor.sampler.dragEnd)
			{
				tmp32 = editor.sampler.samOffset + editor.sampler.samDisplay;
				if (tmp32+editor.sampler.samDisplay <= editor.sampler.samLength)
				{
					if (tmp32 == editor.sampler.samOffset)
						return;

					editor.sampler.samOffset = tmp32;
				}
				else
				{
					tmp32 = editor.sampler.samLength - editor.sampler.samDisplay;
					if (tmp32 == editor.sampler.samOffset)
						return;

					editor.sampler.samOffset = tmp32;
				}

				updateSamOffset();
				displaySample();
				return;
			}

			editor.sampler.lastSamPos = (uint16_t)input.mouse.x;
			editor.sampler.saveMouseX = editor.sampler.lastSamPos - editor.sampler.dragStart;

			editor.ui.forceSampleDrag = true;
		}
	}

	if (input.mouse.x != editor.sampler.lastSamPos)
	{
		editor.sampler.lastSamPos = (uint16_t)input.mouse.x;

		tmp32 = editor.sampler.lastSamPos - editor.sampler.saveMouseX - 4;
		if (tmp32 < 0)
			tmp32 = 0;

		tmp32 = (int32_t)(((tmp32 * editor.sampler.samLength) / 311.0) + 0.5);
		if (tmp32+editor.sampler.samDisplay <= editor.sampler.samLength)
		{
			if (tmp32 == editor.sampler.samOffset)
				return;

			editor.sampler.samOffset = tmp32;
		}
		else
		{
			tmp32 = editor.sampler.samLength - editor.sampler.samDisplay;
			if (tmp32 == editor.sampler.samOffset)
				return;

			editor.sampler.samOffset = tmp32;
		}

		updateSamOffset();
		displaySample();
	}
}

static int32_t x2LoopX(int32_t mouseX)
{
	moduleSample_t *s = &modEntry->samples[editor.currSample];

	mouseX -= 3;
	if (mouseX < 0)
		mouseX = 0;

	mouseX = scr2SmpPos(mouseX);
	mouseX = CLAMP(mouseX, 0, s->length);

	return mouseX;
}

static int32_t xToSmpX(int32_t x, int32_t smpLen)
{
	x = scr2SmpPos(x);
	x = CLAMP(x, 0, smpLen - 1);

	return x;
}

static int8_t yToSmpY(int32_t mouseY)
{
	mouseY = (SAMPLE_AREA_Y_CENTER - mouseY) * 4;
	CLAMP8(mouseY);

	return mouseY;
}

void samplerEditSample(bool mouseButtonHeld)
{
	int8_t y;
	int32_t mouseY, x, smp_x0, smp_x1, xDistance, smp_y0, smp_y1, yDistance, smp;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (input.mouse.y < 0 || input.mouse.x < 0 || input.mouse.y >= SCREEN_H || input.mouse.x >= SCREEN_W)
		return;

	s = &modEntry->samples[editor.currSample];

	if (!mouseButtonHeld)
	{
		if (input.mouse.x >= 3 && input.mouse.x <= 316 && input.mouse.y >= 138 && input.mouse.y <= 201)
		{
			if (s->length == 0)
			{
				displayErrorMsg("SAMPLE LENGTH = 0");
			}
			else
			{
				editor.sampler.lastMouseX = input.mouse.x;
				editor.sampler.lastMouseY = input.mouse.y;
				editor.ui.forceSampleEdit = true;
				updateWindowTitle(MOD_IS_MODIFIED);
			}
		}

		return;
	}

	mouseY = input.keyb.shiftPressed ? editor.sampler.lastMouseY : input.mouse.y;

	x = xToSmpX(input.mouse.x - 3, s->length);
	y = yToSmpY(mouseY);

	modEntry->sampleData[s->offset+x] = y;

	// interpolate x gaps
	if (input.mouse.x != editor.sampler.lastMouseX)
	{
		smp_y0 = yToSmpY(editor.sampler.lastMouseY);

		smp_y1 = y;
		yDistance = smp_y1 - smp_y0;

		if (input.mouse.x > editor.sampler.lastMouseX)
		{
			smp_x1 = x;
			smp_x0 = xToSmpX(editor.sampler.lastMouseX - 3, s->length);

			xDistance = smp_x1 - smp_x0;
			if (xDistance > 0)
			{
				for (x = smp_x0; x < smp_x1; x++)
				{
					assert(x < s->length);

					smp = smp_y0 + (((x - smp_x0) * yDistance) / xDistance);
					CLAMP8(smp);
					modEntry->sampleData[s->offset + x] = (int8_t)smp;
				}
			}
		}
		else if (input.mouse.x < editor.sampler.lastMouseX)
		{
			smp_x0 = x;
			smp_x1 = xToSmpX(editor.sampler.lastMouseX - 3, s->length);

			xDistance = smp_x1 - smp_x0;
			if (xDistance > 0)
			{
				for (x = smp_x0; x < smp_x1; x++)
				{
					assert(x < s->length);

					smp = smp_y0 + (((smp_x1 - x) * yDistance) / xDistance);
					CLAMP8(smp);
					modEntry->sampleData[s->offset + x] = (int8_t)smp;
				}
			}
		}

		editor.sampler.lastMouseX = input.mouse.x;

		if (!input.keyb.shiftPressed)
			editor.sampler.lastMouseY = input.mouse.y;
	}

	displaySample();
}

void samplerSamplePressed(bool mouseButtonHeld)
{
	int16_t mouseX;
	int32_t tmpPos;
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	if (!mouseButtonHeld)
	{
		if (input.mouse.y < 142)
		{
			if (input.mouse.x >= editor.sampler.loopStartPos && input.mouse.x <= editor.sampler.loopStartPos+3)
			{
				editor.ui.leftLoopPinMoving = true;
				editor.ui.rightLoopPinMoving = false;
				editor.ui.sampleMarkingPos = 1;
				editor.sampler.lastMouseX = input.mouse.x;
				return;
			}
			else if (input.mouse.x >= editor.sampler.loopEndPos+3 && input.mouse.x <= editor.sampler.loopEndPos+6)
			{
				editor.ui.rightLoopPinMoving = true;
				editor.ui.leftLoopPinMoving = false;
				editor.ui.sampleMarkingPos = 1;
				editor.sampler.lastMouseX = input.mouse.x;
				return;
			}
		}
	}

	mouseX = (int16_t)input.mouse.x;
	s = &modEntry->samples[editor.currSample];

	if (editor.ui.leftLoopPinMoving)
	{
		if (editor.sampler.lastMouseX != mouseX)
		{
			editor.sampler.lastMouseX = mouseX;

			tmpPos = (x2LoopX(mouseX + 2) - s->loopStart) & 0xFFFFFFFE;
			if (tmpPos > MAX_SAMPLE_LEN)
				tmpPos = MAX_SAMPLE_LEN;

			if (s->loopStart+tmpPos >= (s->loopStart+s->loopLength)-2)
			{
				s->loopStart  = (s->loopStart + s->loopLength) - 2;
				s->loopLength = 2;
			}
			else
			{
				s->loopStart = s->loopStart + tmpPos;

				if (s->loopLength-tmpPos > 2)
					s->loopLength -= tmpPos;
				else
					s->loopLength = 2;
			}

			editor.ui.updateCurrSampleRepeat = true;
			editor.ui.updateCurrSampleReplen = true;

			setLoopSprites();
			mixerUpdateLoops();
			updateWindowTitle(MOD_IS_MODIFIED);
		}

		return;
	}

	if (editor.ui.rightLoopPinMoving)
	{
		if (editor.sampler.lastMouseX != mouseX)
		{
			editor.sampler.lastMouseX = mouseX;

			s = &modEntry->samples[editor.currSample];

			tmpPos = (x2LoopX(mouseX - 1) - s->loopStart) & 0xFFFFFFFE;
			tmpPos = CLAMP(tmpPos, 2, MAX_SAMPLE_LEN);

			s->loopLength = tmpPos;

			editor.ui.updateCurrSampleRepeat = true;
			editor.ui.updateCurrSampleReplen = true;

			setLoopSprites();
			mixerUpdateLoops();
			updateWindowTitle(MOD_IS_MODIFIED);
		}

		return;
	}

	if (!mouseButtonHeld)
	{
		if (mouseX < 3 || mouseX > 319)
			return;

		editor.ui.sampleMarkingPos = (int16_t)mouseX;
		editor.sampler.lastSamPos = editor.ui.sampleMarkingPos;

		invertRange();
		if (s->length == 0)
		{
			editor.markStartOfs = -1; // clear marking
		}
		else
		{
			editor.markStartOfs = scr2SmpPos(editor.ui.sampleMarkingPos - 3);
			editor.markEndOfs = scr2SmpPos(editor.ui.sampleMarkingPos - 3);

			if (editor.markEndOfs > s->length)
				editor.markEndOfs = s->length;

			invertRange();
		}

		if (s->length == 0)
		{
			editor.samplePos = 0;
		}
		else
		{
			tmpPos = scr2SmpPos(mouseX - 3);
			if (tmpPos > s->length)
				tmpPos = s->length;

			editor.samplePos = (uint16_t)tmpPos;
		}

		updateSamplePos();

		return;
	}

	mouseX = CLAMP(mouseX, 3, 319);

	if (mouseX != editor.sampler.lastSamPos)
	{
		editor.sampler.lastSamPos = (uint16_t)mouseX;

		invertRange();
		if (s->length == 0)
		{
			editor.markStartOfs = -1; // clear marking
		}
		else
		{
			if (editor.sampler.lastSamPos > editor.ui.sampleMarkingPos)
			{
				editor.markStartOfs = scr2SmpPos(editor.ui.sampleMarkingPos - 3);
				editor.markEndOfs = scr2SmpPos(editor.sampler.lastSamPos - 3);
			}
			else
			{
				editor.markStartOfs = scr2SmpPos(editor.sampler.lastSamPos - 3);
				editor.markEndOfs = scr2SmpPos(editor.ui.sampleMarkingPos - 3);
			}

			if (editor.markEndOfs > s->length)
				editor.markEndOfs = s->length;

			invertRange();
		}
	}

	if (s->length == 0)
	{
		editor.samplePos = 0;
	}
	else
	{
		tmpPos = scr2SmpPos(mouseX - 3);
		if (tmpPos > s->length)
			tmpPos = s->length;

		 editor.samplePos = (uint16_t)tmpPos;
	}

	updateSamplePos();
}

void samplerLoopToggle(void)
{
	moduleSample_t *s;

	assert(editor.currSample >= 0 && editor.currSample <= 30);

	s = &modEntry->samples[editor.currSample];
	if (s->length < 2)
		return;

	turnOffVoices();

	if (s->loopStart+s->loopLength > 2)
	{
		// disable loop

		editor.sampler.tmpLoopStart = s->loopStart;
		editor.sampler.tmpLoopLength = s->loopLength;

		s->loopStart = 0;
		s->loopLength = 2;
	}
	else
	{
		// enable loop

		if (editor.sampler.tmpLoopStart == 0 && editor.sampler.tmpLoopLength == 0)
		{
			s->loopStart = 0;
			s->loopLength = s->length;
		}
		else
		{
			s->loopStart = editor.sampler.tmpLoopStart;
			s->loopLength = editor.sampler.tmpLoopLength;

			if (s->loopStart+s->loopLength > s->length)
			{
				s->loopStart = 0;
				s->loopLength = s->length;
			}
		}
	}

	editor.ui.updateCurrSampleRepeat = true;
	editor.ui.updateCurrSampleReplen = true;

	displaySample();
	mixerUpdateLoops();
	recalcChordLength();
	updateWindowTitle(MOD_IS_MODIFIED);
}

void exitFromSam(void)
{
	editor.ui.samplerScreenShown = false;
	memcpy(&pixelBuffer[121 * SCREEN_W], &trackerFrameBMP[121 * SCREEN_W], 320 * 134 * sizeof (int32_t));

	updateCursorPos();
	setLoopSprites();

	editor.ui.updateStatusText = true;
	editor.ui.updateSongSize = true;
	editor.ui.updateSongTiming = true;
	editor.ui.updateSongBPM = true;
	editor.ui.updateCurrPattText = true;
	editor.ui.updatePatternData = true;

	editor.markStartOfs = -1;
}

void samplerScreen(void)
{
	if (editor.ui.samplerScreenShown)
	{
		exitFromSam();
		return;
	}

	editor.ui.samplerScreenShown = true;
	memcpy(&pixelBuffer[(121 * SCREEN_W)], samplerScreenBMP, 320 * 134 * sizeof (int32_t));
	hideSprite(SPRITE_PATTERN_CURSOR);

	editor.ui.updateStatusText = true;
	editor.ui.updateSongSize = true;
	editor.ui.updateSongTiming = true;
	editor.ui.updateResampleNote = true;
	editor.ui.update9xxPos = true;

	redrawSample();
}

void drawSamplerLine(void)
{
	uint8_t i;
	int32_t pos;

	hideSprite(SPRITE_SAMPLING_POS_LINE);
	if (!editor.ui.samplerScreenShown || editor.ui.samplerVolBoxShown || editor.ui.samplerFiltersBoxShown)
		return;

	for (i = 0; i < AMIGA_VOICES; i++)
	{
		if (modEntry->channels[i].n_samplenum == editor.currSample && !editor.muted[i])
		{
			pos = getSampleReadPos(i, editor.currSample);
			if (pos >= 0)
			{
				pos = 3 + smpPos2Scr(pos);
				if (pos >= 3 && pos <= 316)
					setSpritePos(SPRITE_SAMPLING_POS_LINE, pos, 138);
			}
		}
	}
}