I just want to celebrate that, after some tweaking to the way the audio code works, the main menu theme for the game finally plays without major skips.

This is a significant step because first, there’s a lot of Xbox ADPCM audio in the game; an audio codec which is not much used anymore (but thankfully OpenAL-soft supports it), but Halo: Combat Evolved uses it extensively. Secondly it means we’re parsing the audio data correctly, down to the amount of milliseconds it’s supposed to last.

Another challenge was that, while OpenAL-soft’s native implementation supports a variety of audio codecs, the Emscripten equivalent does not. This meant that I would need to a) find some free-standing, openly licensed decoder or b) try to port the code running in OpenAL-soft to the web somehow. I opted for the second approach, however there were some challenges: OpenAL-soft is LGPL-licensed, while this project aims to be MIT-licensed. The only way to make it work is to use shared linkage between the LGPL module and the rest of the code, which is not the most popular feature for Emscripten, but it works. This means that, if you were to inspect the network tab of the engine running in Emscripten, there’s a separate libOpenAL_ImaDecoder.wasm file being loaded containing the code brought over from OpenAL. I believe this should be sufficient to comply with the licensing requirements, since it would be trivial to modify the compiled bundle to point to another implementation.

As part of the effort, I also wanted to make sure that custom maps and their sounds would load correctly; several such examples are provided on the online demos page. The main differentiator is whether the music is stored in the map file or in the sounds.map file, which requires a bit of trickery to get right. In code, I decided to tag references to sounds, bitmaps and other data differently, such that we can know when dereferencing it whether we should look in bitmaps.map, sounds.map or only the map file itself. What was especially confusing was that custom maps would opt to store pitch permutations and ranges at relative offsets to the tag it was referenced from, while the audio data itself was still stored in the sounds.map file.

At a top-level, the resulting code to fetch the structures becomes:


template<typename T>
std::optional<span<T>> data(
    tag_t const& tag, 
    reference<T, Version, atlas_type_t::sounds> ref)
{
    /* is custom edition and T is either either pitch range/permutation */
    constexpr bool is_relative_offset = ...;
    if(is_relative_offset && tag.storage == tag_storage_t::external)
        /* adjust ref's offset with tag data offset */;
    if(/* try to retrieve data from map file */)
        /* return data if the pointer is valid */;
    if(/* not custom edtion OR is tagged external */)
        /* return data pointing to the sounds.map file */;
    return std::nullopt;
}

Making sure that it works correctly across the Xbox version, PC version and Custom Edition makes this code a bit more complex, but overall there are enough shared patterns here to make it worth the effort.

Finally, here’s the main menu with working sound! (As long as I haven’t broken it later on :) )

Start the demo