clowndoom II

A few more things have happened with my Doom engine since the last post. They’re kinda neat, so I figure it would be fun to go over them.

I suppose I’ll talk about the most recent change, since it’s freshest in my mind: music support.

Music

For whatever reason, Linux Doom doesn’t support MIDI. The functions are still there, but they’ve been stubbed-out. At least it gives me something to build upon.

Looking around at open-source MIDI libraries, my options actually seem pretty limited (or maybe I’m just not looking hard enough): the only libraries I could find are TiMidity++, FluidSynth, and WildMIDI.

I opted for WildMIDI, since the other two support actual hardware MIDI using sound cards, and they have support for running as system daemons. Too much bloat: I just want something that renders PCM. Plus, WildMIDI was already installed on my system (required by one of PulseEffects’s dependencies). It also supports Doom’s MUS music format directly, which is extra nice.

With a library in hand, a series of stubbed-out functions, and an existing audio pipeline used by the sound effects, implementing music was easy: I was basically just filling in the gaps. Load a song here, play it there, loop it here, etc. Before I knew it, Doom’s dormant music data had sprung back to life.

Sound

But music’s not the only thing I had to reimplement: for whatever reason, some sound-effect functions were stubbed-out too – namely the ones for stopping a sound mid-playback and checking if a sound is currently playing. I guess they weren’t possible with the previous system of audio being handled by a separate process.

Since the current audio system is built into the engine itself, I don’t have that problem, so restoring these functions wasn’t an issue. By doing this, I accidentally fixed the chainsaw, which would previously continue to make its attack sound long after the attack button was released.

Volume

Speaking of sounds, I discovered some interesting stuff about the volume bug I mentioned in the last part. Remember how I said the mixer expected there to be 128 volume levels when there were only 16? Well, it’s not alone. It turns out, before the code was cleaned-up for release, the volume did have 128 levels, but during the cleanup, the volume variable was merged with the variable for the volume setting in the options menu, which only has 16 levels.

This not only ruins encapsulation, but it wasn’t even done properly: a number of functions still expect 128 volume levels:

void S_SetSfxVolume(int volume)
{

    if (volume < 0 || volume > 127)
	I_Error("Attempt to set sfx volume at %d", volume);

    snd_SfxVolume = volume;

}

Additionally, some functions are outright made redundant by this change and yet aren’t removed. For example, when the options menu sets the snd_SfxVolume variable, it also calls the S_SetSfxVolume function… which exists to set the snd_SfxVolume variable. This logic makes more sense when you consider that there used to be two variables, one ranging from 0-15, and another from 0-128 – one for the menu and one for the sound engine respectively.

This careless change is also responsible for a pretty funny bug, where if you set the sound volume to 0, and then go to any eighth level of Doom 1 or 2, the positional volume of the sounds will invert, becoming louder as you get further away from the origin. You can read all about it on the Doom wiki.

Mixer

Another thing I worked on was Linux Doom’s mixer. It’s pretty good by default, but it has some flaws.

For one, it’s hardcoded to assume all sounds are 11025Hz. This is mostly fine, but Doom 1 and 2 have a handful of sounds that are 22050Hz – this includes the multiplayer respawn sound and the Super Shotgun reload sounds. It’s extremely noticeable when these sounds are played at half-speed.

[I’d include recordings here, but this site’s hosting won’t let me. 😦 ]

Since the mixer sports an interpolator, there’s no real reason it can’t support arbitrary sample rates. All it takes is an extra multiplication and division in the addsfx function. With that taken care of, the Super Shotgun sounds like it’s meant to once again.

By the way, this bug has been present in every official Doom port since 2005 up until it was finally fixed in March 2020. Amazing.

Speaking of hardcoding to 11025Hz, the mixer is hardcoded to output at 11025Hz. This isn’t too bad, but, again, the interpolator means it should theoretically be able to output at anything. So, to avoid having Doom interpolate the audio, and then having miniaudio interpolate it again to match the sample rate of the backend, I’ve adjusted the mixer to request a sample rate from miniaudio, and then output at that instead.

…But this exposed an issue: Doom’s mixer uses a nearest-neighbour interpolator. By bypassing miniaudio’s interpolator, and leaving everything up to Doom’s one, I’m left with very crunchy audio output.

To remedy this, I converted Doom’s mixer to a linear interpolator. It might have been a little much for 1994 hardware, but today it’s no problem. All it took was changing one line, too:

sample = *channels[ chan ];

Becomes:

interpolation_scale = channelstepremainder[ chan ] >> 8;
sample = ((channels[ chan ][0] * (0x100 - interpolation_scale)) + (channels[ chan ][1] * interpolation_scale)) >> 8;

64-bit

Cucky wouldn’t stop nagging me to make clowndoom 64-bit, so I finally gave in and fixed enough of Doom’s gross code to allow for working 64-bit builds. Save files are incompatible with 32-bit builds, and I imagine some bugs have slipped through, but it’s playable.

There isn’t really too much to explain here: it’s all the usual 32-bit-dependency stuff – using 4 instead of sizeof(void*), casting pointers to int instead of intptr_t or size_t, shifting unsigned long variables to the right without performing an AND first, etc.

The worst part, I suppose, is the settings code: the settings file (“.doomrc”) supports both integer and string values. For example, the keybinding settings are integers, while the chat macros are strings. How is this implemented? As a giant hack:

    {"use_joystick",&usejoystick, 0},
    {"joyb_fire",&joybfire,0},
    {"joyb_strafe",&joybstrafe,1},
    {"joyb_use",&joybuse,3},
    {"joyb_speed",&joybspeed,2},

    {"screenblocks",&screenblocks, 9},
    {"detaillevel",&detailLevel, 0},

    {"snd_channels",&numChannels, 3},



    {"usegamma",&usegamma, 0},

    {"chatmacro0", (int *) &chat_macros[0], (int) HUSTR_CHATMACRO0 },
    {"chatmacro1", (int *) &chat_macros[1], (int) HUSTR_CHATMACRO1 },
    {"chatmacro2", (int *) &chat_macros[2], (int) HUSTR_CHATMACRO2 },
    {"chatmacro3", (int *) &chat_macros[3], (int) HUSTR_CHATMACRO3 },
    {"chatmacro4", (int *) &chat_macros[4], (int) HUSTR_CHATMACRO4 },
    {"chatmacro5", (int *) &chat_macros[5], (int) HUSTR_CHATMACRO5 },
    {"chatmacro6", (int *) &chat_macros[6], (int) HUSTR_CHATMACRO6 },
    {"chatmacro7", (int *) &chat_macros[7], (int) HUSTR_CHATMACRO7 },
    {"chatmacro8", (int *) &chat_macros[8], (int) HUSTR_CHATMACRO8 },
    {"chatmacro9", (int *) &chat_macros[9], (int) HUSTR_CHATMACRO9 }

How exactly does the game know the difference between an integer and a string pointer? It checks if its value is between -0xFFF and 0xFFF. If it is, then it’s an integer – if not, then it’s a string pointer. Because of course that’s how you do it.

Misc.

Thanks to a compiler warning, I was able to find a bug that prevented the background in Doom 1’s intermission screens from animating. Spot the bug in this code:

void WI_drawAnimatedBack(void)
{
    int			i;
    anim_t*		a;

    if (commercial)
	return;

    if (wbs->epsd > 2)
	return;

    for (i=0 ; i<NUMANIMS[wbs->epsd] ; i++)
    {
	a = &anims[wbs->epsd][i];

	if (a->ctr >= 0)
	    V_DrawPatch(a->loc.x, a->loc.y, FB, a->p[a->ctr]);
    }

}

Don’t see it? What if I told you that commercial is an enum value. The variable it’s meant to be compared against is missing. Because of this, the check always evaluates to true.

Finally, here’s one more bug: I tried compiling and running clowndoom on my Raspberry Pi 3B+. It runs great! Full speed and everything. Unfortunately, when you try to move right or backwards, you fly off in the opposite direction at lightspeed.

I’ve encountered this type of bug plenty of times before: you see, on ARM CPUs the char type defaults to unsigned, in contrast to x86 CPUs where the default is signed.

Curiously, the original source code was very mindful to denote when a char is signed or unsigned, so maybe the devs encountered this issue before with another platform (Doom was ported to a lot of things back in the day, after all). I guess this must have been a bug introduced during the cleanup made before the source code was released, because the slip-up was in just one area:

typedef struct
{
    char	forwardmove;	// *2048 for move
    char	sidemove;	// *2048 for move
    short	angleturn;	// <<16 for angle delta
    short	consistancy;	// checks for net game
    byte	chatchar;
    byte	buttons;
} ticcmd_t;

Closing

That’s about all for now. The competition between me and Cucky is going well enough. though his fork definitely has an edge on doing fancy stuff like replacing Xlib with SDL2, changing the Makefile to a CMake script, and rewriting the sound mixer from scratch. I imagine I’ll start making some drastic changes soon too: Windows support would be cool.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Create your website with WordPress.com
Get started
%d bloggers like this: