在浏览器中加载mod文件
#javascript #webaudio #retrocomputing

mod文件格式是1990年代删除的主食。这是一种简单的低级格式,最初与Commodore Amiga的硬件体系结构非常紧密。早在1990年代,我确实写了一些演示,但是它们从来都不是很成功,现在他们的源代码永远丢失了。那时我从未真正理解过的一件事是如何编写自己的mod播放器,因此我总是使用BBSE上发表的其他编码器的代码。但是现在,三十年后,我决定在JavaScript上写一个Mod播放器,并了解沿途的格式。

查找文档

据我所知,没有官方的mod文件格式规范。有一些非正式的规格,它们的细节和清晰度差异很大。一些是在1990年代写的,通常是由反向现有mod播放器的格式进行逆转的人们写的。其中一些包含一种非常少年的语言,所以我认为它们是当时是由青少年写的。

这些是对我最有用的资源:

加载字节

我们需要做的第一件事是将mod文件加载到内存中。我将使用fetch api来做到这一点,然后将结果的koude0传递给Mod类的构造函数,将进行实际的解析。

// 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;
};
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文件,我可以开始播放其中的样本。首先,我必须扩展播放器工作点,以便它可以接收一系列签名字节(int8array)并以合理的速度播放。

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];

                // 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);

最后,我将添加一个keydown事件侦听器,以通过在键盘上按键来播放样品。

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
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
    });
});

结论

现在,可以通过按键盘上的相应键播放mod文件的各个样本。下一步是解析图案,播放单个图案,最后播放一首歌。之后,我将深入研究注释效果的细节,并尝试使其中的尽可能多地工作。我的目标是能够正确播放Arte by SanityEnigma by Phenomena的音乐。

您可以在atornblad.github.io/js-mod-player上尝试此解决方案。

该代码的最新版本始终在the GitHub repository中可用。