Loading MOD files in the browser

#retrocomputing #javascript #demoscene #web-audio

Written by Anders Marzi Tornblad

Published on dev.to

This is part 2 of the JavaScript MOD Player series. If you haven't read the first part, here it is: Generating sound in modern Web Audio API

The MOD file format is a staple of the 1990s demoscene. It's a simple low-level format, and was originally very closely tied to the hardware architecture of the Commodore Amiga. Back in the 1990s, I did write some demos, but they were never very successful, and now their source code is unfortunately lost forever. One thing I never really understood back then was how to write my own MOD player, so I always used code from other coders, published on BBSes. But now, thirty years later, I decided to write a MOD player in JavaScript, and learn about the format along the way.

Finding documentation

To the best of my knowledge, there is no official MOD file format specification. There are a few unofficial specifications, and they vary a lot in level of detail and clarity. Some were written in the 1990s, often by people who reverse-engineered the format from existing MOD players. A few of those contain a pretty juvenile language, so I assume they were written by teenagers at the time.

These are the resources that have been the most useful to me:

Loading bytes

The first thing we need to do is to load the MOD file into memory. I'll use the Fetch API to do that and pass the resulting ArrayBuffer to the constructor of a Mod class, that will do the actual parsing.

// Import the Mod class
import { Mod } from './mod.js';

// Load MOD file from a url
export const loadMod = async (url) => {
    const response = await fetch(url);
    const arrayBuffer = await response.arrayBuffer();
    const mod = new Mod(arrayBuffer);
    return mod;
};
loader.js
class Instrument {
    constructor(modfile, index, sampleStart) {
        // Instrument data starts at index 20, and each instrument is 30 bytes long
        const data = new Uint8Array(modfile, 20 + index * 30, 30);

        // Trim trailing null bytes
        const nameBytes = data.slice(0, 21).filter(a => !!a);
        this.name = String.fromCodePoint(...nameBytes).trim();

        this.length = 2 * (data[22] * 256 + data[23]);

        this.finetune = data[24] & 0x0f; // Signed 4 bit integer
        if (this.finetune > 7) this.finetune -= 16;

        this.volume = data[25];
        this.repeatOffset = 2 * (data[26] * 256 + data[27]);
        this.repeatLength = 2 * (data[28] * 256 + data[29]);
        this.bytes = new Int8Array(modfile, sampleStart, this.length);
    }
}

export class Mod {
    constructor(modfile) {
        // Store the pattern table
        this.patternTable = new Uint8Array(modfile, 952, 128);

        // Find the highest pattern number
        const maxPatternIndex = Math.max(...this.patternTable);

        // Extract all instruments
        this.instruments = [];
        let sampleStart = 1084 + (maxPatternIndex + 1) * 1024;
        for (let i = 0; i < 31; ++i) {
            const instr = new Instrument(modfile, i, sampleStart);
            this.instruments.push(instr);
            sampleStart += instr.length;
        }
    }
}
mod.js

Playing a sample

Now that we have the MOD file loaded, I can start playing the samples from it. First, I have to extend the player worklet so that it can receive an array of signed bytes (an Int8Array) and play them in a reasonable speed.

class PlayerWorklet extends AudioWorkletProcessor {
    constructor() {
        super();
        this.port.onmessage = this.onmessage.bind(this);
        this.sample = null;
        this.index = 0;
    }

    onmessage(e) {
        if (e.data.type === 'play') {
            // Start at the beginning of the sample
            this.sample = e.data.sample;
            this.index = 0;
        }
    }

    process(inputs, outputs) {
        const output = outputs[0];
        const channel = output[0];

        for (let i = 0; i < channel.length; ++i) {
            if (this.sample) {
                // Using a bitwise OR ZERO forces the index to be an integer
                channel[i] = this.sample[this.index | 0] / 256.0;

                // Increment the index with 0.32 for a
                // sample rate of 15360 or 14112 Hz, depending
                // on the playback rate (48000 or 44100 Hz)
                this.index += 0.32;

                // Stop playing when reaching the end of the sample
                if (this.index >= this.sample.length) {
                    this.sample = null;
                }
            } else {
                channel[i] = 0;
            }
        }

        return true;
    }
}

registerProcessor('player-worklet', PlayerWorklet);
player-worklet.js

Finally, I will add a keydown event listener to let the user play the samples by pressing keys on the keyboard.

import { loadMod } from './loader.js';

// Create the audio context
const audio = new AudioContext();

// Load an audio worklet
await audio.audioWorklet.addModule('player-worklet.js');

// Create a player
const player = new AudioWorkletNode(audio, 'player-worklet');

// Connect the player to the audio context
player.connect(audio.destination);

// Load Elekfunk from api.modarchive.org
const url = 'https://api.modarchive.org/downloads.php?moduleid=41529';
const mod = await loadMod(url);

// Keyboard map for the 31 instruments
// Press one of these keys to play the corresponding sample
const keyMap = '1234567890qwertyuiopasdfghjklzx,.';

// Play a sample when the user clicks
window.addEventListener('keydown', (e) => {
    const instrIndex = keyMap.indexOf(e.key);
    if (instrIndex === -1) return;

    const instrument = mod.instruments[instrIndex];
    console.log(instrument);

    audio.resume();
    player.port.postMessage({
        type: 'play',
        sample: instrument.bytes
    });
});
player.js

Try it out

You can try this version of the player here

Conclusion

Now it's possible to play the individual samples of a MOD file by pressing the corresponding keys on the keyboard. Next step is to parse the patterns, play a single pattern, and finally play a whole song. After that, I will dive into the details of note effects and try to get as many of them working correctly as possible. My goal is to be able to play the music from Arte by Sanity and Enigma by Phenomena properly.

You can try this solution at atornblad.github.io/js-mod-player. The latest version of the code is always available in the GitHub repository.

Articles in this series: