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.

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.

.MOD File Properties Dumper
Drag and drop a .MOD file here

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