Task 406: .MOD File Format
Task 406: .MOD File Format
1. List of Properties Intrinsic to the .MOD File Format
The .MOD file format refers to the music module format originating from the Amiga (not the video variant, as the structure and decoding requirements align with the tracker music spec). Based on standard specifications (e.g., ProTracker/NoiseTracker variant with 4 channels and 31 instruments, identified by signatures like 'M.K.'), the intrinsic properties are the metadata and structural elements extractable from the file's binary layout. These include:
- Song Title: A 20-byte string (ASCII, space-padded).
- Samples: An array of 31 sample descriptors, each containing:
- Name: A 22-byte string (ASCII, zero-padded).
- Length: An integer (uint16_t value * 2, representing byte length of the sample data).
- Finetune: An integer (uint8_t, lower nibble only; range 0-15, where 0-7 are positive and 8-15 are negative -8 to -1).
- Volume: An integer (uint8_t, range 0-64).
- Loop Start: An integer (uint16_t value * 2, offset in bytes where loop begins).
- Loop Length: An integer (uint16_t value * 2, length in bytes of the loop segment; if < 4, no loop).
- Song Length: An integer (uint8_t, range 1-128, number of positions in the pattern sequence).
- Restart Byte: An integer (uint8_t, used for song looping in some trackers like NoiseTracker; often 127 or 0).
- Pattern Sequence: An array of 128 integers (uint8_t each, representing the order of patterns to play; unused positions are 0).
- Signature: A 4-byte string (ASCII, e.g., 'M.K.' for 4-channel/31-instrument ProTracker MOD).
- Computed Properties (derived but intrinsic):
- Number of Patterns: An integer (maximum value in Pattern Sequence + 1).
- Number of Channels: An integer (typically 4 for 'M.K.', but varies by signature, e.g., 6 for '6CHN', 8 for '8CHN').
Note: Pattern data (64 rows per pattern × channels × 4 bytes per note) and raw sample data (8-bit signed PCM) follow the header but are not listed as "properties" here, as they are bulk data rather than metadata. The classes below will parse and print the above properties only, as per the task focus.
2. Two Direct Download Links for .MOD Files
- https://api.modarchive.org/downloads.php?moduleid=57925#space_debris.mod
- https://api.modarchive.org/downloads.php?moduleid=59344#stardstm.mod
These are classic Amiga-era .MOD music files (Space Debris by Captain and Stardust Memories by Jester).
3. HTML/JavaScript for Drag-and-Drop .MOD File Dumper
Below is a self-contained HTML page with embedded JavaScript that allows drag-and-drop of a .MOD file. It parses the file using FileReader and DataView, then dumps the properties to the screen in a readable format. Embed this in a blog post (e.g., Ghost platform) as raw HTML.
4. Python Class for .MOD Handling
import struct
import sys
class ModFile:
def __init__(self, filename=None):
self.song_title = ''
self.samples = [{} for _ in range(31)]
self.song_length = 0
self.restart_byte = 0
self.pattern_sequence = [0] * 128
self.signature = ''
self.num_patterns = 0
self.num_channels = 4 # Default
self._data = b'' # For writing back
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
self._data = f.read()
# Song Title (0-19)
self.song_title = self._data[0:20].decode('ascii', errors='ignore').rstrip('\x00 ')
# Samples (20-949)
for i in range(31):
offset = 20 + i * 30
self.samples[i] = {
'name': self._data[offset:offset+22].decode('ascii', errors='ignore').rstrip('\x00'),
'length': struct.unpack('>H', self._data[offset+22:offset+24])[0] * 2,
'finetune': struct.unpack('B', self._data[offset+24:offset+25])[0] & 0x0F,
'volume': struct.unpack('B', self._data[offset+25:offset+26])[0],
'loop_start': struct.unpack('>H', self._data[offset+26:offset+28])[0] * 2,
'loop_length': struct.unpack('>H', self._data[offset+28:offset+30])[0] * 2
}
if self.samples[i]['finetune'] > 7:
self.samples[i]['finetune'] -= 16
# Song Length (950)
self.song_length = self._data[950]
# Restart Byte (951)
self.restart_byte = self._data[951]
# Pattern Sequence (952-1079)
self.pattern_sequence = list(self._data[952:1080])
self.num_patterns = max(self.pattern_sequence) + 1
# Signature (1080-1083)
self.signature = self._data[1080:1084].decode('ascii')
# Channels based on signature
if self.signature == '6CHN':
self.num_channels = 6
elif self.signature in ['8CHN', 'FLT8']:
self.num_channels = 8
def print_properties(self):
print(f'Song Title: {self.song_title}')
for i, sample in enumerate(self.samples):
print(f'Sample {i+1}:')
print(f' Name: {sample["name"]}')
print(f' Length: {sample["length"]}')
print(f' Finetune: {sample["finetune"]}')
print(f' Volume: {sample["volume"]}')
print(f' Loop Start: {sample["loop_start"]}')
print(f' Loop Length: {sample["loop_length"]}')
print(f'Song Length: {self.song_length}')
print(f'Restart Byte: {self.restart_byte}')
print(f'Pattern Sequence: {", ".join(map(str, self.pattern_sequence))}')
print(f'Signature: {self.signature}')
print(f'Number of Patterns: {self.num_patterns}')
print(f'Number of Channels: {self.num_channels}')
def write(self, filename):
if not self._data:
raise ValueError('No data to write; read a file first.')
# Note: This writes the original data back; to modify, update self._data or properties and re-pack (not implemented for brevity)
with open(filename, 'wb') as f:
f.write(self._data)
# Example usage:
# mod = ModFile('example.mod')
# mod.print_properties()
# mod.write('output.mod')
5. Java Class for .MOD Handling
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;
public class ModFile {
private String songTitle;
private Sample[] samples = new Sample[31];
private int songLength;
private int restartByte;
private int[] patternSequence = new int[128];
private String signature;
private int numPatterns;
private int numChannels = 4; // Default
private byte[] data; // For writing back
static class Sample {
String name;
int length;
int finetune;
int volume;
int loopStart;
int loopLength;
}
public ModFile(String filename) throws IOException {
if (filename != null) {
read(filename);
}
}
public void read(String filename) throws IOException {
data = Files.readAllBytes(Paths.get(filename));
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.BIG_ENDIAN);
// Song Title (0-19)
byte[] titleBytes = new byte[20];
buffer.position(0);
buffer.get(titleBytes);
songTitle = new String(titleBytes, "ASCII").trim();
// Samples (20-949)
for (int i = 0; i < 31; i++) {
int offset = 20 + i * 30;
buffer.position(offset);
samples[i] = new Sample();
byte[] nameBytes = new byte[22];
buffer.get(nameBytes);
samples[i].name = new String(nameBytes, "ASCII").replaceAll("\0.*", "");
samples[i].length = (buffer.getShort() & 0xFFFF) * 2;
int ft = buffer.get() & 0x0F;
samples[i].finetune = (ft > 7) ? ft - 16 : ft;
samples[i].volume = buffer.get() & 0xFF;
samples[i].loopStart = (buffer.getShort() & 0xFFFF) * 2;
samples[i].loopLength = (buffer.getShort() & 0xFFFF) * 2;
}
// Song Length (950)
buffer.position(950);
songLength = buffer.get() & 0xFF;
// Restart Byte (951)
restartByte = buffer.get() & 0xFF;
// Pattern Sequence (952-1079)
numPatterns = 0;
for (int i = 0; i < 128; i++) {
patternSequence[i] = buffer.get() & 0xFF;
if (patternSequence[i] > numPatterns) numPatterns = patternSequence[i];
}
numPatterns++;
// Signature (1080-1083)
byte[] sigBytes = new byte[4];
buffer.position(1080);
buffer.get(sigBytes);
signature = new String(sigBytes, "ASCII");
// Channels
if (signature.equals("6CHN")) numChannels = 6;
else if (signature.equals("8CHN") || signature.equals("FLT8")) numChannels = 8;
}
public void printProperties() {
System.out.println("Song Title: " + songTitle);
for (int i = 0; i < 31; i++) {
System.out.println("Sample " + (i + 1) + ":");
System.out.println(" Name: " + samples[i].name);
System.out.println(" Length: " + samples[i].length);
System.out.println(" Finetune: " + samples[i].finetune);
System.out.println(" Volume: " + samples[i].volume);
System.out.println(" Loop Start: " + samples[i].loopStart);
System.out.println(" Loop Length: " + samples[i].loopLength);
}
System.out.println("Song Length: " + songLength);
System.out.println("Restart Byte: " + restartByte);
System.out.print("Pattern Sequence: ");
for (int i = 0; i < 128; i++) {
System.out.print(patternSequence[i] + (i < 127 ? ", " : ""));
}
System.out.println();
System.out.println("Signature: " + signature);
System.out.println("Number of Patterns: " + numPatterns);
System.out.println("Number of Channels: " + numChannels);
}
public void write(String filename) throws IOException {
if (data == null) {
throw new IllegalStateException("No data to write; read a file first.");
}
// Note: Writes original data; to modify, update data buffer (not implemented for brevity)
Files.write(Paths.get(filename), data);
}
// Example usage:
// public static void main(String[] args) throws IOException {
// ModFile mod = new ModFile("example.mod");
// mod.printProperties();
// mod.write("output.mod");
// }
}
6. JavaScript Class for .MOD Handling
class ModFile {
constructor(buffer = null) {
this.songTitle = '';
this.samples = Array.from({ length: 31 }, () => ({}));
this.songLength = 0;
this.restartByte = 0;
this.patternSequence = new Array(128).fill(0);
this.signature = '';
this.numPatterns = 0;
this.numChannels = 4; // Default
this._buffer = buffer; // ArrayBuffer for writing back
if (buffer) {
this.read(buffer);
}
}
read(buffer) {
const view = new DataView(buffer);
// Song Title (0-19)
this.songTitle = this._getString(view, 0, 20);
// Samples (20-949)
for (let i = 0; i < 31; i++) {
const offset = 20 + i * 30;
this.samples[i] = {
name: this._getString(view, offset, 22),
length: view.getUint16(offset + 22, false) * 2,
finetune: (() => {
let ft = view.getUint8(offset + 24) & 0x0F;
return ft > 7 ? ft - 16 : ft;
})(),
volume: view.getUint8(offset + 25),
loopStart: view.getUint16(offset + 26, false) * 2,
loopLength: view.getUint16(offset + 28, false) * 2
};
}
// Song Length (950)
this.songLength = view.getUint8(950);
// Restart Byte (951)
this.restartByte = view.getUint8(951);
// Pattern Sequence (952-1079)
for (let i = 0; i < 128; i++) {
this.patternSequence[i] = view.getUint8(952 + i);
if (this.patternSequence[i] > this.numPatterns) this.numPatterns = this.patternSequence[i];
}
this.numPatterns++;
// Signature (1080-1083)
this.signature = this._getString(view, 1080, 4);
// Channels
if (this.signature === '6CHN') this.numChannels = 6;
else if (this.signature === '8CHN' || this.signature === 'FLT8') this.numChannels = 8;
}
printProperties() {
console.log(`Song Title: ${this.songTitle}`);
this.samples.forEach((sample, i) => {
console.log(`Sample ${i + 1}:`);
console.log(` Name: ${sample.name}`);
console.log(` Length: ${sample.length}`);
console.log(` Finetune: ${sample.finetune}`);
console.log(` Volume: ${sample.volume}`);
console.log(` Loop Start: ${sample.loopStart}`);
console.log(` Loop Length: ${sample.loopLength}`);
});
console.log(`Song Length: ${this.songLength}`);
console.log(`Restart Byte: ${this.restartByte}`);
console.log(`Pattern Sequence: ${this.patternSequence.join(', ')}`);
console.log(`Signature: ${this.signature}`);
console.log(`Number of Patterns: ${this.numPatterns}`);
console.log(`Number of Channels: ${this.numChannels}`);
}
write() {
if (!this._buffer) {
throw new Error('No buffer to write; provide one in constructor or read first.');
}
// Returns the original buffer as Blob for download; to modify, update buffer (not implemented for brevity)
return new Blob([this._buffer], { type: 'application/octet-stream' });
}
_getString(view, offset, length) {
let str = '';
for (let i = 0; i < length; i++) {
const char = view.getUint8(offset + i);
if (char === 0) break;
str += String.fromCharCode(char);
}
return str.trim();
}
}
// Example usage (in browser or Node with fs):
// const fs = require('fs'); // For Node
// const buffer = fs.readFileSync('example.mod').buffer;
// const mod = new ModFile(buffer);
// mod.printProperties();
// const blob = mod.write();
// // Save blob as file...
7. C Class (Using Struct and Functions for Class-Like Behavior)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char name[23];
uint32_t length;
int8_t finetune;
uint8_t volume;
uint32_t loop_start;
uint32_t loop_length;
} Sample;
typedef struct {
char song_title[21];
Sample samples[31];
uint8_t song_length;
uint8_t restart_byte;
uint8_t pattern_sequence[128];
char signature[5];
uint8_t num_patterns;
uint8_t num_channels; // Default 4
uint8_t* data; // For writing back
size_t data_size;
} ModFile;
void modfile_init(ModFile* mod) {
memset(mod, 0, sizeof(ModFile));
mod->num_channels = 4;
}
int modfile_read(ModFile* mod, const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) return -1;
fseek(f, 0, SEEK_END);
mod->data_size = ftell(f);
fseek(f, 0, SEEK_SET);
mod->data = malloc(mod->data_size);
if (!mod->data) {
fclose(f);
return -1;
}
fread(mod->data, 1, mod->data_size, f);
fclose(f);
// Song Title (0-19)
strncpy(mod->song_title, (char*)mod->data, 20);
mod->song_title[20] = '\0';
// Samples (20-949)
for (int i = 0; i < 31; i++) {
uint8_t* offset = mod->data + 20 + i * 30;
strncpy(mod->samples[i].name, (char*)offset, 22);
mod->samples[i].name[22] = '\0';
uint16_t len = (offset[22] << 8) | offset[23];
mod->samples[i].length = len * 2;
uint8_t ft = offset[24] & 0x0F;
mod->samples[i].finetune = (ft > 7) ? ft - 16 : ft;
mod->samples[i].volume = offset[25];
uint16_t ls = (offset[26] << 8) | offset[27];
mod->samples[i].loop_start = ls * 2;
uint16_t ll = (offset[28] << 8) | offset[29];
mod->samples[i].loop_length = ll * 2;
}
// Song Length (950)
mod->song_length = mod->data[950];
// Restart Byte (951)
mod->restart_byte = mod->data[951];
// Pattern Sequence (952-1079)
mod->num_patterns = 0;
memcpy(mod->pattern_sequence, mod->data + 952, 128);
for (int i = 0; i < 128; i++) {
if (mod->pattern_sequence[i] > mod->num_patterns) mod->num_patterns = mod->pattern_sequence[i];
}
mod->num_patterns++;
// Signature (1080-1083)
strncpy(mod->signature, (char*)(mod->data + 1080), 4);
mod->signature[4] = '\0';
// Channels
if (strcmp(mod->signature, "6CHN") == 0) mod->num_channels = 6;
else if (strcmp(mod->signature, "8CHN") == 0 || strcmp(mod->signature, "FLT8") == 0) mod->num_channels = 8;
return 0;
}
void modfile_print_properties(const ModFile* mod) {
printf("Song Title: %s\n", mod->song_title);
for (int i = 0; i < 31; i++) {
printf("Sample %d:\n", i + 1);
printf(" Name: %s\n", mod->samples[i].name);
printf(" Length: %u\n", mod->samples[i].length);
printf(" Finetune: %d\n", mod->samples[i].finetune);
printf(" Volume: %u\n", mod->samples[i].volume);
printf(" Loop Start: %u\n", mod->samples[i].loop_start);
printf(" Loop Length: %u\n", mod->samples[i].loop_length);
}
printf("Song Length: %u\n", mod->song_length);
printf("Restart Byte: %u\n", mod->restart_byte);
printf("Pattern Sequence: ");
for (int i = 0; i < 128; i++) {
printf("%u%s", mod->pattern_sequence[i], (i < 127 ? ", " : "\n"));
}
printf("Signature: %s\n", mod->signature);
printf("Number of Patterns: %u\n", mod->num_patterns);
printf("Number of Channels: %u\n", mod->num_channels);
}
int modfile_write(const ModFile* mod, const char* filename) {
if (!mod->data) return -1;
FILE* f = fopen(filename, "wb");
if (!f) return -1;
fwrite(mod->data, 1, mod->data_size, f);
fclose(f);
return 0;
}
void modfile_free(ModFile* mod) {
free(mod->data);
mod->data = NULL;
}
// Example usage:
// int main() {
// ModFile mod;
// modfile_init(&mod);
// if (modfile_read(&mod, "example.mod") == 0) {
// modfile_print_properties(&mod);
// modfile_write(&mod, "output.mod");
// }
// modfile_free(&mod);
// return 0;
// }