ref: ba9a38b8266fd42f2bc327b7e6e60866812cbb92
dir: /src/pt2_visuals_sync.c/
// used for syncing audio from Paula writes to tracker visuals #include <stdint.h> #include <stdbool.h> #include "pt2_audio.h" #include "pt2_visuals_sync.h" #include "pt2_scopes.h" #include "pt2_visuals.h" #include "pt2_tables.h" typedef struct syncVoice_t { const int8_t *newData, *data; uint8_t flags, volume; uint16_t period; uint16_t newLength, length; } syncVoice_t; static volatile bool chQueueClearing; static uint32_t audLatencyPerfValInt; static uint64_t audLatencyPerfValFrac; static uint64_t tickTime64, tickTime64Frac; static uint32_t tickTimeLenInt; static uint64_t tickTimeLenFrac; static syncVoice_t syncVoice[PAULA_VOICES]; static chSyncData_t *chSyncEntry; static chSync_t chSync; static void startDMA(int32_t ch) { syncVoice_t *sv = &syncVoice[ch]; if (editor.songPlaying) { sv->data = sv->newData; sv->length = sv->newLength; sv->flags |= TRIGGER_SCOPE; } else { scope_t *s = &scope[ch]; s->data = sv->newData; s->length = sv->newLength * 2; scopeTrigger(ch); } } static void stopDMA(int32_t ch) { if (editor.songPlaying) syncVoice[ch].flags |= STOP_SCOPE; else scope[ch].active = false; } void setVisualsDMACON(uint16_t bits) { if (bits & 0x8000) { // set if (bits & 1) startDMA(0); if (bits & 2) startDMA(1); if (bits & 4) startDMA(2); if (bits & 8) startDMA(3); } else { // clear if (bits & 1) stopDMA(0); if (bits & 2) stopDMA(1); if (bits & 4) stopDMA(2); if (bits & 8) stopDMA(3); } } void setVisualsVolume(int32_t ch, uint16_t vol) { int32_t realVol = vol & 127; if (realVol > 64) realVol = 64; if (editor.songPlaying) { syncVoice_t *sv = &syncVoice[ch]; sv->volume = (uint8_t)realVol; sv->flags |= SET_SCOPE_VOLUME; } else { scope[ch].volume = (uint8_t)realVol; } } void setVisualsPeriod(int32_t ch, uint16_t period) { if (period == 0) period = 65535; // On Amiga: period 0 = one full cycle with period 65536, then period 65535 for the rest else if (period < 113) period = 113; // close to what happens on real Amiga (and needed for BLEP synthesis) if (editor.songPlaying) { syncVoice_t *sv = &syncVoice[ch]; sv->period = period; sv->flags |= SET_SCOPE_PERIOD; } else { scopeSetPeriod(ch, period); } } void setVisualsLength(int32_t ch, uint16_t len) { syncVoice_t *sv = &syncVoice[ch]; sv->newLength = len; if (editor.songPlaying) sv->flags |= SET_SCOPE_LENGTH; else scope[ch].newLength = len * 2; } void setVisualsDataPtr(int32_t ch, const int8_t *src) { syncVoice_t *sv = &syncVoice[ch]; if (src == NULL) src = paulaGetNullSamplePtr(); sv->newData = src; if (editor.songPlaying) sv->flags |= SET_SCOPE_DATA; else scope[ch].newData = src; } void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq) { double dInt; if (audioFreq == 0) return; const double dAudioLatencySecs = audioBufferSize / (double)audioFreq; double dFrac = modf(dAudioLatencySecs * (double)hpcFreq.freq64, &dInt); audLatencyPerfValInt = (uint32_t)dInt; audLatencyPerfValFrac = (uint64_t)((dFrac * TICK_TIME_FRAC_SCALE) + 0.5); // rounded } void setSyncTickTimeLen(uint32_t timeLenInt, uint64_t timeLenFrac) { tickTimeLenInt = timeLenInt; tickTimeLenFrac = timeLenFrac; } void resetChSyncQueue(void) { chSync.data[0].timestamp = 0; chSync.writePos = 0; chSync.readPos = 0; } static int32_t chQueueReadSize(void) { while (chQueueClearing); if (chSync.writePos > chSync.readPos) return chSync.writePos - chSync.readPos; else if (chSync.writePos < chSync.readPos) return chSync.writePos - chSync.readPos + SYNC_QUEUE_LEN + 1; else return 0; } static int32_t chQueueWriteSize(void) { int32_t size; if (chSync.writePos > chSync.readPos) { size = chSync.readPos - chSync.writePos + SYNC_QUEUE_LEN; } else if (chSync.writePos < chSync.readPos) { chQueueClearing = true; /* Buffer is full, reset the read/write pos. This is actually really nasty since ** read/write are two different threads, but because of timestamp validation it ** shouldn't be that dangerous. ** It will also create a small visual stutter while the buffer is getting filled, ** though that is barely noticable on normal buffer sizes, and it takes a minute ** or two at max BPM between each time (when queue size is default, 8191) */ chSync.data[0].timestamp = 0; chSync.readPos = 0; chSync.writePos = 0; size = SYNC_QUEUE_LEN; chQueueClearing = false; } else { size = SYNC_QUEUE_LEN; } return size; } bool chQueuePush(chSyncData_t t) { if (!chQueueWriteSize()) return false; assert(chSync.writePos <= SYNC_QUEUE_LEN); chSync.data[chSync.writePos] = t; chSync.writePos = (chSync.writePos + 1) & SYNC_QUEUE_LEN; return true; } static bool chQueuePop(void) { if (!chQueueReadSize()) return false; chSync.readPos = (chSync.readPos + 1) & SYNC_QUEUE_LEN; assert(chSync.readPos <= SYNC_QUEUE_LEN); return true; } static chSyncData_t *chQueuePeek(void) { if (!chQueueReadSize()) return NULL; assert(chSync.readPos <= SYNC_QUEUE_LEN); return &chSync.data[chSync.readPos]; } static uint64_t getChQueueTimestamp(void) { if (!chQueueReadSize()) return 0; assert(chSync.readPos <= SYNC_QUEUE_LEN); return chSync.data[chSync.readPos].timestamp; } void fillVisualsSyncBuffer(void) { chSyncData_t chSyncData; if (audio.resetSyncTickTimeFlag) { audio.resetSyncTickTimeFlag = false; tickTime64 = SDL_GetPerformanceCounter() + audLatencyPerfValInt; tickTime64Frac = audLatencyPerfValFrac; } if (song != NULL) { moduleChannel_t *ch = song->channels; syncVoice_t *sv = syncVoice; syncedChannel_t *sc = chSyncData.channels; for (int32_t i = 0; i < PAULA_VOICES; i++, ch++, sc++, sv++) { sc->flags = sv->flags | ch->syncFlags; ch->syncFlags = sv->flags = 0; // clear sync flags sc->volume = sv->volume; sc->period = sv->period; sc->data = sv->data; sc->length = sv->length; sc->newData = sv->newData; sc->newLength = sv->newLength; sc->vuVolume = ch->syncVuVolume; sc->analyzerVolume = ch->syncAnalyzerVolume; sc->analyzerPeriod = ch->syncAnalyzerPeriod; } chSyncData.timestamp = tickTime64; chQueuePush(chSyncData); } tickTime64 += tickTimeLenInt; tickTime64Frac += tickTimeLenFrac; if (tickTime64Frac >= TICK_TIME_FRAC_SCALE) { tickTime64Frac &= TICK_TIME_FRAC_MASK; tickTime64++; } } void updateChannelSyncBuffer(void) { uint8_t updateFlags[PAULA_VOICES]; chSyncEntry = NULL; *(uint32_t *)updateFlags = 0; // clear all channel update flags (this is needed) const uint64_t frameTime64 = SDL_GetPerformanceCounter(); // handle channel sync queue while (chQueueClearing); while (chQueueReadSize() > 0) { if (frameTime64 < getChQueueTimestamp()) break; // we have no more stuff to render for now chSyncEntry = chQueuePeek(); if (chSyncEntry == NULL) break; for (int32_t i = 0; i < PAULA_VOICES; i++) updateFlags[i] |= chSyncEntry->channels[i].flags; // yes, OR the status if (!chQueuePop()) break; } /* Extra validation because of possible issues when the buffer is full ** and positions are being reset, which is not entirely thread safe. */ if (chSyncEntry != NULL && chSyncEntry->timestamp == 0) chSyncEntry = NULL; // do actual updates if (chSyncEntry != NULL) { scope_t *s = scope; syncedChannel_t *c = chSyncEntry->channels; for (int32_t ch = 0; ch < PAULA_VOICES; ch++, s++, c++) { const uint8_t flags = updateFlags[ch]; if (flags == 0) continue; if (flags & SET_SCOPE_VOLUME) scope[ch].volume = c->volume; if (flags & SET_SCOPE_PERIOD) scopeSetPeriod(ch, c->period); // the following handling order is important, don't change it! if (flags & STOP_SCOPE) scope[ch].active = false; if (flags & TRIGGER_SCOPE) { s->data = c->data; s->length = c->length * 2; scopeTrigger(ch); } if (flags & SET_SCOPE_DATA) scope[ch].newData = c->newData; if (flags & SET_SCOPE_LENGTH) scope[ch].newLength = c->newLength * 2; // --------------------------------------------------------------- if (flags & UPDATE_SPECTRUM_ANALYZER) updateSpectrumAnalyzer(c->analyzerVolume, c ->analyzerPeriod); if (flags & UPDATE_VUMETER) // for fake VU-meters only { if (c->vuVolume <= 64) editor.vuMeterVolumes[ch] = vuMeterHeights[c->vuVolume]; } } } }