ref: 13b6db91928fea27f956cba68ee4169b7604a752
dir: /src/pt2_paula.c/
/* Simple Paula emulator by 8bitbubsy (with BLEP synthesis by aciddose). ** The Amiga filters are handled in pt2_amigafilters.c ** ** Limitation: The audio output frequency can't be below 31389Hz ( ceil(PAULA_PAL_CLK / 113.0) ) */ #include <stdint.h> #include <stdbool.h> #include "pt2_audio.h" #include "pt2_paula.h" #include "pt2_blep.h" #include "pt2_sync.h" #include "pt2_scopes.h" #include "pt2_config.h" paulaVoice_t paula[PAULA_VOICES]; // globalized static blep_t blep[PAULA_VOICES]; static double dPeriodToDeltaDiv; void paulaSetOutputFrequency(double dAudioFreq, bool oversampling2x) { dPeriodToDeltaDiv = PAULA_PAL_CLK / dAudioFreq; if (oversampling2x) dPeriodToDeltaDiv /= 2.0; } void paulaSetPeriod(int32_t ch, uint16_t period) { paulaVoice_t *v = &paula[ch]; int32_t realPeriod = period; if (realPeriod == 0) realPeriod = 1+65535; // confirmed behavior on real Amiga else if (realPeriod < 113) realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis) // to be read on next sampling step (or on DMA trigger) v->AUD_PER_delta = dPeriodToDeltaDiv / realPeriod; v->AUD_PER_deltamul = 1.0 / v->AUD_PER_delta; // for BLEP synthesis (prevents division in inner mixing loop) // handle BLEP synthesis edge-cases if (v->dLastDelta == 0.0) v->dLastDelta = v->AUD_PER_delta; if (v->dLastDeltaMul == 0.0) v->dLastDeltaMul = v->AUD_PER_deltamul; // handle visualizers if (editor.songPlaying) { v->syncPeriod = realPeriod; v->syncFlags |= SET_SCOPE_PERIOD; } else { scopeSetPeriod(ch, realPeriod); } } void paulaSetVolume(int32_t ch, uint16_t vol) { paulaVoice_t *v = &paula[ch]; int32_t realVol = vol & 127; if (realVol > 64) realVol = 64; // multiplying sample point by this also scales the sample from -128..127 -> -1.0 .. ~0.99 v->AUD_VOL = realVol * (1.0 / (128.0 * 64.0)); // handle visualizers if (editor.songPlaying) { v->syncVolume = (uint8_t)realVol; v->syncFlags |= SET_SCOPE_VOLUME; } else { scope[ch].volume = (uint8_t)realVol; } } void paulaSetLength(int32_t ch, uint16_t len) { paulaVoice_t *v = &paula[ch]; v->AUD_LEN = len; // handle visualizers if (editor.songPlaying) v->syncFlags |= SET_SCOPE_LENGTH; else scope[ch].newLength = len * 2; } void paulaSetData(int32_t ch, const int8_t *src) { paulaVoice_t *v = &paula[ch]; // if pointer is NULL, use empty 128kB sample slot after sample 31 in the tracker if (src == NULL) src = &song->sampleData[config.reservedSampleOffset]; v->AUD_LC = src; // handle visualizers if (editor.songPlaying) v->syncFlags |= SET_SCOPE_DATA; else scope[ch].newData = src; } static inline void refetchPeriod(paulaVoice_t *v) // Paula stage { // set BLEP stuff v->dLastPhase = v->dPhase; v->dLastDelta = v->dDelta; v->dLastDeltaMul = v->dDeltaMul; v->dBlepOffset = v->dLastPhase * v->dLastDeltaMul; // Paula only updates period (delta) during period refetching (this stage) v->dDelta = v->AUD_PER_delta; v->dDeltaMul = v->AUD_PER_deltamul; v->nextSampleStage = true; } static void startPaulaDMA(int32_t ch) { paulaVoice_t *v = &paula[ch]; // if pointer is NULL, use empty 128kB sample slot after sample 31 in the tracker if (v->AUD_LC == NULL) v->AUD_LC = &song->sampleData[config.reservedSampleOffset]; // immediately update AUD_LC/AUD_LEN v->location = v->AUD_LC; v->lengthCounter = v->AUD_LEN; // make Paula fetch new samples immediately v->sampleCounter = 0; v->DMATriggerFlag = true; refetchPeriod(v); v->dPhase = 0.0; // kludge: must be cleared *after* refetchPeriod() v->DMA_active = true; // handle visualizers if (editor.songPlaying) { v->syncTriggerData = v->AUD_LC; v->syncTriggerLength = v->AUD_LEN * 2; v->syncFlags |= TRIGGER_SCOPE; } else { scope_t *s = &scope[ch]; s->newData = v->AUD_LC; s->newLength = v->AUD_LEN * 2; scopeTrigger(ch); } } static void stopPaulaDMA(int32_t ch) { paulaVoice_t *v = &paula[ch]; v->DMA_active = false; // handle visualizers if (editor.songPlaying) v->syncFlags |= STOP_SCOPE; else scope[ch].active = false; } void paulaSetDMACON(uint16_t bits) // $DFF096 register write (only controls paula DMAs) { if (bits & 0x8000) { // set if (bits & 1) startPaulaDMA(0); if (bits & 2) startPaulaDMA(1); if (bits & 4) startPaulaDMA(2); if (bits & 8) startPaulaDMA(3); } else { // clear if (bits & 1) stopPaulaDMA(0); if (bits & 2) stopPaulaDMA(1); if (bits & 4) stopPaulaDMA(2); if (bits & 8) stopPaulaDMA(3); } } static inline void nextSample(paulaVoice_t *v, blep_t *b) { if (v->sampleCounter == 0) { // it's time to read new samples from DMA // don't update AUD_LEN/AUD_LC yet on DMA trigger if (!v->DMATriggerFlag) { if (--v->lengthCounter == 0) { v->lengthCounter = v->AUD_LEN; v->location = v->AUD_LC; } } v->DMATriggerFlag = false; // fill DMA data buffer v->AUD_DAT[0] = *v->location++; v->AUD_DAT[1] = *v->location++; v->sampleCounter = 2; } /* Pre-compute current sample point. ** Output volume is only read from AUDxVOL at this stage, ** and we don't emulate volume PWM anyway, so we can ** pre-multiply by volume here. */ v->dSample = v->AUD_DAT[0] * v->AUD_VOL; // -128..127 * 0.0 .. 1.0 // fill BLEP buffer if the new sample differs from the old one if (v->dSample != b->dLastValue) { if (v->dLastDelta > v->dLastPhase) blepAdd(b, v->dBlepOffset, b->dLastValue - v->dSample); b->dLastValue = v->dSample; } // progress AUD_DAT buffer v->AUD_DAT[0] = v->AUD_DAT[1]; v->sampleCounter--; } void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples) { double *dMixBufSelect[PAULA_VOICES] = { dOutL, dOutR, dOutR, dOutL }; memset(dOutL, 0, numSamples * sizeof (double)); memset(dOutR, 0, numSamples * sizeof (double)); paulaVoice_t *v = paula; blep_t *b = blep; for (int32_t i = 0; i < PAULA_VOICES; i++, v++, b++) { if (!v->DMA_active || v->location == NULL || v->AUD_LC == NULL) continue; double *dMixBuffer = dMixBufSelect[i]; // what output channel to mix into (L, R, R, L) for (int32_t j = 0; j < numSamples; j++) { if (v->nextSampleStage) { v->nextSampleStage = false; nextSample(v, b); // inlined } double dSample = v->dSample; // current Paula sample, pre-multiplied by volume, scaled to -1.0 .. 0.9921875 if (b->samplesLeft > 0) dSample = blepRun(b, dSample); dMixBuffer[j] += dSample; v->dPhase += v->dDelta; if (v->dPhase >= 1.0) { v->dPhase -= 1.0; refetchPeriod(v); // inlined } } } }