ref: 22c28f1c0de3a76cac067309abfd77f8d508c2bb
dir: /src/ft2_scopes.c/
// 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_bmp.h"
#include "ft2_scopes.h"
#include "ft2_mouse.h"
#include "ft2_video.h"
#include "ft2_scopedraw.h"
#include "ft2_tables.h"
#include "ft2_structs.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;
static volatile bool scopesUpdatingFlag, scopesDisplayingFlag;
static uint32_t scopeTimeLen, scopeTimeLenFrac;
static uint64_t timeNext64, timeNext64Frac;
static volatile scope_t scope[MAX_VOICES];
static SDL_Thread *scopeThread;
static uint8_t *scopeMuteBMP_Ptrs[16];
lastChInstr_t lastChInstr[MAX_VOICES]; // global
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(int32_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)
{
scopeXOffs++;
scopeYOffs++;
channel++;
if (outline)
{
if (channel < 10) // one digit?
{
charOutOutlined(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + channel);
}
else
{
charOutOutlined(scopeXOffs, scopeYOffs, PAL_MOUSEPT, chDecTab1[channel]);
charOutOutlined(scopeXOffs + 7, scopeYOffs, PAL_MOUSEPT, chDecTab2[channel]);
}
}
else
{
if (channel < 10) // one digit?
{
charOut(scopeXOffs, scopeYOffs, PAL_MOUSEPT, '0' + channel);
}
else
{
charOut(scopeXOffs, scopeYOffs, PAL_MOUSEPT, chDecTab1[channel]);
charOut(scopeXOffs + 7, scopeYOffs, PAL_MOUSEPT, chDecTab2[channel]);
}
}
}
static void redrawScope(int32_t ch)
{
const uint16_t *scopeLens;
uint16_t x, y, scopeLen, muteGfxLen, muteGfxX;
int32_t i, chanLookup, chansPerRow;
chansPerRow = (uint32_t)song.antChn >> 1;
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 = scopeMuteBMP_Widths[chanLookup];
muteGfxX = x + ((scopeLen - muteGfxLen) >> 1);
blitFastClipX(muteGfxX, y + 6, scopeMuteBMP_Ptrs[chanLookup], 162, scopeMuteBMP_Heights[chanLookup], muteGfxLen);
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(int32_t chn)
{
bool m, m2, test;
int32_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)
{
uint16_t x;
const uint16_t *scopeLens;
int32_t i, chansPerRow, chanToToggle;
if (!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 = (uint32_t)song.antChn >> 1;
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(int32_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.backwards = false;
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.wasCleared = sc->wasCleared;
tempState.SFrq = sc->SFrq;
tempState.DFrq = sc->DFrq;
tempState.SVol = sc->SVol;
tempState.active = true;
/* 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;
sc = scope;
for (int32_t i = 0; i < song.antChn; i++, sc++)
{
tempState = *sc; // cache it
if (!tempState.active)
continue; // scope is not active, no need
// scope position update
tempState.SPosDec += tempState.SFrq;
const uint32_t posAdd = tempState.SPosDec >> SCOPE_FRAC_BITS;
if (tempState.backwards)
tempState.SPos -= posAdd;
else
tempState.SPos += posAdd;
tempState.SPosDec &= SCOPE_FRAC_MASK;
// handle loop wrapping or sample end
if (tempState.backwards && tempState.SPos < tempState.SRepS) // sampling backwards (definitely pingpong loop)
{
tempState.backwards = false; // change direction to forwards
if (tempState.SRepL >= 2)
tempState.SPos = tempState.SRepS + ((tempState.SRepS - tempState.SPos - 1) % tempState.SRepL);
else
tempState.SPos = tempState.SRepS;
assert(tempState.SPos >= tempState.SRepS && tempState.SPos < tempState.SLen);
}
else if (tempState.SPos >= tempState.SLen)
{
if (tempState.SRepL >= 2)
loopOverflowVal = (tempState.SPos - tempState.SLen) % tempState.SRepL;
else
loopOverflowVal = 0;
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.backwards = true; // 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;
}
void drawScopes(void)
{
int16_t scopeLineY;
const uint16_t *scopeLens;
uint16_t scopeXOffs, scopeYOffs, scopeDrawLen;
int32_t chansPerRow;
volatile scope_t *sc;
scope_t s;
scopesDisplayingFlag = true;
chansPerRow = (uint32_t)song.antChn >> 1;
scopeLens = scopeLenTab[chansPerRow-1];
scopeXOffs = 3;
scopeYOffs = 95;
scopeLineY = 112;
for (int32_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);
// draw scope
bool linedScopes = !!(config.specialFlags & LINED_SCOPES);
scopeDrawRoutineTable[(linedScopes * 6) + (s.sample16Bit * 3) + s.loopType](&s, scopeXOffs, scopeLineY, scopeDrawLen);
}
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 + 1, scopeYOffs + 31, bmp.scopeRec, 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;
sc = scope;
ch = chSyncData->channels;
for (int32_t i = 0; i < song.antChn; i++, sc++, ch++)
{
status = scopeUpdateStatus[i];
if (status & IS_Vol)
sc->SVol = (int8_t)(((ch->finalVol * SCOPE_HEIGHT) + (1 << 15)) >> 16); // rounded
if (status & IS_Period)
{
sc->SFrq = getScopeFrequenceValue(ch->finalPeriod);
sc->DFrq = (uint32_t)(sc->SFrq >> (SCOPE_FRAC_BITS - 10)); // amount of samples to skip after drawing a pixel
}
if (status & IS_NyTon)
{
if (instr[ch->instrNr] != NULL)
{
scopeTrigger(i, &instr[ch->instrNr]->samp[ch->sampleNr], ch->smpStartPos);
// set some 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)
{
// 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;
uint64_t time64 = SDL_GetPerformanceCounter();
if (time64 < timeNext64)
{
time64 = timeNext64 - time64;
if (time64 > UINT32_MAX)
time64 = UINT32_MAX;
const uint32_t diff32 = (uint32_t)time64;
// convert to microseconds and round to integer
const int32_t time32 = (int32_t)((diff32 * editor.dPerfFreqMulMicro) + 0.5);
// delay until we have reached the next frame
if (time32 > 0)
usleep(time32);
}
// update next tick time
timeNext64 += scopeTimeLen;
timeNext64Frac += scopeTimeLenFrac;
if (timeNext64Frac > 0xFFFFFFFF)
{
timeNext64Frac &= 0xFFFFFFFF;
timeNext64++;
}
}
(void)ptr;
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 = (int32_t)dInt;
// fractional part (scaled to 0..2^32-1)
dFrac *= UINT32_MAX;
dFrac += 0.5;
if (dFrac > UINT32_MAX)
dFrac = UINT32_MAX;
scopeTimeLenFrac = (uint32_t)dFrac;
// setup scope mute BMP pointers
assert(bmp.scopeMute != NULL);
for (int32_t i = 0; i < 16; i++)
scopeMuteBMP_Ptrs[i] = bmp.scopeMute + scopeMuteBMP_Offs[i];
scopeThread = SDL_CreateThread(scopeThreadFunc, NULL, NULL);
if (scopeThread == NULL)
{
showErrorMsgBox("Couldn't create channel scope thread!");
return false;
}
SDL_DetachThread(scopeThread);
return true;
}