shithub: ft²

ref: 93d00d9f3b880f932342f816ee86da3977e95bd3
dir: /src/ft2_scopes.c/

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

#include <stdint.h>
#include <stdbool.h>
#include <math.h> // modf()
#ifndef _WIN32
#include <unistd.h> // usleep()
#endif
#include "ft2_header.h"
#include "ft2_events.h"
#include "ft2_config.h"
#include "ft2_audio.h"
#include "ft2_gui.h"
#include "ft2_midi.h"
#include "ft2_gfxdata.h"
#include "ft2_scopes.h"
#include "ft2_mouse.h"
#include "ft2_video.h"

enum
{
	LOOP_NONE = 0,
	LOOP_FORWARD = 1,
	LOOP_PINGPONG = 2
};

#define SCOPE_HEIGHT 36

// data to be read from main update thread during sample trigger
typedef struct scopeState_t
{
	int8_t *pek;
	uint8_t typ;
	int32_t len, repS, repL, playOffset;
} scopeState_t;

// actual scope data
typedef struct scope_t
{
	volatile bool active;
	const int8_t *sampleData8;
	const int16_t *sampleData16;
	int8_t SVol;
	bool wasCleared, sample16Bit;
	uint8_t loopType;
	int32_t SRepS, SRepL, SLen, SPos;
	uint32_t SFrq, SPosDec, posXOR;
} scope_t;

static volatile bool scopesUpdatingFlag, scopesDisplayingFlag;
static uint32_t oldVoiceDelta, oldSFrq, scopeTimeLen, scopeTimeLenFrac;
static uint64_t timeNext64, timeNext64Frac;
static volatile scope_t scope[MAX_VOICES];
static SDL_Thread *scopeThread;

lastChInstr_t lastChInstr[MAX_VOICES]; // global

static const uint8_t scopeMuteBMPWidths[16] =
{
	162,111, 76, 56, 42, 35, 28, 24,
	 21, 21, 17, 17, 12, 12,  9,  9
};

static const uint8_t scopeMuteBMPHeights[16] =
{
	27, 27, 26, 25, 25, 25, 24, 24,
	24, 24, 24, 24, 24, 24, 24, 24
};

static const uint8_t *scopeMuteBMPPointers[16] =
{
	scopeMuteBMP1, scopeMuteBMP2, scopeMuteBMP3, scopeMuteBMP4,
	scopeMuteBMP5, scopeMuteBMP6, scopeMuteBMP7, scopeMuteBMP8,
	scopeMuteBMP9, scopeMuteBMP9, scopeMuteBMP10,scopeMuteBMP10,
	scopeMuteBMP11,scopeMuteBMP11,scopeMuteBMP12,scopeMuteBMP12
};

static const uint16_t scopeLenTab[16][32] =
{
	/*  2 ch */ {285,285},
	/*  4 ch */ {141,141,141,141},
	/*  6 ch */ {93,93,93,93,93,93},
	/*  8 ch */ {69,69,69,69,69,69,69,69},
	/* 10 ch */ {55,55,55,54,54,55,55,55,54,54},
	/* 12 ch */ {45,45,45,45,45,45,45,45,45,45,45,45},
	/* 14 ch */ {39,38,38,38,38,38,38,39,38,38,38,38,38,38},
	/* 16 ch */ {33,33,33,33,33,33,33,33,33,33,33,33,33,33,33,33},
	/* 18 ch */ {29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29,29},
	/* 20 ch */ {26,26,26,26,26,26,26,26,25,25,26,26,26,26,26,26,26,26,25,25},
	/* 22 ch */ {24,24,23,23,23,23,23,23,23,23,23,24,24,23,23,23,23,23,23,23,23,23},
	/* 24 ch */ {21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21,21},
	/* 26 ch */ {20,20,19,19,19,19,19,19,19,19,19,19,19,20,20,19,19,19,19,19,19,19,19,19,19,19},
	/* 28 ch */ {18,18,18,18,18,18,18,18,17,17,17,17,17,17,18,18,18,18,18,18,18,18,17,17,17,17,17,17},
	/* 30 ch */ {17,17,17,16,16,16,16,16,16,16,16,16,16,16,16,17,17,17,16,16,16,16,16,16,16,16,16,16,16,16},
	/* 32 ch */ {15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15,15}
};

void resetOldScopeRates(void)
{
	oldVoiceDelta = 0;
	oldSFrq = 0;
}

int32_t getSamplePosition(uint8_t ch)
{
	volatile bool active, sample16Bit;
	volatile int32_t pos, len;
	volatile scope_t *sc;

	if (ch >= song.antChn)
		return -1;

	sc = &scope[ch];

	// cache some stuff
	active = sc->active;
	pos = sc->SPos;
	len = sc->SLen;
	sample16Bit = sc->sample16Bit;

	if (!active || len == 0)
		return -1;

	if (pos >= 0 && pos < len)
	{
		if (sample16Bit)
			pos <<= 1;

		return pos;
	}

	return -1; // not active or overflown
}

void stopAllScopes(void)
{
	// wait for scopes to finish updating
	while (scopesUpdatingFlag);

	for (uint8_t i = 0; i < MAX_VOICES; i++)
		scope[i].active = false;

	// wait for scope displaying to be done (safety)
	while (scopesDisplayingFlag);
}

// toggle mute
static void setChannel(int16_t nr, bool on)
{
	stmTyp *ch;

	ch = &stm[nr];

	ch->stOff = !on;
	if (ch->stOff)
	{
		ch->effTyp = 0;
		ch->eff = 0;
		ch->realVol = 0;
		ch->outVol = 0;
		ch->oldVol = 0;
		ch->finalVol = 0;
		ch->outPan = 128;
		ch->oldPan = 128;
		ch->finalPan = 128;
		ch->status = IS_Vol;

		ch->envSustainActive = false; // non-FT2 bug fix for stuck piano keys
	}

	scope[nr].wasCleared = false;
}

static void drawScopeNumber(uint16_t scopeXOffs, uint16_t scopeYOffs, uint8_t channel, bool outline)
{
	scopeYOffs++;
	channel++;

	if (outline)
	{
		if (channel < 10) // one digit?
		{
			charOutOutlined(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + channel);
		}
		else
		{
			charOutOutlined(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + (channel / 10));
			charOutOutlined(scopeXOffs + 7, scopeYOffs, PAL_MOUSEPT, '0' + (channel % 10));
		}
	}
	else
	{
		if (channel < 10) // one digit?
		{
			charOut(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + channel);
		}
		else
		{
			charOut(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + (channel / 10));
			charOut(scopeXOffs + 7, scopeYOffs, PAL_MOUSEPT, '0' + (channel % 10));
		}
	}
}

static void redrawScope(int16_t ch)
{
	uint8_t chansPerRow;
	const uint16_t *scopeLens;
	uint16_t x, y, i, chanLookup, scopeLen, muteGfxLen, muteGfxX;

	chansPerRow = song.antChn / 2;
	chanLookup = chansPerRow - 1;
	scopeLens = scopeLenTab[chanLookup];

	x = 2;
	y = 94;

	scopeLen = 0; // prevent compiler warning
	for (i = 0; i < song.antChn; i++)
	{
		scopeLen = scopeLens[i];

		if (i == chansPerRow) // did we reach end of row?
		{
			// yes, go one row down
			x  = 2;
			y += 39;
		}

		if (i == ch)
			break;

		// adjust position to next channel
		x += scopeLen + 3;
	}

	drawFramework(x, y, scopeLen + 2, 38, FRAMEWORK_TYPE2);

	// draw mute graphics if channel is muted
	if (!editor.chnMode[i])
	{
		muteGfxLen = scopeMuteBMPWidths[chanLookup];
		muteGfxX = x + ((scopeLen - muteGfxLen) / 2);

		blitFast(muteGfxX, y + 6, scopeMuteBMPPointers[chanLookup], muteGfxLen, scopeMuteBMPHeights[chanLookup]);

		if (config.ptnChnNumbers)
			drawScopeNumber(x + 1, y + 1, (uint8_t)i, true);
	}

	scope[ch].wasCleared = false;
}

void refreshScopes(void)
{
	for (int16_t i = 0; i < MAX_VOICES; i++)
		scope[i].wasCleared = false;
}

static void channelMode(int16_t chn)
{
	bool m, m2, test;
	int16_t i;
	
	assert(chn < song.antChn);

	m = mouse.leftButtonPressed && !mouse.rightButtonPressed;
	m2 = mouse.rightButtonPressed && mouse.leftButtonPressed;

	if (m2)
	{
		test = false;
		for (i = 0; i < song.antChn; i++)
		{
			if (i != chn && !editor.chnMode[i])
				test = true;
		}

		if (test)
		{
			for (i = 0; i < song.antChn; i++)
				editor.chnMode[i] = true;
		}
		else
		{
			for (i = 0; i < song.antChn; i++)
				editor.chnMode[i] = (i == chn);
		}
	}
	else if (m)
	{
		editor.chnMode[chn] ^= 1;
	}
	else
	{
		if (editor.chnMode[chn])
		{
			config.multiRecChn[chn] ^= 1;
		}
		else
		{
			config.multiRecChn[chn] = true;
			editor.chnMode[chn] = true;
			m = true;
		}
	}

	for (i = 0; i < song.antChn; i++)
		setChannel(i, editor.chnMode[i]);

	if (m2)
	{
		for (i = 0; i < song.antChn; i++)
			redrawScope(i);
	}
	else
	{
		redrawScope(chn);
	}
}

bool testScopesMouseDown(void)
{
	int8_t chanToToggle;
	uint8_t i, chansPerRow;
	uint16_t x;
	const uint16_t *scopeLens;

	if (!editor.ui.scopesShown)
		return false;

	if (mouse.y >= 95 && mouse.y <= 169 && mouse.x >= 3 && mouse.x <= 288)
	{
		if (mouse.y > 130 && mouse.y < 134)
			return true;

		chansPerRow = song.antChn / 2;
		scopeLens = scopeLenTab[chansPerRow-1];

		// find out if we clicked inside a scope
		x = 3;
		for (i = 0; i < chansPerRow; i++)
		{
			if (mouse.x >= x && mouse.x < x+scopeLens[i])
				break;

			x += scopeLens[i] + 3;
		}

		if (i == chansPerRow)
			return true; // scope framework was clicked instead

		chanToToggle = i;
		if (mouse.y >= 134) // second row of scopes?
			chanToToggle += chansPerRow; // yes, increase lookup offset

		channelMode(chanToToggle);
		return true;
	}

	return false;
}

static void scopeTrigger(uint8_t ch, sampleTyp *s, int32_t playOffset)
{
	bool sampleIs16Bit;
	uint8_t loopType;
	int32_t length, loopBegin, loopLength;
	volatile scope_t *sc;
	scope_t tempState;

	sc = &scope[ch];

	length = s->len;
	loopBegin = s->repS;
	loopLength = s->repL;
	loopType = s->typ & 3;
	sampleIs16Bit = (s->typ >> 4) & 1;

	if (sampleIs16Bit)
	{
		assert(!(length & 1));
		assert(!(loopBegin & 1));
		assert(!(loopLength & 1));

		length >>= 1;
		loopBegin >>= 1;
		loopLength >>= 1;
	}

	if (s->pek == NULL || length < 1)
	{
		sc->active = false; // shut down scope (illegal parameters)
		return;
	}

	if (loopLength < 1) // disable loop if loopLength is below 1
		loopType = 0;

	if (sampleIs16Bit)
		tempState.sampleData16 = (const int16_t *)s->pek;
	else
		tempState.sampleData8 = (const int8_t *)s->pek;

	tempState.sample16Bit = sampleIs16Bit;
	tempState.loopType = loopType;

	tempState.posXOR = 0; // forwards
	tempState.SLen = (loopType > 0) ? (loopBegin + loopLength) : length;
	tempState.SRepS = loopBegin;
	tempState.SRepL = loopLength;
	tempState.SPos = playOffset;
	tempState.SPosDec = 0; // position fraction
	
	// if 9xx position overflows, shut down scopes
	if (tempState.SPos >= tempState.SLen)
	{
		sc->active = false;
		return;
	}

	// these has to be read
	tempState.active = true;
	tempState.wasCleared = sc->wasCleared;
	tempState.SFrq = sc->SFrq;
	tempState.SVol = sc->SVol;

	/* Update live scope now.
	** In theory it -can- be written to in the middle of a cached read,
    ** then the read thread writes its own non-updated cached copy back and
	** the trigger never happens. So far I have never seen it happen,
	** so it's probably very rare. Yes, this is not good coding... */
	*sc = tempState;
}

static void updateScopes(void)
{
	int32_t loopOverflowVal;
	volatile scope_t *sc;
	scope_t tempState;

	scopesUpdatingFlag = true;
	for (uint32_t i = 0; i < song.antChn; i++)
	{
		sc = &scope[i];
		tempState = *sc; // cache it

		if (!tempState.active)
			continue; // scope is not active, no need

		// scope position update

		tempState.SPosDec += tempState.SFrq;
		tempState.SPos += ((tempState.SPosDec >> 16) ^ tempState.posXOR);
		tempState.SPosDec &= 0xFFFF;

		// handle loop wrapping or sample end

		if (tempState.posXOR == 0xFFFFFFFF && tempState.SPos < tempState.SRepS) // sampling backwards (definitely pingpong loop)
		{
			tempState.posXOR = 0; // change direction to forwards

			if (tempState.SRepL < 2)
				tempState.SPos = tempState.SRepS;
			else
				tempState.SPos = tempState.SRepS + ((tempState.SRepS - tempState.SPos - 1) % tempState.SRepL);

			assert(tempState.SPos >= tempState.SRepS && tempState.SPos < tempState.SLen);
		}
		else if (tempState.SPos >= tempState.SLen)
		{
			if (tempState.SRepL < 2)
				loopOverflowVal = 0;
			else
				loopOverflowVal = (tempState.SPos - tempState.SLen) % tempState.SRepL;

			if (tempState.loopType == LOOP_NONE)
			{
				tempState.active = false;
			}
			else if (tempState.loopType == LOOP_FORWARD)
			{
				tempState.SPos = tempState.SRepS + loopOverflowVal;
				assert(tempState.SPos >= tempState.SRepS && tempState.SPos < tempState.SLen);
			}
			else // pingpong loop
			{
				tempState.posXOR = 0xFFFFFFFF; // change direction to backwards
				tempState.SPos = (tempState.SLen - 1) - loopOverflowVal;
				assert(tempState.SPos >= tempState.SRepS && tempState.SPos < tempState.SLen);
			}
		}
		assert(tempState.SPos >= 0);

		*sc = tempState; // update scope state
	}
	scopesUpdatingFlag = false;
}

static void scopeLine(int16_t x1, int16_t y1, int16_t y2)
{
	int16_t d, sy, dy;
	uint16_t ay;
	int32_t pitch;
	uint32_t pixVal, *dst32;

	dy = y2 - y1;
	ay = ABS(dy);
	sy = SGN(dy);

	pixVal = video.palette[PAL_PATTEXT];
	pitch = sy * SCREEN_W;

	dst32 = &video.frameBuffer[(y1 * SCREEN_W) + x1];
	*dst32 = pixVal;

	if (ay <= 1)
	{
		if (ay != 0)
			dst32 += pitch;

		*++dst32 = pixVal;
		return;
	}

	d = 2 - ay;

	ay *= 2;
	while (y1 != y2)
	{
		if (d >= 0)
		{
			d -= ay;
			dst32++;
		}

		y1 += sy;
		d  += 2;

		 dst32 += pitch;
		*dst32  = pixVal;
	}
}

static inline int8_t getScaledScopeSample8(scope_t *sc, int32_t drawPos)
{
	if (!sc->active)
		return 0;

	assert(drawPos >= 0 && drawPos < sc->SLen);
	return (sc->sampleData8[drawPos] * sc->SVol) >> 8;
}

static inline int8_t getScaledScopeSample16(scope_t *sc, int32_t drawPos)
{
	if (!sc->active)
		return 0;

	assert(drawPos >= 0 && drawPos < sc->SLen);
	return (int8_t)((sc->sampleData16[drawPos] * sc->SVol) >> 16);
}

#define SCOPE_UPDATE_DRAWPOS \
	scopeDrawFrac += s.SFrq >> 6; \
	scopeDrawPos += ((scopeDrawFrac >> 16) ^ drawPosXOR); \
	scopeDrawFrac &= 0xFFFF; \
	\
	if (drawPosXOR == 0xFFFFFFFF && scopeDrawPos < s.SRepS) /* sampling backwards (definitely pingpong loop) */ \
	{ \
		drawPosXOR = 0; /* change direction to forwards */ \
		\
		if (s.SRepL < 2) \
			scopeDrawPos = s.SRepS; \
		else \
			scopeDrawPos = s.SRepS + ((s.SRepS - scopeDrawPos - 1) % s.SRepL); \
		\
		assert(scopeDrawPos >= s.SRepS && scopeDrawPos < s.SLen); \
	} \
	else if (scopeDrawPos >= s.SLen) \
	{ \
		if (s.SRepL < 2) \
			loopOverflowVal = 0; \
		else \
			loopOverflowVal = (scopeDrawPos - s.SLen) % s.SRepL; \
		\
		if (s.loopType == LOOP_NONE) \
		{ \
			s.active = false; \
		} \
		else if (s.loopType == LOOP_FORWARD) \
		{ \
			scopeDrawPos = s.SRepS + loopOverflowVal; \
			assert(scopeDrawPos >= s.SRepS && scopeDrawPos < s.SLen); \
		} \
		else /* pingpong loop */ \
		{ \
			drawPosXOR = 0xFFFFFFFF; /* change direction to backwards */ \
			scopeDrawPos = (s.SLen - 1) - loopOverflowVal; \
			assert(scopeDrawPos >= s.SRepS && scopeDrawPos < s.SLen); \
		} \
		\
	} \
	assert(scopeDrawPos >= 0); \

void drawScopes(void)
{
	int16_t y1, y2, sample, scopeLineY;
	const uint16_t *scopeLens;
	uint16_t chansPerRow, x16, scopeXOffs, scopeYOffs, scopeDrawLen;
	int32_t scopeDrawPos, loopOverflowVal;
	uint32_t x, len, drawPosXOR, scopeDrawFrac, scopePixelColor;
	volatile scope_t *sc;
	scope_t s;

	scopesDisplayingFlag = true;
	chansPerRow = song.antChn / 2;

	scopeLens = scopeLenTab[chansPerRow-1];
	scopeXOffs = 3;
	scopeYOffs = 95;
	scopeLineY = 112;

	for (int16_t i = 0; i < song.antChn; i++)
	{
		// if we reached the last scope on the row, go to first scope on the next row
		if (i == chansPerRow)
		{
			scopeXOffs = 3;
			scopeYOffs = 134;
			scopeLineY = 151;
		}

		scopeDrawLen = scopeLens[i];

		if (!editor.chnMode[i]) // scope muted (mute graphics blit()'ed elsewhere)
		{
			scopeXOffs += scopeDrawLen + 3; // align x to next scope
			continue;
		}

		s = scope[i]; // cache scope to lower thread race condition issues
		if (s.active && s.SVol > 0 && !audio.locked)
		{
			// scope is active

			scope[i].wasCleared = false;

			// clear scope background
			clearRect(scopeXOffs, scopeYOffs, scopeDrawLen, SCOPE_HEIGHT);

			scopeDrawPos = s.SPos;
			scopeDrawFrac = 0;
			drawPosXOR = s.posXOR;

			// draw current scope
			if (config.specialFlags & LINED_SCOPES)
			{
				// LINE SCOPE

				if (s.sample16Bit)
				{
					y1 = scopeLineY - getScaledScopeSample16(&s, scopeDrawPos);
					SCOPE_UPDATE_DRAWPOS

					x16 = scopeXOffs;
					len = scopeXOffs + (scopeDrawLen - 1);

					for (; x16 < len; x16++)
					{
						y2 = scopeLineY - getScaledScopeSample16(&s, scopeDrawPos);
						scopeLine(x16, y1, y2);
						y1 = y2;

						SCOPE_UPDATE_DRAWPOS
					}
				}
				else
				{
					y1 = scopeLineY - getScaledScopeSample8(&s, scopeDrawPos);
					SCOPE_UPDATE_DRAWPOS

					x16 = scopeXOffs;
					len = scopeXOffs + (scopeDrawLen - 1);

					for (; x16 < len; x16++)
					{
						y2 = scopeLineY - getScaledScopeSample8(&s, scopeDrawPos);
						scopeLine(x16, y1, y2);
						y1 = y2;

						SCOPE_UPDATE_DRAWPOS
					}
				}
			}
			else
			{
				// PIXEL SCOPE

				scopePixelColor = video.palette[PAL_PATTEXT];

				x = scopeXOffs;
				len = scopeXOffs + scopeDrawLen;

				if (s.sample16Bit)
				{
					for (; x < len; x++)
					{
						sample = getScaledScopeSample16(&s, scopeDrawPos);
						video.frameBuffer[((scopeLineY - sample) * SCREEN_W) + x] = scopePixelColor;

						SCOPE_UPDATE_DRAWPOS
					}
				}
				else
				{
					for (; x < len; x++)
					{
						sample = getScaledScopeSample8(&s, scopeDrawPos);
						video.frameBuffer[((scopeLineY - sample) * SCREEN_W) + x] = scopePixelColor;

						SCOPE_UPDATE_DRAWPOS
					}
				}
			}
		}
		else
		{
			// scope is inactive

			sc = &scope[i];
			if (!sc->wasCleared)
			{
				// clear scope background
				clearRect(scopeXOffs, scopeYOffs, scopeDrawLen, SCOPE_HEIGHT);

				// draw empty line
				hLine(scopeXOffs, scopeLineY, scopeDrawLen, PAL_PATTEXT);

				sc->wasCleared = true;
			}
		}

		// draw channel numbering (if enabled)
		if (config.ptnChnNumbers)
			drawScopeNumber(scopeXOffs, scopeYOffs, (uint8_t)i, false);

		// draw rec. symbol (if enabled)
		if (config.multiRecChn[i])
			blit(scopeXOffs, scopeYOffs + 31, scopeRecBMP, 13, 4);

		scopeXOffs += scopeDrawLen + 3; // align x to next scope
	}

	scopesDisplayingFlag = false;
}

void drawScopeFramework(void)
{
	drawFramework(0, 92, 291, 81, FRAMEWORK_TYPE1);
	for (uint8_t i = 0; i < song.antChn; i++)
		redrawScope(i);
}

void handleScopesFromChQueue(chSyncData_t *chSyncData, uint8_t *scopeUpdateStatus)
{
	uint8_t status;
	syncedChannel_t *ch;
	volatile scope_t *sc;
	sampleTyp *smpPtr;

	for (int32_t i = 0; i < song.antChn; i++)
	{
		sc = &scope[i];
		ch = &chSyncData->channels[i];
		status = scopeUpdateStatus[i];

		// set scope volume
		if (status & IS_Vol)
			sc->SVol = (int8_t)(((ch->finalVol * SCOPE_HEIGHT) + (1 << 10)) >> 11); // rounded

		// set scope frequency
		if (status & IS_Period)
		{
			if (ch->voiceDelta != oldVoiceDelta)
			{
				oldVoiceDelta = ch->voiceDelta;
				oldSFrq = (uint32_t)((oldVoiceDelta * audio.dScopeFreqMul) + 0.5); // rounded
			}

			sc->SFrq = oldSFrq;
		}

		// start scope sample
		if (status & IS_NyTon)
		{
			if (instr[ch->instrNr] != NULL)
			{
				smpPtr = &instr[ch->instrNr]->samp[ch->sampleNr];
				scopeTrigger((uint8_t)i, smpPtr, ch->smpStartPos);

				// set stuff used by Smp. Ed. for sampling position line

				if (ch->instrNr == 130 || (ch->instrNr == editor.curInstr && ch->sampleNr == editor.curSmp))
					editor.curSmpChannel = (uint8_t)i;

				lastChInstr[i].instrNr = ch->instrNr;
				lastChInstr[i].sampleNr = ch->sampleNr;
			}
		}
	}
}

static int32_t SDLCALL scopeThreadFunc(void *ptr)
{
	int32_t time32;
	uint32_t diff32;
	uint64_t time64;

	(void)ptr;

	// this is needed for scope stability (confirmed)
	SDL_SetThreadPriority(SDL_THREAD_PRIORITY_HIGH);

	// set next frame time
	timeNext64 = SDL_GetPerformanceCounter() + scopeTimeLen;
	timeNext64Frac = scopeTimeLenFrac;

	while (editor.programRunning)
	{
		editor.scopeThreadMutex = true;
		updateScopes();
		editor.scopeThreadMutex = false;

		time64 = SDL_GetPerformanceCounter();
		if (time64 < timeNext64)
		{
			assert(timeNext64-time64 <= 0xFFFFFFFFULL);
			diff32 = (uint32_t)(timeNext64 - time64);

			// convert to microseconds and round to integer
			time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);

			// delay until we have reached next tick
			if (time32 > 0)
				usleep(time32);
		}

		// update next tick time
		timeNext64 += scopeTimeLen;

		timeNext64Frac += scopeTimeLenFrac;
		if (timeNext64Frac >= (1ULL << 32))
		{
			timeNext64++;
			timeNext64Frac &= 0xFFFFFFFF;
		}
	}

	return true;
}

bool initScopes(void)
{
	double dInt, dFrac;

	// calculate scope time for performance counters and split into int/frac
	dFrac = modf(editor.dPerfFreq / SCOPE_HZ, &dInt);

	// integer part
	scopeTimeLen = (uint32_t)dInt;

	// fractional part scaled to 0..2^32-1
	dFrac *= UINT32_MAX + 1.0;
	if (dFrac > (double)UINT32_MAX)
		dFrac = (double)UINT32_MAX;
	scopeTimeLenFrac = (uint32_t)round(dFrac);

	scopeThread = SDL_CreateThread(scopeThreadFunc, NULL, NULL);
	if (scopeThread == NULL)
	{
		showErrorMsgBox("Couldn't create channel scope thread!");
		return false;
	}

	SDL_DetachThread(scopeThread);
	return true;
}