Task 631: .S3M File Format

Task 631: .S3M File Format

1. List of all the properties of this file format intrinsic to its file system

Based on the .S3M file format specifications, the intrinsic properties (structural fields and metadata) are as follows. These include the main header, channel settings, order list, instrument metadata (with type-specific fields), and pattern metadata. Sample data and full pattern packed data are not listed as properties here, as they are variable content rather than fixed properties, but pointers and sizes to them are included.

  • Song title: 28-byte null-terminated string.
  • Signature 1: 1-byte value (always 0x1A).
  • Type: 1-byte value (always 0x10 for S3M modules).
  • Reserved (header): 2-byte value (always 0x0000).
  • Order count: 2-byte little-endian unsigned integer (number of orders, even value).
  • Instrument count: 2-byte little-endian unsigned integer (number of instruments).
  • Pattern pointer count: 2-byte little-endian unsigned integer (number of patterns).
  • Flags: 2-byte little-endian unsigned integer (bit flags for features like optimizations, limits, etc.).
  • Tracker version: 2-byte little-endian unsigned integer (e.g., 0x1320 for Scream Tracker 3.20).
  • Sample type: 2-byte little-endian unsigned integer (1 for signed samples, 2 for unsigned).
  • Signature 2: 4-byte string ("SCRM").
  • Global volume: 1-byte unsigned integer (0-64).
  • Initial speed: 1-byte unsigned integer (frames per row).
  • Initial tempo: 1-byte unsigned integer (frames per second).
  • Master volume: 1-byte unsigned integer (bit 7 for stereo/mono, bits 0-6 for volume).
  • Ultra click removal: 1-byte unsigned integer (GUS-specific).
  • Default pan: 1-byte unsigned integer (252 to use header pan values).
  • Reserved (extended): 8-byte array (unused).
  • Pointer to special data: 2-byte little-endian unsigned integer (parapointer, if flags bit 7 set).
  • Channel settings: 32-byte array (channel types and enable/disable flags).
  • Order list: Variable-length array (order count bytes) of pattern indices (0xFE skip, 0xFF end).
  • Instrument pointers: Variable-length array (instrument count * 2 bytes) of little-endian parapointers to instruments.
  • Pattern pointers: Variable-length array (pattern pointer count * 2 bytes) of little-endian parapointers to patterns.

For each instrument (repeated for instrument count):

  • Instrument type: 1-byte unsigned integer (0=empty, 1=PCM, 2-7=Adlib variants).
  • Filename: 12-byte string (DOS 8.3 format, no null).
  • If PCM (type 1):
  • Data pointer high: 1-byte unsigned integer (upper bits of parapointer).
  • Data pointer low: 2-byte little-endian unsigned integer (lower bits of parapointer).
  • Length: 4-byte little-endian unsigned integer (sample length in bytes).
  • Loop start: 4-byte little-endian unsigned integer (loop start offset).
  • Loop end: 4-byte little-endian unsigned integer (loop end offset).
  • Volume: 1-byte unsigned integer (0-63).
  • Reserved (instrument): 1-byte (0).
  • Pack: 1-byte unsigned integer (0=unpacked, 1=packed [deprecated]).
  • Flags (instrument): 1-byte unsigned integer (bits: 1=loop, 2=stereo, 4=16-bit).
  • C2 speed: 4-byte little-endian unsigned integer (sample rate for C-4).
  • Internal: 12-byte array (unused, zeros).
  • Instrument title: 28-byte null-terminated string.
  • Instrument signature: 4-byte string ("SCRS").
  • If Adlib (types 2-7):
  • Reserved (Adlib): 3-byte array (zeros).
  • OPL values: 12-byte array (OPL register values for synthesis).
  • Volume: 1-byte unsigned integer (0-63).
  • Disk: 1-byte unsigned integer (unknown).
  • Reserved (Adlib extended): 2-byte little-endian (0).
  • C2 speed: 4-byte little-endian unsigned integer (rate for C-4).
  • Unused: 12-byte array (zeros).
  • Instrument title: 28-byte null-terminated string.
  • Instrument signature: 4-byte string ("SCRI").

For each pattern (repeated for pattern pointer count):

  • Packed size: 2-byte little-endian unsigned integer (size of packed pattern data, including header; 0=empty).
  • Packed data: Variable-length (packed commands for 64 rows x 32 channels).

3. Ghost blog embedded HTML JavaScript for drag and drop .S3M file dump

S3M File Properties Dumper
Drag and drop .S3M file here

4. Python class for .S3M file handling

import struct
import os

class S3MFile:
    def __init__(self, filename):
        self.filename = filename
        self.data = None
        self.properties = {}
        self.instruments = []
        self.patterns = []
        self.load()

    def load(self):
        with open(self.filename, 'rb') as f:
            self.data = f.read()
        self.decode()

    def decode(self):
        offset = 0
        self.properties['song_title'] = struct.unpack_from('<28s', self.data, offset)[0].decode('ascii', errors='ignore').rstrip('\x00')
        offset += 28
        self.properties['sig1'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['type'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['reserved_header'] = struct.unpack_from('<H', self.data, offset)[0]
        offset += 2
        order_count = struct.unpack_from('<H', self.data, offset)[0]
        self.properties['order_count'] = order_count
        offset += 2
        instrument_count = struct.unpack_from('<H', self.data, offset)[0]
        self.properties['instrument_count'] = instrument_count
        offset += 2
        pattern_ptr_count = struct.unpack_from('<H', self.data, offset)[0]
        self.properties['pattern_ptr_count'] = pattern_ptr_count
        offset += 2
        self.properties['flags'] = struct.unpack_from('<H', self.data, offset)[0]
        offset += 2
        self.properties['tracker_version'] = struct.unpack_from('<H', self.data, offset)[0]
        offset += 2
        self.properties['sample_type'] = struct.unpack_from('<H', self.data, offset)[0]
        offset += 2
        self.properties['sig2'] = struct.unpack_from('<4s', self.data, offset)[0].decode('ascii')
        offset += 4
        self.properties['global_volume'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['initial_speed'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['initial_tempo'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['master_volume'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['ultra_click_removal'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        self.properties['default_pan'] = struct.unpack_from('<B', self.data, offset)[0]
        offset += 1
        offset += 8  # reserved_extended
        self.properties['ptr_special'] = struct.unpack_from('<H', self.data, offset)[0]
        offset += 2
        channel_settings = list(struct.unpack_from('<32B', self.data, offset))
        self.properties['channel_settings'] = channel_settings
        offset += 32
        order_list = list(struct.unpack_from(f'<{order_count}B', self.data, offset))
        self.properties['order_list'] = order_list
        offset += order_count
        inst_ptrs = [struct.unpack_from('<H', self.data, offset + i*2)[0] * 16 for i in range(instrument_count)]
        self.properties['instrument_pointers'] = inst_ptrs
        offset += instrument_count * 2
        pat_ptrs = [struct.unpack_from('<H', self.data, offset + i*2)[0] * 16 for i in range(pattern_ptr_count)]
        self.properties['pattern_pointers'] = pat_ptrs
        offset += pattern_ptr_count * 2

        # Instruments
        for ptr in inst_ptrs:
            inst = {}
            offset = ptr
            inst['type'] = struct.unpack_from('<B', self.data, offset)[0]
            offset += 1
            inst['filename'] = struct.unpack_from('<12s', self.data, offset)[0].decode('ascii', errors='ignore')
            offset += 12
            if inst['type'] == 1:  # PCM
                inst['data_ptr_high'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['data_ptr_low'] = struct.unpack_from('<H', self.data, offset)[0]
                offset += 2
                inst['length'] = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                inst['loop_start'] = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                inst['loop_end'] = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                inst['volume'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['reserved_inst'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['pack'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['flags_inst'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['c2_speed'] = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                offset += 12  # internal
                inst['title'] = struct.unpack_from('<28s', self.data, offset)[0].decode('ascii', errors='ignore').rstrip('\x00')
                offset += 28
                inst['sig'] = struct.unpack_from('<4s', self.data, offset)[0].decode('ascii')
                offset += 4
            elif 2 <= inst['type'] <= 7:  # Adlib
                offset += 3  # reserved_adlib
                offset += 12  # opl_values
                inst['volume'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['disk'] = struct.unpack_from('<B', self.data, offset)[0]
                offset += 1
                inst['reserved_adlib_ext'] = struct.unpack_from('<H', self.data, offset)[0]
                offset += 2
                inst['c2_speed'] = struct.unpack_from('<I', self.data, offset)[0]
                offset += 4
                offset += 12  # unused
                inst['title'] = struct.unpack_from('<28s', self.data, offset)[0].decode('ascii', errors='ignore').rstrip('\x00')
                offset += 28
                inst['sig'] = struct.unpack_from('<4s', self.data, offset)[0].decode('ascii')
                offset += 4
            self.instruments.append(inst)

        # Patterns (packed size only for print)
        for ptr in pat_ptrs:
            if ptr == 0: continue
            offset = ptr
            packed_size = struct.unpack_from('<H', self.data, offset)[0]
            self.patterns.append({'packed_size': packed_size})
            # Skip packed data for now

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")
        for i, inst in enumerate(self.instruments):
            print(f"\nInstrument {i+1}:")
            for key, value in inst.items():
                print(f"  {key}: {value}")
        for i, pat in enumerate(self.patterns):
            print(f"\nPattern {i}:")
            for key, value in pat.items():
                print(f"  {key}: {value}")

    def write(self, output_filename):
        # For simplicity, write back the original data (no modifications)
        with open(output_filename, 'wb') as f:
            f.write(self.data)

# Example usage:
# s3m = S3MFile('example.s3m')
# s3m.print_properties()
# s3m.write('output.s3m')

5. Java class for .S3M file handling

import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.util.*;

public class S3MFile {
    private String filename;
    private ByteBuffer buffer;
    private Map<String, Object> properties = new HashMap<>();
    private List<Map<String, Object>> instruments = new ArrayList<>();
    private List<Map<String, Object>> patterns = new ArrayList<>();

    public S3MFile(String filename) {
        this.filename = filename;
        load();
    }

    private void load() {
        try (RandomAccessFile raf = new RandomAccessFile(filename, "r")) {
            FileChannel channel = raf.getChannel();
            buffer = ByteBuffer.allocate((int) channel.size()).order(ByteOrder.LITTLE_ENDIAN);
            channel.read(buffer);
            buffer.flip();
            decode();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void decode() {
        int offset = 0;
        byte[] titleBytes = new byte[28];
        buffer.position(offset);
        buffer.get(titleBytes);
        properties.put("song_title", new String(titleBytes, java.nio.charset.StandardCharsets.US_ASCII).trim());
        offset += 28;
        properties.put("sig1", buffer.get(offset++) & 0xFF);
        properties.put("type", buffer.get(offset++) & 0xFF);
        properties.put("reserved_header", (int) buffer.getShort(offset) & 0xFFFF);
        offset += 2;
        int orderCount = buffer.getShort(offset) & 0xFFFF;
        properties.put("order_count", orderCount);
        offset += 2;
        int instrumentCount = buffer.getShort(offset) & 0xFFFF;
        properties.put("instrument_count", instrumentCount);
        offset += 2;
        int patternPtrCount = buffer.getShort(offset) & 0xFFFF;
        properties.put("pattern_ptr_count", patternPtrCount);
        offset += 2;
        properties.put("flags", (int) buffer.getShort(offset) & 0xFFFF);
        offset += 2;
        properties.put("tracker_version", (int) buffer.getShort(offset) & 0xFFFF);
        offset += 2;
        properties.put("sample_type", (int) buffer.getShort(offset) & 0xFFFF);
        offset += 2;
        byte[] sig2Bytes = new byte[4];
        buffer.position(offset);
        buffer.get(sig2Bytes);
        properties.put("sig2", new String(sig2Bytes));
        offset += 4;
        properties.put("global_volume", buffer.get(offset++) & 0xFF);
        properties.put("initial_speed", buffer.get(offset++) & 0xFF);
        properties.put("initial_tempo", buffer.get(offset++) & 0xFF);
        properties.put("master_volume", buffer.get(offset++) & 0xFF);
        properties.put("ultra_click_removal", buffer.get(offset++) & 0xFF);
        properties.put("default_pan", buffer.get(offset++) & 0xFF);
        offset += 8; // reserved_extended
        properties.put("ptr_special", (int) buffer.getShort(offset) & 0xFFFF);
        offset += 2;
        int[] channelSettings = new int[32];
        for (int i = 0; i < 32; i++) {
            channelSettings[i] = buffer.get(offset++) & 0xFF;
        }
        properties.put("channel_settings", channelSettings);
        int[] orderList = new int[orderCount];
        for (int i = 0; i < orderCount; i++) {
            orderList[i] = buffer.get(offset++) & 0xFF;
        }
        properties.put("order_list", orderList);
        int[] instPtrs = new int[instrumentCount];
        for (int i = 0; i < instrumentCount; i++) {
            instPtrs[i] = (buffer.getShort(offset) & 0xFFFF) * 16;
            offset += 2;
        }
        properties.put("instrument_pointers", instPtrs);
        int[] patPtrs = new int[patternPtrCount];
        for (int i = 0; i < patternPtrCount; i++) {
            patPtrs[i] = (buffer.getShort(offset) & 0xFFFF) * 16;
            offset += 2;
        }
        properties.put("pattern_pointers", patPtrs);

        // Instruments
        for (int ptr : instPtrs) {
            Map<String, Object> inst = new HashMap<>();
            offset = ptr;
            inst.put("type", buffer.get(offset++) & 0xFF);
            byte[] filenameBytes = new byte[12];
            buffer.position(offset);
            buffer.get(filenameBytes);
            inst.put("filename", new String(filenameBytes).trim());
            offset += 12;
            int type = (int) inst.get("type");
            if (type == 1) {
                inst.put("data_ptr_high", buffer.get(offset++) & 0xFF);
                inst.put("data_ptr_low", (int) buffer.getShort(offset) & 0xFFFF);
                offset += 2;
                inst.put("length", buffer.getInt(offset));
                offset += 4;
                inst.put("loop_start", buffer.getInt(offset));
                offset += 4;
                inst.put("loop_end", buffer.getInt(offset));
                offset += 4;
                inst.put("volume", buffer.get(offset++) & 0xFF);
                inst.put("reserved_inst", buffer.get(offset++) & 0xFF);
                inst.put("pack", buffer.get(offset++) & 0xFF);
                inst.put("flags_inst", buffer.get(offset++) & 0xFF);
                inst.put("c2_speed", buffer.getInt(offset));
                offset += 4;
                offset += 12; // internal
                byte[] instTitleBytes = new byte[28];
                buffer.position(offset);
                buffer.get(instTitleBytes);
                inst.put("title", new String(instTitleBytes).trim());
                offset += 28;
                byte[] instSigBytes = new byte[4];
                buffer.get(instSigBytes);
                inst.put("sig", new String(instSigBytes));
                offset += 4;
            } else if (type >= 2 && type <= 7) {
                offset += 3; // reserved_adlib
                offset += 12; // opl_values
                inst.put("volume", buffer.get(offset++) & 0xFF);
                inst.put("disk", buffer.get(offset++) & 0xFF);
                inst.put("reserved_adlib_ext", (int) buffer.getShort(offset) & 0xFFFF);
                offset += 2;
                inst.put("c2_speed", buffer.getInt(offset));
                offset += 4;
                offset += 12; // unused
                byte[] instTitleBytes = new byte[28];
                buffer.position(offset);
                buffer.get(instTitleBytes);
                inst.put("title", new String(instTitleBytes).trim());
                offset += 28;
                byte[] instSigBytes = new byte[4];
                buffer.get(instSigBytes);
                inst.put("sig", new String(instSigBytes));
                offset += 4;
            }
            instruments.add(inst);
        }

        // Patterns
        for (int ptr : patPtrs) {
            if (ptr == 0) continue;
            Map<String, Object> pat = new HashMap<>();
            offset = ptr;
            int packedSize = buffer.getShort(offset) & 0xFFFF;
            pat.put("packed_size", packedSize);
            patterns.add(pat);
            // Skip packed data
        }
    }

    public void printProperties() {
        properties.forEach((key, value) -> System.out.println(key + ": " + value));
        for (int i = 0; i < instruments.size(); i++) {
            System.out.println("\nInstrument " + (i + 1) + ":");
            instruments.get(i).forEach((key, value) -> System.out.println("  " + key + ": " + value));
        }
        for (int i = 0; i < patterns.size(); i++) {
            System.out.println("\nPattern " + i + ":");
            patterns.get(i).forEach((key, value) -> System.out.println("  " + key + ": " + value));
        }
    }

    public void write(String outputFilename) {
        try (FileOutputStream fos = new FileOutputStream(outputFilename)) {
            buffer.position(0);
            fos.getChannel().write(buffer);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    // Example usage:
    // public static void main(String[] args) {
    //     S3MFile s3m = new S3MFile("example.s3m");
    //     s3m.printProperties();
    //     s3m.write("output.s3m");
    // }
}

6. JavaScript class for .S3M file handling (Node.js)

const fs = require('fs');

class S3MFile {
    constructor(filename) {
        this.filename = filename;
        this.buffer = null;
        this.properties = {};
        this.instruments = [];
        this.patterns = [];
        this.load();
    }

    load() {
        this.buffer = fs.readFileSync(this.filename);
        this.decode();
    }

    decode() {
        let offset = 0;
        const view = new DataView(this.buffer.buffer);

        this.properties.song_title = this.readString(view, offset, 28);
        offset += 28;
        this.properties.sig1 = view.getUint8(offset++);
        this.properties.type = view.getUint8(offset++);
        this.properties.reserved_header = view.getUint16(offset, true);
        offset += 2;
        const orderCount = view.getUint16(offset, true);
        this.properties.order_count = orderCount;
        offset += 2;
        const instrumentCount = view.getUint16(offset, true);
        this.properties.instrument_count = instrumentCount;
        offset += 2;
        const patternPtrCount = view.getUint16(offset, true);
        this.properties.pattern_ptr_count = patternPtrCount;
        offset += 2;
        this.properties.flags = view.getUint16(offset, true);
        offset += 2;
        this.properties.tracker_version = view.getUint16(offset, true);
        offset += 2;
        this.properties.sample_type = view.getUint16(offset, true);
        offset += 2;
        this.properties.sig2 = this.readString(view, offset, 4);
        offset += 4;
        this.properties.global_volume = view.getUint8(offset++);
        this.properties.initial_speed = view.getUint8(offset++);
        this.properties.initial_tempo = view.getUint8(offset++);
        this.properties.master_volume = view.getUint8(offset++);
        this.properties.ultra_click_removal = view.getUint8(offset++);
        this.properties.default_pan = view.getUint8(offset++);
        offset += 8; // reserved_extended
        this.properties.ptr_special = view.getUint16(offset, true);
        offset += 2;
        this.properties.channel_settings = [];
        for (let i = 0; i < 32; i++) {
            this.properties.channel_settings.push(view.getUint8(offset++));
        }
        this.properties.order_list = [];
        for (let i = 0; i < orderCount; i++) {
            this.properties.order_list.push(view.getUint8(offset++));
        }
        this.properties.instrument_pointers = [];
        for (let i = 0; i < instrumentCount; i++) {
            this.properties.instrument_pointers.push(view.getUint16(offset, true) * 16);
            offset += 2;
        }
        this.properties.pattern_pointers = [];
        for (let i = 0; i < patternPtrCount; i++) {
            this.properties.pattern_pointers.push(view.getUint16(offset, true) * 16);
            offset += 2;
        }

        // Instruments
        this.properties.instrument_pointers.forEach(ptr => {
            let inst = {};
            offset = ptr;
            inst.type = view.getUint8(offset++);
            inst.filename = this.readString(view, offset, 12);
            offset += 12;
            if (inst.type === 1) {
                inst.data_ptr_high = view.getUint8(offset++);
                inst.data_ptr_low = view.getUint16(offset, true);
                offset += 2;
                inst.length = view.getUint32(offset, true);
                offset += 4;
                inst.loop_start = view.getUint32(offset, true);
                offset += 4;
                inst.loop_end = view.getUint32(offset, true);
                offset += 4;
                inst.volume = view.getUint8(offset++);
                inst.reserved_inst = view.getUint8(offset++);
                inst.pack = view.getUint8(offset++);
                inst.flags_inst = view.getUint8(offset++);
                inst.c2_speed = view.getUint32(offset, true);
                offset += 4;
                offset += 12; // internal
                inst.title = this.readString(view, offset, 28);
                offset += 28;
                inst.sig = this.readString(view, offset, 4);
                offset += 4;
            } else if (inst.type >= 2 && inst.type <= 7) {
                offset += 3; // reserved_adlib
                offset += 12; // opl_values
                inst.volume = view.getUint8(offset++);
                inst.disk = view.getUint8(offset++);
                inst.reserved_adlib_ext = view.getUint16(offset, true);
                offset += 2;
                inst.c2_speed = view.getUint32(offset, true);
                offset += 4;
                offset += 12; // unused
                inst.title = this.readString(view, offset, 28);
                offset += 28;
                inst.sig = this.readString(view, offset, 4);
                offset += 4;
            }
            this.instruments.push(inst);
        });

        // Patterns
        this.properties.pattern_pointers.forEach(ptr => {
            if (ptr === 0) return;
            let pat = {};
            offset = ptr;
            pat.packed_size = view.getUint16(offset, true);
            this.patterns.push(pat);
            // Skip packed data
        });
    }

    readString(view, offset, len) {
        let str = '';
        for (let i = 0; i < len; i++) {
            const char = view.getUint8(offset + i);
            if (char === 0) break;
            str += String.fromCharCode(char);
        }
        return str;
    }

    printProperties() {
        for (const [key, value] of Object.entries(this.properties)) {
            console.log(`${key}: ${JSON.stringify(value)}`);
        }
        this.instruments.forEach((inst, i) => {
            console.log(`\nInstrument ${i + 1}:`);
            for (const [key, value] of Object.entries(inst)) {
                console.log(`  ${key}: ${value}`);
            }
        });
        this.patterns.forEach((pat, i) => {
            console.log(`\nPattern ${i}:`);
            for (const [key, value] of Object.entries(pat)) {
                console.log(`  ${key}: ${value}`);
            }
        });
    }

    write(outputFilename) {
        fs.writeFileSync(outputFilename, this.buffer);
    }
}

// Example usage:
// const s3m = new S3MFile('example.s3m');
// s3m.printProperties();
// s3m.write('output.s3m');

7. C class for .S3M file handling (using C++ for class support)

#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <cstdint>
#include <cstring>

class S3MFile {
private:
    std::string filename;
    std::vector<uint8_t> data;
    std::map<std::string, std::string> properties; // Using string for simplicity in printing
    std::vector<std::map<std::string, std::string>> instruments;
    std::vector<std::map<std::string, std::string>> patterns;

    uint16_t readUint16LE(size_t &offset) {
        uint16_t val = (data[offset + 1] << 8) | data[offset];
        offset += 2;
        return val;
    }

    uint32_t readUint32LE(size_t &offset) {
        uint32_t val = (data[offset + 3] << 24) | (data[offset + 2] << 16) | (data[offset + 1] << 8) | data[offset];
        offset += 4;
        return val;
    }

    std::string readString(size_t &offset, size_t len) {
        std::string str(reinterpret_cast<char*>(&data[offset]), len);
        str.erase(std::find(str.begin(), str.end(), '\0'), str.end());
        offset += len;
        return str;
    }

public:
    S3MFile(const std::string& fn) : filename(fn) {
        load();
    }

    void load() {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) {
            std::cerr << "Failed to open file." << std::endl;
            return;
        }
        size_t size = file.tellg();
        data.resize(size);
        file.seekg(0);
        file.read(reinterpret_cast<char*>(data.data()), size);
        decode();
    }

    void decode() {
        size_t offset = 0;
        properties["song_title"] = readString(offset, 28);
        properties["sig1"] = std::to_string(data[offset++]);
        properties["type"] = std::to_string(data[offset++]);
        properties["reserved_header"] = std::to_string(readUint16LE(offset));
        uint16_t orderCount = readUint16LE(offset);
        properties["order_count"] = std::to_string(orderCount);
        uint16_t instrumentCount = readUint16LE(offset);
        properties["instrument_count"] = std::to_string(instrumentCount);
        uint16_t patternPtrCount = readUint16LE(offset);
        properties["pattern_ptr_count"] = std::to_string(patternPtrCount);
        properties["flags"] = std::to_string(readUint16LE(offset));
        properties["tracker_version"] = std::to_string(readUint16LE(offset));
        properties["sample_type"] = std::to_string(readUint16LE(offset));
        properties["sig2"] = readString(offset, 4);
        properties["global_volume"] = std::to_string(data[offset++]);
        properties["initial_speed"] = std::to_string(data[offset++]);
        properties["initial_tempo"] = std::to_string(data[offset++]);
        properties["master_volume"] = std::to_string(data[offset++]);
        properties["ultra_click_removal"] = std::to_string(data[offset++]);
        properties["default_pan"] = std::to_string(data[offset++]);
        offset += 8; // reserved_extended
        properties["ptr_special"] = std::to_string(readUint16LE(offset));
        std::string channelSettings = "[";
        for (int i = 0; i < 32; i++) {
            channelSettings += std::to_string(data[offset++]) + (i < 31 ? ", " : "");
        }
        channelSettings += "]";
        properties["channel_settings"] = channelSettings;
        std::string orderList = "[";
        for (int i = 0; i < orderCount; i++) {
            orderList += std::to_string(data[offset++]) + (i < orderCount - 1 ? ", " : "");
        }
        orderList += "]";
        properties["order_list"] = orderList;
        std::string instPtrsStr = "[";
        std::vector<uint32_t> instPtrs;
        for (int i = 0; i < instrumentCount; i++) {
            uint32_t ptr = readUint16LE(offset) * 16;
            instPtrs.push_back(ptr);
            instPtrsStr += std::to_string(ptr) + (i < instrumentCount - 1 ? ", " : "");
        }
        instPtrsStr += "]";
        properties["instrument_pointers"] = instPtrsStr;
        std::string patPtrsStr = "[";
        std::vector<uint32_t> patPtrs;
        for (int i = 0; i < patternPtrCount; i++) {
            uint32_t ptr = readUint16LE(offset) * 16;
            patPtrs.push_back(ptr);
            patPtrsStr += std::to_string(ptr) + (i < patternPtrCount - 1 ? ", " : "");
        }
        patPtrsStr += "]";
        properties["pattern_pointers"] = patPtrsStr;

        // Instruments
        for (auto ptr : instPtrs) {
            std::map<std::string, std::string> inst;
            offset = ptr;
            uint8_t type = data[offset++];
            inst["type"] = std::to_string(type);
            inst["filename"] = readString(offset, 12);
            if (type == 1) {
                inst["data_ptr_high"] = std::to_string(data[offset++]);
                inst["data_ptr_low"] = std::to_string(readUint16LE(offset));
                inst["length"] = std::to_string(readUint32LE(offset));
                inst["loop_start"] = std::to_string(readUint32LE(offset));
                inst["loop_end"] = std::to_string(readUint32LE(offset));
                inst["volume"] = std::to_string(data[offset++]);
                inst["reserved_inst"] = std::to_string(data[offset++]);
                inst["pack"] = std::to_string(data[offset++]);
                inst["flags_inst"] = std::to_string(data[offset++]);
                inst["c2_speed"] = std::to_string(readUint32LE(offset));
                offset += 12; // internal
                inst["title"] = readString(offset, 28);
                inst["sig"] = readString(offset, 4);
            } else if (type >= 2 && type <= 7) {
                offset += 3; // reserved_adlib
                offset += 12; // opl_values
                inst["volume"] = std::to_string(data[offset++]);
                inst["disk"] = std::to_string(data[offset++]);
                inst["reserved_adlib_ext"] = std::to_string(readUint16LE(offset));
                inst["c2_speed"] = std::to_string(readUint32LE(offset));
                offset += 12; // unused
                inst["title"] = readString(offset, 28);
                inst["sig"] = readString(offset, 4);
            }
            instruments.push_back(inst);
        }

        // Patterns
        for (auto ptr : patPtrs) {
            if (ptr == 0) continue;
            std::map<std::string, std::string> pat;
            offset = ptr;
            pat["packed_size"] = std::to_string(readUint16LE(offset));
            patterns.push_back(pat);
            // Skip packed data
        }
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
        for (size_t i = 0; i < instruments.size(); ++i) {
            std::cout << "\nInstrument " << (i + 1) << ":" << std::endl;
            for (const auto& instProp : instruments[i]) {
                std::cout << "  " << instProp.first << ": " << instProp.second << std::endl;
            }
        }
        for (size_t i = 0; i < patterns.size(); ++i) {
            std::cout << "\nPattern " << i << ":" << std::endl;
            for (const auto& patProp : patterns[i]) {
                std::cout << "  " << patProp.first << ": " << patProp.second << std::endl;
            }
        }
    }

    void write(const std::string& outputFilename) {
        std::ofstream outFile(outputFilename, std::ios::binary);
        if (!outFile) {
            std::cerr << "Failed to write file." << std::endl;
            return;
        }
        outFile.write(reinterpret_cast<const char*>(data.data()), data.size());
    }
};

// Example usage:
// int main() {
//     S3MFile s3m("example.s3m");
//     s3m.printProperties();
//     s3m.write("output.s3m");
//     return 0;
// }