803 lines
27 KiB
C
803 lines
27 KiB
C
// Doom audio mixes PureDOOM SFX with optional SoundFont MIDI into one native render stream.
|
|
|
|
#include <stdint.h>
|
|
#include <stddef.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <intrin.h>
|
|
#include <ntlite/syscall.h>
|
|
|
|
#include "doom_audio.h"
|
|
#include "PureDOOM.h"
|
|
|
|
#define DOOM_AUDIO_SAMPLE_RATE DOOM_SAMPLERATE
|
|
#define DOOM_AUDIO_CHANNELS 2u
|
|
#define DOOM_AUDIO_BLOCK_FRAMES 512u
|
|
#define DOOM_AUDIO_WRITE_QUANTUM_FRAMES 256u
|
|
#define DOOM_AUDIO_PREFILL_FRAMES DOOM_AUDIO_BLOCK_FRAMES
|
|
#define DOOM_AUDIO_REFILL_WATERMARK_FRAMES DOOM_AUDIO_WRITE_QUANTUM_FRAMES
|
|
#define DOOM_AUDIO_MIDI_BATCH_MESSAGES 256u
|
|
#define DOOM_AUDIO_SOUNDFONT_PATH "doom.sf2"
|
|
#define DOOM_AUDIO_MAX_SOUNDFONT_BYTES (64u * 1024u * 1024u)
|
|
#define DOOM_AUDIO_WAIT_MILLISECONDS 100u
|
|
#define DOOM_AUDIO_WORKER_PRIORITY 2u
|
|
#define DOOM_AUDIO_SYNTH_LOADER_PRIORITY 0u
|
|
#define DOOM_AUDIO_MAX_VOICES 128
|
|
#define DOOM_AUDIO_MUSIC_GAIN 1.0f
|
|
|
|
// Computes a natural logarithm for the positive finite values used by TinySoundFont.
|
|
static double DoomAudioLog(double value) {
|
|
union {
|
|
double Value;
|
|
uint64_t Bits;
|
|
} bits;
|
|
const double ln2 = 0.69314718055994530942;
|
|
int exponent;
|
|
double mantissa;
|
|
double ratio;
|
|
double ratio2;
|
|
double term;
|
|
double sum;
|
|
|
|
if (value <= 0.0)
|
|
return -1.0e300;
|
|
|
|
bits.Value = value;
|
|
exponent = (int)((bits.Bits >> 52) & 0x7FFu) - 1023;
|
|
bits.Bits = (bits.Bits & UINT64_C(0x000FFFFFFFFFFFFF)) |
|
|
UINT64_C(0x3FF0000000000000);
|
|
mantissa = bits.Value;
|
|
ratio = (mantissa - 1.0) / (mantissa + 1.0);
|
|
ratio2 = ratio * ratio;
|
|
term = ratio;
|
|
sum = term;
|
|
term *= ratio2;
|
|
sum += term / 3.0;
|
|
term *= ratio2;
|
|
sum += term / 5.0;
|
|
term *= ratio2;
|
|
sum += term / 7.0;
|
|
term *= ratio2;
|
|
sum += term / 9.0;
|
|
term *= ratio2;
|
|
sum += term / 11.0;
|
|
term *= ratio2;
|
|
sum += term / 13.0;
|
|
return (double)exponent * ln2 + 2.0 * sum;
|
|
}
|
|
|
|
// Scales a normal double by an integral power of two without a runtime math library.
|
|
static double DoomAudioScalePowerOfTwo(double value, int exponent) {
|
|
union {
|
|
double Value;
|
|
uint64_t Bits;
|
|
} scale;
|
|
|
|
if (exponent > 1023)
|
|
return 1.0e300;
|
|
if (exponent < -1022)
|
|
return 0.0;
|
|
|
|
scale.Bits = (uint64_t)(exponent + 1023) << 52;
|
|
return value * scale.Value;
|
|
}
|
|
|
|
// Computes an exponential over the bounded ranges used by TinySoundFont envelopes.
|
|
static double DoomAudioExp(double value) {
|
|
const double ln2 = 0.69314718055994530942;
|
|
const double inverseLn2 = 1.4426950408889634074;
|
|
int exponent;
|
|
double reduced;
|
|
double result;
|
|
|
|
if (value >= 700.0)
|
|
return 1.0e300;
|
|
if (value <= -700.0)
|
|
return 0.0;
|
|
|
|
exponent = (int)(value * inverseLn2 + (value >= 0.0 ? 0.5 : -0.5));
|
|
reduced = value - (double)exponent * ln2;
|
|
result = 1.0 + reduced *
|
|
(1.0 + reduced *
|
|
(0.5 + reduced *
|
|
(0.16666666666666666667 + reduced *
|
|
(0.04166666666666666667 + reduced *
|
|
(0.00833333333333333333 + reduced *
|
|
(0.00138888888888888889 + reduced *
|
|
0.00019841269841269841))))));
|
|
return DoomAudioScalePowerOfTwo(result, exponent);
|
|
}
|
|
|
|
// Computes a positive real power for TinySoundFont frequency and gain conversion.
|
|
static double DoomAudioPow(double base, double exponent) {
|
|
if (base <= 0.0)
|
|
return 0.0;
|
|
return DoomAudioExp(exponent * DoomAudioLog(base));
|
|
}
|
|
|
|
// Computes the float exponential required by TinySoundFont envelopes.
|
|
static float DoomAudioExpf(float value) {
|
|
return (float)DoomAudioExp((double)value);
|
|
}
|
|
|
|
// Computes the float power required by TinySoundFont frequency and gain conversion.
|
|
static float DoomAudioPowf(float base, float exponent) {
|
|
return (float)DoomAudioPow((double)base, (double)exponent);
|
|
}
|
|
|
|
// Computes base-ten logarithms from the shared natural logarithm approximation.
|
|
static double DoomAudioLog10(double value) {
|
|
return DoomAudioLog(value) * 0.43429448190325182765;
|
|
}
|
|
|
|
// Computes square roots with a bit-derived seed and stable Newton refinement.
|
|
static double DoomAudioSqrt(double value) {
|
|
union {
|
|
double Value;
|
|
uint64_t Bits;
|
|
} estimate;
|
|
|
|
if (value <= 0.0)
|
|
return 0.0;
|
|
|
|
estimate.Value = value;
|
|
estimate.Bits = (estimate.Bits >> 1) + UINT64_C(0x1FF8000000000000);
|
|
for (int iteration = 0; iteration < 6; iteration++)
|
|
estimate.Value = 0.5 * (estimate.Value + value / estimate.Value);
|
|
return estimate.Value;
|
|
}
|
|
|
|
// Computes the float square root required by TinySoundFont panning.
|
|
static float DoomAudioSqrtf(float value) {
|
|
return (float)DoomAudioSqrt((double)value);
|
|
}
|
|
|
|
// Computes tangent accurately over TinySoundFont's normalized low-pass range.
|
|
static double DoomAudioTan(double value) {
|
|
double squared = value * value;
|
|
double sine = value *
|
|
(1.0 + squared *
|
|
(-0.16666666666666666667 + squared *
|
|
(0.00833333333333333333 + squared *
|
|
(-0.00019841269841269841 + squared *
|
|
(0.00000275573192239859 + squared *
|
|
(-0.00000002505210838544 + squared *
|
|
0.00000000016059043837))))));
|
|
double cosine = 1.0 + squared *
|
|
(-0.5 + squared *
|
|
(0.04166666666666666667 + squared *
|
|
(-0.00138888888888888889 + squared *
|
|
(0.00002480158730158730 + squared *
|
|
(-0.00000027557319223986 + squared *
|
|
(0.00000000208767569879 + squared *
|
|
-0.00000000001147074560))))));
|
|
|
|
if (cosine > -1.0e-12 && cosine < 1.0e-12)
|
|
cosine = cosine < 0.0 ? -1.0e-12 : 1.0e-12;
|
|
return sine / cosine;
|
|
}
|
|
|
|
#pragma warning(push)
|
|
#pragma warning(disable: 4244 4267 4706)
|
|
#define TSF_NO_STDIO
|
|
#define TSF_MALLOC malloc
|
|
#define TSF_REALLOC realloc
|
|
#define TSF_FREE free
|
|
#define TSF_MEMCPY memcpy
|
|
#define TSF_MEMSET memset
|
|
#define TSF_POW DoomAudioPow
|
|
#define TSF_POWF DoomAudioPowf
|
|
#define TSF_EXPF DoomAudioExpf
|
|
#define TSF_LOG DoomAudioLog
|
|
#define TSF_TAN DoomAudioTan
|
|
#define TSF_LOG10 DoomAudioLog10
|
|
#define TSF_SQRT DoomAudioSqrt
|
|
#define TSF_SQRTF DoomAudioSqrtf
|
|
#define TSF_IMPLEMENTATION
|
|
#include "third_party/tsf.h"
|
|
#pragma warning(pop)
|
|
|
|
typedef struct DOOM_AUDIO_STATE {
|
|
uint32_t CoreMutex;
|
|
uint32_t StreamHandle;
|
|
uint32_t WorkerHandle;
|
|
uint32_t WorkerId;
|
|
uint32_t SynthLoaderHandle;
|
|
uint32_t SynthLoaderId;
|
|
volatile long StopRequested;
|
|
tsf* volatile Synth;
|
|
} DOOM_AUDIO_STATE;
|
|
|
|
static DOOM_AUDIO_STATE g_doomAudio;
|
|
|
|
// Writes one complete diagnostic line to the process console.
|
|
static void DoomAudioPrint(const char* text) {
|
|
SYS_WRITE_CONSOLE_REQUEST request = { SysSvFromCstr(text) };
|
|
(void)SysWriteConsole(&request);
|
|
}
|
|
|
|
// Opens the default available native render endpoint.
|
|
static KSTATUS DoomAudioFindEndpoint(uint64_t* endpointIdOut) {
|
|
SYS_AUDIO_ENDPOINT_INFO endpoints[SYS_AUDIO_MAX_ENDPOINTS];
|
|
SYS_AUDIO_ENUMERATE_ENDPOINTS_REQUEST request;
|
|
uint32_t count = 0;
|
|
const SYS_AUDIO_ENDPOINT_INFO* fallback = NULL;
|
|
KSTATUS status;
|
|
|
|
if (!endpointIdOut)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
*endpointIdOut = 0;
|
|
|
|
memset(endpoints, 0, sizeof(endpoints));
|
|
memset(&request, 0, sizeof(request));
|
|
request.Size = sizeof(request);
|
|
request.Endpoints = endpoints;
|
|
request.Capacity = SYS_AUDIO_MAX_ENDPOINTS;
|
|
request.CountOut = &count;
|
|
status = SysAudioEnumerateEndpoints(&request);
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
return status;
|
|
if (count > SYS_AUDIO_MAX_ENDPOINTS)
|
|
count = SYS_AUDIO_MAX_ENDPOINTS;
|
|
|
|
for (uint32_t index = 0; index < count; index++) {
|
|
const SYS_AUDIO_ENDPOINT_INFO* endpoint = &endpoints[index];
|
|
if (endpoint->State != SysAudioEndpointReady ||
|
|
(endpoint->Flags & SYS_AUDIO_ENDPOINT_FLAG_AVAILABLE) == 0)
|
|
continue;
|
|
if (!fallback)
|
|
fallback = endpoint;
|
|
if (endpoint->Flags & SYS_AUDIO_ENDPOINT_FLAG_DEFAULT) {
|
|
*endpointIdOut = endpoint->EndpointId;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
}
|
|
|
|
if (!fallback)
|
|
return NTLSTATUS_NOT_FOUND;
|
|
*endpointIdOut = fallback->EndpointId;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Returns the currently published synthesizer without taking the PureDOOM lock.
|
|
static tsf* DoomAudioGetSynth(void) {
|
|
return (tsf*)_InterlockedCompareExchangePointer(
|
|
(void* volatile*)&g_doomAudio.Synth, NULL, NULL);
|
|
}
|
|
|
|
// Maps the optional SoundFont so TinySoundFont can parse it without a separate input copy.
|
|
static KSTATUS DoomAudioMapSoundFont(void** dataOut, uint32_t* sizeOut) {
|
|
uint32_t fileHandle = 0;
|
|
uint32_t mappingHandle = 0;
|
|
uint64_t fileSize = 0;
|
|
void* base = NULL;
|
|
KSTATUS status;
|
|
|
|
if (!dataOut || !sizeOut)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
*dataOut = NULL;
|
|
*sizeOut = 0;
|
|
|
|
SYS_OPEN_FILE_REQUEST open = SysMakeOpenFileRequest(
|
|
SysSvFromCstr(DOOM_AUDIO_SOUNDFONT_PATH),
|
|
SYS_FILE_ACCESS_QUERY | SYS_FILE_ACCESS_READ,
|
|
SYS_FILE_OPEN_EXISTING,
|
|
SYS_FILE_FLAG_NONE,
|
|
&fileHandle);
|
|
status = SysOpenFile(&open);
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
return status;
|
|
|
|
status = SysGetFileSize(fileHandle, &fileSize);
|
|
if (status != NTLSTATUS_SUCCESS || fileSize == 0 ||
|
|
fileSize > DOOM_AUDIO_MAX_SOUNDFONT_BYTES ||
|
|
fileSize > INT32_MAX) {
|
|
(void)SysCloseHandle(fileHandle);
|
|
return status == NTLSTATUS_SUCCESS ? NTLSTATUS_INVALID_PARAMETER : status;
|
|
}
|
|
|
|
status = SysCreateFileMapping(fileHandle, SYS_VM_READ, SYS_MAP_FLAG_NONE,
|
|
fileSize, &mappingHandle);
|
|
if (status != NTLSTATUS_SUCCESS || !mappingHandle) {
|
|
(void)SysCloseHandle(fileHandle);
|
|
return status == NTLSTATUS_SUCCESS ? NTLSTATUS_FAILURE : status;
|
|
}
|
|
|
|
status = SysMapFile(mappingHandle, 0, (size_t)fileSize,
|
|
SYS_MAP_ACCESS_READ, SYS_MAP_FLAG_NONE, &base);
|
|
(void)SysCloseHandle(mappingHandle);
|
|
(void)SysCloseHandle(fileHandle);
|
|
if (status != NTLSTATUS_SUCCESS || !base)
|
|
return status == NTLSTATUS_SUCCESS ? NTLSTATUS_FAILURE : status;
|
|
|
|
*dataOut = base;
|
|
*sizeOut = (uint32_t)fileSize;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Configures one parsed General MIDI synthesizer for PureDOOM's native sample rate.
|
|
static KSTATUS DoomAudioConfigureSynth(tsf* synth) {
|
|
if (!synth)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
|
|
tsf_set_output(synth, TSF_STEREO_INTERLEAVED,
|
|
(int)DOOM_AUDIO_SAMPLE_RATE, 0.0f);
|
|
if (!tsf_set_max_voices(synth, DOOM_AUDIO_MAX_VOICES))
|
|
return NTLSTATUS_NO_MEMORY;
|
|
|
|
for (int channel = 0; channel < 16; channel++) {
|
|
if (!tsf_channel_set_presetnumber(synth, channel, 0, channel == 9))
|
|
return NTLSTATUS_FAILURE;
|
|
}
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Loads optional MIDI state at low priority without blocking Doom or SFX startup.
|
|
static void DoomAudioSynthLoader(uintptr_t argument) {
|
|
uint32_t soundFontSize = 0;
|
|
void* soundFontBase = NULL;
|
|
tsf* synth = NULL;
|
|
KSTATUS status;
|
|
(void)argument;
|
|
|
|
status = DoomAudioMapSoundFont(&soundFontBase, &soundFontSize);
|
|
if (status == NTLSTATUS_NOT_FOUND) {
|
|
DoomAudioPrint("doom: doom.sf2 unavailable; continuing with sound effects only\n");
|
|
SysExitThread(0);
|
|
}
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: doom.sf2 could not be mapped; continuing with sound effects only\n");
|
|
SysExitThread(0);
|
|
}
|
|
|
|
if (_InterlockedCompareExchange(&g_doomAudio.StopRequested, 0, 0)) {
|
|
(void)SysFreeVm(soundFontBase);
|
|
SysExitThread(0);
|
|
}
|
|
|
|
synth = tsf_load_memory(soundFontBase, (int)soundFontSize);
|
|
(void)SysFreeVm(soundFontBase);
|
|
if (!synth) {
|
|
DoomAudioPrint("doom: doom.sf2 is invalid or synth allocation failed; continuing with sound effects only\n");
|
|
SysExitThread(0);
|
|
}
|
|
|
|
status = DoomAudioConfigureSynth(synth);
|
|
if (status != NTLSTATUS_SUCCESS ||
|
|
_InterlockedCompareExchange(&g_doomAudio.StopRequested, 0, 0)) {
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
DoomAudioPrint("doom: MIDI synthesizer initialization failed; continuing with sound effects only\n");
|
|
tsf_close(synth);
|
|
SysExitThread(0);
|
|
}
|
|
|
|
(void)_InterlockedExchangePointer(
|
|
(void* volatile*)&g_doomAudio.Synth, synth);
|
|
DoomAudioPrint("doom: MIDI synthesizer ready\n");
|
|
SysExitThread(0);
|
|
}
|
|
|
|
// Applies one packed WinMM-style short MIDI message to TinySoundFont.
|
|
static void DoomAudioDispatchMidi(tsf* synth, uint32_t message) {
|
|
uint8_t status = (uint8_t)(message & 0xFFu);
|
|
uint8_t data1 = (uint8_t)((message >> 8) & 0x7Fu);
|
|
uint8_t data2 = (uint8_t)((message >> 16) & 0x7Fu);
|
|
int channel = status & 0x0F;
|
|
|
|
if (!synth)
|
|
return;
|
|
|
|
switch (status & 0xF0) {
|
|
case 0x80:
|
|
tsf_channel_note_off(synth, channel, data1);
|
|
break;
|
|
case 0x90:
|
|
if (data2)
|
|
(void)tsf_channel_note_on(synth, channel, data1,
|
|
(float)data2 / 127.0f);
|
|
else
|
|
tsf_channel_note_off(synth, channel, data1);
|
|
break;
|
|
case 0xB0:
|
|
(void)tsf_channel_midi_control(synth, channel, data1, data2);
|
|
break;
|
|
case 0xC0:
|
|
(void)tsf_channel_set_presetnumber(synth, channel, data1,
|
|
channel == 9);
|
|
break;
|
|
case 0xE0:
|
|
(void)tsf_channel_set_pitchwheel(synth, channel,
|
|
(int)data1 | ((int)data2 << 7));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Advances one PureDOOM MIDI tick and dispatches every event outside the core lock.
|
|
static void DoomAudioTickMidi(tsf* synth) {
|
|
uint32_t messages[DOOM_AUDIO_MIDI_BATCH_MESSAGES];
|
|
int tickComplete = 0;
|
|
|
|
if (!synth)
|
|
return;
|
|
|
|
while (!tickComplete) {
|
|
uint32_t count = 0;
|
|
|
|
DoomAudioLockCore();
|
|
while (count < DOOM_AUDIO_MIDI_BATCH_MESSAGES) {
|
|
uint32_t message = (uint32_t)doom_tick_midi();
|
|
if (!message) {
|
|
tickComplete = 1;
|
|
break;
|
|
}
|
|
messages[count++] = message;
|
|
}
|
|
DoomAudioUnlockCore();
|
|
|
|
for (uint32_t index = 0; index < count; index++)
|
|
DoomAudioDispatchMidi(synth, messages[index]);
|
|
}
|
|
}
|
|
|
|
// Copies one complete PureDOOM SFX block while holding the shared core lock.
|
|
static void DoomAudioReadSfxBlock(int16_t* frames) {
|
|
short* source;
|
|
|
|
DoomAudioLockCore();
|
|
source = doom_get_sound_buffer();
|
|
memcpy(frames, source,
|
|
DOOM_AUDIO_BLOCK_FRAMES * DOOM_AUDIO_CHANNELS * sizeof(int16_t));
|
|
DoomAudioUnlockCore();
|
|
}
|
|
|
|
// Mixes rendered floating-point music into signed 16-bit PureDOOM SFX.
|
|
static void DoomAudioMixMusic(int16_t* destination,
|
|
const float* music,
|
|
uint32_t frameOffset,
|
|
uint32_t frameCount) {
|
|
uint32_t firstSample = frameOffset * DOOM_AUDIO_CHANNELS;
|
|
uint32_t sampleCount = frameCount * DOOM_AUDIO_CHANNELS;
|
|
|
|
for (uint32_t index = 0; index < sampleCount; index++) {
|
|
float scaled = music[index] * (32767.0f * DOOM_AUDIO_MUSIC_GAIN);
|
|
int32_t synthSample;
|
|
int32_t mixed;
|
|
|
|
if (scaled > 32767.0f)
|
|
synthSample = 32767;
|
|
else if (scaled < -32768.0f)
|
|
synthSample = -32768;
|
|
else
|
|
synthSample = (int32_t)scaled;
|
|
|
|
mixed = (int32_t)destination[firstSample + index] + synthSample;
|
|
if (mixed > 32767)
|
|
mixed = 32767;
|
|
else if (mixed < -32768)
|
|
mixed = -32768;
|
|
destination[firstSample + index] = (int16_t)mixed;
|
|
}
|
|
}
|
|
|
|
// Renders MIDI at exactly 140 ticks per second into one native PureDOOM block.
|
|
static void DoomAudioRenderMusic(tsf* synth,
|
|
int16_t* frames,
|
|
uint32_t* phaseInOut) {
|
|
float music[DOOM_AUDIO_BLOCK_FRAMES * DOOM_AUDIO_CHANNELS];
|
|
uint32_t frameOffset = 0;
|
|
|
|
memset(music, 0, sizeof(music));
|
|
while (frameOffset < DOOM_AUDIO_BLOCK_FRAMES) {
|
|
uint32_t remaining;
|
|
uint32_t framesUntilTick;
|
|
|
|
while (*phaseInOut >= DOOM_AUDIO_SAMPLE_RATE) {
|
|
*phaseInOut -= DOOM_AUDIO_SAMPLE_RATE;
|
|
DoomAudioTickMidi(synth);
|
|
}
|
|
|
|
remaining = DOOM_AUDIO_BLOCK_FRAMES - frameOffset;
|
|
framesUntilTick =
|
|
(DOOM_AUDIO_SAMPLE_RATE - *phaseInOut + DOOM_MIDI_RATE - 1u) /
|
|
DOOM_MIDI_RATE;
|
|
if (framesUntilTick > remaining)
|
|
framesUntilTick = remaining;
|
|
|
|
tsf_render_float(synth,
|
|
music + frameOffset * DOOM_AUDIO_CHANNELS,
|
|
(int)framesUntilTick,
|
|
0);
|
|
DoomAudioMixMusic(frames, music + frameOffset * DOOM_AUDIO_CHANNELS,
|
|
frameOffset, framesUntilTick);
|
|
frameOffset += framesUntilTick;
|
|
*phaseInOut += framesUntilTick * DOOM_MIDI_RATE;
|
|
}
|
|
}
|
|
|
|
// Renders one complete native-rate block containing synchronized music and SFX.
|
|
static void DoomAudioRenderBlock(int16_t* frames,
|
|
tsf** activeSynthInOut,
|
|
uint32_t* midiPhaseInOut) {
|
|
tsf* synth;
|
|
|
|
DoomAudioReadSfxBlock(frames);
|
|
synth = DoomAudioGetSynth();
|
|
if (synth != *activeSynthInOut) {
|
|
*activeSynthInOut = synth;
|
|
*midiPhaseInOut = DOOM_AUDIO_SAMPLE_RATE;
|
|
}
|
|
if (synth)
|
|
DoomAudioRenderMusic(synth, frames, midiPhaseInOut);
|
|
}
|
|
|
|
// Queries one coherent native stream snapshot.
|
|
static KSTATUS DoomAudioQueryStream(SYS_AUDIO_STREAM_INFO* infoOut) {
|
|
if (!infoOut)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
memset(infoOut, 0, sizeof(*infoOut));
|
|
infoOut->Size = sizeof(*infoOut);
|
|
return SysAudioQueryStream(g_doomAudio.StreamHandle, infoOut);
|
|
}
|
|
|
|
// Arms a wait for writable space or a terminal stream condition.
|
|
static KSTATUS DoomAudioWaitWritable(const SYS_AUDIO_STREAM_INFO* info,
|
|
uint32_t writableFrames) {
|
|
SYS_AUDIO_ARM_WAIT_REQUEST request;
|
|
KSTATUS status;
|
|
|
|
if (!info || !writableFrames || writableFrames > info->CapacityFrames)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
|
|
memset(&request, 0, sizeof(request));
|
|
request.Size = sizeof(request);
|
|
request.StreamHandle = g_doomAudio.StreamHandle;
|
|
request.Mask = SYS_AUDIO_WAIT_WRITABLE |
|
|
SYS_AUDIO_WAIT_FAILED |
|
|
SYS_AUDIO_WAIT_DISCONTINUITY;
|
|
request.WritableFrames = writableFrames;
|
|
request.DiscontinuityGeneration = info->DiscontinuityGeneration;
|
|
status = SysAudioArmWait(&request);
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
return status;
|
|
return SysWaitSingle(g_doomAudio.StreamHandle,
|
|
SYS_WAIT_FLAG_NONE,
|
|
DOOM_AUDIO_WAIT_MILLISECONDS);
|
|
}
|
|
|
|
// Waits until the buffered source has fallen to the low-latency refill watermark.
|
|
static KSTATUS DoomAudioWaitForRefill(const SYS_AUDIO_STREAM_INFO* info) {
|
|
if (!info || info->CapacityFrames <= DOOM_AUDIO_REFILL_WATERMARK_FRAMES)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
return DoomAudioWaitWritable(
|
|
info, info->CapacityFrames - DOOM_AUDIO_REFILL_WATERMARK_FRAMES);
|
|
}
|
|
|
|
// Writes as much of one pending block as the nonblocking stream accepts.
|
|
static KSTATUS DoomAudioWritePending(const int16_t* frames,
|
|
uint32_t* frameOffsetInOut) {
|
|
SYS_AUDIO_WRITE_REQUEST request;
|
|
uint32_t written = 0;
|
|
KSTATUS status;
|
|
|
|
if (!frames || !frameOffsetInOut ||
|
|
*frameOffsetInOut >= DOOM_AUDIO_BLOCK_FRAMES)
|
|
return NTLSTATUS_INVALID_PARAMETER;
|
|
|
|
memset(&request, 0, sizeof(request));
|
|
request.Size = sizeof(request);
|
|
request.StreamHandle = g_doomAudio.StreamHandle;
|
|
request.FrameCount = DOOM_AUDIO_BLOCK_FRAMES - *frameOffsetInOut;
|
|
if (request.FrameCount > DOOM_AUDIO_WRITE_QUANTUM_FRAMES)
|
|
request.FrameCount = DOOM_AUDIO_WRITE_QUANTUM_FRAMES;
|
|
request.Frames = frames + *frameOffsetInOut * DOOM_AUDIO_CHANNELS;
|
|
request.WrittenFramesOut = &written;
|
|
status = SysAudioWrite(&request);
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
return status;
|
|
if (!written)
|
|
return NTLSTATUS_WOULD_BLOCK;
|
|
|
|
*frameOffsetInOut += written;
|
|
return NTLSTATUS_SUCCESS;
|
|
}
|
|
|
|
// Keeps a bounded low-latency stream queue at PureDOOM's native cadence.
|
|
static void DoomAudioWorker(uintptr_t argument) {
|
|
int16_t block[DOOM_AUDIO_BLOCK_FRAMES * DOOM_AUDIO_CHANNELS];
|
|
uint32_t pendingOffset = DOOM_AUDIO_BLOCK_FRAMES;
|
|
uint32_t midiPhase = 0;
|
|
tsf* activeSynth = NULL;
|
|
uint8_t started = 0;
|
|
(void)argument;
|
|
|
|
while (true) {
|
|
SYS_AUDIO_STREAM_INFO info;
|
|
uint32_t requiredFrames;
|
|
KSTATUS status;
|
|
|
|
if (_InterlockedCompareExchange(&g_doomAudio.StopRequested, 0, 0))
|
|
break;
|
|
|
|
status = DoomAudioQueryStream(&info);
|
|
if (status != NTLSTATUS_SUCCESS || info.State == SysAudioStreamFailed) {
|
|
DoomAudioPrint("doom: native audio stream failed\n");
|
|
break;
|
|
}
|
|
|
|
if (!started && info.BufferedFrames >= DOOM_AUDIO_PREFILL_FRAMES) {
|
|
status = SysAudioStart(g_doomAudio.StreamHandle);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: native audio start failed\n");
|
|
break;
|
|
}
|
|
started = 1;
|
|
continue;
|
|
}
|
|
|
|
if (pendingOffset == DOOM_AUDIO_BLOCK_FRAMES) {
|
|
if (started &&
|
|
info.BufferedFrames > DOOM_AUDIO_REFILL_WATERMARK_FRAMES) {
|
|
status = DoomAudioWaitForRefill(&info);
|
|
if (status != NTLSTATUS_SUCCESS && status != NTLSTATUS_TIMEOUT) {
|
|
DoomAudioPrint("doom: native audio wait failed\n");
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
DoomAudioRenderBlock(block, &activeSynth, &midiPhase);
|
|
pendingOffset = 0;
|
|
}
|
|
|
|
requiredFrames = DOOM_AUDIO_BLOCK_FRAMES - pendingOffset;
|
|
if (requiredFrames > DOOM_AUDIO_WRITE_QUANTUM_FRAMES)
|
|
requiredFrames = DOOM_AUDIO_WRITE_QUANTUM_FRAMES;
|
|
if (info.WritableFrames < requiredFrames) {
|
|
status = DoomAudioWaitWritable(&info, requiredFrames);
|
|
if (status != NTLSTATUS_SUCCESS && status != NTLSTATUS_TIMEOUT) {
|
|
DoomAudioPrint("doom: native audio wait failed\n");
|
|
break;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
status = DoomAudioWritePending(block, &pendingOffset);
|
|
if (status == NTLSTATUS_WOULD_BLOCK)
|
|
continue;
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: native audio write failed\n");
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (started)
|
|
(void)SysAudioStop(g_doomAudio.StreamHandle);
|
|
SysExitThread(0);
|
|
}
|
|
|
|
// Releases resources owned before or after the producer thread lifetime.
|
|
static void DoomAudioReleaseResources(void) {
|
|
if (g_doomAudio.StreamHandle) {
|
|
(void)SysCloseHandle(g_doomAudio.StreamHandle);
|
|
g_doomAudio.StreamHandle = 0;
|
|
}
|
|
tsf* synth = (tsf*)_InterlockedExchangePointer(
|
|
(void* volatile*)&g_doomAudio.Synth, NULL);
|
|
if (synth)
|
|
tsf_close(synth);
|
|
if (g_doomAudio.CoreMutex) {
|
|
(void)SysCloseHandle(g_doomAudio.CoreMutex);
|
|
g_doomAudio.CoreMutex = 0;
|
|
}
|
|
}
|
|
|
|
// Starts one process-owned audio stream and its exclusive producer thread.
|
|
void DoomAudioStart(void) {
|
|
uint64_t endpointId = 0;
|
|
uint32_t streamHandle = 0;
|
|
KSTATUS status;
|
|
|
|
if (DOOM_AUDIO_PREFILL_FRAMES >= SYS_AUDIO_STREAM_CAPACITY_FRAMES ||
|
|
DOOM_AUDIO_REFILL_WATERMARK_FRAMES >= DOOM_AUDIO_PREFILL_FRAMES ||
|
|
DOOM_AUDIO_WRITE_QUANTUM_FRAMES > DOOM_AUDIO_BLOCK_FRAMES) {
|
|
DoomAudioPrint("doom: invalid native audio buffering configuration\n");
|
|
return;
|
|
}
|
|
|
|
memset(&g_doomAudio, 0, sizeof(g_doomAudio));
|
|
status = SysCreateMutex(&g_doomAudio.CoreMutex);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: audio synchronization unavailable; continuing without sound\n");
|
|
return;
|
|
}
|
|
|
|
status = DoomAudioFindEndpoint(&endpointId);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: no audio endpoint; continuing without sound\n");
|
|
DoomAudioReleaseResources();
|
|
return;
|
|
}
|
|
|
|
SYS_AUDIO_OPEN_STREAM_REQUEST open;
|
|
memset(&open, 0, sizeof(open));
|
|
open.Size = sizeof(open);
|
|
open.EndpointId = endpointId;
|
|
open.Format.SampleRate = DOOM_AUDIO_SAMPLE_RATE;
|
|
open.Format.ChannelCount = DOOM_AUDIO_CHANNELS;
|
|
open.Format.SampleFormat = SysAudioSampleS16;
|
|
open.StreamHandleOut = &streamHandle;
|
|
status = SysAudioOpenStream(&open);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: native audio open failed; continuing without sound\n");
|
|
DoomAudioReleaseResources();
|
|
return;
|
|
}
|
|
|
|
g_doomAudio.StreamHandle = streamHandle;
|
|
|
|
SYS_CREATE_THREAD_REQUEST thread;
|
|
memset(&thread, 0, sizeof(thread));
|
|
thread.Entry = (uintptr_t)DoomAudioWorker;
|
|
thread.Priority = DOOM_AUDIO_WORKER_PRIORITY;
|
|
thread.ThreadHandleOut = &g_doomAudio.WorkerHandle;
|
|
thread.ThreadIdOut = &g_doomAudio.WorkerId;
|
|
status = SysCreateThread(&thread);
|
|
if (status != NTLSTATUS_SUCCESS) {
|
|
DoomAudioPrint("doom: audio worker creation failed; continuing without sound\n");
|
|
DoomAudioReleaseResources();
|
|
return;
|
|
}
|
|
|
|
memset(&thread, 0, sizeof(thread));
|
|
thread.Entry = (uintptr_t)DoomAudioSynthLoader;
|
|
thread.Priority = DOOM_AUDIO_SYNTH_LOADER_PRIORITY;
|
|
thread.ThreadHandleOut = &g_doomAudio.SynthLoaderHandle;
|
|
thread.ThreadIdOut = &g_doomAudio.SynthLoaderId;
|
|
status = SysCreateThread(&thread);
|
|
if (status != NTLSTATUS_SUCCESS)
|
|
DoomAudioPrint("doom: MIDI loader creation failed; continuing with sound effects only\n");
|
|
}
|
|
|
|
// Stops playback immediately without waiting for the producer thread.
|
|
void DoomAudioAbort(void) {
|
|
_InterlockedExchange(&g_doomAudio.StopRequested, 1);
|
|
if (g_doomAudio.StreamHandle)
|
|
(void)SysAudioStop(g_doomAudio.StreamHandle);
|
|
}
|
|
|
|
// Stops the producer before releasing its stream, synthesizer, and core lock.
|
|
void DoomAudioShutdown(void) {
|
|
DoomAudioAbort();
|
|
if (g_doomAudio.WorkerHandle) {
|
|
(void)SysWaitSingle(g_doomAudio.WorkerHandle,
|
|
SYS_WAIT_FLAG_NONE,
|
|
SYS_WAIT_TIMEOUT_INFINITE);
|
|
(void)SysCloseHandle(g_doomAudio.WorkerHandle);
|
|
g_doomAudio.WorkerHandle = 0;
|
|
}
|
|
if (g_doomAudio.SynthLoaderHandle) {
|
|
(void)SysWaitSingle(g_doomAudio.SynthLoaderHandle,
|
|
SYS_WAIT_FLAG_NONE,
|
|
SYS_WAIT_TIMEOUT_INFINITE);
|
|
(void)SysCloseHandle(g_doomAudio.SynthLoaderHandle);
|
|
g_doomAudio.SynthLoaderHandle = 0;
|
|
}
|
|
DoomAudioReleaseResources();
|
|
}
|
|
|
|
// Enters the lock shared by the game loop and PureDOOM audio callbacks.
|
|
void DoomAudioLockCore(void) {
|
|
if (g_doomAudio.CoreMutex)
|
|
(void)SysWaitSingle(g_doomAudio.CoreMutex,
|
|
SYS_WAIT_FLAG_NONE,
|
|
SYS_WAIT_TIMEOUT_INFINITE);
|
|
}
|
|
|
|
// Leaves the lock shared by the game loop and PureDOOM audio callbacks.
|
|
void DoomAudioUnlockCore(void) {
|
|
if (g_doomAudio.CoreMutex)
|
|
(void)SysReleaseMutex(g_doomAudio.CoreMutex);
|
|
}
|