ref: 5ccd729ea5a67c3c80fe3b117d4bd58c58226745
parent: b8e8e8a8ef726fbe428edc74bdd9de886863075c
author: Olav Sørensen <olav.sorensen@live.no>
date: Thu Sep 16 14:24:01 EDT 2021
Pushed v1.34 code
--- a/release/macos/protracker.ini
+++ b/release/macos/protracker.ini
@@ -203,12 +203,21 @@
;
DOTTEDCENTER=TRUE
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+; Syntax: TRUE or FALSE
+; Default value: FALSE
+; Comment: This ProTracker command low-pass filters the current sample.
+; It's a little used effect despite being present in original PT,
+; and it was often replaced for syncing visuals with the music in
+; demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
[AUDIO SETTINGS]
; Audio output frequency
; Syntax: Number, in hertz
; Default value: 48000
-; Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-; OS is set to mix shared audio at 96kHz or higher.
+; Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
;
FREQUENCY=48000
--- a/release/other/protracker.ini
+++ b/release/other/protracker.ini
@@ -203,12 +203,21 @@
;
DOTTEDCENTER=TRUE
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+; Syntax: TRUE or FALSE
+; Default value: FALSE
+; Comment: This ProTracker command low-pass filters the current sample.
+; It's a little used effect despite being present in original PT,
+; and it was often replaced for syncing visuals with the music in
+; demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
[AUDIO SETTINGS]
; Audio output frequency
; Syntax: Number, in hertz
; Default value: 48000
-; Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-; OS is set to mix shared audio at 96kHz or higher.
+; Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
;
FREQUENCY=48000
--- a/release/win32/protracker.ini
+++ b/release/win32/protracker.ini
@@ -203,12 +203,21 @@
;
DOTTEDCENTER=TRUE
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+; Syntax: TRUE or FALSE
+; Default value: FALSE
+; Comment: This ProTracker command low-pass filters the current sample.
+; It's a little used effect despite being present in original PT,
+; and it was often replaced for syncing visuals with the music in
+; demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
[AUDIO SETTINGS]
; Audio output frequency
; Syntax: Number, in hertz
; Default value: 48000
-; Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-; OS is set to mix shared audio at 96kHz or higher.
+; Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
;
FREQUENCY=48000
--- a/release/win64/protracker.ini
+++ b/release/win64/protracker.ini
@@ -203,12 +203,21 @@
;
DOTTEDCENTER=TRUE
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+; Syntax: TRUE or FALSE
+; Default value: FALSE
+; Comment: This ProTracker command low-pass filters the current sample.
+; It's a little used effect despite being present in original PT,
+; and it was often replaced for syncing visuals with the music in
+; demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
[AUDIO SETTINGS]
; Audio output frequency
; Syntax: Number, in hertz
; Default value: 48000
-; Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-; OS is set to mix shared audio at 96kHz or higher.
+; Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
;
FREQUENCY=48000
--- a/src/pt2_audio.c
+++ b/src/pt2_audio.c
@@ -49,7 +49,7 @@
static uint64_t tickTime64, tickTime64Frac;
static double *dMixBufferL, *dMixBufferR, *dMixBufferLUnaligned, *dMixBufferRUnaligned;
static double dPrngStateL, dPrngStateR, dSideFactor;
-static blep_t blep[AMIGA_VOICES], blepVol[AMIGA_VOICES];
+static blep_t blep[AMIGA_VOICES];
static rcFilter_t filterLoA500, filterHiA500, filterLoA1200, filterHiA1200;
static ledFilter_t filterLED;
static SDL_AudioDeviceID dev;
@@ -67,7 +67,7 @@
audio_t audio;
paulaVoice_t paula[AMIGA_VOICES];
-bool intMusic(void); // defined in pt_modplayer.c
+bool intMusic(void); // defined in pt2_replayer.c
static void updateFilterFunc(void)
{
@@ -89,6 +89,9 @@
void setLEDFilter(bool state, bool doLockAudio)
{
+ if (ledFilterEnabled == state)
+ return; // same state as before!
+
const bool audioWasntLocked = !audio.locked;
if (doLockAudio && audioWasntLocked)
lockAudio();
@@ -189,7 +192,6 @@
memset(&paula[ch], 0, sizeof (paulaVoice_t));
memset(&blep[ch], 0, sizeof (blep_t));
- memset(&blepVol[ch], 0, sizeof (blep_t));
stopScope(ch); // it should be safe to clear the scope now
memset(&scope[ch], 0, sizeof (scope_t));
@@ -268,23 +270,22 @@
else
dPeriodToDeltaDiv = audio.dPeriodToDeltaDiv;
- v->dOldVoiceDelta = (dPeriodToDeltaDiv / realPeriod) * 0.5; // /2 since we do 2x oversampling
+ v->dOldVoiceDelta = dPeriodToDeltaDiv / realPeriod;
+ if (audio.oversamplingFlag || editor.isSMPRendering)
+ v->dOldVoiceDelta *= 0.5; // /2 since we do 2x oversampling
+
// for BLEP synthesis (prevents division in inner mix loop)
v->dOldVoiceDeltaMul = 1.0 / v->dOldVoiceDelta;
}
- v->dDelta = v->dOldVoiceDelta;
+ v->dNewDelta = v->dOldVoiceDelta;
+ if (v->dLastDelta == 0.0) // for BLEP
+ v->dLastDelta = v->dNewDelta;
- // for BLEP synthesis
- v->dDeltaMul = v->dOldVoiceDeltaMul;
-
- if (v->dLastDelta == 0.0)
- v->dLastDelta = v->dDelta;
-
- if (v->dLastDeltaMul == 0.0)
- v->dLastDeltaMul = v->dDeltaMul;
- // ------------------
+ v->dNewDeltaMul = v->dOldVoiceDeltaMul;
+ if (v->dLastDeltaMul == 0.0) // for BLEP
+ v->dLastDeltaMul = v->dNewDeltaMul;
}
void paulaSetVolume(int32_t ch, uint16_t vol)
@@ -293,13 +294,14 @@
int32_t realVol = vol;
- // this is what WinUAE does, so I assume it's what Paula does too
+ // this is what WinUAE does
realVol &= 127;
if (realVol > 64)
realVol = 64;
- // ----------------
+ // ------------------------
- v->dVolume = realVol * (1.0 / 64.0);
+ // multiplying by this also scales the sample from -128..127 -> -1.0 .. ~0.99
+ v->dScaledVolume = realVol * (1.0 / (128.0 * 64.0));
if (editor.songPlaying)
{
@@ -356,7 +358,7 @@
{
paulaVoice_t *v = &paula[ch];
- v->active = false;
+ v->DMA_active = false;
if (editor.songPlaying)
v->syncFlags |= STOP_SCOPE;
@@ -376,12 +378,22 @@
if (length == 0)
length = 1+65535;
- v->dPhase = 0.0;
+ v->dPhase = v->dLastPhase = 0.0;
v->pos = 0;
v->data = dat;
v->length = length;
- v->active = true;
+ v->dDelta = v->dLastDelta = v->dNewDelta;
+ v->dDeltaMul = v->dLastDeltaMul = v->dNewDeltaMul;
+ /* Read first sample data point into cache now.
+ **
+ ** (multiplying by dScaledVolume will also change the scale
+ ** from -128..127 to -1.0 .. ~0.99.)
+ */
+ v->dCachedSamplePoint = v->data[0] * v->dScaledVolume;
+
+ v->DMA_active = true;
+
if (editor.songPlaying)
{
v->syncTriggerData = dat;
@@ -424,34 +436,32 @@
void mixChannels(int32_t numSamples)
{
double *dMixBufSelect[AMIGA_VOICES] = { dMixBufferL, dMixBufferR, dMixBufferR, dMixBufferL };
- double dSmp, dVol;
- blep_t *bSmp, *bVol;
- paulaVoice_t *v;
memset(dMixBufferL, 0, numSamples * sizeof (double));
memset(dMixBufferR, 0, numSamples * sizeof (double));
- v = paula;
- bSmp = blep;
- bVol = blepVol;
+ paulaVoice_t *v = paula;
+ blep_t *bSmp = blep;
- for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++, bVol++)
+ for (int32_t i = 0; i < AMIGA_VOICES; i++, v++, bSmp++)
{
- if (!v->active || v->data == NULL)
+ /* We only need to test for a NULL-pointer once.
+ ** When pointers are messed with in the tracker, the mixer
+ ** is temporarily forced offline, and its voice pointers are
+ ** cleared to prevent expired pointer addresses.
+ */
+ if (!v->DMA_active || v->data == NULL)
continue;
- double *dMixBuf = dMixBufSelect[i];
+ double *dMixBuf = dMixBufSelect[i]; // what output channel to mix into (L, R, R, L)
for (int32_t j = 0; j < numSamples; j++)
{
- assert(v->data != NULL);
- dSmp = v->data[v->pos] * (1.0 / 128.0);
- dVol = v->dVolume;
-
+ double dSmp = v->dCachedSamplePoint;
if (dSmp != bSmp->dLastValue)
{
if (v->dLastDelta > v->dLastPhase)
{
- // div->mul trick: v->dLastDeltaMul is 1.0 / v->dLastDelta
+ // v->dLastDeltaMul is (1.0 / v->dLastDelta) (pre-computed for speed, div -> mul)
blepAdd(bSmp, v->dLastPhase * v->dLastDeltaMul, bSmp->dLastValue - dSmp);
}
@@ -458,22 +468,21 @@
bSmp->dLastValue = dSmp;
}
- if (dVol != bVol->dLastValue)
- {
- blepVolAdd(bVol, bVol->dLastValue - dVol);
- bVol->dLastValue = dVol;
- }
+ if (bSmp->samplesLeft > 0)
+ dSmp = blepRun(bSmp, dSmp);
- if (bSmp->samplesLeft > 0) dSmp = blepRun(bSmp, dSmp);
- if (bVol->samplesLeft > 0) dVol = blepRun(bVol, dVol);
+ dMixBuf[j] += dSmp;
- dMixBuf[j] += dSmp * dVol;
-
v->dPhase += v->dDelta;
if (v->dPhase >= 1.0) // deltas can't be >= 1.0, so this is safe
{
v->dPhase -= 1.0;
+ // Paula only updates period (delta) during sample fetching
+ v->dDelta = v->dNewDelta;
+ v->dDeltaMul = v->dNewDeltaMul;
+ // --------------------------------------------------------
+
v->dLastPhase = v->dPhase;
v->dLastDelta = v->dDelta;
v->dLastDeltaMul = v->dDeltaMul;
@@ -486,6 +495,16 @@
v->length = v->newLength;
v->data = v->newData;
}
+
+ /* Read sample into cache now.
+ ** Also change volume here as well. It has recently been
+ ** discovered that Paula only updates its volume during period
+ ** fetching (when it's reading the next sample point).
+ **
+ ** (multiplying by dScaledVolume will also change the scale
+ ** from -128..127 to -1.0 .. ~0.99.)
+ */
+ v->dCachedSamplePoint = v->data[v->pos] * v->dScaledVolume;
}
}
}
@@ -595,9 +614,77 @@
}
#define NORM_FACTOR 2.0 /* nominally correct, but can clip from high-pass filter overshoot */
+
static inline void processMixedSamplesAmigaPanning(int32_t i, int16_t *out)
{
int32_t smp32;
+ double dPrng;
+
+ double dL = dMixBufferL[i];
+ double dR = dMixBufferR[i];
+
+ // normalize w/ phase-inversion (A500/A1200 has a phase-inverted audio signal)
+ dL *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+ dR *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+
+ // left channel - 1-bit triangular dithering (high-pass filtered)
+ dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+ dL = (dL + dPrng) - dPrngStateL;
+ dPrngStateL = dPrng;
+ smp32 = (int32_t)dL;
+ CLAMP16(smp32);
+ out[0] = (int16_t)smp32;
+
+ // right channel - 1-bit triangular dithering (high-pass filtered)
+ dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+ dR = (dR + dPrng) - dPrngStateR;
+ dPrngStateR = dPrng;
+ smp32 = (int32_t)dR;
+ CLAMP16(smp32);
+ out[1] = (int16_t)smp32;
+}
+
+static inline void processMixedSamples(int32_t i, int16_t *out)
+{
+ int32_t smp32;
+ double dPrng;
+
+ double dL = dMixBufferL[i];
+ double dR = dMixBufferR[i];
+
+ // apply stereo separation
+ const double dOldL = dL;
+ const double dOldR = dR;
+ double dMid = (dOldL + dOldR) * STEREO_NORM_FACTOR;
+ double dSide = (dOldL - dOldR) * dSideFactor;
+ dL = dMid + dSide;
+ dR = dMid - dSide;
+
+ // normalize w/ phase-inversion (A500/A1200 has a phase-inverted audio signal)
+ dL *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+ dR *= NORM_FACTOR * (-INT16_MAX / (double)AMIGA_VOICES);
+
+ // left channel - 1-bit triangular dithering (high-pass filtered)
+ dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+ dL = (dL + dPrng) - dPrngStateL;
+ dPrngStateL = dPrng;
+ smp32 = (int32_t)dL;
+ CLAMP16(smp32);
+ out[0] = (int16_t)smp32;
+
+ // right channel - 1-bit triangular dithering (high-pass filtered)
+ dPrng = random32() * (0.5 / INT32_MAX); // -0.5..0.5
+ dR = (dR + dPrng) - dPrngStateR;
+ dPrngStateR = dPrng;
+ smp32 = (int32_t)dR;
+ CLAMP16(smp32);
+ out[1] = (int16_t)smp32;
+}
+
+
+static inline void processMixedSamplesAmigaPanning_2x(int32_t i, int16_t *out) // 2x oversampling
+{
+ int32_t smp32;
double dPrng, dL, dR;
// 2x downsampling (decimation)
@@ -627,7 +714,7 @@
out[1] = (int16_t)smp32;
}
-static inline void processMixedSamples(int32_t i, int16_t *out)
+static inline void processMixedSamples_2x(int32_t i, int16_t *out) // 2x oversampling
{
int32_t smp32;
double dPrng, dL, dR;
@@ -677,7 +764,7 @@
if (editor.pat2SmpPos+samplesTodo > MAX_SAMPLE_LEN)
samplesTodo = MAX_SAMPLE_LEN-editor.pat2SmpPos;
- // mix channels (at 2x rate, we do 2x oversampling)
+ // mix channels (with 2x oversampling, PAT2SMP needs it)
mixChannels(samplesTodo*2);
double *dOutStream = &editor.dPat2SmpBuf[editor.pat2SmpPos];
@@ -702,33 +789,63 @@
}
else
{
- // mix and filter channels (at 2x rate, we do 2x oversampling)
- mixChannels(numSamples*2);
- processFiltersFunc(numSamples*2);
-
- // downsample, normalize and dither
- int16_t out[2];
- int16_t *outStream = target;
- if (stereoSeparation == 100)
+ if (audio.oversamplingFlag) // 2x oversampling
{
- for (int32_t i = 0; i < numSamples; i++)
- {
- processMixedSamplesAmigaPanning(i, out); // also does 2x downsampling
+ // mix and filter channels (at 2x rate)
+ mixChannels(numSamples*2);
+ processFiltersFunc(numSamples*2);
- *outStream++ = out[0];
- *outStream++ = out[1];
+ // downsample, normalize and dither
+ int16_t out[2];
+ int16_t *outStream = target;
+ if (stereoSeparation == 100)
+ {
+ for (int32_t i = 0; i < numSamples; i++)
+ {
+ processMixedSamplesAmigaPanning_2x(i, out);
+ *outStream++ = out[0];
+ *outStream++ = out[1];
+ }
}
+ else
+ {
+ for (int32_t i = 0; i < numSamples; i++)
+ {
+ processMixedSamples_2x(i, out);
+ *outStream++ = out[0];
+ *outStream++ = out[1];
+ }
+ }
}
else
{
- for (int32_t i = 0; i < numSamples; i++)
- {
- processMixedSamples(i, out); // also does 2x downsampling
+ // mix and filter channels
+ mixChannels(numSamples);
+ processFiltersFunc(numSamples);
- *outStream++ = out[0];
- *outStream++ = out[1];
+ // normalize and dither
+ int16_t out[2];
+ int16_t *outStream = target;
+ if (stereoSeparation == 100)
+ {
+ for (int32_t i = 0; i < numSamples; i++)
+ {
+ processMixedSamplesAmigaPanning(i, out);
+ *outStream++ = out[0];
+ *outStream++ = out[1];
+ }
}
+ else
+ {
+ for (int32_t i = 0; i < numSamples; i++)
+ {
+ processMixedSamples(i, out);
+ *outStream++ = out[0];
+ *outStream++ = out[1];
+ }
+ }
}
+
}
}
@@ -872,9 +989,12 @@
** - 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
*/
- const double dAudioFreq = audio.outputRate * 2.0; // *2 because we do 2x oversampling
+ double dAudioFreq = audio.outputRate;
double R, C, R1, R2, C1, C2, fc, fb;
+ if (audio.oversamplingFlag)
+ dAudioFreq *= 2.0; // 2x oversampling
+
// A500 1-pole (6db/oct) static RC low-pass filter:
R = 360.0; // R321 (360 ohm)
C = 1e-7; // C321 (0.1uF)
@@ -1021,7 +1141,10 @@
audio.audioBufferSize = have.samples;
audio.dPeriodToDeltaDiv = (double)PAULA_PAL_CLK / audio.outputRate;
- updateReplayerTimingMode();
+ // we do 2x oversampling if the audio output rate is below 96kHz
+ audio.oversamplingFlag = (audio.outputRate < 96000);
+
+ updateReplayerTimingMode(); // also generates the BPM tables used below
const int32_t lowestBPM = 32;
const int32_t pat2SmpMaxSamples = (audio.bpmTable20kHz[lowestBPM-32] + (1LL + 31)) >> 32; // ceil (rounded upwards)
--- a/src/pt2_audio.h
+++ b/src/pt2_audio.h
@@ -8,7 +8,7 @@
{
volatile bool locked, isSampling;
- bool forceSoundCardSilence;
+ bool forceSoundCardSilence, oversamplingFlag;
uint32_t outputRate, audioBufferSize;
int64_t tickSampleCounter64, samplesPerTick64;
@@ -25,11 +25,13 @@
typedef struct voice_t
{
- volatile bool active;
+ volatile bool DMA_active;
const int8_t *data, *newData;
int32_t length, newLength, pos;
- double dVolume, dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase;
+
+ double dDelta, dDeltaMul, dPhase, dLastDelta, dLastDeltaMul, dLastPhase;
+ double dCachedSamplePoint, dScaledVolume, dNewDelta, dNewDeltaMul;
// period cache
int32_t oldPeriod;
--- a/src/pt2_config.c
+++ b/src/pt2_config.c
@@ -42,6 +42,7 @@
FILE *f;
// set default config values first
+ config.disableE8xEffect = false;
config.fullScreenStretch = false;
config.pattDots = false;
config.waveformCenterLine = true;
@@ -190,6 +191,13 @@
{
configLine = strtok(NULL, "\n");
continue;
+ }
+
+ // DISABLE_E8X (Karplus-Strong command)
+ else if (!_strnicmp(configLine, "DISABLE_E8X=", 12))
+ {
+ if (!_strnicmp(&configLine[12], "TRUE", 4)) config.disableE8xEffect = true;
+ else if (!_strnicmp(&configLine[12], "FALSE", 5)) config.disableE8xEffect = false;
}
// HWMOUSE
--- a/src/pt2_config.h
+++ b/src/pt2_config.h
@@ -15,7 +15,7 @@
char *defModulesDir, *defSamplesDir;
bool waveformCenterLine, pattDots, compoMode, autoCloseDiskOp, hideDiskOpDates, hwMouse;
bool transDel, fullScreenStretch, vsyncOff, modDot, blankZeroFlag, realVuMeters, rememberPlayMode;
- bool startInFullscreen, integerScaling;
+ bool startInFullscreen, integerScaling, disableE8xEffect;
int8_t stereoSeparation, videoScaleFactor, accidental;
uint8_t pixelFilter, filterModel;
uint16_t quantizeValue;
--- a/src/pt2_edit.c
+++ b/src/pt2_edit.c
@@ -52,7 +52,7 @@
-1, 24, 26, 28
};
-void setPattern(int16_t pattern); // pt_modplayer.c
+void setPattern(int16_t pattern); // pt2_replayer.c
void jamAndPlaceSample(SDL_Scancode scancode, bool normalMode);
uint8_t quantizeCheck(uint8_t row);
--- 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.33"
+#define PROG_VER_STR "1.34"
#ifdef _WIN32
#define DIR_DELIMITER '\\'
--- a/src/pt2_keyboard.c
+++ b/src/pt2_keyboard.c
@@ -43,8 +43,8 @@
static bool handleGeneralModes(SDL_Keycode keycode, SDL_Scancode scancode);
bool handleTextEditMode(SDL_Scancode scancode);
-void sampleUpButton(void); // pt_mouse.c
-void sampleDownButton(void); // pt_mouse.c
+void sampleUpButton(void); // pt2_mouse.c
+void sampleDownButton(void); // pt2_mouse.c
void gotoNextMulti(void)
{
--- a/src/pt2_mod2wav.c
+++ b/src/pt2_mod2wav.c
@@ -18,7 +18,7 @@
#define TICKS_PER_RENDER_CHUNK 64
-// pt_modplayer.c
+// pt2_replayer.c
void storeTempVariables(void);
bool intMusic(void);
// ---------------------
--- a/src/pt2_pat2smp.c
+++ b/src/pt2_pat2smp.c
@@ -16,8 +16,8 @@
#include "pt2_pat2smp.h"
#include "pt2_downsamplers2x.h"
-bool intMusic(void); // pt_modplayer.c
-void storeTempVariables(void); // pt_modplayer.c
+bool intMusic(void); // pt2_replayer.c
+void storeTempVariables(void); // pt2_replayer.c
void doPat2Smp(void)
{
--- a/src/pt2_replayer.c
+++ b/src/pt2_replayer.c
@@ -143,7 +143,7 @@
{
ch->n_funkoffset = 0;
- if (ch->n_loopstart != NULL && ch->n_wavestart != NULL) // non-PT2 bug fix
+ if (ch->n_loopstart != NULL && ch->n_wavestart != NULL) // ProTracker bugfix
{
if (++ch->n_wavestart >= ch->n_loopstart + (ch->n_replen << 1))
ch->n_wavestart = ch->n_loopstart;
@@ -203,16 +203,34 @@
ch->n_wavecontrol = ((ch->n_cmd & 0xF) << 4) | (ch->n_wavecontrol & 0xF);
}
+/* This is a little used effect, despite being present in original ProTracker.
+** E8x was sometimes entirely replaced with code used for demo fx syncing in
+** demo mod players, so it can be turned off by looking at DISABLE_E8X in
+** protracker.ini if you so desire.
+*/
static void karplusStrong(moduleChannel_t *ch)
{
- /* This effect is definitely the least used PT effect there is!
- ** It trashes (filters) the sample data.
- ** The reason I'm not implementing it is because a lot of songs used
- ** E8x for syncing to demos/intros, and because I have never ever
- ** seen this effect being used intentionally.
- */
+ int8_t a, b;
- (void)ch;
+ if (config.disableE8xEffect)
+ return;
+
+ if (ch->n_loopstart == NULL)
+ return; // ProTracker bugfix
+
+ int8_t *ptr8 = ch->n_loopstart;
+ int16_t end = ((ch->n_replen * 2) & 0xFFFF) - 2;
+ do
+ {
+ a = ptr8[0];
+ b = ptr8[1];
+ *ptr8++ = (a + b) >> 1;
+ }
+ while (--end >= 0);
+
+ a = ptr8[0];
+ b = ch->n_loopstart[0];
+ *ptr8 = (a + b) >> 1;
}
static void doRetrg(moduleChannel_t *ch)
@@ -247,17 +265,16 @@
static void volumeSlide(moduleChannel_t *ch)
{
- uint8_t cmd = ch->n_cmd & 0xFF;
-
- if ((cmd & 0xF0) == 0)
+ uint8_t param = ch->n_cmd & 0xFF;
+ if ((param & 0xF0) == 0)
{
- ch->n_volume -= cmd & 0x0F;
+ ch->n_volume -= param & 0x0F;
if (ch->n_volume < 0)
ch->n_volume = 0;
}
else
{
- ch->n_volume += cmd >> 4;
+ ch->n_volume += param >> 4;
if (ch->n_volume > 64)
ch->n_volume = 64;
}
@@ -306,6 +323,7 @@
if (song->tick == 0)
{
ch->n_glissfunk = ((ch->n_cmd & 0xF) << 4) | (ch->n_glissfunk & 0xF);
+
if ((ch->n_glissfunk & 0xF0) > 0)
updateFunk(ch);
}
@@ -313,7 +331,7 @@
static void positionJump(moduleChannel_t *ch)
{
- modOrder = (ch->n_cmd & 0xFF) - 1; // 0xFF (B00) jumps to pat 0
+ modOrder = (ch->n_cmd & 0xFF) - 1; // B00 results in -1, but it safely jumps to order 0
pBreakPosition = 0;
posJumpAssert = true;
}
@@ -360,9 +378,7 @@
uint8_t arpTick, arpNote;
const int16_t *periods;
- assert(song->tick < 32);
- arpTick = arpTickTable[song->tick]; // 0, 1, 2
-
+ arpTick = song->tick % 3; // 0, 1, 2
if (arpTick == 1)
{
arpNote = (uint8_t)(ch->n_cmd >> 4);
@@ -398,7 +414,7 @@
ch->n_period -= (ch->n_cmd & 0xFF) & lowMask;
lowMask = 0xFF;
- if ((ch->n_period & 0xFFF) < 113) // PT BUG: unsigned comparison, underflow not clamped!
+ 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);
@@ -417,7 +433,11 @@
static void filterOnOff(moduleChannel_t *ch)
{
- setLEDFilter(!(ch->n_cmd & 1), false);
+ if (song->tick == 0) // added this (just pointless to call this during all ticks!)
+ {
+ const bool filterOn = (ch->n_cmd & 1) ^ 1;
+ setLEDFilter(filterOn, false);
+ }
}
static void finePortaUp(moduleChannel_t *ch)
@@ -649,6 +669,7 @@
uint16_t newOffset = ch->n_sampleoffset << 7;
+ // this signed test is the reason for the 9xx "sample >64kB = silence" bug
if ((int16_t)newOffset < ch->n_length)
{
ch->n_length -= newOffset;
@@ -662,18 +683,19 @@
static void E_Commands(moduleChannel_t *ch)
{
- const uint8_t cmd = (ch->n_cmd & 0xF0) >> 4;
- switch (cmd)
+ const uint8_t ecmd = (ch->n_cmd & 0x00F0) >> 4;
+ switch (ecmd)
{
- case 0x0: filterOnOff(ch); break;
- case 0x1: finePortaUp(ch); break;
- case 0x2: finePortaDown(ch); break;
- case 0x3: setGlissControl(ch); break;
- case 0x4: setVibratoControl(ch); break;
- case 0x5: setFineTune(ch); break;
- case 0x6: jumpLoop(ch); break;
- case 0x7: setTremoloControl(ch); break;
- case 0x8: karplusStrong(ch); break;
+ case 0x0: filterOnOff(ch); return;
+ case 0x1: finePortaUp(ch); return;
+ case 0x2: finePortaDown(ch); return;
+ case 0x3: setGlissControl(ch); return;
+ case 0x4: setVibratoControl(ch); return;
+ case 0x5: setFineTune(ch); return;
+ case 0x6: jumpLoop(ch); return;
+ case 0x7: setTremoloControl(ch); return;
+ case 0x8: karplusStrong(ch); return;
+ case 0xE: patternDelay(ch); return;
default: break;
}
@@ -680,15 +702,14 @@
if (editor.muted[ch->n_chanindex])
return;
- switch (cmd)
+ switch (ecmd)
{
- case 0x9: retrigNote(ch); break;
- case 0xA: volumeFineUp(ch); break;
- case 0xB: volumeFineDown(ch); break;
- case 0xC: noteCut(ch); break;
- case 0xD: noteDelay(ch); break;
- case 0xE: patternDelay(ch); break;
- case 0xF: funkIt(ch); break;
+ case 0x9: retrigNote(ch); return;
+ case 0xA: volumeFineUp(ch); return;
+ case 0xB: volumeFineDown(ch); return;
+ case 0xC: noteCut(ch); return;
+ case 0xD: noteDelay(ch); return;
+ case 0xF: funkIt(ch); return;
default: break;
}
}
@@ -695,71 +716,74 @@
static void checkMoreEffects(moduleChannel_t *ch)
{
- switch ((ch->n_cmd & 0xF00) >> 8)
+ const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+ switch (cmd)
{
- case 0x9: sampleOffset(ch); break;
- case 0xB: positionJump(ch); break;
+ case 0x9: sampleOffset(ch); return; // note the returns here, not breaks!
+ case 0xB: positionJump(ch); return;
+ case 0xD: patternBreak(ch); return;
+ case 0xE: E_Commands(ch); return;
+ case 0xF: setSpeed(ch); return;
+ default: break;
+ }
- case 0xC:
- {
- if (!editor.muted[ch->n_chanindex])
- volumeChange(ch);
- }
- break;
+ if (editor.muted[ch->n_chanindex])
+ return;
- case 0xD: patternBreak(ch); break;
- case 0xE: E_Commands(ch); break;
- case 0xF: setSpeed(ch); break;
-
- default:
- {
- if (!editor.muted[ch->n_chanindex])
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
- }
- break;
+ if (cmd == 0xC)
+ {
+ volumeChange(ch);
+ return;
}
+
+ paulaSetPeriod(ch->n_chanindex, ch->n_period);
}
-static void checkEffects(moduleChannel_t *ch)
+static void chkefx2(moduleChannel_t *ch)
{
- if (editor.muted[ch->n_chanindex])
- return;
-
updateFunk(ch);
- const uint8_t effect = (ch->n_cmd & 0xF00) >> 8;
- if ((ch->n_cmd & 0xFFF) > 0)
+ if ((ch->n_cmd & 0xFFF) == 0)
+ return;
+
+ const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+ switch (cmd)
{
- switch (effect)
- {
- case 0x0: arpeggio(ch); break;
- case 0x1: portaUp(ch); break;
- case 0x2: portaDown(ch); break;
- case 0x3: tonePortamento(ch); break;
- case 0x4: vibrato(ch); break;
- case 0x5: tonePlusVolSlide(ch); break;
- case 0x6: vibratoPlusVolSlide(ch); break;
- case 0xE: E_Commands(ch); break;
+ case 0x0: arpeggio(ch); return; // note the returns here, not breaks!
+ case 0x1: portaUp(ch); return;
+ case 0x2: portaDown(ch); return;
+ case 0x3: tonePortamento(ch); return;
+ case 0x4: vibrato(ch); return;
+ case 0x5: tonePlusVolSlide(ch); return;
+ case 0x6: vibratoPlusVolSlide(ch); return;
+ case 0xE: E_Commands(ch); return;
+ default: break;
+ }
- case 0x7:
- {
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
- tremolo(ch);
- }
- break;
+ paulaSetPeriod(ch->n_chanindex, ch->n_period);
- case 0xA:
- {
- paulaSetPeriod(ch->n_chanindex, ch->n_period);
- volumeSlide(ch);
- }
- break;
+ if (cmd == 0x7)
+ tremolo(ch);
+ else if (cmd == 0xA)
+ volumeSlide(ch);
+}
- default: paulaSetPeriod(ch->n_chanindex, ch->n_period); break;
- }
- }
+static void checkEffects(moduleChannel_t *ch)
+{
+ if (editor.muted[ch->n_chanindex])
+ return;
- if (effect != 0x7)
+ chkefx2(ch);
+
+ /* This is not very clear in the original PT replayer code,
+ ** but the tremolo effect skips chkefx2()'s return address
+ ** in the stack so that it jumps to checkEffects()'s return
+ ** address instead of ending up here. In other words, volume
+ ** is not updated here after tremolo (it's done inside the
+ ** tremolo routine itself).
+ */
+ const uint8_t cmd = (ch->n_cmd & 0x0F00) >> 8;
+ if (cmd != 0x7)
paulaSetVolume(ch->n_chanindex, ch->n_volume);
}
@@ -865,7 +889,7 @@
ch->n_wavestart = ch->n_start;
}
- // non-PT2 quirk
+ // 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[RESERVED_SAMPLE_OFFSET]; // 128K reserved sample
}
@@ -879,7 +903,7 @@
}
else
{
- cmd = (ch->n_cmd & 0xF00) >> 8;
+ cmd = (ch->n_cmd & 0x0F00) >> 8;
if (cmd == 3 || cmd == 5)
{
setVUMeterHeight(ch);
@@ -1032,7 +1056,7 @@
song->tick++;
bool readNewNote = false;
- if ((unsigned)song->tick >= (unsigned)song->speed)
+ if ((uint32_t)song->tick >= (uint32_t)song->speed)
{
song->tick = 0;
readNewNote = true;
--- a/src/pt2_sampling.c
+++ b/src/pt2_sampling.c
@@ -3,7 +3,7 @@
** have the proper knowledge on this stuff.
**
** Some functions like sin() may be different depending on
-** math library implementation, but we don't use pt_math.c
+** math library implementation, but we don't use pt2_math.c
** replacements for speed reasons.
*/
--- a/src/pt2_structs.h
+++ b/src/pt2_structs.h
@@ -267,4 +267,4 @@
extern cursor_t cursor;
extern ui_t ui;
-extern module_t *song; // pt_main.c
+extern module_t *song; // pt2_main.c
--- a/src/pt2_tables.c
+++ b/src/pt2_tables.c
@@ -86,13 +86,6 @@
0xB4, 0xA1, 0x8D, 0x78, 0x61, 0x4A, 0x31, 0x18
};
-const uint8_t arpTickTable[32] =
-{
- 0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,
- 0,1,2,0,1,2,0,1,2,0,1,2,0,1,2,
- 0,1
-};
-
const int16_t periodTable[(37*16)+15] =
{
856,808,762,720,678,640,604,570,538,508,480,453,
--- a/src/pt2_tables.h
+++ b/src/pt2_tables.h
@@ -14,7 +14,6 @@
extern const char *noteNames3[2+36];
extern const char *noteNames4[2+36];
extern const uint8_t vibratoTable[32];
-extern const uint8_t arpTickTable[32];
extern const int16_t periodTable[(37*16)+15];
extern int8_t pNoteTable[32];
extern const uint64_t musicTimeTab64[256-32];
--- a/src/pt2_visuals.c
+++ b/src/pt2_visuals.c
@@ -2111,7 +2111,7 @@
renderSprites();
SDL_UpdateTexture(video.texture, NULL, video.frameBuffer, SCREEN_W * sizeof (int32_t));
- // SDL2 bug on Windows (?): This function consumes ever-increasing memory if the program is minimized
+ // SDL 2.0.14 bug on Windows (?): This function consumes ever-increasing memory if the program is minimized
if (!minimized)
SDL_RenderClear(video.renderer);
--- a/vs2019_project/pt2-clone/protracker.ini
+++ b/vs2019_project/pt2-clone/protracker.ini
@@ -203,12 +203,21 @@
;
DOTTEDCENTER=TRUE
+; Disable the Karplus-Strong (E8x) ProTracker replayer effect
+; Syntax: TRUE or FALSE
+; Default value: FALSE
+; Comment: This ProTracker command low-pass filters the current sample.
+; It's a little used effect despite being present in original PT,
+; and it was often replaced for syncing visuals with the music in
+; demos. You can turn it off if you need to.
+;
+DISABLE_E8X=FALSE
+
[AUDIO SETTINGS]
; Audio output frequency
; Syntax: Number, in hertz
; Default value: 48000
-; Comment: Ranges from 44100 to 192000. 96000 is recommended if your
-; OS is set to mix shared audio at 96kHz or higher.
+; Comment: Ranges from 44100 to 192000. Also applies to MOD2WAV.
;
FREQUENCY=48000