Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Caches the sound lump from the WAD using DOOM’s W_CacheLumpNum (the lump number is pre-resolved by HW_GetSfxLumpNum)
  2. 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
  3. 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 playback
  • HW_SoundIsPlaying(channel): Returns the active flag
  • HW_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:

  1. Each sample is converted from unsigned (0–255, center=128) to signed (-128–127, center=0) by subtracting 128
  2. All active channels’ current samples are summed (simple additive mixing without attenuation)
  3. The sum is clamped to the 8-bit signed range
  4. Converted back to unsigned and pushed into the hardware FIFO
  5. 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.