Files
nt-lite/code/user/apps/doom/doom_audio.c
T

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);
}