ref: 871c59c2513acc611f3b1f4242453fc3bd1f6460
parent: 1e43b50e5d6817507fd8dc0849f052d4d7a6c0a8
author: Olav Sørensen <[email protected]>
date: Sun Dec 4 17:29:15 EST 2022
Pushed v1.54 code
--- a/src/pt2_amigafilters.c
+++ /dev/null
@@ -1,397 +1,0 @@
-/* Amiga 500 / Amiga 1200 filter implementation.
-**
-** Route:
-** Paula output -> low-pass filter -> LED filter (if turned on) -> high-pass filter (centering of waveform)
-*/
-
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_structs.h"
-#include "pt2_audio.h"
-#include "pt2_paula.h"
-#include "pt2_rcfilter.h"
-#include "pt2_math.h"
-#include "pt2_textout.h"
-
-typedef struct ledFilter_t
-{
- double LIn1, LIn2, LOut1, LOut2;
- double RIn1, RIn2, ROut1, ROut2;
- double a1, a2, a3, b1, b2;
-} ledFilter_t;
-
-static int32_t filterModel;
-static bool ledFilterEnabled, useA1200LowPassFilter;
-static rcFilter_t filterLoA500, filterHiA500, filterLoA1200, filterHiA1200;
-static ledFilter_t filterLED;
-
-void (*processAmigaFilters)(double *, double *, int32_t); // globalized
-
-static void processFiltersA1200_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA1200_LED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA500_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples);
-static void processFiltersA500_LED(double *dBufferL, double *dBufferR, int32_t numSamples);
-
-// --------------------------------------------------------
-// Crude LED filter implementation
-// --------------------------------------------------------
-
-void clearLEDFilterState(ledFilter_t *f)
-{
- f->LIn1 = f->LIn2 = f->LOut1 = f->LOut2 = 0.0;
- f->RIn1 = f->RIn2 = f->ROut1 = f->ROut2 = 0.0;
-}
-
-static void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter)
-{
- const double c = 1.0 / pt2_tan((PT2_PI * hz) / sr);
- const double r = 1.0 / qfactor;
-
- filter->a1 = 1.0 / (1.0 + r * c + c * c);
- filter->a2 = 2.0 * filter->a1;
- filter->a3 = filter->a1;
- filter->b1 = 2.0 * (1.0 - c*c) * filter->a1;
- filter->b2 = (1.0 - r * c + c * c) * filter->a1;
-}
-
-static void LEDFilter(ledFilter_t *f, const double *in, double *out)
-{
- const double LOut = (f->a1 * in[0]) + (f->a2 * f->LIn1) + (f->a3 * f->LIn2) - (f->b1 * f->LOut1) - (f->b2 * f->LOut2);
- const double ROut = (f->a1 * in[1]) + (f->a2 * f->RIn1) + (f->a3 * f->RIn2) - (f->b1 * f->ROut1) - (f->b2 * f->ROut2);
-
- // shift states
-
- f->LIn2 = f->LIn1;
- f->LIn1 = in[0];
- f->LOut2 = f->LOut1;
- f->LOut1 = LOut;
-
- f->RIn2 = f->RIn1;
- f->RIn1 = in[1];
- f->ROut2 = f->ROut1;
- f->ROut1 = ROut;
-
- // set output
- out[0] = LOut;
- out[1] = ROut;
-}
-
-// --------------------------------------------------------
-// --------------------------------------------------------
-
-void setupAmigaFilters(double dAudioFreq)
-{
- /* Amiga 500/1200 filter emulation
- **
- ** aciddose:
- ** First comes a static low-pass 6dB formed by the supply current
- ** from the Paula's mixture of channels A+B / C+D into the opamp with
- ** 0.1uF capacitor and 360 ohm resistor feedback in inverting mode biased by
- ** dac vRef (used to center the output).
- **
- ** R = 360 ohm
- ** C = 0.1uF
- ** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001)
- **
- ** Under spice simulation the circuit yields -3dB = 4400Hz.
- ** In the Amiga 1200, the low-pass cutoff is ~34kHz, so the
- ** static low-pass filter is disabled in the mixer in A1200 mode.
- **
- ** Next comes a bog-standard Sallen-Key filter ("LED") with:
- ** R1 = 10K ohm
- ** R2 = 10K ohm
- ** C1 = 6800pF
- ** C2 = 3900pF
- ** Q ~= 1/sqrt(2)
- **
- ** This filter is optionally bypassed by an MPF-102 JFET chip when
- ** the LED filter is turned off.
- **
- ** Under spice simulation the circuit yields -3dB = 2800Hz.
- ** 90 degrees phase = 3000Hz (so, should oscillate at 3kHz!)
- **
- ** The buffered output of the Sallen-Key passes into an RC high-pass with:
- ** R = 1.39K ohm (1K ohm + 390 ohm)
- ** C = 22uF (also C = 330nF, for improved high-frequency)
- **
- ** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022)
- ** Under spice simulation the circuit yields -3dB = 5.2Hz.
- **
- ** 8bitbubsy:
- ** Keep in mind that many of the Amiga schematics that are floating around on
- ** the internet have wrong RC values! They were most likely very early schematics
- ** that didn't change before production (or changes that never reached production).
- ** This has been confirmed by measuring the components on several Amiga motherboards.
- **
- ** Correct values for A500, >rev3 (?) (A500_R6.pdf):
- ** - 1-pole RC 6dB/oct low-pass: R=360 ohm, C=0.1uF
- ** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF
- ** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22.33uF (22+0.33)
- **
- ** Correct values for A1200, all revs (A1200_R2.pdf):
- ** - 1-pole RC 6dB/oct low-pass: R=680 ohm, C=6800pF
- ** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (same as A500)
- ** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
- */
- double R, C, R1, R2, C1, C2, cutoff, qfactor;
-
- if (audio.oversamplingFlag)
- dAudioFreq *= 2.0; // 2x oversampling
-
- const bool audioWasntLocked = !audio.locked;
- if (audioWasntLocked)
- lockAudio();
-
- // A500 1-pole (6db/oct) static RC low-pass filter:
- R = 360.0; // R321 (360 ohm)
- C = 1e-7; // C321 (0.1uF)
- cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~4420.971Hz
- calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA500);
-
- // (optional) A1200 1-pole (6db/oct) static RC low-pass filter:
- R = 680.0; // R321 (680 ohm)
- C = 6.8e-9; // C321 (6800pF)
- cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~34419.322Hz
-
- useA1200LowPassFilter = false;
- if (dAudioFreq/2.0 > cutoff)
- {
- calcRCFilterCoeffs(dAudioFreq, cutoff, &filterLoA1200);
- useA1200LowPassFilter = true;
- }
-
- // Sallen-Key low-pass filter ("LED" filter, same values on A500/A1200):
- R1 = 10000.0; // R322 (10K ohm)
- R2 = 10000.0; // R323 (10K ohm)
- C1 = 6.8e-9; // C322 (6800pF)
- C2 = 3.9e-9; // C323 (3900pF)
- cutoff = 1.0 / (PT2_TWO_PI * pt2_sqrt(R1 * R2 * C1 * C2)); // ~3090.533Hz
- qfactor = pt2_sqrt(R1 * R2 * C1 * C2) / (C2 * (R1 + R2)); // ~0.660225
- calcLEDFilterCoeffs(dAudioFreq, cutoff, qfactor, &filterLED);
-
- // A500 1-pole (6dB/oct) static RC high-pass filter:
- R = 1390.0; // R324 (1K ohm) + R325 (390 ohm)
- C = 2.233e-5; // C334 (22uF) + C335 (0.33uF)
- cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.128Hz
- calcRCFilterCoeffs(dAudioFreq, cutoff, &filterHiA500);
-
- // A1200 1-pole (6dB/oct) static RC high-pass filter:
- R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
- C = 2.2e-5; // C334 (22uF capacitor)
- cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.205Hz
- calcRCFilterCoeffs(dAudioFreq, cutoff, &filterHiA1200);
-
- if (audioWasntLocked)
- unlockAudio();
-}
-
-void resetAmigaFilterStates(void)
-{
- const bool audioWasntLocked = !audio.locked;
- if (audioWasntLocked)
- lockAudio();
-
- clearRCFilterState(&filterLoA500);
- clearRCFilterState(&filterLoA1200);
- clearRCFilterState(&filterHiA500);
- clearRCFilterState(&filterHiA1200);
- clearLEDFilterState(&filterLED);
-
- if (audioWasntLocked)
- unlockAudio();
-}
-
-static void processFiltersA1200_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
- if (useA1200LowPassFilter)
- {
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // low-pass filter
- RCLowPassFilterStereo(&filterLoA1200, dOut, dOut);
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
- }
- else
- {
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
- }
-}
-
-static void processFiltersA1200_LED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
- if (useA1200LowPassFilter)
- {
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // low-pass filter
- RCLowPassFilterStereo(&filterLoA1200, dOut, dOut);
-
- // "LED" Sallen-Key filter
- LEDFilter(&filterLED, dOut, dOut);
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
- }
- else
- {
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // "LED" Sallen-Key filter
- LEDFilter(&filterLED, dOut, dOut);
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA1200, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
- }
-}
-
-static void processFiltersA500_NoLED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // low-pass RC filter
- RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
-}
-
-static void processFiltersA500_LED(double *dBufferL, double *dBufferR, int32_t numSamples)
-{
- for (int32_t i = 0; i < numSamples; i++)
- {
- double dOut[2];
-
- dOut[0] = dBufferL[i];
- dOut[1] = dBufferR[i];
-
- // low-pass RC filter
- RCLowPassFilterStereo(&filterLoA500, dOut, dOut);
-
- // "LED" Sallen-Key filter
- LEDFilter(&filterLED, dOut, dOut);
-
- // high-pass RC filter
- RCHighPassFilterStereo(&filterHiA500, dOut, dOut);
-
- dBufferL[i] = dOut[0];
- dBufferR[i] = dOut[1];
- }
-}
-
-static void updateAmigaFilterFunctions(void)
-{
- if (filterModel == FILTERMODEL_A500)
- {
- if (ledFilterEnabled)
- processAmigaFilters = processFiltersA500_LED;
- else
- processAmigaFilters = processFiltersA500_NoLED;
- }
- else // A1200
- {
- if (ledFilterEnabled)
- processAmigaFilters = processFiltersA1200_LED;
- else
- processAmigaFilters = processFiltersA1200_NoLED;
- }
-}
-
-void setAmigaFilterModel(uint8_t model)
-{
- const bool audioWasntLocked = !audio.locked;
- if (audioWasntLocked)
- lockAudio();
-
- filterModel = model;
- updateAmigaFilterFunctions();
-
- if (audioWasntLocked)
- unlockAudio();
-}
-
-void setLEDFilter(bool state)
-{
- if (ledFilterEnabled == state)
- return; // same state as before!
-
- const bool audioWasntLocked = !audio.locked;
- if (audioWasntLocked)
- lockAudio();
-
- clearLEDFilterState(&filterLED);
- ledFilterEnabled = editor.useLEDFilter;
- updateAmigaFilterFunctions();
-
- if (audioWasntLocked)
- unlockAudio();
-}
-
-void toggleAmigaFilterModel(void)
-{
- const bool audioWasntLocked = !audio.locked;
- if (audioWasntLocked)
- lockAudio();
-
- resetAmigaFilterStates();
-
- filterModel ^= 1;
- updateAmigaFilterFunctions();
-
- if (audioWasntLocked)
- unlockAudio();
-
- if (filterModel == FILTERMODEL_A500)
- displayMsg("AUDIO: AMIGA 500");
- else
- displayMsg("AUDIO: AMIGA 1200");
-}
--- a/src/pt2_amigafilters.h
+++ /dev/null
@@ -1,12 +1,0 @@
-#pragma once
-
-#include <stdint.h>
-#include <stdbool.h>
-
-void setupAmigaFilters(double dAudioFreq);
-void resetAmigaFilterStates(void);
-void setAmigaFilterModel(uint8_t amigaModel);
-void setLEDFilter(bool state);
-void toggleAmigaFilterModel(void);
-
-extern void (*processAmigaFilters)(double *, double *, int32_t);
--- a/src/pt2_askbox.c
+++ b/src/pt2_askbox.c
@@ -4,7 +4,7 @@
#include "pt2_visuals.h"
#include "pt2_mouse.h"
#include "pt2_structs.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
#include "pt2_keyboard.h"
#include "pt2_diskop.h"
#include "pt2_mod2wav.h"
@@ -96,7 +96,7 @@
if (ui.diskOpScreenShown)
{
- diskOpRenderFileList();
+ renderDiskOpScreen();
}
else if (ui.posEdScreenShown)
{
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -23,11 +23,10 @@
#include "pt2_config.h"
#include "pt2_textout.h"
#include "pt2_scopes.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
#include "pt2_downsample2x.h"
#include "pt2_replayer.h"
#include "pt2_paula.h"
-#include "pt2_amigafilters.h"
// cumulative mid/side normalization factor (1/sqrt(2))*(1/sqrt(2))
#define STEREO_NORM_FACTOR 0.5
@@ -36,41 +35,79 @@
static uint8_t panningMode;
static int32_t randSeed = INITIAL_DITHER_SEED, stereoSeparation = 100;
-static uint32_t audLatencyPerfValInt, audLatencyPerfValFrac;
-static uint64_t tickTime64, tickTime64Frac;
static double *dMixBufferL, *dMixBufferR;
static double dPrngStateL, dPrngStateR, dSideFactor;
static SDL_AudioDeviceID dev;
-// for audio/video syncing
-static uint32_t tickTimeLen, tickTimeLenFrac;
-
audio_t audio; // globalized
-static void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq)
+void setAmigaFilterModel(uint8_t model)
{
- double dInt, dFrac;
+ if (audio.amigaModel == model)
+ return; // same state as before!
- if (audioFreq == 0)
- return;
+ const bool audioWasntLocked = !audio.locked;
+ if (audioWasntLocked)
+ lockAudio();
- const double dAudioLatencySecs = audioBufferSize / (double)audioFreq;
+ audio.amigaModel = model;
- dFrac = modf(dAudioLatencySecs * hpcFreq.dFreq, &dInt);
+ const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
- // integer part
- audLatencyPerfValInt = (uint32_t)dInt;
+ if (audioWasntLocked)
+ unlockAudio();
+}
- // fractional part (scaled to 0..2^32-1)
- audLatencyPerfValFrac = (uint32_t)((dFrac * (UINT32_MAX+1.0)) + 0.5); // rounded
+void toggleAmigaFilterModel(void)
+{
+ const bool audioWasntLocked = !audio.locked;
+ if (audioWasntLocked)
+ lockAudio();
+
+ audio.amigaModel ^= 1;
+
+ const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
+
+ if (audioWasntLocked)
+ unlockAudio();
+
+ if (audio.amigaModel == MODEL_A500)
+ displayMsg("AUDIO: AMIGA 500");
+ else
+ displayMsg("AUDIO: AMIGA 1200");
}
-void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac)
+void setLEDFilter(bool state)
{
- tickTimeLen = timeLen;
- tickTimeLenFrac = timeLenFrac;
+ if (audio.ledFilterEnabled == state)
+ return; // same state as before!
+
+ const bool audioWasntLocked = !audio.locked;
+ if (audioWasntLocked)
+ lockAudio();
+
+ audio.ledFilterEnabled = state;
+ paulaWriteByte(0xBFE001, audio.ledFilterEnabled << 1);
+
+ if (audioWasntLocked)
+ unlockAudio();
}
+void toggleLEDFilter(void)
+{
+ const bool audioWasntLocked = !audio.locked;
+ if (audioWasntLocked)
+ lockAudio();
+
+ audio.ledFilterEnabled ^= 1;
+ paulaWriteByte(0xBFE001, audio.ledFilterEnabled << 1);
+
+ if (audioWasntLocked)
+ unlockAudio();
+}
+
void lockAudio(void)
{
if (dev != 0)
@@ -252,9 +289,7 @@
{
if (audio.oversamplingFlag) // 2x oversampling
{
- // mix and filter channels (at 2x rate)
paulaGenerateSamples(dMixBufferL, dMixBufferR, numSamples*2);
- processAmigaFilters(dMixBufferL, dMixBufferR, numSamples*2);
// downsample, normalize and dither
int16_t out[2];
@@ -280,9 +315,7 @@
}
else
{
- // mix and filter channels
paulaGenerateSamples(dMixBufferL, dMixBufferR, numSamples);
- processAmigaFilters(dMixBufferL, dMixBufferR, numSamples);
// normalize and dither
int16_t out[2];
@@ -308,53 +341,6 @@
}
}
-static 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;
- paulaVoice_t *v = paula;
- syncedChannel_t *sc = chSyncData.channels;
-
- for (int32_t i = 0; i < PAULA_VOICES; i++, ch++, sc++, v++)
- {
- sc->flags = v->syncFlags | ch->syncFlags;
- ch->syncFlags = v->syncFlags = 0; // clear sync flags
-
- sc->volume = v->syncVolume;
- sc->period = v->syncPeriod;
- sc->triggerData = v->syncTriggerData;
- sc->triggerLength = v->syncTriggerLength;
- sc->newData = v->AUD_LC;
- sc->newLength = v->AUD_LEN * 2;
- sc->vuVolume = ch->syncVuVolume;
- sc->analyzerVolume = ch->syncAnalyzerVolume;
- sc->analyzerPeriod = ch->syncAnalyzerPeriod;
- }
-
- chSyncData.timestamp = tickTime64;
- chQueuePush(chSyncData);
- }
-
- tickTime64 += tickTimeLen;
- tickTime64Frac += tickTimeLenFrac;
- if (tickTime64Frac > UINT32_MAX)
- {
- tickTime64Frac &= UINT32_MAX;
- tickTime64++;
- }
-}
-
static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
{
if (editor.mod2WavOngoing || editor.pat2SmpOngoing) // send silence to sound output device
@@ -374,7 +360,7 @@
if (editor.songPlaying)
{
- intMusic();
+ intMusic(); // PT replayer ticker
fillVisualsSyncBuffer();
}
@@ -504,9 +490,10 @@
audio.outputRate = have.freq;
audio.audioBufferSize = have.samples;
audio.oversamplingFlag = (audio.outputRate < 96000); // we do 2x oversampling if the audio output rate is below 96kHz
+ audio.amigaModel = config.amigaModel;
- const int32_t audioFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
- const uint32_t maxSamplesToMix = (int32_t)ceil(audioFrequency / (REPLAYER_MIN_BPM / 2.5));
+ const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+ const uint32_t maxSamplesToMix = (int32_t)ceil(paulaMixFrequency / (REPLAYER_MIN_BPM / 2.5));
dMixBufferL = (double *)malloc((maxSamplesToMix + 1) * sizeof (double));
dMixBufferR = (double *)malloc((maxSamplesToMix + 1) * sizeof (double));
@@ -519,12 +506,10 @@
return false;
}
- paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
audioSetStereoSeparation(config.stereoSeparation);
updateReplayerTimingMode(); // also generates the BPM table (audio.bpmTable)
- setAmigaFilterModel(config.filterModel);
setLEDFilter(false);
- setupAmigaFilters(audio.outputRate);
calcAudioLatencyVars(audio.audioBufferSize, audio.outputRate);
clearMixerDownsamplerStates();
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -6,12 +6,6 @@
// for the low-pass/high-pass filters in the SAMPLER screen
#define FILTERS_BASE_FREQ (PAULA_PAL_CLK / 214.0)
-enum
-{
- AUDIO_NO_OVERSAMPLING = 0,
- AUDIO_2X_OVERSAMPLING = 1
-};
-
typedef struct audio_t
{
volatile bool locked, isSampling;
@@ -18,7 +12,7 @@
bool ledFilterEnabled, oversamplingFlag;
- uint32_t outputRate, audioBufferSize;
+ uint32_t amigaModel, outputRate, audioBufferSize;
int64_t tickSampleCounter64, samplesPerTick64;
int64_t samplesPerTickTable[256-32]; // 32.32 fixed-point
@@ -29,6 +23,11 @@
bool resetSyncTickTimeFlag;
uint64_t tickLengthTable[224];
} audio_t;
+
+void setAmigaFilterModel(uint8_t model);
+void toggleAmigaFilterModel(void);
+void setLEDFilter(bool state);
+void toggleLEDFilter(void);
void updateReplayerTimingMode(void);
void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac);
--- a/src/pt2_config.c
+++ b/src/pt2_config.c
@@ -43,7 +43,7 @@
config.fullScreenStretch = false;
config.pattDots = false;
config.waveformCenterLine = true;
- config.filterModel = FILTERMODEL_A1200;
+ config.amigaModel = MODEL_A1200;
config.soundFrequency = 48000;
config.rememberPlayMode = false;
config.stereoSeparation = 20;
@@ -65,9 +65,7 @@
config.audioInputFrequency = 44100;
config.mod2WavOutputFreq = 44100;
config.keepEditModeAfterStepPlay = false;
-
config.maxSampleLength = 65534;
- config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
#ifndef _WIN32
getcwd(oldCwd, PATH_MAX);
@@ -205,15 +203,9 @@
else if (!_strnicmp(configLine, "64K_LIMIT=", 10))
{
if (!_strnicmp(&configLine[10], "TRUE", 4))
- {
config.maxSampleLength = 65534;
- config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
- }
else if (!_strnicmp(&configLine[10], "FALSE", 5))
- {
config.maxSampleLength = 131070;
- config.reservedSampleOffset = (MOD_SAMPLES+1) * config.maxSampleLength;
- }
}
// NO_DWNSMP_ON_SMP_LOAD (no dialog for 2x downsample after >22kHz sample load)
@@ -407,22 +399,15 @@
// FILTERMODEL
else if (!_strnicmp(configLine, "FILTERMODEL=", 12))
{
- if (!_strnicmp(&configLine[12], "A500", 4)) config.filterModel = FILTERMODEL_A500;
- else if (!_strnicmp(&configLine[12], "A1200", 5)) config.filterModel = FILTERMODEL_A1200;
+ if (!_strnicmp(&configLine[12], "A500", 4)) config.amigaModel = MODEL_A500;
+ else if (!_strnicmp(&configLine[12], "A1200", 5)) config.amigaModel = MODEL_A1200;
}
// A500LOWPASSFILTER (deprecated, same as A4000LOWPASSFILTER)
else if (!_strnicmp(configLine, "A500LOWPASSFILTER=", 18))
{
- if (!_strnicmp(&configLine[18], "TRUE", 4)) config.filterModel = FILTERMODEL_A500;
- else if (!_strnicmp(&configLine[18], "FALSE", 5)) config.filterModel = FILTERMODEL_A1200;
- }
-
- // A4000LOWPASSFILTER (deprecated)
- else if (!_strnicmp(configLine, "A4000LOWPASSFILTER=", 19))
- {
- if (!_strnicmp(&configLine[19], "TRUE", 4)) config.filterModel = FILTERMODEL_A500;
- else if (!_strnicmp(&configLine[19], "FALSE", 5)) config.filterModel = FILTERMODEL_A1200;
+ if (!_strnicmp(&configLine[18], "TRUE", 4)) config.amigaModel = MODEL_A500;
+ else if (!_strnicmp(&configLine[18], "FALSE", 5)) config.amigaModel = MODEL_A1200;
}
// SAMPLINGFREQ
--- a/src/pt2_config.h
+++ b/src/pt2_config.h
@@ -17,10 +17,10 @@
bool transDel, fullScreenStretch, vsyncOff, modDot, blankZeroFlag, realVuMeters, rememberPlayMode;
bool startInFullscreen, integerScaling, disableE8xEffect, noDownsampleOnSmpLoad, keepEditModeAfterStepPlay;
int8_t stereoSeparation, videoScaleFactor, accidental;
- uint8_t pixelFilter, filterModel;
+ uint8_t pixelFilter, amigaModel;
uint16_t quantizeValue;
int32_t maxSampleLength;
- uint32_t soundFrequency, soundBufferSize, audioInputFrequency, mod2WavOutputFreq, reservedSampleOffset;
+ uint32_t soundFrequency, soundBufferSize, audioInputFrequency, mod2WavOutputFreq;
} config_t;
extern config_t config; // pt2_config.c
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -25,6 +25,7 @@
#include "pt2_chordmaker.h"
#include "pt2_edit.h"
#include "pt2_replayer.h"
+#include "pt2_visuals_sync.h"
static const int8_t scancode2NoteLo[52] = // "USB usage page standard" order
{
@@ -731,20 +732,36 @@
if (ch->n_length == 0)
ch->n_length = 1;
- paulaSetVolume(chNum, ch->n_volume);
- paulaSetPeriod(chNum, ch->n_period);
- paulaSetData(chNum, ch->n_start);
- paulaSetLength(chNum, ch->n_length);
+ const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
+ paulaWriteWord(voiceAddr + 8, ch->n_volume);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ paulaWritePtr(voiceAddr + 0, ch->n_start);
+ paulaWriteWord(voiceAddr + 4, ch->n_length);
if (!editor.muted[chNum])
- paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+ paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
else
- paulaSetDMACON(ch->n_dmabit); // voice DMA off
+ paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
// these take effect after the current DMA cycle is done
- paulaSetData(chNum, ch->n_loopstart);
- paulaSetLength(chNum, ch->n_replen);
+ paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+ paulaWriteWord(voiceAddr + 4, ch->n_replen);
+ // update tracker visuals
+
+ setVisualsVolume(chNum, ch->n_volume);
+ setVisualsPeriod(chNum, ch->n_period);
+ setVisualsDataPtr(chNum, ch->n_start);
+ setVisualsLength(chNum, ch->n_length);
+
+ if (!editor.muted[chNum])
+ setVisualsDMACON(0x8000 | ch->n_dmabit);
+ else
+ setVisualsDMACON(ch->n_dmabit);
+
+ setVisualsDataPtr(chNum, ch->n_loopstart);
+ setVisualsLength(chNum, ch->n_replen);
+
unlockAudio();
}
@@ -1059,19 +1076,35 @@
ch->n_samplenum = editor.currSample; // needed for sample playback/sampling line
- paulaSetVolume(chNum, vol);
- paulaSetPeriod(chNum, period);
- paulaSetData(chNum, n_start);
- paulaSetLength(chNum, n_length);
+ const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
+ paulaWriteWord(voiceAddr + 8, vol);
+ paulaWriteWord(voiceAddr + 6, period);
+ paulaWritePtr(voiceAddr + 0, n_start);
+ paulaWriteWord(voiceAddr + 4, n_length);
if (!editor.muted[chNum])
- paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+ paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
else
- paulaSetDMACON(ch->n_dmabit); // voice DMA off
+ paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
// these take effect after the current DMA cycle is done
- paulaSetData(chNum, NULL); // NULL = reserved buffer (empty)
- paulaSetLength(chNum, 1);
+ paulaWritePtr(voiceAddr + 0, NULL); // data
+ paulaWriteWord(voiceAddr + 4, 1); // length
+
+ // update tracker visuals
+
+ setVisualsVolume(chNum, vol);
+ setVisualsPeriod(chNum, period);
+ setVisualsDataPtr(chNum, n_start);
+ setVisualsLength(chNum, n_length);
+
+ if (!editor.muted[chNum])
+ setVisualsDMACON(0x8000 | ch->n_dmabit);
+ else
+ setVisualsDMACON(ch->n_dmabit);
+
+ setVisualsDataPtr(chNum, NULL);
+ setVisualsLength(chNum, 1);
unlockAudio();
}
--- a/src/pt2_header.h
+++ b/src/pt2_header.h
@@ -14,7 +14,7 @@
#include "pt2_unicode.h"
#include "pt2_palette.h"
-#define PROG_VER_STR "1.53"
+#define PROG_VER_STR "1.54"
#ifdef _WIN32
#define DIR_DELIMITER '\\'
@@ -90,10 +90,6 @@
TEMPFLAG_START = 1,
TEMPFLAG_DELAY = 2,
-
- FILTERMODEL_A1200 = 0,
- FILTERMODEL_A500 = 1,
- FILTER_LED_ENABLED = 2,
NO_CARRY = 0,
DO_CARRY = 1,
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -19,7 +19,6 @@
#include "pt2_edit.h"
#include "pt2_sampler.h"
#include "pt2_audio.h"
-#include "pt2_amigafilters.h"
#include "pt2_tables.h"
#include "pt2_module_saver.h"
#include "pt2_sample_saver.h"
@@ -2264,17 +2263,12 @@
}
else if (keyb.leftCtrlPressed)
{
- editor.useLEDFilter ^= 1;
- if (editor.useLEDFilter)
- {
- setLEDFilter(true);
+ toggleLEDFilter();
+
+ if (audio.ledFilterEnabled)
displayMsg("LED FILTER ON");
- }
else
- {
- setLEDFilter(false);
displayMsg("LED FILTER OFF");
- }
}
else if (keyb.leftAltPressed)
{
--- /dev/null
+++ b/src/pt2_ledfilter.c
@@ -1,0 +1,44 @@
+// Crude Amiga "LED" filter implementation
+
+#include "pt2_ledfilter.h"
+#include "pt2_math.h"
+
+void clearLEDFilterState(ledFilter_t *f)
+{
+ f->LIn1 = f->LIn2 = f->LOut1 = f->LOut2 = 0.0;
+ f->RIn1 = f->RIn2 = f->ROut1 = f->ROut2 = 0.0;
+}
+
+void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter)
+{
+ const double c = 1.0 / pt2_tan((PT2_PI * hz) / sr);
+ const double r = 1.0 / qfactor;
+
+ filter->a1 = 1.0 / (1.0 + r * c + c * c);
+ filter->a2 = 2.0 * filter->a1;
+ filter->a3 = filter->a1;
+ filter->b1 = 2.0 * (1.0 - c*c) * filter->a1;
+ filter->b2 = (1.0 - r * c + c * c) * filter->a1;
+}
+
+void LEDFilter(ledFilter_t *f, const double *in, double *out)
+{
+ const double LOut = (f->a1 * in[0]) + (f->a2 * f->LIn1) + (f->a3 * f->LIn2) - (f->b1 * f->LOut1) - (f->b2 * f->LOut2);
+ const double ROut = (f->a1 * in[1]) + (f->a2 * f->RIn1) + (f->a3 * f->RIn2) - (f->b1 * f->ROut1) - (f->b2 * f->ROut2);
+
+ // shift states
+
+ f->LIn2 = f->LIn1;
+ f->LIn1 = in[0];
+ f->LOut2 = f->LOut1;
+ f->LOut1 = LOut;
+
+ f->RIn2 = f->RIn1;
+ f->RIn1 = in[1];
+ f->ROut2 = f->ROut1;
+ f->ROut1 = ROut;
+
+ // set output
+ out[0] = LOut;
+ out[1] = ROut;
+}
--- /dev/null
+++ b/src/pt2_ledfilter.h
@@ -1,0 +1,15 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+
+typedef struct ledFilter_t
+{
+ double LIn1, LIn2, LOut1, LOut2;
+ double RIn1, RIn2, ROut1, ROut2;
+ double a1, a2, a3, b1, b2;
+} ledFilter_t;
+
+void clearLEDFilterState(ledFilter_t *f);
+void calcLEDFilterCoeffs(double sr, double hz, double qfactor, ledFilter_t *filter);
+void LEDFilter(ledFilter_t *f, const double *in, double *out);
--- a/src/pt2_main.c
+++ b/src/pt2_main.c
@@ -31,7 +31,7 @@
#include "pt2_scopes.h"
#include "pt2_audio.h"
#include "pt2_bmp.h"
-#include "pt2_sync.h"
+#include "pt2_visuals_sync.h"
#include "pt2_sampling.h"
#include "pt2_askbox.h"
#include "pt2_replayer.h"
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -8,7 +8,6 @@
#include <stdbool.h>
#include <sys/stat.h> // stat()
#include "pt2_audio.h"
-#include "pt2_amigafilters.h"
#include "pt2_mouse.h"
#include "pt2_textout.h"
#include "pt2_visuals.h"
@@ -135,8 +134,8 @@
static void resetAudio(void)
{
- setupAmigaFilters(audio.outputRate);
- paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+ const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
generateBpmTable(audio.outputRate, editor.timingMode == TEMPO_MODE_VBLANK);
clearMixerDownsamplerStates();
modSetTempo(song->currBPM, true); // update BPM (samples per tick) with the tracker's audio frequency
@@ -352,8 +351,8 @@
strncpy(lastFilename, filename, PATH_MAX-1);
- const int32_t audioFrequency = config.mod2WavOutputFreq * 2; // *2 for oversampling
- const uint32_t maxSamplesToMix = (int32_t)ceil(audioFrequency / (REPLAYER_MIN_BPM / 2.5));
+ const int32_t paulaMixFrequency = config.mod2WavOutputFreq * 2; // *2 for oversampling
+ const uint32_t maxSamplesToMix = (int32_t)ceil(paulaMixFrequency / (REPLAYER_MIN_BPM / 2.5));
mod2WavBuffer = (int16_t *)malloc(((TICKS_PER_RENDER_CHUNK * maxSamplesToMix) + 1) * sizeof (int16_t) * 2);
if (mod2WavBuffer == NULL)
@@ -367,8 +366,7 @@
// do some prep work
generateBpmTable(config.mod2WavOutputFreq, editor.timingMode == TEMPO_MODE_VBLANK);
- setupAmigaFilters(config.mod2WavOutputFreq);
- paulaSetOutputFrequency(config.mod2WavOutputFreq, AUDIO_2X_OVERSAMPLING);
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
storeTempVariables();
calcMod2WavTotalRows();
restartSong(); // this also updates BPM (samples per tick) with the MOD2WAV audio output rate
--- a/src/pt2_module_loader.c
+++ b/src/pt2_module_loader.c
@@ -20,7 +20,6 @@
#include "pt2_replayer.h"
#include "pt2_textout.h"
#include "pt2_audio.h"
-#include "pt2_amigafilters.h"
#include "pt2_helpers.h"
#include "pt2_visuals.h"
#include "pt2_sample_loader.h"
@@ -460,6 +459,10 @@
note->command = bytes[2] & 0x0F;
note->param = bytes[3];
+ // added sanitation not present in original PT
+ if (note->sample > 31)
+ note->sample = 0;
+
if (modFormat == FORMAT_STK)
{
if (note->command == 0xC || note->command == 0xD || note->command == 0xE)
@@ -930,7 +933,6 @@
editor.hiLowInstr = 0;
// disable LED filter after module load (real PT doesn't do this)
- editor.useLEDFilter = false;
setLEDFilter(false);
updateWindowTitle(MOD_NOT_MODIFIED);
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -246,7 +246,8 @@
// do some prep work
generateBpmTable(dPat2SmpFreq, editor.timingMode == TEMPO_MODE_VBLANK);
- paulaSetOutputFrequency(dPat2SmpFreq, AUDIO_2X_OVERSAMPLING);
+ paulaSetup(dPat2SmpFreq*2.0, MODEL_A1200);
+ paulaDisableFilters();
storeTempVariables();
restartSong(); // this also updates BPM (samples per tick) with the PAT2SMP audio output rate
clearMixerDownsamplerStates();
@@ -289,7 +290,8 @@
song->currRow = song->row = oldRow; // set back old row
// set back audio configurations
- paulaSetOutputFrequency(audio.outputRate, audio.oversamplingFlag);
+ const int32_t paulaMixFrequency = audio.oversamplingFlag ? audio.outputRate*2 : audio.outputRate;
+ paulaSetup(paulaMixFrequency, audio.amigaModel);
generateBpmTable(audio.outputRate, editor.timingMode == TEMPO_MODE_VBLANK);
clearMixerDownsamplerStates();
resetSong(); // this also updates BPM (samples per tick) with the tracker's audio output rate
--- a/src/pt2_paula.c
+++ b/src/pt2_paula.c
@@ -1,39 +1,176 @@
/* 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) )
+**
+** WARNING: These functions must not be called while paulaGenerateSamples() is running!
+** If so, lock the audio first so that you're sure it's not running.
*/
#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"
+#include "pt2_rcfilter.h"
+#include "pt2_ledfilter.h"
+#include "pt2_math.h"
-paulaVoice_t paula[PAULA_VOICES]; // globalized
+typedef struct voice_t
+{
+ volatile bool DMA_active;
+ // internal registers
+ bool DMATriggerFlag, nextSampleStage;
+ int8_t AUD_DAT[2]; // DMA data buffer
+ const int8_t *location; // current location
+ uint16_t lengthCounter; // current length
+ int32_t sampleCounter; // how many bytes left in AUD_DAT
+ double dSample; // currently held sample point (multiplied by volume)
+ double dDelta, dPhase;
+
+ // for BLEP synthesis
+ double dLastDelta, dLastPhase, dLastDeltaMul, dBlepOffset, dDeltaMul;
+
+ // registers modified by Paula functions
+ const int8_t *AUD_LC; // location (data pointer)
+ uint16_t AUD_LEN;
+ double AUD_PER_delta, AUD_PER_deltamul;
+ double AUD_VOL;
+} paulaVoice_t;
+
+static bool useLEDFilter, useLowpassFilter, useHighpassFilter;
+static int8_t nullSample[0xFFFF*2]; // buffer for NULL data pointer
+static double dPaulaOutputFreq, dPeriodToDeltaDiv;
static blep_t blep[PAULA_VOICES];
-static double dPeriodToDeltaDiv;
+static rcFilter_t filterLo, filterHi;
+static ledFilter_t filterLED;
+static paulaVoice_t paula[PAULA_VOICES];
-void paulaSetOutputFrequency(double dAudioFreq, bool oversampling2x)
+void paulaSetup(double dOutputFreq, uint32_t amigaModel)
{
- dPeriodToDeltaDiv = PAULA_PAL_CLK / dAudioFreq;
- if (oversampling2x)
- dPeriodToDeltaDiv /= 2.0;
+ if (dOutputFreq <= 0.0)
+ dOutputFreq = 44100.0;
+
+ dPaulaOutputFreq = dOutputFreq;
+ dPeriodToDeltaDiv = PAULA_PAL_CLK / dPaulaOutputFreq;
+
+ useLowpassFilter = useHighpassFilter = true;
+ clearRCFilterState(&filterLo);
+ clearRCFilterState(&filterHi);
+ clearLEDFilterState(&filterLED);
+
+ /* Amiga 500/1200 filter emulation
+ **
+ ** aciddose:
+ ** First comes a static low-pass 6dB formed by the supply current
+ ** from the Paula's mixture of channels A+B / C+D into the opamp with
+ ** 0.1uF capacitor and 360 ohm resistor feedback in inverting mode biased by
+ ** dac vRef (used to center the output).
+ **
+ ** R = 360 ohm
+ ** C = 0.1uF
+ ** Low Hz = 4420.97~ = 1 / (2pi * 360 * 0.0000001)
+ **
+ ** Under spice simulation the circuit yields -3dB = 4400Hz.
+ ** In the Amiga 1200, the low-pass cutoff is ~34kHz, so the
+ ** static low-pass filter is disabled in the mixer in A1200 mode.
+ **
+ ** Next comes a bog-standard Sallen-Key filter ("LED") with:
+ ** R1 = 10K ohm
+ ** R2 = 10K ohm
+ ** C1 = 6800pF
+ ** C2 = 3900pF
+ ** Q ~= 1/sqrt(2)
+ **
+ ** This filter is optionally bypassed by an MPF-102 JFET chip when
+ ** the LED filter is turned off.
+ **
+ ** Under spice simulation the circuit yields -3dB = 2800Hz.
+ ** 90 degrees phase = 3000Hz (so, should oscillate at 3kHz!)
+ **
+ ** The buffered output of the Sallen-Key passes into an RC high-pass with:
+ ** R = 1.39K ohm (1K ohm + 390 ohm)
+ ** C = 22uF (also C = 330nF, for improved high-frequency)
+ **
+ ** High Hz = 5.2~ = 1 / (2pi * 1390 * 0.000022)
+ ** Under spice simulation the circuit yields -3dB = 5.2Hz.
+ **
+ ** 8bitbubsy:
+ ** Keep in mind that many of the Amiga schematics that are floating around on
+ ** the internet have wrong RC values! They were most likely very early schematics
+ ** that didn't change before production (or changes that never reached production).
+ ** This has been confirmed by measuring the components on several Amiga motherboards.
+ **
+ ** Correct values for A500, >rev3 (?) (A500_R6.pdf):
+ ** - 1-pole RC 6dB/oct low-pass: R=360 ohm, C=0.1uF
+ ** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF
+ ** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22.33uF (22+0.33)
+ **
+ ** Correct values for A1200, all revs (A1200_R2.pdf):
+ ** - 1-pole RC 6dB/oct low-pass: R=680 ohm, C=6800pF
+ ** - Sallen-key low-pass ("LED"): R1/R2=10k ohm, C1=6800pF, C2=3900pF (same as A500)
+ ** - 1-pole RC 6dB/oct high-pass: R=1390 ohm (1000+390), C=22uF
+ */
+ double R, C, R1, R2, C1, C2, cutoff, qfactor;
+
+ if (amigaModel == MODEL_A500)
+ {
+ // A500 1-pole (6db/oct) static RC low-pass filter:
+ R = 360.0; // R321 (360 ohm)
+ C = 1e-7; // C321 (0.1uF)
+ cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~4420.971Hz
+ calcRCFilterCoeffs(dPaulaOutputFreq, cutoff, &filterLo);
+
+ // A500 1-pole (6dB/oct) static RC high-pass filter:
+ R = 1390.0; // R324 (1K ohm) + R325 (390 ohm)
+ C = 2.233e-5; // C334 (22uF) + C335 (0.33uF)
+ cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.128Hz
+ calcRCFilterCoeffs(dPaulaOutputFreq, cutoff, &filterHi);
+ }
+ else
+ {
+ /* Don't use the A1200 low-pass filter since its cutoff
+ ** is well above human hearable range anyway (~34.4kHz).
+ ** We don't do volume PWM, so we have nothing we need to
+ ** filter away.
+ */
+ useLowpassFilter = false;
+
+ // A1200 1-pole (6dB/oct) static RC high-pass filter:
+ R = 1390.0; // R324 (1K ohm resistor) + R325 (390 ohm resistor)
+ C = 2.2e-5; // C334 (22uF capacitor)
+ cutoff = 1.0 / (PT2_TWO_PI * R * C); // ~5.205Hz
+ calcRCFilterCoeffs(dPaulaOutputFreq, cutoff, &filterHi);
+ }
+
+ // Sallen-Key low-pass filter ("LED" filter, same values on A500/A1200):
+ R1 = 10000.0; // R322 (10K ohm)
+ R2 = 10000.0; // R323 (10K ohm)
+ C1 = 6.8e-9; // C322 (6800pF)
+ C2 = 3.9e-9; // C323 (3900pF)
+ cutoff = 1.0 / (PT2_TWO_PI * pt2_sqrt(R1 * R2 * C1 * C2)); // ~3090.533Hz
+ qfactor = pt2_sqrt(R1 * R2 * C1 * C2) / (C2 * (R1 + R2)); // ~0.660225
+ calcLEDFilterCoeffs(dPaulaOutputFreq, cutoff, qfactor, &filterLED);
}
-void paulaSetPeriod(int32_t ch, uint16_t period)
+void paulaDisableFilters(void) // disables low-pass/high-pass filter ("LED" filter is kept)
{
+ useHighpassFilter = false;
+ useLowpassFilter = false;
+}
+
+int8_t *paulaGetNullSamplePtr(void)
+{
+ return nullSample;
+}
+
+static void audxper(int32_t ch, uint16_t period)
+{
paulaVoice_t *v = &paula[ch];
int32_t realPeriod = period;
if (realPeriod == 0)
- realPeriod = 65535; // On Amiga: period 0 = one full cycle with period 65536, then period 65535 for the rest
+ realPeriod = 65536; // On Amiga: period 0 = period 65536 (1+65535)
else if (realPeriod < 113)
- realPeriod = 113; // close to what happens on real Amiga (and needed for BLEP synthesis)
+ realPeriod = 113; // close to what happens on real Amiga (and low-limit needed for BLEP synthesis)
// to be read on next sampling step (or on DMA trigger)
v->AUD_PER_delta = dPeriodToDeltaDiv / realPeriod;
@@ -46,74 +183,29 @@
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)
+static void audxvol(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;
- }
+ // multiplying sample point by this also scales the sample from -128..127 -> -1.000 .. ~0.992
+ paula[ch].AUD_VOL = realVol * (1.0 / (128.0 * 64.0));
}
-void paulaSetLength(int32_t ch, uint16_t len)
+static void audxlen(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;
+ paula[ch].AUD_LEN = len;
}
-void paulaSetData(int32_t ch, const int8_t *src)
+static void audxdat(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];
+ src = nullSample;
- v->AUD_LC = src;
-
- // handle visualizers
-
- if (editor.songPlaying)
- v->syncFlags |= SET_SCOPE_DATA;
- else
- scope[ch].newData = src;
+ paula[ch].AUD_LC = src;
}
static inline void refetchPeriod(paulaVoice_t *v) // Paula stage
@@ -131,13 +223,12 @@
v->nextSampleStage = true;
}
-static void startPaulaDMA(int32_t ch)
+static void startDMA(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];
+ v->AUD_LC = nullSample;
// immediately update AUD_LC/AUD_LEN
v->location = v->AUD_LC;
@@ -151,56 +242,108 @@
v->dPhase = 0.0; // kludge: must be cleared *after* refetchPeriod()
v->DMA_active = true;
+}
- // handle visualizers
+static void stopDMA(int32_t ch)
+{
+ paula[ch].DMA_active = false;
+}
- if (editor.songPlaying)
+void paulaWriteByte(uint32_t address, uint16_t data8)
+{
+ if (address == 0)
+ return;
+
+ switch (address)
{
- v->syncTriggerData = v->AUD_LC;
- v->syncTriggerLength = v->AUD_LEN * 2;
- v->syncFlags |= TRIGGER_SCOPE;
+ // CIA-A ("LED" filter control only)
+ case 0xBFE001:
+ {
+ const bool oldLedFilterState = useLEDFilter;
+
+ useLEDFilter = !!(data8 & 2);
+
+ if (useLEDFilter != oldLedFilterState)
+ clearLEDFilterState(&filterLED);
+ }
+ break;
+
+ // AUDxVOL ( byte-write to AUDxVOL works on most Amigas (not 68040/68060) )
+ case 0xDFF0A8: audxvol(0, data8); break;
+ case 0xDFF0B8: audxvol(1, data8); break;
+ case 0xDFF0C8: audxvol(2, data8); break;
+ case 0xDFF0D8: audxvol(3, data8); break;
+
+ default: return;
}
- 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)
+void paulaWriteWord(uint32_t address, uint16_t data16)
{
- paulaVoice_t *v = &paula[ch];
+ if (address == 0)
+ return;
- v->DMA_active = false;
+ switch (address)
+ {
+ // DMACON
+ case 0xDFF096:
+ {
+ if (data16 & 0x8000)
+ {
+ // set
+ if (data16 & 1) startDMA(0);
+ if (data16 & 2) startDMA(1);
+ if (data16 & 4) startDMA(2);
+ if (data16 & 8) startDMA(3);
+ }
+ else
+ {
+ // clear
+ if (data16 & 1) stopDMA(0);
+ if (data16 & 2) stopDMA(1);
+ if (data16 & 4) stopDMA(2);
+ if (data16 & 8) stopDMA(3);
+ }
+ }
+ break;
- // handle visualizers
-
- if (editor.songPlaying)
- v->syncFlags |= STOP_SCOPE;
- else
- scope[ch].active = false;
+ // AUDxLEN
+ case 0xDFF0A4: audxlen(0, data16); break;
+ case 0xDFF0B4: audxlen(1, data16); break;
+ case 0xDFF0C4: audxlen(2, data16); break;
+ case 0xDFF0D4: audxlen(3, data16); break;
+
+ // AUDxPER
+ case 0xDFF0A6: audxper(0, data16); break;
+ case 0xDFF0B6: audxper(1, data16); break;
+ case 0xDFF0C6: audxper(2, data16); break;
+ case 0xDFF0D6: audxper(3, data16); break;
+
+ // AUDxVOL
+ case 0xDFF0A8: audxvol(0, data16); break;
+ case 0xDFF0B8: audxvol(1, data16); break;
+ case 0xDFF0C8: audxvol(2, data16); break;
+ case 0xDFF0D8: audxvol(3, data16); break;
+
+ default: return;
+ }
}
-void paulaSetDMACON(uint16_t bits) // $DFF096 register write (only controls paula DMAs)
+void paulaWritePtr(uint32_t address, const void *ptr)
{
- if (bits & 0x8000)
+ if (address == 0)
+ return;
+
+ switch (address)
{
- // set
- if (bits & 1) startPaulaDMA(0);
- if (bits & 2) startPaulaDMA(1);
- if (bits & 4) startPaulaDMA(2);
- if (bits & 8) startPaulaDMA(3);
+ // AUDxDAT
+ case 0xDFF0A0: audxdat(0, ptr); break;
+ case 0xDFF0B0: audxdat(1, ptr); break;
+ case 0xDFF0C0: audxdat(2, ptr); break;
+ case 0xDFF0D0: audxdat(3, ptr); break;
+
+ default: return;
}
- 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)
@@ -248,13 +391,20 @@
v->sampleCounter--;
}
+// output is -4.00 .. 3.97 (can be louder because of high-pass filter)
void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples)
{
double *dMixBufSelect[PAULA_VOICES] = { dOutL, dOutR, dOutR, dOutL };
+ if (numSamples <= 0)
+ return;
+
+ // clear mix buffer block
memset(dOutL, 0, numSamples * sizeof (double));
memset(dOutR, 0, numSamples * sizeof (double));
+ // mix samples
+
paulaVoice_t *v = paula;
blep_t *b = blep;
@@ -269,10 +419,10 @@
if (v->nextSampleStage)
{
v->nextSampleStage = false;
- nextSample(v, b); // inlined
+ nextSample(v, b);
}
- double dSample = v->dSample; // current Paula sample, pre-multiplied by volume, scaled to -1.0 .. 0.9921875
+ double dSample = v->dSample; // current sample, pre-multiplied by vol, scaled to -1.0 .. 0.992
if (b->samplesLeft > 0)
dSample = blepRun(b, dSample);
@@ -282,8 +432,26 @@
if (v->dPhase >= 1.0)
{
v->dPhase -= 1.0;
- refetchPeriod(v); // inlined
+ refetchPeriod(v);
}
}
+ }
+
+ // apply Amiga filters
+ for (int32_t i = 0; i < numSamples; i++)
+ {
+ double dOut[2] = { dOutL[i], dOutR[i] };
+
+ if (useLowpassFilter)
+ RCLowPassFilterStereo(&filterLo, dOut, dOut);
+
+ if (useLEDFilter)
+ LEDFilter(&filterLED, dOut, dOut);
+
+ if (useHighpassFilter)
+ RCHighPassFilterStereo(&filterHi, dOut, dOut);
+
+ dOutL[i] = dOut[0];
+ dOutR[i] = dOut[1];
}
}
--- a/src/pt2_paula.h
+++ b/src/pt2_paula.h
@@ -4,37 +4,12 @@
#include <stdbool.h>
#include "pt2_header.h"
-typedef struct voice_t
+enum
{
- volatile bool DMA_active;
+ MODEL_A1200 = 0,
+ MODEL_A500 = 1,
+};
- // internal values (don't modify directly!)
- bool DMATriggerFlag, nextSampleStage;
- int8_t AUD_DAT[2]; // DMA data buffer
- const int8_t *location; // current location
- uint16_t lengthCounter; // current length
- int32_t sampleCounter; // how many bytes left in AUD_DAT
- double dSample; // current sample point
-
- // registers modified by Paula functions
- const int8_t *AUD_LC; // location
- uint16_t AUD_LEN; // length (in words)
- double AUD_PER_delta, AUD_PER_deltamul; // delta
- double AUD_VOL; // volume
-
- double dDelta, dPhase;
-
- // for BLEP synthesis
- double dLastDelta, dLastPhase, dLastDeltaMul, dBlepOffset, dDeltaMul;
-
- // used for pt2_sync.c (visualizers)
- uint8_t syncFlags;
- uint8_t syncVolume;
- int32_t syncPeriod;
- int32_t syncTriggerLength;
- const int8_t *syncTriggerData;
-} paulaVoice_t;
-
#define PAULA_VOICES 4
#define PAULA_PAL_CLK AMIGA_PAL_CCK_HZ
#define PAL_PAULA_MIN_PERIOD 113
@@ -42,12 +17,14 @@
#define PAL_PAULA_MAX_HZ (PAULA_PAL_CLK / (double)PAL_PAULA_MIN_PERIOD)
#define PAL_PAULA_MAX_SAFE_HZ (PAULA_PAL_CLK / (double)PAL_PAULA_MIN_SAFE_PERIOD)
-void paulaSetOutputFrequency(double dAudioFreq, bool oversampling2x);
-void paulaSetDMACON(uint16_t bits); // $DFF096 register write (only controls paula DMA)
-void paulaSetPeriod(int32_t ch, uint16_t period);
-void paulaSetVolume(int32_t ch, uint16_t vol);
-void paulaSetLength(int32_t ch, uint16_t len);
-void paulaSetData(int32_t ch, const int8_t *src);
-void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples);
+void paulaSetup(double dOutputFreq, uint32_t amigaModel);
+void paulaDisableFilters(void); // disables low-pass & high-pass filters ("LED" filter is kept)
-extern paulaVoice_t paula[PAULA_VOICES]; // pt2_paula.c
+int8_t *paulaGetNullSamplePtr(void);
+
+void paulaWriteByte(uint32_t address, uint16_t data8);
+void paulaWriteWord(uint32_t address, uint16_t data16);
+void paulaWritePtr(uint32_t address, const void *ptr);
+
+// output is -4.00 .. 3.97 (can be louder because of high-pass filter)
+void paulaGenerateSamples(double *dOutL, double *dOutR, int32_t numSamples);
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -13,8 +13,8 @@
#include "pt2_config.h"
#include "pt2_visuals.h"
#include "pt2_scopes.h"
-#include "pt2_sync.h"
-#include "pt2_amigafilters.h"
+#include "pt2_paula.h"
+#include "pt2_visuals_sync.h"
static bool posJumpAssert, pBreakFlag, modRenderDone;
static bool doStopSong; // from F00 (Set Speed)
@@ -52,8 +52,12 @@
{
const moduleSample_t *s = &song->samples[editor.currSample];
- paulaSetData(i, ch->n_start + s->loopStart);
- paulaSetLength(i, (uint16_t)(s->loopLength >> 1));
+ const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+ paulaWritePtr(voiceAddr + 0, ch->n_start + s->loopStart);
+ paulaWriteWord(voiceAddr + 4, (uint16_t)(s->loopLength >> 1));
+
+ setVisualsDataPtr(i, ch->n_start + s->loopStart);
+ setVisualsLength(i, (uint16_t)(s->loopLength >> 1));
}
}
@@ -67,13 +71,19 @@
if (audioWasntLocked)
lockAudio();
- paulaSetDMACON(0x000F); // turn off all voice DMAs
+ paulaWriteWord(0xDFF096, 0x000F); // turn off all voice DMAs
+ setVisualsDMACON(0x000F);
+ // clear all volumes
for (int32_t i = 0; i < PAULA_VOICES; i++)
- paulaSetVolume(i, 0);
+ {
+ const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+ paulaWriteWord(voiceAddr + 8, 0);
+ setVisualsVolume(i, 0);
+ }
+
// reset dithering/filter states (so that every playback session is identical)
- resetAmigaFilterStates();
resetAudioDithering();
if (audioWasntLocked)
@@ -289,19 +299,28 @@
static void doRetrg(moduleChannel_t *ch)
{
- paulaSetDMACON(ch->n_dmabit); // voice DMA off
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
- paulaSetData(ch->n_chanindex, ch->n_start); // n_start is increased on 9xx
- paulaSetLength(ch->n_chanindex, ch->n_length);
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
+ paulaWritePtr(voiceAddr + 0, ch->n_start); // n_start is increased on 9xx
+ paulaWriteWord(voiceAddr + 4, ch->n_length);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
+
+ // these take effect after the current DMA cycle is done
+ paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+ paulaWriteWord(voiceAddr + 4, ch->n_replen);
- paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+ // update tracker visuals
- // these take effect after the current DMA cycle is done
- paulaSetData(ch->n_chanindex, ch->n_loopstart);
- paulaSetLength(ch->n_chanindex, ch->n_replen);
+ setVisualsDMACON(ch->n_dmabit);
+ setVisualsDataPtr(ch->n_chanindex, ch->n_start);
+ setVisualsLength(ch->n_chanindex, ch->n_length);
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
+ setVisualsDMACON(0x8000 | ch->n_dmabit);
- // set visuals
+ setVisualsDataPtr(ch->n_chanindex, ch->n_loopstart);
+ setVisualsLength(ch->n_chanindex, ch->n_replen);
ch->syncAnalyzerVolume = ch->n_volume;
ch->syncAnalyzerPeriod = ch->n_period;
@@ -434,6 +453,8 @@
{
int32_t arpNote;
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+
int32_t arpTick = song->tick % 3; // 0, 1, 2
if (arpTick == 1)
{
@@ -445,7 +466,8 @@
}
else // arpTick 0
{
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
return;
}
@@ -459,7 +481,8 @@
{
if (ch->n_period >= periods[baseNote])
{
- paulaSetPeriod(ch->n_chanindex, periods[baseNote+arpNote]);
+ paulaWriteWord(voiceAddr + 6, periods[baseNote+arpNote]);
+ setVisualsPeriod(ch->n_chanindex, periods[baseNote+arpNote]);
break;
}
}
@@ -473,7 +496,10 @@
if ((ch->n_period & 0xFFF) < 113) // PT BUG: sign removed before comparison, underflow not clamped!
ch->n_period = (ch->n_period & 0xF000) | 113;
- paulaSetPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, ch->n_period & 0xFFF);
+
+ setVisualsPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
}
static void portaDown(moduleChannel_t *ch)
@@ -484,7 +510,10 @@
if ((ch->n_period & 0xFFF) > 856)
ch->n_period = (ch->n_period & 0xF000) | 856;
- paulaSetPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, ch->n_period & 0xFFF);
+
+ setVisualsPeriod(ch->n_chanindex, ch->n_period & 0xFFF);
}
static void filterOnOff(moduleChannel_t *ch)
@@ -492,16 +521,8 @@
if (song->tick == 0) // added this (just pointless to call this during all ticks!)
{
const bool filterOn = (ch->n_cmd & 1) ^ 1;
- if (filterOn)
- {
- editor.useLEDFilter = true;
- setLEDFilter(true);
- }
- else
- {
- editor.useLEDFilter = false;
- setLEDFilter(false);
- }
+ paulaWriteByte(0xBFE001, filterOn << 1);
+ audio.ledFilterEnabled = filterOn;
}
}
@@ -576,9 +597,12 @@
}
}
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+
if ((ch->n_glissfunk & 0xF) == 0)
{
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
}
else
{
@@ -598,7 +622,8 @@
}
}
- paulaSetPeriod(ch->n_chanindex, portaPointer[i]);
+ paulaWriteWord(voiceAddr + 6, portaPointer[i]);
+ setVisualsPeriod(ch->n_chanindex, portaPointer[i]);
}
}
@@ -643,8 +668,11 @@
else
vibratoData = ch->n_period - vibratoData;
- paulaSetPeriod(ch->n_chanindex, vibratoData);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, vibratoData); // period
+ setVisualsPeriod(ch->n_chanindex, vibratoData);
+
ch->n_vibratopos += (ch->n_vibratocmd >> 2) & 0x3C;
}
@@ -715,8 +743,11 @@
tremoloData = 0;
}
- paulaSetVolume(ch->n_chanindex, tremoloData);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 8, tremoloData); // volume
+ setVisualsVolume(ch->n_chanindex, tremoloData);
+
ch->n_tremolopos += (ch->n_tremolocmd >> 2) & 0x3C;
}
@@ -794,7 +825,10 @@
return;
}
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
}
static void chkefx2(moduleChannel_t *ch)
@@ -818,8 +852,11 @@
default: break;
}
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
+
if (cmd == 0x7)
tremolo(ch);
else if (cmd == 0xA)
@@ -842,7 +879,12 @@
*/
const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
if (cmd != 0x7)
- paulaSetVolume(ch->n_chanindex, ch->n_volume);
+ {
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 8, ch->n_volume);
+
+ setVisualsVolume(ch->n_chanindex, ch->n_volume);
+ }
}
static void setPeriod(moduleChannel_t *ch)
@@ -862,24 +904,38 @@
if ((ch->n_cmd & 0xFF0) != 0xED0) // no note delay
{
- paulaSetDMACON(ch->n_dmabit); // voice DMA off (turned on in setDMA() later)
+ paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off (turned on in setDMA() later)
if ((ch->n_wavecontrol & 0x04) == 0) ch->n_vibratopos = 0;
if ((ch->n_wavecontrol & 0x40) == 0) ch->n_tremolopos = 0;
- paulaSetLength(ch->n_chanindex, ch->n_length);
- paulaSetData(ch->n_chanindex, ch->n_start);
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 4, ch->n_length);
+ paulaWritePtr(voiceAddr + 0, ch->n_start);
+
if (ch->n_start == NULL)
{
ch->n_loopstart = NULL;
- paulaSetLength(ch->n_chanindex, 1);
+ paulaWriteWord(voiceAddr + 4, 1); // length
ch->n_replen = 1;
}
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
DMACONtemp |= ch->n_dmabit;
+
+ // update tracker visuals
+
+ setVisualsDMACON(ch->n_dmabit);
+
+ setVisualsLength(ch->n_chanindex, ch->n_length);
+ setVisualsDataPtr(ch->n_chanindex, ch->n_start);
+
+ if (ch->n_start == NULL)
+ setVisualsLength(ch->n_chanindex, 1);
+
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
}
checkMoreEffects(ch);
@@ -889,7 +945,7 @@
{
if (editor.metroFlag && editor.metroChannel > 0)
{
- if (ch->n_chanindex == editor.metroChannel-1 && (song->row % editor.metroSpeed) == 0)
+ if (ch->n_chanindex == (uint32_t)editor.metroChannel-1 && (song->row % editor.metroSpeed) == 0)
{
note->sample = 31;
note->period = (((song->row / editor.metroSpeed) % editor.metroSpeed) == 0) ? 160 : 214;
@@ -900,8 +956,13 @@
static void playVoice(moduleChannel_t *ch)
{
if (ch->n_note == 0 && ch->n_cmd == 0) // test period, command and command parameter
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
+ {
+ const uint32_t voiceAddr = 0xDFF0A0 + (ch->n_chanindex * 16);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ setVisualsPeriod(ch->n_chanindex, ch->n_period);
+ }
+
note_t note = song->patterns[modPattern][(song->row * PAULA_VOICES) + ch->n_chanindex];
checkMetronome(ch, ¬e);
@@ -935,7 +996,7 @@
// non-PT2 requirement (set safe sample space for uninitialized voices - f.ex. "the ultimate beeper.mod")
if (ch->n_length == 0)
- ch->n_loopstart = ch->n_wavestart = &song->sampleData[config.reservedSampleOffset]; // 128K reserved sample
+ ch->n_loopstart = ch->n_wavestart = paulaGetNullSamplePtr();
}
if ((ch->n_note & 0xFFF) > 0)
@@ -1120,8 +1181,10 @@
if (editor.muted[2]) DMACONtemp &= ~4;
if (editor.muted[3]) DMACONtemp &= ~8;
- paulaSetDMACON(0x8000 | DMACONtemp);
+ paulaWriteWord(0xDFF096, 0x8000 | DMACONtemp); // start DMAs for selected voices
+ setVisualsDMACON(0x8000 | DMACONtemp);
+
moduleChannel_t *ch = song->channels;
for (int32_t i = 0; i < PAULA_VOICES; i++, ch++)
{
@@ -1135,8 +1198,12 @@
}
// these take effect after the current DMA cycle is done
- paulaSetData(i, ch->n_loopstart);
- paulaSetLength(i, ch->n_replen);
+ const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+ paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+ paulaWriteWord(voiceAddr + 4, ch->n_replen);
+
+ setVisualsDataPtr(i, ch->n_loopstart);
+ setVisualsLength(i, ch->n_replen);
}
}
@@ -1209,7 +1276,11 @@
for (int32_t i = 0; i < PAULA_VOICES; i++, ch++)
{
playVoice(ch);
- paulaSetVolume(i, ch->n_volume);
+
+ const uint32_t voiceAddr = 0xDFF0A0 + (i * 16);
+ paulaWriteWord(voiceAddr + 8, ch->n_volume);
+
+ setVisualsVolume(i, ch->n_volume);
}
setDMA();
@@ -1583,7 +1654,6 @@
modSetSpeed(editor.initialSpeed);
// disable the LED filter after clearing the song (real PT2 doesn't do this)
- editor.useLEDFilter = false;
setLEDFilter(false);
updateCurrSample();
--- a/src/pt2_sampler.c
+++ b/src/pt2_sampler.c
@@ -19,6 +19,7 @@
#include "pt2_rcfilter.h"
#include "pt2_chordmaker.h"
#include "pt2_replayer.h"
+#include "pt2_visuals_sync.h"
#define CENTER_LINE_COLOR 0x303030
#define MARK_COLOR_1 0x666666 /* inverted background */
@@ -1247,15 +1248,25 @@
lockAudio();
- paulaSetDMACON(TToneBit); // voice DMA off
+ const uint32_t voiceAddr = 0xDFF0A0 + (chNum * 16);
- paulaSetPeriod(chNum, periodTable[editor.tuningNote]);
- paulaSetVolume(chNum, 64);
- paulaSetData(chNum, tuneToneData);
- paulaSetLength(chNum, sizeof (tuneToneData) / 2);
+ paulaWriteWord(0xDFF096, TToneBit); // voice DMA off
- paulaSetDMACON(0x8000 | TToneBit); // voice DMA on
+ paulaWriteWord(voiceAddr + 6, periodTable[editor.tuningNote]);
+ paulaWriteWord(voiceAddr + 8, 64); // volume
+ paulaWritePtr(voiceAddr + 0, tuneToneData);
+ paulaWriteWord(voiceAddr + 4, sizeof (tuneToneData) / 2); // length
+ paulaWriteWord(0xDFF096, 0x8000 | TToneBit); // voice DMA on
+
+ // update tracker visuals
+ setVisualsDMACON(TToneBit);
+ setVisualsPeriod(chNum, periodTable[editor.tuningNote]);
+ setVisualsVolume(chNum, 64);
+ setVisualsDataPtr(chNum, tuneToneData);
+ setVisualsLength(chNum, sizeof (tuneToneData) / 2);
+ setVisualsDMACON(0x8000 | TToneBit);
+
unlockAudio();
}
else
@@ -1263,7 +1274,8 @@
// turn tuning tone off
lockAudio();
- paulaSetDMACON(TToneBit); // voice DMA off
+ paulaWriteWord(0xDFF096, TToneBit); // voice DMA off
+ setVisualsDMACON(TToneBit);
unlockAudio();
}
}
@@ -1772,26 +1784,51 @@
if (ch->n_length == 0)
ch->n_length = 1;
- paulaSetVolume(chn, ch->n_volume);
- paulaSetPeriod(chn, ch->n_period);
- paulaSetData(chn, ch->n_start);
- paulaSetLength(chn, ch->n_length);
+ const uint32_t voiceAddr = 0xDFF0A0 + (chn * 16);
+ paulaWriteWord(voiceAddr + 8, ch->n_volume);
+ paulaWriteWord(voiceAddr + 6, ch->n_period);
+ paulaWritePtr(voiceAddr + 0, ch->n_start);
+ paulaWriteWord(voiceAddr + 4, ch->n_length);
+
if (!editor.muted[chn])
- paulaSetDMACON(0x8000 | ch->n_dmabit); // voice DMA on
+ paulaWriteWord(0xDFF096, 0x8000 | ch->n_dmabit); // voice DMA on
else
- paulaSetDMACON(ch->n_dmabit); // voice DMA off
+ paulaWriteWord(0xDFF096, ch->n_dmabit); // voice DMA off
// these take effect after the current DMA cycle is done
if (playWaveformFlag)
{
- paulaSetData(chn, ch->n_loopstart);
- paulaSetLength(chn, ch->n_replen);
+ paulaWritePtr(voiceAddr + 0, ch->n_loopstart);
+ paulaWriteWord(voiceAddr + 4, ch->n_replen);
}
else
{
- paulaSetData(chn, NULL);
- paulaSetLength(chn, 1);
+ paulaWritePtr(voiceAddr + 0, NULL); // data
+ paulaWriteWord(voiceAddr + 4, 1); // length
+ }
+
+ // update tracker visuals
+
+ setVisualsVolume(chn, ch->n_volume);
+ setVisualsPeriod(chn, ch->n_period);
+ setVisualsDataPtr(chn, ch->n_start);
+ setVisualsLength(chn, ch->n_length);
+
+ if (!editor.muted[chn])
+ setVisualsDMACON(0x8000 | ch->n_dmabit);
+ else
+ setVisualsDMACON(ch->n_dmabit);
+
+ if (playWaveformFlag)
+ {
+ setVisualsDataPtr(chn, ch->n_loopstart);
+ setVisualsLength(chn, ch->n_replen);
+ }
+ else
+ {
+ setVisualsDataPtr(chn, NULL);
+ setVisualsLength(chn, 1);
}
unlockAudio();
--- a/src/pt2_scopes.c
+++ b/src/pt2_scopes.c
@@ -88,23 +88,19 @@
scope[ch].dDelta = (PAULA_PAL_CLK / (double)SCOPE_HZ) / period;
}
-void scopeTrigger(int32_t ch)
+void scopeTrigger(int32_t ch) // expects data & length variables to be set already
{
volatile scope_t *sc = &scope[ch];
scope_t tempState = *sc; // cache it
- const int8_t *newData = tempState.newData;
- if (newData == NULL)
- newData = &song->sampleData[config.reservedSampleOffset]; // 128K reserved sample
+ if (tempState.data == NULL)
+ tempState.data = paulaGetNullSamplePtr();
- int32_t newLength = tempState.newLength; // in bytes, not words
- if (newLength < 2)
- newLength = 2; // for safety
+ if (tempState.length < 2)
+ tempState.length = 2; // for safety
tempState.dPhase = 0.0;
tempState.pos = 0;
- tempState.data = newData;
- tempState.length = newLength;
tempState.active = true;
/* Update live scope now.
@@ -240,14 +236,12 @@
{
scope_t tmpScope = *sc; // cache it
+ // clear scope background
+ fillRect(scopeX, 55, SCOPE_WIDTH, SCOPE_HEIGHT, bgColor);
+
// 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;
@@ -275,17 +269,10 @@
}
}
}
- else if (!sc->emptyScopeDrawn)
+ else
{
- // 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
+ // draw centered scope line
hLine(scopeX, 71, SCOPE_WIDTH, fgColor);
-
- sc->emptyScopeDrawn = true;
}
scopeX += SCOPE_WIDTH+8;
--- a/src/pt2_scopes.h
+++ b/src/pt2_scopes.h
@@ -7,14 +7,12 @@
typedef struct scope_t
{
- const int8_t *data;
- bool active, emptyScopeDrawn;
+ volatile bool active;
+ const int8_t *data, *newData;
uint8_t volume;
- int32_t length, pos;
-
+ int32_t length, newLength;
+ int32_t pos;
double dDelta, dPhase;
- const int8_t *newData;
- int32_t newLength;
} scope_t;
void scopeSetPeriod(int32_t ch, int32_t period);
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -68,7 +68,7 @@
typedef struct moduleChannel_t
{
- int8_t *n_start, *n_wavestart, *n_loopstart, n_chanindex, n_volume, n_dmabit;
+ int8_t *n_start, *n_wavestart, *n_loopstart, n_volume, n_dmabit;
int8_t n_toneportdirec, n_pattpos, n_loopcount;
uint8_t n_wavecontrol, n_glissfunk, n_sampleoffset, n_toneportspeed;
uint8_t n_vibratocmd, n_tremolocmd, n_finetune, n_funkoffset, n_samplenum;
@@ -75,7 +75,7 @@
uint8_t n_vibratopos, n_tremolopos;
int16_t n_period, n_note, n_wantedperiod;
uint16_t n_cmd, n_length, n_replen;
- uint32_t n_scopedelta;
+ uint32_t n_scopedelta, n_chanindex;
// for pt2_sync.c
uint8_t syncFlags;
@@ -162,7 +162,7 @@
UNICHAR *fileNameTmpU, *currPathU, *modulesPathU, *samplesPathU;
bool errorMsgActive, errorMsgBlock, multiFlag, metroFlag, keypadToggle8CFlag, normalizeFiltersFlag;
- bool sampleAllFlag, halfClipFlag, newOldFlag, pat2SmpHQ, mixFlag, useLEDFilter;
+ bool sampleAllFlag, halfClipFlag, newOldFlag, pat2SmpHQ, mixFlag;
bool modLoaded, autoInsFlag, repeatKeyFlag, sampleZero, tuningToneFlag;
bool stepPlayEnabled, stepPlayBackwards, blockBufferFlag, blockMarkFlag, didQuantize;
bool swapChannelFlag, configFound, chordLengthMin, rowVisitTable[MOD_ORDERS * MOD_ROWS];
--- a/src/pt2_sync.c
+++ /dev/null
@@ -1,190 +1,0 @@
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_sync.h"
-#include "pt2_scopes.h"
-#include "pt2_visuals.h"
-#include "pt2_tables.h"
-
-static volatile bool chQueueClearing;
-
-chSyncData_t *chSyncEntry; // globalized
-chSync_t chSync; // globalized
-
-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 updateChannelSyncBuffer(void)
-{
- uint8_t updateFlags[PAULA_VOICES];
-
- chSyncEntry = NULL;
-
- memset(updateFlags, 0, sizeof (updateFlags)); // 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->newData = c->triggerData;
- s->newLength = c->triggerLength;
- scopeTrigger(ch);
- }
-
- if (flags & SET_SCOPE_DATA)
- scope[ch].newData = c->newData;
-
- if (flags & SET_SCOPE_LENGTH)
- scope[ch].newLength = c->newLength;
-
- // ---------------------------------------------------------------
-
- if (flags & UPDATE_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];
- }
- }
- }
-}
--- a/src/pt2_sync.h
+++ /dev/null
@@ -1,51 +1,0 @@
-#pragma once
-
-#include <stdint.h>
-#include <stdbool.h>
-#include "pt2_paula.h"
-
-enum // flags
-{
- SET_SCOPE_VOLUME = 1,
- SET_SCOPE_PERIOD = 2,
- SET_SCOPE_DATA = 4,
- SET_SCOPE_LENGTH = 8,
- TRIGGER_SCOPE = 16,
- STOP_SCOPE = 32,
-
- UPDATE_VUMETER = 64,
- UPDATE_ANALYZER = 128
-};
-
-// 2^n-1 - don't change this! Queue buffer is already ~1MB in size
-#define SYNC_QUEUE_LEN 8191
-
-typedef struct syncedChannel_t
-{
- uint8_t flags;
- const int8_t *triggerData, *newData;
- int32_t triggerLength, newLength;
- uint8_t volume, vuVolume, analyzerVolume;
- uint16_t analyzerPeriod;
- int32_t period;
-} syncedChannel_t;
-
-typedef struct chSyncData_t
-{
- syncedChannel_t channels[PAULA_VOICES];
- uint64_t timestamp;
-} chSyncData_t;
-
-typedef struct chSync_t
-{
- volatile int32_t readPos, writePos;
- chSyncData_t data[SYNC_QUEUE_LEN + 1];
-} chSync_t;
-
-void resetChSyncQueue(void);
-bool chQueuePush(chSyncData_t t);
-void updateChannelSyncBuffer(void);
-
-extern chSyncData_t *chSyncEntry; // pt2_sync.c
-extern chSync_t chSync; // pt2_sync.c
-
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -1073,9 +1073,6 @@
srcPtr += SCREEN_W;
dstPtr += SCREEN_W;
}
-
- for (int32_t i = 0; i < PAULA_VOICES; i++)
- scope[i].emptyScopeDrawn = false;
}
void renderSpectrumAnalyzerBg(void)
--- /dev/null
+++ b/src/pt2_visuals_sync.c
@@ -1,0 +1,388 @@
+// 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, audLatencyPerfValFrac;
+static uint64_t tickTime64, tickTime64Frac;
+static uint32_t tickTimeLen, 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, dFrac;
+
+ if (audioFreq == 0)
+ return;
+
+ const double dAudioLatencySecs = audioBufferSize / (double)audioFreq;
+
+ dFrac = modf(dAudioLatencySecs * hpcFreq.dFreq, &dInt);
+
+ // integer part
+ audLatencyPerfValInt = (uint32_t)dInt;
+
+ // fractional part (scaled to 0..2^32-1)
+ audLatencyPerfValFrac = (uint32_t)((dFrac * (UINT32_MAX+1.0)) + 0.5); // rounded
+}
+
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac)
+{
+ tickTimeLen = timeLen;
+ 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 += tickTimeLen;
+ tickTime64Frac += tickTimeLenFrac;
+ if (tickTime64Frac > UINT32_MAX)
+ {
+ tickTime64Frac &= UINT32_MAX;
+ 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_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];
+ }
+ }
+ }
+}
--- /dev/null
+++ b/src/pt2_visuals_sync.h
@@ -1,0 +1,65 @@
+#pragma once
+
+#include <stdint.h>
+#include <stdbool.h>
+#include "pt2_paula.h"
+
+enum // flags
+{
+ SET_SCOPE_VOLUME = 1,
+ SET_SCOPE_PERIOD = 2,
+ SET_SCOPE_DATA = 4,
+ SET_SCOPE_LENGTH = 8,
+ TRIGGER_SCOPE = 16,
+ STOP_SCOPE = 32,
+
+ UPDATE_VUMETER = 64,
+ UPDATE_ANALYZER = 128
+};
+
+// 2^n-1 - don't change this! Total queue buffer length is already big.
+#define SYNC_QUEUE_LEN 8191
+
+#ifdef _MSC_VER
+#pragma pack(push)
+#pragma pack(1)
+#endif
+typedef struct syncedChannel_t // pack to save RAM
+{
+ const int8_t *data, *newData;
+ uint16_t length, newLength;
+ uint16_t period, analyzerPeriod;
+ uint8_t flags;
+ uint8_t volume, analyzerVolume, vuVolume;
+}
+#ifdef __GNUC__
+__attribute__ ((packed))
+#endif
+syncedChannel_t;
+#ifdef _MSC_VER
+#pragma pack(pop)
+#endif
+
+typedef struct chSyncData_t
+{
+ syncedChannel_t channels[PAULA_VOICES];
+ uint64_t timestamp;
+} chSyncData_t;
+
+typedef struct chSync_t
+{
+ volatile int32_t readPos, writePos;
+ chSyncData_t data[SYNC_QUEUE_LEN + 1];
+} chSync_t;
+
+void calcAudioLatencyVars(int32_t audioBufferSize, int32_t audioFreq);
+void setSyncTickTimeLen(uint32_t timeLen, uint32_t timeLenFrac);
+void fillVisualsSyncBuffer(void);
+void resetChSyncQueue(void);
+void updateChannelSyncBuffer(void);
+
+void setVisualsDMACON(uint16_t bits);
+void setVisualsVolume(int32_t ch, uint16_t vol);
+void setVisualsPeriod(int32_t ch, uint16_t period);
+void setVisualsLength(int32_t ch, uint16_t len);
+void setVisualsDataPtr(int32_t ch, const int8_t *src);
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj
@@ -237,7 +237,6 @@
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
- <ClInclude Include="..\..\src\pt2_amigafilters.h" />
<ClInclude Include="..\..\src\pt2_askbox.h" />
<ClInclude Include="..\..\src\pt2_audio.h" />
<ClInclude Include="..\..\src\pt2_blep.h" />
@@ -250,6 +249,7 @@
<ClInclude Include="..\..\src\pt2_helpers.h" />
<ClInclude Include="..\..\src\pt2_hpc.h" />
<ClInclude Include="..\..\src\pt2_keyboard.h" />
+ <ClInclude Include="..\..\src\pt2_ledfilter.h" />
<ClInclude Include="..\..\src\pt2_math.h" />
<ClInclude Include="..\..\src\pt2_mod2wav.h" />
<ClInclude Include="..\..\src\pt2_module_loader.h" />
@@ -268,7 +268,7 @@
<ClInclude Include="..\..\src\pt2_sampling.h" />
<ClInclude Include="..\..\src\pt2_scopes.h" />
<ClInclude Include="..\..\src\pt2_structs.h" />
- <ClInclude Include="..\..\src\pt2_sync.h" />
+ <ClInclude Include="..\..\src\pt2_visuals_sync.h" />
<ClInclude Include="..\..\src\pt2_tables.h" />
<ClInclude Include="..\..\src\pt2_textout.h" />
<ClInclude Include="..\..\src\pt2_unicode.h" />
@@ -290,7 +290,6 @@
<ClCompile Include="..\..\src\gfx\pt2_gfx_spectrum.c" />
<ClCompile Include="..\..\src\gfx\pt2_gfx_tracker.c" />
<ClCompile Include="..\..\src\gfx\pt2_gfx_vumeter.c" />
- <ClCompile Include="..\..\src\pt2_amigafilters.c" />
<ClCompile Include="..\..\src\pt2_askbox.c" />
<ClCompile Include="..\..\src\pt2_audio.c" />
<ClCompile Include="..\..\src\pt2_blep.c" />
@@ -302,6 +301,7 @@
<ClCompile Include="..\..\src\pt2_helpers.c" />
<ClCompile Include="..\..\src\pt2_hpc.c" />
<ClCompile Include="..\..\src\pt2_keyboard.c" />
+ <ClCompile Include="..\..\src\pt2_ledfilter.c" />
<ClCompile Include="..\..\src\pt2_main.c" />
<ClCompile Include="..\..\src\pt2_math.c" />
<ClCompile Include="..\..\src\pt2_mod2wav.c" />
@@ -321,7 +321,7 @@
<ClCompile Include="..\..\src\pt2_sampling.c" />
<ClCompile Include="..\..\src\pt2_scopes.c" />
<ClCompile Include="..\..\src\pt2_structs.c" />
- <ClCompile Include="..\..\src\pt2_sync.c" />
+ <ClCompile Include="..\..\src\pt2_visuals_sync.c" />
<ClCompile Include="..\..\src\pt2_tables.c" />
<ClCompile Include="..\..\src\pt2_textout.c" />
<ClCompile Include="..\..\src\pt2_unicode.c" />
--- a/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
+++ b/vs2019_project/pt2-clone/pt2-clone.vcxproj.filters
@@ -87,7 +87,7 @@
<ClInclude Include="..\..\src\pt2_sampling.h">
<Filter>headers</Filter>
</ClInclude>
- <ClInclude Include="..\..\src\pt2_sync.h">
+ <ClInclude Include="..\..\src\pt2_visuals_sync.h">
<Filter>headers</Filter>
</ClInclude>
<ClInclude Include="..\..\src\pt2_rcfilter.h">
@@ -117,7 +117,7 @@
<ClInclude Include="..\..\src\pt2_paula.h">
<Filter>headers</Filter>
</ClInclude>
- <ClInclude Include="..\..\src\pt2_amigafilters.h">
+ <ClInclude Include="..\..\src\pt2_ledfilter.h">
<Filter>headers</Filter>
</ClInclude>
</ItemGroup>
@@ -191,7 +191,7 @@
<ClCompile Include="..\..\src\pt2_module_saver.c" />
<ClCompile Include="..\..\src\pt2_replayer.c" />
<ClCompile Include="..\..\src\pt2_sampling.c" />
- <ClCompile Include="..\..\src\pt2_sync.c" />
+ <ClCompile Include="..\..\src\pt2_visuals_sync.c" />
<ClCompile Include="..\..\src\pt2_rcfilter.c" />
<ClCompile Include="..\..\src\pt2_chordmaker.c" />
<ClCompile Include="..\..\src\pt2_downsample2x.c" />
@@ -200,7 +200,7 @@
<ClCompile Include="..\..\src\pt2_xpk.c" />
<ClCompile Include="..\..\src\pt2_askbox.c" />
<ClCompile Include="..\..\src\pt2_paula.c" />
- <ClCompile Include="..\..\src\pt2_amigafilters.c" />
+ <ClCompile Include="..\..\src\pt2_ledfilter.c" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="..\..\src\pt2-clone.rc" />