mod文件格式是1990年代删除的主食。这是一种简单的低级格式,最初与Commodore Amiga的硬件体系结构非常紧密。早在1990年代,我确实写了一些演示,但是它们从来都不是很成功,现在他们的源代码永远丢失了。那时我从未真正理解过的一件事是如何编写自己的mod播放器,因此我总是使用BBSE上发表的其他编码器的代码。但是现在,三十年后,我决定在JavaScript上写一个Mod播放器,并了解沿途的格式。
查找文档
据我所知,没有官方的mod文件格式规范。有一些非正式的规格,它们的细节和清晰度差异很大。一些是在1990年代写的,通常是由反向现有mod播放器的格式进行逆转的人们写的。其中一些包含一种非常少年的语言,所以我认为它们是当时是由青少年写的。
这些是对我最有用的资源:
- Excerpt from a mail conversation, citing a text written by Lars "ZAP" Hamre, who created the original ProTracker music editor.
- A collection of sources probably collected in 1993
- A text from the late 1990s or early 2000s describing the MOD format from a PC programmer's perspective
加载字节
我们需要做的第一件事是将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 Sanity和Enigma by Phenomena的音乐。
您可以在atornblad.github.io/js-mod-player上尝试此解决方案。
该代码的最新版本始终在the GitHub repository中可用。