shithub: ft²

Download patch

ref: f0cc39bf0859cec692fbad1431b2c176fb14b4fb
parent: 75f25156e885c667bb15b862c9474c3d60f5e5c2
author: Olav Sørensen <olav.sorensen@live.no>
date: Sun Sep 7 12:00:36 EDT 2025

Pushed v1.98 code

- Re-added audio dithering for 16-bit audio output
- Tracker scopes changed yet again, reverted to original integer-based scaling

--- a/src/ft2_audio.c
+++ b/src/ft2_audio.c
@@ -23,10 +23,13 @@
 #pragma warning(disable: 4996)
 #endif
 
+#define INITIAL_DITHER_SEED 0x12345000
+
 static int32_t smpShiftValue;
-static uint32_t oldAudioFreq, tickTimeLenInt;
+static uint32_t oldAudioFreq, tickTimeLenInt, randSeed = INITIAL_DITHER_SEED;
 static uint64_t tickTimeLenFrac;
-static float fAudioNormalizeMul, fSqrtPanningTable[256+1];
+static float fSqrtPanningTable[256+1];
+static double dAudioNormalizeMul, dPrngStateL, dPrngStateR;
 static voice_t voice[MAX_CHANNELS * 2];
 
 // globalized
@@ -104,7 +107,7 @@
 	if (!bitDepth32Flag)
 		dAmp *= 32768.0;
 
-	fAudioNormalizeMul = (float)dAmp;
+	dAudioNormalizeMul = dAmp;
 }
 
 void decreaseMasterVol(void)
@@ -368,7 +371,7 @@
 		if (status & IS_Vol)
 		{
 			v->fVolume = ch->fFinalVol; // 0.0f .. 1.0f
-			v->scopeVolume = (uint8_t)(v->fVolume * 255.0f);
+			v->scopeVolume = (uint8_t)((ch->fFinalVol * (SCOPE_HEIGHT*4.0f)) + 0.5f);
 		}
 
 		if (status & IS_Pan)
@@ -400,20 +403,47 @@
 	}
 }
 
+void resetAudioDither(void)
+{
+	randSeed = INITIAL_DITHER_SEED;
+	dPrngStateL = dPrngStateR = 0.0;
+}
+
+static inline int32_t random32(void)
+{
+	// LCG 32-bit random
+	randSeed *= 134775813;
+	randSeed++;
+
+	return (int32_t)randSeed;
+}
+
 static void sendSamples16BitStereo(void *stream, uint32_t sampleBlockLength)
 {
+	int32_t out32;
+	double dOut, dPrng;
+
 	int16_t *streamPtr16 = (int16_t *)stream;
 	for (uint32_t i = 0; i < sampleBlockLength; i++)
 	{
-		int32_t L = (int32_t)(audio.fMixBufferL[i] * fAudioNormalizeMul);
-		int32_t R = (int32_t)(audio.fMixBufferR[i] * fAudioNormalizeMul);
+		// left channel - 1-bit triangular dithering
+		dPrng = random32() * (1.0 / (UINT32_MAX+1.0)); // -0.5 .. 0.5
+		dOut = (double)audio.fMixBufferL[i] * dAudioNormalizeMul;
+		dOut = (dOut + dPrng) - dPrngStateL;
+		dPrngStateL = dPrng;
+		out32 = (int32_t)dOut;
+		CLAMP16(out32);
+		*streamPtr16++ = (int16_t)out32;
 
-		CLAMP16(L);
-		CLAMP16(R);
+		// right channel - 1-bit triangular dithering
+		dPrng = random32() * (1.0 / (UINT32_MAX+1.0)); // -0.5 .. 0.5
+		dOut = (double)audio.fMixBufferR[i] * dAudioNormalizeMul;
+		dOut = (dOut + dPrng) - dPrngStateR;
+		dPrngStateR = dPrng;
+		out32 = (int32_t)dOut;
+		CLAMP16(out32);
+		*streamPtr16++ = (int16_t)out32;
 
-		*streamPtr16++ = (int16_t)L;
-		*streamPtr16++ = (int16_t)R;
-
 		// clear what we read from the mixing buffer
 		audio.fMixBufferL[i] = audio.fMixBufferR[i] = 0.0f;
 	}
@@ -421,14 +451,20 @@
 
 static void sendSamples32BitFloatStereo(void *stream, uint32_t sampleBlockLength)
 {
+	double dOut;
+
 	float *fStreamPtr32 = (float *)stream;
 	for (uint32_t i = 0; i < sampleBlockLength; i++)
 	{
-		const float fL = audio.fMixBufferL[i] * fAudioNormalizeMul;
-		const float fR = audio.fMixBufferR[i] * fAudioNormalizeMul;
+		// left channel
+		dOut = (double)audio.fMixBufferL[i] * dAudioNormalizeMul;
+		dOut = CLAMP(dOut, -1.0, 1.0);
+		*fStreamPtr32++ = (float)dOut;
 
-		*fStreamPtr32++ = CLAMP(fL, -1.0f, 1.0f);
-		*fStreamPtr32++ = CLAMP(fR, -1.0f, 1.0f);
+		// right channel
+		dOut = (double)audio.fMixBufferR[i] * dAudioNormalizeMul;
+		dOut = CLAMP(dOut, -1.0, 1.0);
+		*fStreamPtr32++ = (float)dOut;
 
 		// clear what we read from the mixing buffer
 		audio.fMixBufferL[i] = audio.fMixBufferR[i] = 0.0f;
@@ -794,12 +830,17 @@
 static void SDLCALL audioCallback(void *userdata, Uint8 *stream, int len)
 {
 	if (editor.wavIsRendering)
+	{
+		memset(stream, 0, len);
 		return;
+	}
 
 	len >>= smpShiftValue; // bytes -> samples
 	if (len <= 0)
 		return;
 
+	audio.callbackOngoing = true;
+
 	int32_t bufferPosition = 0;
 
 	uint32_t samplesLeft = len;
@@ -845,6 +886,8 @@
 	else
 		sendSamples32BitFloatStereo(stream, len);
 
+	audio.callbackOngoing = false;
+
 	(void)userdata;
 }
 
@@ -1064,4 +1107,6 @@
 	}
 
 	freeAudioBuffers();
+
+	audio.callbackOngoing = false;
 }
--- a/src/ft2_audio.h
+++ b/src/ft2_audio.h
@@ -35,7 +35,7 @@
 {
 	char *currInputDevice, *currOutputDevice, *lastWorkingAudioDeviceName;
 	char *inputDeviceNames[MAX_AUDIO_DEVICES], *outputDeviceNames[MAX_AUDIO_DEVICES];
-	volatile bool locked, resetSyncTickTimeFlag, volumeRampingFlag;
+	volatile bool locked, resetSyncTickTimeFlag, volumeRampingFlag, callbackOngoing;
 	bool linearPeriodsFlag, rescanAudioDevicesSupported, sincInterpolation;
 	volatile uint8_t interpolationType;
 	int32_t inputDeviceNum, outputDeviceNum, lastWorkingAudioFreq, lastWorkingAudioBits;
@@ -140,6 +140,7 @@
 void pauseAudio(void);
 void resumeAudio(void);
 bool setNewAudioSettings(void);
+void resetAudioDither(void);
 void lockAudio(void);
 void unlockAudio(void);
 void lockMixerCallback(void);
--- a/src/ft2_replayer.c
+++ b/src/ft2_replayer.c
@@ -3145,6 +3145,7 @@
 	editor.curPlaySmp = 255;
 
 	stopAllScopes();
+	resetAudioDither();
 
 	// wait for scope thread to finish, making sure pointers aren't illegal
 	while (editor.scopeThreadBusy);
--- a/src/ft2_wav_renderer.c
+++ b/src/ft2_wav_renderer.c
@@ -201,7 +201,10 @@
 	if (wavRenderBuffer == NULL)
 		return false;
 
+	// wait for main audio callback to catch WAV render flag
 	editor.wavIsRendering = true;
+	while (audio.callbackOngoing)
+		SDL_Delay(5);
 
 	setPos(songPos, 0, true);
 	playMode = PLAYMODE_SONG;
--- a/src/scopes/ft2_scope_macros.h
+++ b/src/scopes/ft2_scope_macros.h
@@ -25,13 +25,11 @@
 
 #define LINED_SCOPE_INIT \
 	SCOPE_INIT \
-	float fSample; \
 	int32_t smpY1, smpY2; \
 	width--;
 
 #define LINED_SCOPE_INIT_BIDI \
 	SCOPE_INIT_BIDI \
-	float fSample; \
 	int32_t smpY1, smpY2; \
 	width--;
 
@@ -41,41 +39,41 @@
 
 #define NEAREST_NEIGHGBOR8 \
 { \
-	fSample = s8[0]; \
+	sample = s8[0] << 8; \
 } \
 
 #define LINEAR_INTERPOLATION8(frac) \
 { \
-	const float f = (frac) * (1.0f / SCOPE_FRAC_SCALE); \
-	fSample = s8[0] + ((s8[1] - s8[0]) * f); \
+	const int32_t f = (frac) >> (SCOPE_FRAC_BITS-15); \
+	sample = (s8[0] << 8) + ((((s8[1] - s8[0]) << 8) * f) >> 15); \
 } \
 
 #define NEAREST_NEIGHGBOR16 \
 { \
-	fSample = s16[0]; \
+	sample = s16[0]; \
 } \
 
 #define LINEAR_INTERPOLATION16(frac) \
 { \
-	const float f = (frac) * (1.0f / SCOPE_FRAC_SCALE); \
-	fSample = s16[0] + ((s16[1] - s16[0]) * f); \
+	const int32_t f = (frac) >> (SCOPE_FRAC_BITS-15); \
+	sample = s16[0] + (((s16[1] - s16[0]) * f) >> 15); \
 } \
 
 #define CUBIC_SMP8(frac) \
-	const float *t = fScopeIntrpLUT + (((frac) >> (SCOPE_FRAC_BITS-SCOPE_INTRP_PHASES_BITS)) << SCOPE_INTRP_WIDTH_BITS); \
+	const int16_t *t = scopeIntrpLUT + (((frac) >> (SCOPE_FRAC_BITS-SCOPE_INTRP_PHASES_BITS)) << SCOPE_INTRP_WIDTH_BITS); \
 	\
-	fSample = (s8[-1] * t[0]) + \
+	sample = ((s8[-1] * t[0]) + \
 	          ( s8[0] * t[1]) + \
 	          ( s8[1] * t[2]) + \
-	          ( s8[2] * t[3]);
+	          ( s8[2] * t[3])) >> (SCOPE_INTRP_SCALE_BITS-8);
 
 #define CUBIC_SMP16(frac) \
-	const float *t = fScopeIntrpLUT + (((frac) >> (SCOPE_FRAC_BITS-SCOPE_INTRP_PHASES_BITS)) << SCOPE_INTRP_WIDTH_BITS); \
+	const int16_t *t = scopeIntrpLUT + (((frac) >> (SCOPE_FRAC_BITS-SCOPE_INTRP_PHASES_BITS)) << SCOPE_INTRP_WIDTH_BITS); \
 	\
-	fSample = (s16[-1] * t[0]) + \
+	sample = ((s16[-1] * t[0]) + \
 	          ( s16[0] * t[1]) + \
 	          ( s16[1] * t[2]) + \
-	          ( s16[2] * t[3]);
+	          ( s16[2] * t[3])) >> SCOPE_INTRP_SCALE_BITS;
 
 #define CUBIC_INTERPOLATION8(frac) \
 { \
@@ -111,7 +109,7 @@
 		LINEAR_INTERPOLATION8(frac) \
 	else \
 		CUBIC_INTERPOLATION8(frac) \
-	sample = (int32_t)((fSample * s->fVolume8) - 0.5f);
+	sample = (sample * s->volume) >> (16+2);
 
 #define INTERPOLATE_SMP16(pos, frac) \
 	const int16_t *s16 = s->base16 + pos; \
@@ -121,7 +119,7 @@
 		LINEAR_INTERPOLATION16(frac) \
 	else \
 		CUBIC_INTERPOLATION16(frac) \
-	sample = (int32_t)((fSample * s->fVolume16) - 0.5f);
+	sample = (sample * s->volume) >> (16+2);
 
 #define INTERPOLATE_SMP8_LOOP(pos, frac) \
 	const int8_t *s8 = s->base8 + pos; \
@@ -131,7 +129,7 @@
 		LINEAR_INTERPOLATION8(frac) \
 	else \
 		CUBIC_INTERPOLATION8_LOOP(pos, frac) \
-	sample = (int32_t)((fSample * s->fVolume8) - 0.5f);
+	sample = (sample * s->volume) >> (16+2);
 
 #define INTERPOLATE_SMP16_LOOP(pos, frac) \
 	const int16_t *s16 = s->base16 + pos; \
@@ -141,17 +139,17 @@
 		LINEAR_INTERPOLATION16(frac) \
 	else \
 		CUBIC_INTERPOLATION16_LOOP(pos, frac) \
-	sample = (int32_t)((fSample * s->fVolume16) - 0.5f);
+	sample = (sample * s->volume) >> (16+2);
 
 #define SCOPE_GET_SMP8 \
 	if (s->active) \
-		sample = (int32_t)((s->base8[position] * s->fVolume8) - 0.5f); \
+		sample = (s->base8[position] * s->volume) >> (8+2); \
 	else \
 		sample = 0;
 
 #define SCOPE_GET_SMP16 \
 	if (s->active) \
-		sample = (int32_t)((s->base16[position] * s->fVolume16) - 0.5f); \
+		sample = (s->base16[position] * s->volume) >> (16+2); \
 	else \
 		sample = 0;
 
@@ -159,7 +157,7 @@
 	if (s->active) \
 	{ \
 		GET_BIDI_POSITION \
-		sample = (int32_t)((s->base8[actualPos] * s->fVolume8) - 0.5f); \
+		sample = (s->base8[actualPos] * s->volume) >> (8+2); \
 	} \
 	else \
 	{ \
@@ -170,7 +168,7 @@
 	if (s->active) \
 	{ \
 		GET_BIDI_POSITION \
-		sample = (int32_t)((s->base16[actualPos] * s->fVolume16) - 0.5f); \
+		sample = (s->base16[actualPos] * s->volume) >> (16+2); \
 	} \
 	else \
 	{ \
--- a/src/scopes/ft2_scopedraw.c
+++ b/src/scopes/ft2_scopedraw.c
@@ -2,6 +2,7 @@
 #include <stdint.h>
 #include <stdbool.h>
 #include <stdlib.h>
+#include <math.h>
 #include "../ft2_config.h"
 #include "../ft2_video.h"
 #include "../ft2_palette.h"
@@ -9,14 +10,14 @@
 #include "ft2_scopedraw.h"
 #include "ft2_scope_macros.h"
 
-static float *fScopeIntrpLUT;
+static int16_t *scopeIntrpLUT;
 
 static void scopeLine(int32_t x1, int32_t y1, int32_t y2, const uint32_t color);
 
 bool calcScopeIntrpLUT(void)
 {
-	fScopeIntrpLUT = (float *)malloc(SCOPE_INTRP_WIDTH * SCOPE_INTRP_PHASES * sizeof (float));
-	if (fScopeIntrpLUT == NULL)
+	scopeIntrpLUT = (int16_t *)malloc(SCOPE_INTRP_WIDTH * SCOPE_INTRP_PHASES * sizeof (int16_t));
+	if (scopeIntrpLUT == NULL)
 		return false;
 
 	/* Several tests have been done to figure out what interpolation method is most suitable
@@ -26,7 +27,8 @@
 	*/
 
 	// 4-point cubic B-spline (no overshoot)
-	float *fPtr = fScopeIntrpLUT;
+
+	int16_t *ptr16 = scopeIntrpLUT;
 	for (int32_t i = 0; i < SCOPE_INTRP_PHASES; i++)
 	{
 		const double x1 = i * (1.0 / SCOPE_INTRP_PHASES);
@@ -38,10 +40,11 @@
 		const double t3 = (x1 *  (1.0/2.0)) + (x2 * (1.0/2.0)) + (x3 * -(1.0/2.0)) + (1.0/6.0);
 		const double t4 =                                         x3 *  (1.0/6.0);
 
-		*fPtr++ = (float)t1; // tap #1 at sample offset -1
-		*fPtr++ = (float)t2; // tap #2 at sample offset  0 (center)
-		*fPtr++ = (float)t3; // tap #3 at sample offset  1
-		*fPtr++ = (float)t4; // tap #4 at sample offset  2
+		// truncate, do not round!
+		*ptr16++ = (int16_t)(t1 * SCOPE_INTRP_SCALE); // tap #1 at sample offset -1
+		*ptr16++ = (int16_t)(t2 * SCOPE_INTRP_SCALE); // tap #2 at sample offset  0 (center)
+		*ptr16++ = (int16_t)(t3 * SCOPE_INTRP_SCALE); // tap #3 at sample offset  1
+		*ptr16++ = (int16_t)(t4 * SCOPE_INTRP_SCALE); // tap #4 at sample offset  2
 	}
 
 	return true;
@@ -49,10 +52,10 @@
 
 void freeScopeIntrpLUT(void)
 {
-	if (fScopeIntrpLUT != NULL)
+	if (scopeIntrpLUT != NULL)
 	{
-		free(fScopeIntrpLUT);
-		fScopeIntrpLUT = NULL;
+		free(scopeIntrpLUT);
+		scopeIntrpLUT = NULL;
 	}
 }
 
--- a/src/scopes/ft2_scopes.c
+++ b/src/scopes/ft2_scopes.c
@@ -428,7 +428,7 @@
 		}
 
 		volatile scope_t s = scope[i]; // cache scope to lower thread race condition issues
-		if (s.active && s.fVolume16 > 0.0f && !audio.locked)
+		if (s.active && s.volume > 0 && !audio.locked)
 		{
 			// scope is active
 			scope[i].wasCleared = false;
@@ -486,10 +486,7 @@
 		const uint8_t status = scopeUpdateStatus[i];
 
 		if (status & IS_Vol)
-		{
-			sc->fVolume8  = ch->scopeVolume * (((SCOPE_HEIGHT/2.0f) / 255.0f) /   128.0f);
-			sc->fVolume16 = ch->scopeVolume * (((SCOPE_HEIGHT/2.0f) / 255.0f) / 32768.0f);
-		}
+			sc->volume = ch->scopeVolume;
 
 		if (status & IS_Period)
 		{
--- a/src/scopes/ft2_scopes.h
+++ b/src/scopes/ft2_scopes.h
@@ -19,6 +19,8 @@
 
 #define SCOPE_INTRP_WIDTH 4
 #define SCOPE_INTRP_WIDTH_BITS 2 /* log2(SCOPE_INTRP_WIDTH) */
+#define SCOPE_INTRP_SCALE 32768
+#define SCOPE_INTRP_SCALE_BITS 15 /* log2(SCOPE_INTRP_SCALE) */
 #define SCOPE_INTRP_PHASES 512 /* enough for the scopes */
 #define SCOPE_INTRP_PHASES_BITS 9 /* log2(SCOPE_INTRP_PHASES) */
 
@@ -38,9 +40,9 @@
 	const int16_t *base16;
 	bool wasCleared, sample16Bit, samplingBackwards, hasLooped;
 	uint8_t loopType;
+	int16_t volume;
 	int32_t loopStart, loopLength, loopEnd, sampleEnd, position;
 	uint64_t delta, drawDelta, positionFrac;
-	float fVolume8, fVolume16;
 
 	// if (loopEnabled && hasLooped && samplingPos <= loopStart+MAX_LEFT_TAPS) readFixedTapsFromThisPointer();
 	const int8_t *leftEdgeTaps8;
--