ref: 4dfccacdd7d6538d2073a020a1b7f7d1fb4bf774
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_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; }