Audio Implementation
Sound Module Architecture
DOOM’s sound system is exposed to the port through the sound_module_t structure, which is populated with HaDes-V-specific callback functions in hades_v_doom.c. The audio subsystem supports 8 simultaneous sound channels with software mixing feeding into the hardware PWM FIFO.
flowchart LR
subgraph SOUND["DOOM Sound Engine"]
START["HW_StartSound"] --> LUMP["W_CacheLumpNum"]
LUMP --> PCM["Extract PCM from lump"]
end
subgraph MIXER["Software Mixer"]
CH["8 Channels"] --> MIX["update_audio_mixer"]
MIX --> CLIP["Clamp [-128,127]"]
end
subgraph HW["Hardware"]
CLIP --> FIFO["8 KB PWM FIFO<br/>(11,025 Hz drain)"]
FIFO --> PWM["PWM Modulator"]
PWM --> JACK["3.5mm Jack"]
end
Channel Management
Eight sound channels are defined as a static array of sound_channel_t structures:
#define NUM_CHANNELS 8
typedef struct {
const uint8_t* data; // Pointer to PCM sample data in WAD
uint32_t length; // Number of samples
uint32_t position; // Current playback position
uint8_t active; // 1 = currently playing
} sound_channel_t;
static sound_channel_t audio_channels[NUM_CHANNELS];
Playing a Sound (HW_StartSound)
When DOOM wants to play a sound effect, HW_StartSound is called with the sound’s sfxinfo_t structure. The function:
- Caches the sound lump from the WAD using DOOM’s
W_CacheLumpNum(the lump number is pre-resolved byHW_GetSfxLumpNum) - Parses the DOOM sound lump header: byte 4–7 contain the number of samples (little-endian 32-bit), and byte 8 onwards are the raw 8-bit unsigned PCM data
- Sets up the channel with the PCM data pointer and length
static int HW_StartSound(sfxinfo_t *sfxinfo, int channel, int vol, int sep) {
void* raw_lump = W_CacheLumpNum(sfxinfo->lumpnum, PU_STATIC);
uint32_t num_samples = *((uint32_t*)((uint8_t*)raw_lump + 4));
uint8_t* pcm_data = (uint8_t*)raw_lump + 8;
audio_channels[channel].data = pcm_data;
audio_channels[channel].length = num_samples;
audio_channels[channel].position = 0;
audio_channels[channel].active = 1;
return channel;
}
Sound Query & Control
HW_StopSound(channel): Sets the channel’s active flag to 0, stopping playbackHW_SoundIsPlaying(channel): Returns the active flagHW_UpdateSoundParams(channel, vol, sep): No-op — volume and panning are applied during mixing
Software Mixer (update_audio_mixer)
The mixer is called once per frame from I_FinishUpdate(). It polls the hardware FIFO’s free space and fills it with mixed samples:
void update_audio_mixer(void) {
uint32_t free_space = AUDIO_ADDRESS[AUDIO_FREE_SPACE_OFFSET];
if (free_space == 0) return;
for (uint32_t i = 0; i < free_space; i++) {
int32_t mixed_sample = 0;
for (int c = 0; c < NUM_CHANNELS; c++) {
if (audio_channels[c].active) {
int8_t sample = (int8_t)(audio_channels[c].data[audio_channels[c].position] - 127);
mixed_sample += sample;
audio_channels[c].position++;
if (audio_channels[c].position >= audio_channels[c].length) {
audio_channels[c].active = 0; // Auto-stop
}
}
}
// Clamp and convert back to unsigned
if (mixed_sample > 127) mixed_sample = 127;
if (mixed_sample < -128) mixed_sample = -128;
uint8_t final_sample = (uint8_t)(mixed_sample + 128);
AUDIO_ADDRESS[AUDIO_DATA_OFFSET] = final_sample;
}
}
The mixing process:
- Each sample is converted from unsigned (0–255, center=128) to signed (-128–127, center=0) by subtracting 128
- All active channels’ current samples are summed (simple additive mixing without attenuation)
- The sum is clamped to the 8-bit signed range
- Converted back to unsigned and pushed into the hardware FIFO
- Each channel’s position is advanced; completed sounds are auto-deactivated
The mixer fills exactly as many slots as the FIFO has available, ensuring it never blocks. The FIFO drains autonomously at 11,025 Hz, so the mixer is called at frame rate (~10–30 Hz) and refills whatever has been consumed since the last frame.
Lump Name Resolution (HW_GetSfxLumpNum)
DOOM sound lumps in the WAD follow the naming convention DSxxxxx (e.g., DSPISTOL for the pistol sound). The function constructs this name from the sound’s internal name, converts lowercase to uppercase, and looks it up in the WAD directory:
static int HW_GetSfxLumpNum(sfxinfo_t *sfx) {
char namebuf[9];
namebuf[0] = 'D';
namebuf[1] = 'S';
int i = 0;
while (sfx->name[i] != '\0' && i < 6) {
char c = sfx->name[i];
if (c >= 'a' && c <= 'z') c -= 32;
namebuf[2 + i] = c;
i++;
}
namebuf[2 + i] = '\0';
return W_GetNumForName(namebuf);
}
Music Module
The music module is fully stubbed out. DOOM’s music playback (MUS format → MIDI → synthesized audio) would require a significantly more complex audio pipeline, including a MIDI synthesizer or a MUS-to-PCM converter. Since the hardware only supports simple 8-bit PCM output via PWM, music was disabled. All music callback functions return immediately with no-ops or dummy success values. The game plays sound effects without background music, providing adequate gameplay feedback.
Hardware FIFO Integration
The hardware FIFO is accessed through Wishbone-mapped registers at peripheral address offsets. The mixer writes to AUDIO_DATA_OFFSET (offset 0), which pushes a single byte into the 8 KB circular buffer. The free space is read from AUDIO_FREE_SPACE_OFFSET (offset 4), returning the number of samples that can be written without overflow. The hardware independently drains the FIFO at 11,025 Hz and feeds samples into the PWM modulator.