ref: 9cbd880b3085fe0661b581ee48798db9bebde928
dir: /src/pt2_scopes.c/
#include <stdint.h> #include <stdbool.h> #include <math.h> // modf() #ifndef _WIN32 #include <unistd.h> // usleep() #endif #include "pt2_header.h" #include "pt2_helpers.h" #include "pt2_visuals.h" #include "pt2_scopes.h" #include "pt2_tables.h" #include "pt2_structs.h" #include "pt2_config.h" // this uses code that is not entirely thread safe, but I have never had any issues so far... static volatile bool scopesUpdatingFlag, scopesDisplayingFlag; static int32_t oldPeriod = -1; static uint32_t scopeTimeLen, scopeTimeLenFrac; static uint64_t timeNext64, timeNext64Frac; static double dOldScopeDelta; static SDL_Thread *scopeThread; scope_t scope[AMIGA_VOICES]; // global void resetCachedScopePeriod(void) { oldPeriod = -1; dOldScopeDelta = 0.0; } // this is quite hackish, but fixes sample swapping issues static int32_t getSampleSlotFromReadAddress(const int8_t *sampleReadAddress) { assert(song != NULL); const int8_t *sampleData = song->sampleData; const int32_t sampleSlotSize = MAX_SAMPLE_LEN; if (sampleData == NULL) // shouldn't really happen, but just in case return -1; int32_t sampleSlot = 30; // start at last slot const int8_t *sampleBaseAddress = &sampleData[sampleSlot * sampleSlotSize]; if (sampleReadAddress == NULL || sampleReadAddress >= sampleBaseAddress+sampleSlotSize) return -1; // out of range for (; sampleSlot >= 0; sampleSlot--) { if (sampleReadAddress >= sampleBaseAddress) break; sampleBaseAddress -= sampleSlotSize; } return sampleSlot; // 0..30, or -1 if out of range } int32_t getSampleReadPos(int32_t ch) // used for the sampler screen { // cache some stuff const scope_t *sc = &scope[ch]; const bool active = sc->active; const int8_t *data = sc->data; const int32_t pos = sc->pos; const int32_t len = sc->length; if (song == NULL || !active || data == NULL) return -1; /* Because the scopes work like the Paula emulation, we have a DATA ** and LENGTH variable, which are not static. This means that we have ** to get creative to get the absolute sampling position. */ int32_t sample = getSampleSlotFromReadAddress(data); if (sample != editor.currSample) return -1; // sample is not the one we're seeing in the sampler screen const moduleSample_t *s = &song->samples[sample]; const int8_t *sampleReadAddress = &data[pos]; const int8_t *sampleBaseAddress = &song->sampleData[s->offset]; const int32_t realPos = (int32_t)(sampleReadAddress - sampleBaseAddress); // return -1 if sample has no loop and read length is 2 (playing sample "loop" area) const bool loopEnabled = (s->loopStart + s->loopLength) > 2; if (!loopEnabled && len == 2) return -1; if (realPos < 0 || realPos >= s->length) return -1; return realPos; } void scopeSetPeriod(int32_t ch, int32_t period) { // if the new period was the same as the previous period, use cached deltas if (period != oldPeriod) { oldPeriod = period; const double dPeriodToScopeDeltaDiv = PAULA_PAL_CLK / (double)SCOPE_HZ; dOldScopeDelta = dPeriodToScopeDeltaDiv / period; } scope[ch].dDelta = dOldScopeDelta; } void scopeTrigger(int32_t ch) { volatile scope_t *sc = &scope[ch]; scope_t tempState = *sc; // cache it const int8_t *newData = tempState.newData; if (newData == NULL) newData = &song->sampleData[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample int32_t newLength = tempState.newLength; // in bytes, not words if (newLength < 2) newLength = 2; // for safety tempState.dPhase = 0.0; tempState.pos = 0; tempState.data = newData; tempState.length = newLength; 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; } void updateScopes(void) { scope_t tempState; if (editor.isWAVRendering) return; volatile scope_t *sc = scope; scopesUpdatingFlag = true; for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++) { tempState = *sc; // cache it if (!tempState.active) continue; // scope is not active tempState.dPhase += tempState.dDelta; const int32_t wholeSamples = (int32_t)tempState.dPhase; tempState.dPhase -= wholeSamples; tempState.pos += wholeSamples; if (tempState.pos >= tempState.length) { // sample reached end, simulate Paula register update (sample swapping) /* Wrap pos around one time with current length, then set new length ** and wrap around it (handles one-shot loops and sample swapping). */ tempState.pos -= tempState.length; tempState.length = tempState.newLength; if (tempState.pos >= tempState.length && tempState.length > 0) tempState.pos %= tempState.length; tempState.data = tempState.newData; } *sc = tempState; // update scope state } scopesUpdatingFlag = false; } /* This routine gets the average sample amplitude through the running scope voices. ** This gives a somewhat more stable result than getting the peak from the mixer, ** and we don't care about including filters/BLEP in the peak calculation. */ static void updateRealVuMeters(void) { scope_t tmpScope, *sc; // sink VU-meters first for (int32_t i = 0; i < AMIGA_VOICES; i++) { editor.realVuMeterVolumes[i] -= 4; if (editor.realVuMeterVolumes[i] < 0) editor.realVuMeterVolumes[i] = 0; } // get peak sample data from running scope voices sc = scope; for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++) { tmpScope = *sc; // cache it if (!tmpScope.active || tmpScope.data == NULL || tmpScope.volume == 0 || tmpScope.length == 0) continue; // amount of integer samples getting skipped every frame const int32_t samplesToScan = (const int32_t)tmpScope.dDelta; if (samplesToScan <= 0) continue; int32_t pos = tmpScope.pos; int32_t length = tmpScope.length; const int8_t *data = tmpScope.data; int32_t runningAmplitude = 0; for (int32_t x = 0; x < samplesToScan; x++) { int32_t amplitude = 0; if (data != NULL) amplitude = data[pos] * tmpScope.volume; runningAmplitude += ABS(amplitude); if (++pos >= length) { pos = 0; /* Read cycle done, temporarily update the display data/length variables ** before the scope thread does it. */ data = tmpScope.newData; length = tmpScope.newLength; } } double dAvgAmplitude = runningAmplitude / (double)samplesToScan; dAvgAmplitude *= 96.0 / (128.0 * 64.0); // normalize int32_t vuHeight = (int32_t)(dAvgAmplitude + 0.5); // rounded if (vuHeight > 48) // max VU-meter height vuHeight = 48; if ((int8_t)vuHeight > editor.realVuMeterVolumes[i]) editor.realVuMeterVolumes[i] = (int8_t)vuHeight; } } void drawScopes(void) { volatile scope_t *sc = scope; // cache it int32_t scopeX = 128; const uint32_t bgColor = video.palette[PAL_BACKGRD]; const uint32_t fgColor = video.palette[PAL_QADSCP]; scopesDisplayingFlag = true; for (int32_t i = 0; i < AMIGA_VOICES; i++, sc++) { scope_t tmpScope = *sc; // cache it // render scope if (tmpScope.active && tmpScope.data != NULL && tmpScope.volume != 0 && tmpScope.length > 0) { sc->emptyScopeDrawn = false; // fill scope background fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor); // render scope data int16_t scopeData; int32_t pos = tmpScope.pos; int32_t length = tmpScope.length; const int16_t volume = -(tmpScope.volume << 7); const int8_t *data = tmpScope.data; uint32_t *scopeDrawPtr = &video.frameBuffer[(71 * SCREEN_W) + scopeX]; for (int32_t x = 0; x < SCOPE_WIDTH; x++) { scopeData = 0; if (data != NULL) scopeData = (data[pos] * volume) >> 16; scopeDrawPtr[(scopeData * SCREEN_W) + x] = fgColor; if (++pos >= length) { pos = 0; // read cycle done, update the drawing data/length variables length = tmpScope.newLength; data = tmpScope.newData; } } } else if (!sc->emptyScopeDrawn) { // scope is inactive (or vol=0), draw empty scope once until it gets active again // fill scope background fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor); // draw scope line hLine(scopeX, 71, SCOPE_WIDTH, fgColor); sc->emptyScopeDrawn = true; } scopeX += SCOPE_WIDTH+8; } scopesDisplayingFlag = false; } 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) { if (config.realVuMeters) updateRealVuMeters(); updateScopes(); 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+1.0; scopeTimeLenFrac = (uint32_t)dFrac; scopeThread = SDL_CreateThread(scopeThreadFunc, NULL, NULL); if (scopeThread == NULL) { showErrorMsgBox("Couldn't create scope thread!"); return false; } SDL_DetachThread(scopeThread); return true; } void stopScope(int32_t ch) { // wait for scopes to finish updating while (scopesUpdatingFlag); scope[ch].active = false; // wait for scope displaying to be done (safety) while (scopesDisplayingFlag); } void stopAllScopes(void) { // wait for scopes to finish updating while (scopesUpdatingFlag); for (int32_t i = 0; i < AMIGA_VOICES; i++) scope[i].active = false; // wait for scope displaying to be done (safety) while (scopesDisplayingFlag); }