Task 837: .XM File Format
Task 837: .XM File Format
The .XM file format, known as the Extended Module format, is a digital audio module format introduced by the FastTracker 2 tracker software. It supports up to 32 channels, multiple samples per instrument, envelopes for volume and panning, and various effects. The format is structured with a fixed header, followed by pattern data, instrument headers, sample headers, and sample data. Specifications are documented in sources such as the Unofficial XM File Format Specification and Kaitai Struct definitions.
The properties intrinsic to the .XM file format, derived from its structural elements within the file system, are as follows. These encompass the header fields, pattern metadata, instrument metadata, and sample metadata (excluding raw pattern or sample data, which are content rather than intrinsic properties):
- ID text (fixed string: "Extended Module: ")
- Module name (20-byte string)
- Separator (1 byte, typically 0x1A)
- Tracker name (20-byte string)
- Version number (2 bytes, e.g., 0x0104 for version 1.04)
- Header size (4 bytes, size of the header block from offset 60)
- Song length (2 bytes, number of entries in the pattern order table)
- Restart position (2 bytes, index for song restart)
- Number of channels (2 bytes, typically 2 to 32)
- Number of patterns (2 bytes, up to 256)
- Number of instruments (2 bytes, up to 128)
- Flags (2 bytes, bit 0 indicates frequency table type: 0 for Amiga, 1 for linear)
- Default tempo (2 bytes)
- Default BPM (2 bytes)
- Pattern order table (256 bytes, array of pattern indices; only first "song length" entries are used)
- For each pattern (repeated "number of patterns" times):
- Pattern header length (4 bytes)
- Packing type (1 byte, always 0)
- Number of rows (2 bytes, 1 to 256)
- Packed pattern data size (2 bytes)
- For each instrument (repeated "number of instruments" times):
- Instrument size (4 bytes)
- Instrument name (22-byte string)
- Instrument type (1 byte, usually 0)
- Number of samples (2 bytes, up to 16)
- If number of samples > 0:
- Sample header size (4 bytes)
- Sample number for all notes (96 bytes, keymap assignment array)
- Volume envelope points (48 bytes, 12 points as x/y pairs)
- Panning envelope points (48 bytes, 12 points as x/y pairs)
- Number of volume points (1 byte)
- Number of panning points (1 byte)
- Volume sustain point (1 byte)
- Volume loop start point (1 byte)
- Volume loop end point (1 byte)
- Panning sustain point (1 byte)
- Panning loop start point (1 byte)
- Panning loop end point (1 byte)
- Volume type (1 byte, bitfield: 0=on, 1=sustain, 2=loop)
- Panning type (1 byte, bitfield: 0=on, 1=sustain, 2=loop)
- Vibrato type (1 byte)
- Vibrato sweep (1 byte)
- Vibrato depth (1 byte)
- Vibrato rate (1 byte)
- Volume fadeout (2 bytes)
- Reserved (2 bytes)
- For each sample (repeated "number of samples" times):
- Sample length (4 bytes, in samples)
- Sample loop start (4 bytes, in samples)
- Sample loop length (4 bytes, in samples)
- Volume (1 byte, 0 to 64)
- Finetune (1 byte, signed -128 to +127)
- Sample type (1 byte, bitfield: bits 0-1 for loop type [0=none, 1=forward, 2=ping-pong]; bit 4 for 16-bit [1] vs. 8-bit [0])
- Panning (1 byte, 0 to 255)
- Relative note number (1 byte, signed)
- Reserved (1 byte)
- Sample name (22-byte string)
Two direct download links for .XM files are:
- https://modarchive.org/data/downloads.php?moduleid=138490#sample.xm
- https://modarchive.org/data/downloads.php?moduleid=137757#in_the_desert.xm
The following is an embeddable HTML snippet with JavaScript for a Ghost blog (or similar platform). It creates a drag-and-drop area that allows users to drop a .XM file, parses it, and dumps all properties listed in item 1 to the screen via a pre-formatted text element.
- The following Python class can open, decode (read), encode (write), and print all properties of a .XM file. It uses the
structmodule for binary unpacking and packing. For writing, it assumes the original file data is preserved for non-property elements (e.g., pattern and sample data are reloaded from the original if unmodified).
import struct
class XMFile:
def __init__(self, filename=None):
self.properties = {}
self.pattern_data = [] # List of bytes for each pattern's data
self.sample_data = [] # List of lists of bytes for each instrument's samples
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
offset = 0
# Header
self.properties['ID text'] = data[offset:offset+17].decode('ascii', errors='ignore').rstrip('\x00')
offset += 17
self.properties['Module name'] = data[offset:offset+20].decode('ascii', errors='ignore').rstrip('\x00')
offset += 20
self.properties['Separator'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
self.properties['Tracker name'] = data[offset:offset+20].decode('ascii', errors='ignore').rstrip('\x00')
offset += 20
self.properties['Version number'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Header size'] = struct.unpack_from('<I', data, offset)[0]
header_size = self.properties['Header size']
offset += 4
self.properties['Song length'] = struct.unpack_from('<H', data, offset)[0]
song_length = self.properties['Song length']
offset += 2
self.properties['Restart position'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Number of channels'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Number of patterns'] = struct.unpack_from('<H', data, offset)[0]
num_patterns = self.properties['Number of patterns']
offset += 2
self.properties['Number of instruments'] = struct.unpack_from('<H', data, offset)[0]
num_instruments = self.properties['Number of instruments']
offset += 2
self.properties['Flags'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Default tempo'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Default BPM'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
self.properties['Pattern order table'] = list(struct.unpack_from('<256B', data, offset))
offset += 256
# Adjust to start of patterns
offset = 60 + header_size - 4
# Patterns
self.properties['Patterns'] = []
self.pattern_data = []
for p in range(num_patterns):
pat_props = {}
pat_props['Pattern header length'] = struct.unpack_from('<I', data, offset)[0]
pat_header_len = pat_props['Pattern header length']
offset += 4
pat_props['Packing type'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
pat_props['Number of rows'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
pat_props['Packed pattern data size'] = struct.unpack_from('<H', data, offset)[0]
pat_data_size = pat_props['Packed pattern data size']
offset += pat_header_len - 9 # Skip extra if any
self.pattern_data.append(data[offset:offset + pat_data_size])
offset += pat_data_size
self.properties['Patterns'].append(pat_props)
# Instruments
self.properties['Instruments'] = []
self.sample_data = [[] for _ in range(num_instruments)]
for i in range(num_instruments):
inst_props = {}
inst_props['Instrument size'] = struct.unpack_from('<I', data, offset)[0]
inst_size = inst_props['Instrument size']
offset += 4
inst_props['Instrument name'] = data[offset:offset+22].decode('ascii', errors='ignore').rstrip('\x00')
offset += 22
inst_props['Instrument type'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Number of samples'] = struct.unpack_from('<H', data, offset)[0]
num_samples = inst_props['Number of samples']
offset += 2
if num_samples > 0:
inst_props['Sample header size'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
inst_props['Sample number for all notes'] = list(struct.unpack_from('<96B', data, offset))
offset += 96
inst_props['Volume envelope points'] = self._unpack_envelope(data, offset, 12)
offset += 48
inst_props['Panning envelope points'] = self._unpack_envelope(data, offset, 12)
offset += 48
inst_props['Number of volume points'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Number of panning points'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Volume sustain point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Volume loop start point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Volume loop end point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Panning sustain point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Panning loop start point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Panning loop end point'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Volume type'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Panning type'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Vibrato type'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Vibrato sweep'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Vibrato depth'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Vibrato rate'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
inst_props['Volume fadeout'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
inst_props['Reserved'] = struct.unpack_from('<H', data, offset)[0]
offset += 2
offset += inst_size - 263 # Adjust for standard size
inst_props['Samples'] = []
for s in range(num_samples):
sample_props = {}
sample_props['Sample length'] = struct.unpack_from('<I', data, offset)[0]
sample_len = sample_props['Sample length']
offset += 4
sample_props['Sample loop start'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
sample_props['Sample loop length'] = struct.unpack_from('<I', data, offset)[0]
offset += 4
sample_props['Volume'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
sample_props['Finetune'] = struct.unpack_from('<b', data, offset)[0]
offset += 1
sample_props['Sample type'] = struct.unpack_from('<B', data, offset)[0]
sample_type = sample_props['Sample type']
offset += 1
sample_props['Panning'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
sample_props['Relative note number'] = struct.unpack_from('<b', data, offset)[0]
offset += 1
sample_props['Reserved'] = struct.unpack_from('<B', data, offset)[0]
offset += 1
sample_props['Sample name'] = data[offset:offset+22].decode('ascii', errors='ignore').rstrip('\x00')
offset += 22
byte_len = sample_len * (2 if (sample_type & 0x10) else 1)
self.sample_data[i].append(data[offset:offset + byte_len])
offset += byte_len
inst_props['Samples'].append(sample_props)
self.properties['Instruments'].append(inst_props)
def _unpack_envelope(self, data, offset, num_points):
points = []
for _ in range(num_points):
x, y = struct.unpack_from('<HH', data, offset)
points.append((x, y))
offset += 4
return points
def print_properties(self):
for key, value in self.properties.items():
if key in ['Patterns', 'Instruments']:
for idx, sub in enumerate(value):
print(f"{key} {idx}:")
for sk, sv in sub.items():
if sk == 'Samples':
for sidx, sample in enumerate(sv):
print(f" Sample {sidx}:")
for sskey, ssvalue in sample.items():
print(f" {sskey}: {ssvalue}")
else:
print(f" {sk}: {sv}")
else:
print(f"{key}: {value}")
def write(self, filename):
with open(filename, 'wb') as f:
# Write header
f.write(self.properties['ID text'].ljust(17, ' ').encode('ascii'))
f.write(self.properties['Module name'].ljust(20, '\x00').encode('ascii'))
f.write(struct.pack('<B', self.properties['Separator']))
f.write(self.properties['Tracker name'].ljust(20, '\x00').encode('ascii'))
f.write(struct.pack('<H', self.properties['Version number']))
f.write(struct.pack('<I', self.properties['Header size']))
f.write(struct.pack('<H', self.properties['Song length']))
f.write(struct.pack('<H', self.properties['Restart position']))
f.write(struct.pack('<H', self.properties['Number of channels']))
f.write(struct.pack('<H', self.properties['Number of patterns']))
f.write(struct.pack('<H', self.properties['Number of instruments']))
f.write(struct.pack('<H', self.properties['Flags']))
f.write(struct.pack('<H', self.properties['Default tempo']))
f.write(struct.pack('<H', self.properties['Default BPM']))
f.write(struct.pack('<256B', *self.properties['Pattern order table']))
# Patterns
for p, pat_props in enumerate(self.properties['Patterns']):
f.write(struct.pack('<I', pat_props['Pattern header length']))
f.write(struct.pack('<B', pat_props['Packing type']))
f.write(struct.pack('<H', pat_props['Number of rows']))
f.write(struct.pack('<H', pat_props['Packed pattern data size']))
# Assume no extra header bytes; add if needed
f.write(self.pattern_data[p])
# Instruments
for i, inst_props in enumerate(self.properties['Instruments']):
f.write(struct.pack('<I', inst_props['Instrument size']))
f.write(inst_props['Instrument name'].ljust(22, '\x00').encode('ascii'))
f.write(struct.pack('<B', inst_props['Instrument type']))
f.write(struct.pack('<H', inst_props['Number of samples']))
if inst_props['Number of samples'] > 0:
f.write(struct.pack('<I', inst_props['Sample header size']))
f.write(struct.pack('<96B', *inst_props['Sample number for all notes']))
f.write(self._pack_envelope(inst_props['Volume envelope points']))
f.write(self._pack_envelope(inst_props['Panning envelope points']))
f.write(struct.pack('<B', inst_props['Number of volume points']))
f.write(struct.pack('<B', inst_props['Number of panning points']))
f.write(struct.pack('<B', inst_props['Volume sustain point']))
f.write(struct.pack('<B', inst_props['Volume loop start point']))
f.write(struct.pack('<B', inst_props['Volume loop end point']))
f.write(struct.pack('<B', inst_props['Panning sustain point']))
f.write(struct.pack('<B', inst_props['Panning loop start point']))
f.write(struct.pack('<B', inst_props['Panning loop end point']))
f.write(struct.pack('<B', inst_props['Volume type']))
f.write(struct.pack('<B', inst_props['Panning type']))
f.write(struct.pack('<B', inst_props['Vibrato type']))
f.write(struct.pack('<B', inst_props['Vibrato sweep']))
f.write(struct.pack('<B', inst_props['Vibrato depth']))
f.write(struct.pack('<B', inst_props['Vibrato rate']))
f.write(struct.pack('<H', inst_props['Volume fadeout']))
f.write(struct.pack('<H', inst_props['Reserved']))
# Assume no extra bytes
for s, sample_props in enumerate(inst_props['Samples']):
f.write(struct.pack('<I', sample_props['Sample length']))
f.write(struct.pack('<I', sample_props['Sample loop start']))
f.write(struct.pack('<I', sample_props['Sample loop length']))
f.write(struct.pack('<B', sample_props['Volume']))
f.write(struct.pack('<b', sample_props['Finetune']))
f.write(struct.pack('<B', sample_props['Sample type']))
f.write(struct.pack('<B', sample_props['Panning']))
f.write(struct.pack('<b', sample_props['Relative note number']))
f.write(struct.pack('<B', sample_props['Reserved']))
f.write(sample_props['Sample name'].ljust(22, '\x00').encode('ascii'))
f.write(self.sample_data[i][s])
def _pack_envelope(self, points):
packed = b''
for x, y in points:
packed += struct.pack('<HH', x, y)
return packed + b'\x00' * (48 - len(packed)) # Pad if fewer points
- The following Java class can open, decode (read), encode (write), and print all properties of a .XM file. It uses
DataInputStreamandDataOutputStreamfor binary operations.
import java.io.*;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.*;
public class XMFile {
private Map<String, Object> properties = new HashMap<>();
private List<byte[]> patternData = new ArrayList<>();
private List<List<byte[]>> sampleData = new ArrayList<>();
public XMFile(String filename) throws IOException {
if (filename != null) {
read(filename);
}
}
public void read(String filename) throws IOException {
try (FileInputStream fis = new FileInputStream(filename);
DataInputStream dis = new DataInputStream(fis)) {
byte[] buffer = fis.readAllBytes();
ByteBuffer bb = ByteBuffer.wrap(buffer).order(ByteOrder.LITTLE_ENDIAN);
int offset = 0;
// Header
properties.put("ID text", new String(buffer, offset, 17, "ASCII").trim());
offset += 17;
properties.put("Module name", new String(buffer, offset, 20, "ASCII").trim());
offset += 20;
properties.put("Separator", bb.get(offset) & 0xFF);
offset += 1;
properties.put("Tracker name", new String(buffer, offset, 20, "ASCII").trim());
offset += 20;
properties.put("Version number", bb.getShort(offset) & 0xFFFF);
offset += 2;
int headerSize = properties.put("Header size", bb.getInt(offset));
offset += 4;
int songLength = properties.put("Song length", bb.getShort(offset) & 0xFFFF);
offset += 2;
properties.put("Restart position", bb.getShort(offset) & 0xFFFF);
offset += 2;
properties.put("Number of channels", bb.getShort(offset) & 0xFFFF);
offset += 2;
int numPatterns = properties.put("Number of patterns", bb.getShort(offset) & 0xFFFF);
offset += 2;
int numInstruments = properties.put("Number of instruments", bb.getShort(offset) & 0xFFFF);
offset += 2;
properties.put("Flags", bb.getShort(offset) & 0xFFFF);
offset += 2;
properties.put("Default tempo", bb.getShort(offset) & 0xFFFF);
offset += 2;
properties.put("Default BPM", bb.getShort(offset) & 0xFFFF);
offset += 2;
int[] patternOrder = new int[256];
for (int i = 0; i < 256; i++) {
patternOrder[i] = bb.get(offset + i) & 0xFF;
}
properties.put("Pattern order table", patternOrder);
offset += 256;
offset = 60 + headerSize - 4;
// Patterns
List<Map<String, Object>> patterns = new ArrayList<>();
for (int p = 0; p < numPatterns; p++) {
Map<String, Object> patProps = new HashMap<>();
int patHeaderLen = patProps.put("Pattern header length", bb.getInt(offset));
offset += 4;
patProps.put("Packing type", bb.get(offset) & 0xFF);
offset += 1;
patProps.put("Number of rows", bb.getShort(offset) & 0xFFFF);
offset += 2;
int patDataSize = patProps.put("Packed pattern data size", bb.getShort(offset) & 0xFFFF);
offset += patHeaderLen - 9;
byte[] patBytes = new byte[patDataSize];
System.arraycopy(buffer, offset, patBytes, 0, patDataSize);
patternData.add(patBytes);
offset += patDataSize;
patterns.add(patProps);
}
properties.put("Patterns", patterns);
// Instruments
List<Map<String, Object>> instruments = new ArrayList<>();
for (int i = 0; i < numInstruments; i++) {
Map<String, Object> instProps = new HashMap<>();
int instSize = instProps.put("Instrument size", bb.getInt(offset));
offset += 4;
instProps.put("Instrument name", new String(buffer, offset, 22, "ASCII").trim());
offset += 22;
instProps.put("Instrument type", bb.get(offset) & 0xFF);
offset += 1;
int numSamples = instProps.put("Number of samples", bb.getShort(offset) & 0xFFFF);
offset += 2;
List<byte[]> instSamples = new ArrayList<>();
sampleData.add(instSamples);
if (numSamples > 0) {
instProps.put("Sample header size", bb.getInt(offset));
offset += 4;
int[] keymap = new int[96];
for (int k = 0; k < 96; k++) {
keymap[k] = bb.get(offset + k) & 0xFF;
}
instProps.put("Sample number for all notes", keymap);
offset += 96;
instProps.put("Volume envelope points", getEnvelopePoints(bb, offset, 12));
offset += 48;
instProps.put("Panning envelope points", getEnvelopePoints(bb, offset, 12));
offset += 48;
instProps.put("Number of volume points", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Number of panning points", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Volume sustain point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Volume loop start point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Volume loop end point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Panning sustain point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Panning loop start point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Panning loop end point", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Volume type", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Panning type", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Vibrato type", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Vibrato sweep", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Vibrato depth", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Vibrato rate", bb.get(offset) & 0xFF);
offset += 1;
instProps.put("Volume fadeout", bb.getShort(offset) & 0xFFFF);
offset += 2;
instProps.put("Reserved", bb.getShort(offset) & 0xFFFF);
offset += 2;
offset += instSize - 263;
List<Map<String, Object>> samples = new ArrayList<>();
for (int s = 0; s < numSamples; s++) {
Map<String, Object> sampleProps = new HashMap<>();
int sampleLen = sampleProps.put("Sample length", bb.getInt(offset));
offset += 4;
sampleProps.put("Sample loop start", bb.getInt(offset));
offset += 4;
sampleProps.put("Sample loop length", bb.getInt(offset));
offset += 4;
sampleProps.put("Volume", bb.get(offset) & 0xFF);
offset += 1;
sampleProps.put("Finetune", bb.get(offset));
offset += 1;
int sampleType = sampleProps.put("Sample type", bb.get(offset) & 0xFF);
offset += 1;
sampleProps.put("Panning", bb.get(offset) & 0xFF);
offset += 1;
sampleProps.put("Relative note number", bb.get(offset));
offset += 1;
sampleProps.put("Reserved", bb.get(offset) & 0xFF);
offset += 1;
sampleProps.put("Sample name", new String(buffer, offset, 22, "ASCII").trim());
offset += 22;
int byteLen = sampleLen * ((sampleType & 0x10) != 0 ? 2 : 1);
byte[] sampBytes = new byte[byteLen];
System.arraycopy(buffer, offset, sampBytes, 0, byteLen);
instSamples.add(sampBytes);
offset += byteLen;
samples.add(sampleProps);
}
instProps.put("Samples", samples);
}
instruments.add(instProps);
}
properties.put("Instruments", instruments);
}
}
private int[][] getEnvelopePoints(ByteBuffer bb, int offset, int numPoints) {
int[][] points = new int[numPoints][2];
for (int i = 0; i < numPoints; i++) {
points[i][0] = bb.getShort(offset) & 0xFFFF;
points[i][1] = bb.getShort(offset + 2) & 0xFFFF;
offset += 4;
}
return points;
}
public void printProperties() {
properties.forEach((key, value) -> {
if ("Patterns".equals(key) || "Instruments".equals(key)) {
List<Map<String, Object>> subs = (List<Map<String, Object>>) value;
for (int idx = 0; idx < subs.size(); idx++) {
System.out.println(key + " " + idx + ":");
subs.get(idx).forEach((sk, sv) -> {
if ("Samples".equals(sk)) {
List<Map<String, Object>> samples = (List<Map<String, Object>>) sv;
for (int sidx = 0; sidx < samples.size(); sidx++) {
System.out.println(" Sample " + sidx + ":");
samples.get(sidx).forEach((ssk, ssv) -> System.out.println(" " + ssk + ": " + ssv));
}
} else {
System.out.println(" " + sk + ": " + (sv instanceof int[] ? Arrays.toString((int[]) sv) :
(sv instanceof int[][] ? Arrays.deepToString((int[][]) sv) : sv)));
}
});
}
} else {
System.out.println(key + ": " + (value instanceof int[] ? Arrays.toString((int[]) value) : value));
}
});
}
public void write(String filename) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filename);
DataOutputStream dos = new DataOutputStream(fos)) {
// Write header
dos.write(properties.get("ID text").toString().getBytes("ASCII"));
dos.write(new byte[17 - ((String) properties.get("ID text")).length()]); // Pad
String moduleName = (String) properties.get("Module name");
dos.write(moduleName.getBytes("ASCII"));
dos.write(new byte[20 - moduleName.length()]);
dos.writeByte((int) properties.get("Separator"));
String trackerName = (String) properties.get("Tracker name");
dos.write(trackerName.getBytes("ASCII"));
dos.write(new byte[20 - trackerName.length()]);
dos.writeShort((int) properties.get("Version number"));
dos.writeInt((int) properties.get("Header size"));
dos.writeShort((int) properties.get("Song length"));
dos.writeShort((int) properties.get("Restart position"));
dos.writeShort((int) properties.get("Number of channels"));
dos.writeShort((int) properties.get("Number of patterns"));
dos.writeShort((int) properties.get("Number of instruments"));
dos.writeShort((int) properties.get("Flags"));
dos.writeShort((int) properties.get("Default tempo"));
dos.writeShort((int) properties.get("Default BPM"));
int[] patternOrder = (int[]) properties.get("Pattern order table");
for (int val : patternOrder) {
dos.writeByte(val);
}
// Patterns
List<Map<String, Object>> patterns = (List<Map<String, Object>>) properties.get("Patterns");
for (int p = 0; p < patterns.size(); p++) {
Map<String, Object> patProps = patterns.get(p);
dos.writeInt((int) patProps.get("Pattern header length"));
dos.writeByte((int) patProps.get("Packing type"));
dos.writeShort((int) patProps.get("Number of rows"));
dos.writeShort((int) patProps.get("Packed pattern data size"));
dos.write(patternData.get(p));
}
// Instruments
List<Map<String, Object>> instruments = (List<Map<String, Object>>) properties.get("Instruments");
for (int i = 0; i < instruments.size(); i++) {
Map<String, Object> instProps = instruments.get(i);
dos.writeInt((int) instProps.get("Instrument size"));
String instName = (String) instProps.get("Instrument name");
dos.write(instName.getBytes("ASCII"));
dos.write(new byte[22 - instName.length()]);
dos.writeByte((int) instProps.get("Instrument type"));
dos.writeShort((int) instProps.get("Number of samples"));
if ((int) instProps.get("Number of samples") > 0) {
dos.writeInt((int) instProps.get("Sample header size"));
int[] keymap = (int[]) instProps.get("Sample number for all notes");
for (int val : keymap) {
dos.writeByte(val);
}
writeEnvelopePoints(dos, (int[][]) instProps.get("Volume envelope points"));
writeEnvelopePoints(dos, (int[][]) instProps.get("Panning envelope points"));
dos.writeByte((int) instProps.get("Number of volume points"));
dos.writeByte((int) instProps.get("Number of panning points"));
dos.writeByte((int) instProps.get("Volume sustain point"));
dos.writeByte((int) instProps.get("Volume loop start point"));
dos.writeByte((int) instProps.get("Volume loop end point"));
dos.writeByte((int) instProps.get("Panning sustain point"));
dos.writeByte((int) instProps.get("Panning loop start point"));
dos.writeByte((int) instProps.get("Panning loop end point"));
dos.writeByte((int) instProps.get("Volume type"));
dos.writeByte((int) instProps.get("Panning type"));
dos.writeByte((int) instProps.get("Vibrato type"));
dos.writeByte((int) instProps.get("Vibrato sweep"));
dos.writeByte((int) instProps.get("Vibrato depth"));
dos.writeByte((int) instProps.get("Vibrato rate"));
dos.writeShort((int) instProps.get("Volume fadeout"));
dos.writeShort((int) instProps.get("Reserved"));
List<Map<String, Object>> samples = (List<Map<String, Object>>) instProps.get("Samples");
for (int s = 0; s < samples.size(); s++) {
Map<String, Object> sampleProps = samples.get(s);
dos.writeInt((int) sampleProps.get("Sample length"));
dos.writeInt((int) sampleProps.get("Sample loop start"));
dos.writeInt((int) sampleProps.get("Sample loop length"));
dos.writeByte((int) sampleProps.get("Volume"));
dos.writeByte((int) sampleProps.get("Finetune"));
dos.writeByte((int) sampleProps.get("Sample type"));
dos.writeByte((int) sampleProps.get("Panning"));
dos.writeByte((int) sampleProps.get("Relative note number"));
dos.writeByte((int) sampleProps.get("Reserved"));
String sampleName = (String) sampleProps.get("Sample name");
dos.write(sampleName.getBytes("ASCII"));
dos.write(new byte[22 - sampleName.length()]);
dos.write(sampleData.get(i).get(s));
}
}
}
}
}
private void writeEnvelopePoints(DataOutputStream dos, int[][] points) throws IOException {
for (int[] point : points) {
dos.writeShort(point[0]);
dos.writeShort(point[1]);
}
}
}
- The following JavaScript class can open, decode (read), encode (write), and print all properties of a .XM file. It uses
DataViewfor binary operations and assumes asynchronous file reading viaFileReader. For writing, it usesBloband URL for download.
class XMFile {
constructor() {
this.properties = {};
this.patternData = [];
this.sampleData = [];
}
async read(file) {
const buffer = await file.arrayBuffer();
const view = new DataView(buffer);
let offset = 0;
// Header
this.properties['ID text'] = this.getString(view, offset, 17);
offset += 17;
this.properties['Module name'] = this.getString(view, offset, 20);
offset += 20;
this.properties['Separator'] = view.getUint8(offset);
offset += 1;
this.properties['Tracker name'] = this.getString(view, offset, 20);
offset += 20;
this.properties['Version number'] = view.getUint16(offset, true);
offset += 2;
this.properties['Header size'] = view.getUint32(offset, true);
const headerSize = this.properties['Header size'];
offset += 4;
this.properties['Song length'] = view.getUint16(offset, true);
const songLength = this.properties['Song length'];
offset += 2;
this.properties['Restart position'] = view.getUint16(offset, true);
offset += 2;
this.properties['Number of channels'] = view.getUint16(offset, true);
offset += 2;
this.properties['Number of patterns'] = view.getUint16(offset, true);
const numPatterns = this.properties['Number of patterns'];
offset += 2;
this.properties['Number of instruments'] = view.getUint16(offset, true);
const numInstruments = this.properties['Number of instruments'];
offset += 2;
this.properties['Flags'] = view.getUint16(offset, true);
offset += 2;
this.properties['Default tempo'] = view.getUint16(offset, true);
offset += 2;
this.properties['Default BPM'] = view.getUint16(offset, true);
offset += 2;
this.properties['Pattern order table'] = Array.from(new Uint8Array(buffer, offset, 256));
offset += 256;
offset = 60 + headerSize - 4;
// Patterns
this.properties['Patterns'] = [];
for (let p = 0; p < numPatterns; p++) {
const patProps = {};
patProps['Pattern header length'] = view.getUint32(offset, true);
const patHeaderLen = patProps['Pattern header length'];
offset += 4;
patProps['Packing type'] = view.getUint8(offset);
offset += 1;
patProps['Number of rows'] = view.getUint16(offset, true);
offset += 2;
patProps['Packed pattern data size'] = view.getUint16(offset, true);
const patDataSize = patProps['Packed pattern data size'];
offset += patHeaderLen - 9;
this.patternData.push(new Uint8Array(buffer, offset, patDataSize));
offset += patDataSize;
this.properties['Patterns'].push(patProps);
}
// Instruments
this.properties['Instruments'] = [];
for (let i = 0; i < numInstruments; i++) {
const instProps = {};
instProps['Instrument size'] = view.getUint32(offset, true);
const instSize = instProps['Instrument size'];
offset += 4;
instProps['Instrument name'] = this.getString(view, offset, 22);
offset += 22;
instProps['Instrument type'] = view.getUint8(offset);
offset += 1;
instProps['Number of samples'] = view.getUint16(offset, true);
const numSamples = instProps['Number of samples'];
offset += 2;
this.sampleData.push([]);
if (numSamples > 0) {
instProps['Sample header size'] = view.getUint32(offset, true);
offset += 4;
instProps['Sample number for all notes'] = Array.from(new Uint8Array(buffer, offset, 96));
offset += 96;
instProps['Volume envelope points'] = this.getEnvelopePoints(view, offset, 12);
offset += 48;
instProps['Panning envelope points'] = this.getEnvelopePoints(view, offset, 12);
offset += 48;
instProps['Number of volume points'] = view.getUint8(offset);
offset += 1;
instProps['Number of panning points'] = view.getUint8(offset);
offset += 1;
instProps['Volume sustain point'] = view.getUint8(offset);
offset += 1;
instProps['Volume loop start point'] = view.getUint8(offset);
offset += 1;
instProps['Volume loop end point'] = view.getUint8(offset);
offset += 1;
instProps['Panning sustain point'] = view.getUint8(offset);
offset += 1;
instProps['Panning loop start point'] = view.getUint8(offset);
offset += 1;
instProps['Panning loop end point'] = view.getUint8(offset);
offset += 1;
instProps['Volume type'] = view.getUint8(offset);
offset += 1;
instProps['Panning type'] = view.getUint8(offset);
offset += 1;
instProps['Vibrato type'] = view.getUint8(offset);
offset += 1;
instProps['Vibrato sweep'] = view.getUint8(offset);
offset += 1;
instProps['Vibrato depth'] = view.getUint8(offset);
offset += 1;
instProps['Vibrato rate'] = view.getUint8(offset);
offset += 1;
instProps['Volume fadeout'] = view.getUint16(offset, true);
offset += 2;
instProps['Reserved'] = view.getUint16(offset, true);
offset += 2;
offset += instSize - 263;
instProps['Samples'] = [];
for (let s = 0; s < numSamples; s++) {
const sampleProps = {};
sampleProps['Sample length'] = view.getUint32(offset, true);
const sampleLen = sampleProps['Sample length'];
offset += 4;
sampleProps['Sample loop start'] = view.getUint32(offset, true);
offset += 4;
sampleProps['Sample loop length'] = view.getUint32(offset, true);
offset += 4;
sampleProps['Volume'] = view.getUint8(offset);
offset += 1;
sampleProps['Finetune'] = view.getInt8(offset);
offset += 1;
sampleProps['Sample type'] = view.getUint8(offset);
const sampleType = sampleProps['Sample type'];
offset += 1;
sampleProps['Panning'] = view.getUint8(offset);
offset += 1;
sampleProps['Relative note number'] = view.getInt8(offset);
offset += 1;
sampleProps['Reserved'] = view.getUint8(offset);
offset += 1;
sampleProps['Sample name'] = this.getString(view, offset, 22);
offset += 22;
const byteLen = sampleLen * ((sampleType & 0x10) ? 2 : 1);
this.sampleData[i].push(new Uint8Array(buffer, offset, byteLen));
offset += byteLen;
instProps['Samples'].push(sampleProps);
}
}
this.properties['Instruments'].push(instProps);
}
}
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();
}
getEnvelopePoints(view, offset, numPoints) {
const points = [];
for (let i = 0; i < numPoints; i++) {
const x = view.getUint16(offset, true);
const y = view.getUint16(offset + 2, true);
points.push([x, y]);
offset += 4;
}
return points;
}
printProperties() {
console.log(this.properties);
}
write() {
// Calculate total size (simplified; assume standard sizes)
let totalSize = 60 + this.properties['Header size'] - 4;
this.properties['Patterns'].forEach((pat, p) => {
totalSize += pat['Pattern header length'] + this.patternData[p].length;
});
this.properties['Instruments'].forEach((inst, i) => {
totalSize += inst['Instrument size'];
if (inst['Number of samples'] > 0) {
inst['Samples'].forEach((samp, s) => {
totalSize += 40 + this.sampleData[i][s].length; // Sample header 40 bytes
});
}
});
const buffer = new ArrayBuffer(totalSize);
const view = new DataView(buffer);
let offset = 0;
// Write header
this.setString(view, offset, this.properties['ID text'], 17);
offset += 17;
this.setString(view, offset, this.properties['Module name'], 20);
offset += 20;
view.setUint8(offset, this.properties['Separator']);
offset += 1;
this.setString(view, offset, this.properties['Tracker name'], 20);
offset += 20;
view.setUint16(offset, this.properties['Version number'], true);
offset += 2;
view.setUint32(offset, this.properties['Header size'], true);
offset += 4;
view.setUint16(offset, this.properties['Song length'], true);
offset += 2;
view.setUint16(offset, this.properties['Restart position'], true);
offset += 2;
view.setUint16(offset, this.properties['Number of channels'], true);
offset += 2;
view.setUint16(offset, this.properties['Number of patterns'], true);
offset += 2;
view.setUint16(offset, this.properties['Number of instruments'], true);
offset += 2;
view.setUint16(offset, this.properties['Flags'], true);
offset += 2;
view.setUint16(offset, this.properties['Default tempo'], true);
offset += 2;
view.setUint16(offset, this.properties['Default BPM'], true);
offset += 2;
this.properties['Pattern order table'].forEach((val) => {
view.setUint8(offset++, val);
});
// Patterns
this.properties['Patterns'].forEach((pat, p) => {
view.setUint32(offset, pat['Pattern header length'], true);
offset += 4;
view.setUint8(offset, pat['Packing type']);
offset += 1;
view.setUint16(offset, pat['Number of rows'], true);
offset += 2;
view.setUint16(offset, pat['Packed pattern data size'], true);
offset += pat['Pattern header length'] - 9;
const patBytes = this.patternData[p];
for (let b = 0; b < patBytes.length; b++) {
view.setUint8(offset++, patBytes[b]);
}
});
// Instruments
this.properties['Instruments'].forEach((inst, i) => {
view.setUint32(offset, inst['Instrument size'], true);
offset += 4;
this.setString(view, offset, inst['Instrument name'], 22);
offset += 22;
view.setUint8(offset, inst['Instrument type']);
offset += 1;
view.setUint16(offset, inst['Number of samples'], true);
offset += 2;
if (inst['Number of samples'] > 0) {
view.setUint32(offset, inst['Sample header size'], true);
offset += 4;
inst['Sample number for all notes'].forEach((val) => {
view.setUint8(offset++, val);
});
this.setEnvelopePoints(view, offset, inst['Volume envelope points']);
offset += 48;
this.setEnvelopePoints(view, offset, inst['Panning envelope points']);
offset += 48;
view.setUint8(offset, inst['Number of volume points']);
offset += 1;
view.setUint8(offset, inst['Number of panning points']);
offset += 1;
view.setUint8(offset, inst['Volume sustain point']);
offset += 1;
view.setUint8(offset, inst['Volume loop start point']);
offset += 1;
view.setUint8(offset, inst['Volume loop end point']);
offset += 1;
view.setUint8(offset, inst['Panning sustain point']);
offset += 1;
view.setUint8(offset, inst['Panning loop start point']);
offset += 1;
view.setUint8(offset, inst['Panning loop end point']);
offset += 1;
view.setUint8(offset, inst['Volume type']);
offset += 1;
view.setUint8(offset, inst['Panning type']);
offset += 1;
view.setUint8(offset, inst['Vibrato type']);
offset += 1;
view.setUint8(offset, inst['Vibrato sweep']);
offset += 1;
view.setUint8(offset, inst['Vibrato depth']);
offset += 1;
view.setUint8(offset, inst['Vibrato rate']);
offset += 1;
view.setUint16(offset, inst['Volume fadeout'], true);
offset += 2;
view.setUint16(offset, inst['Reserved'], true);
offset += 2;
// Skip extra
inst['Samples'].forEach((samp, s) => {
view.setUint32(offset, samp['Sample length'], true);
offset += 4;
view.setUint32(offset, samp['Sample loop start'], true);
offset += 4;
view.setUint32(offset, samp['Sample loop length'], true);
offset += 4;
view.setUint8(offset, samp['Volume']);
offset += 1;
view.setInt8(offset, samp['Finetune']);
offset += 1;
view.setUint8(offset, samp['Sample type']);
offset += 1;
view.setUint8(offset, samp['Panning']);
offset += 1;
view.setInt8(offset, samp['Relative note number']);
offset += 1;
view.setUint8(offset, samp['Reserved']);
offset += 1;
this.setString(view, offset, samp['Sample name'], 22);
offset += 22;
const sampBytes = this.sampleData[i][s];
for (let b = 0; b < sampBytes.length; b++) {
view.setUint8(offset++, sampBytes[b]);
}
});
}
});
const blob = new Blob([buffer]);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'output.xm';
a.click();
}
setString(view, offset, str, length) {
for (let i = 0; i < str.length; i++) {
view.setUint8(offset + i, str.charCodeAt(i));
}
for (let i = str.length; i < length; i++) {
view.setUint8(offset + i, 0);
}
}
setEnvelopePoints(view, offset, points) {
points.forEach(([x, y]) => {
view.setUint16(offset, x, true);
offset += 2;
view.setUint16(offset, y, true);
offset += 2;
});
}
}
- The following C implementation uses a struct as a "class" equivalent, with functions to open, decode (read), encode (write), and print all properties of a .XM file. It uses standard file I/O and little-endian assumptions (adjust for big-endian systems if needed).
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char id_text[18];
char module_name[21];
uint8_t separator;
char tracker_name[21];
uint16_t version_number;
uint32_t header_size;
uint16_t song_length;
uint16_t restart_position;
uint16_t num_channels;
uint16_t num_patterns;
uint16_t num_instruments;
uint16_t flags;
uint16_t default_tempo;
uint16_t default_bpm;
uint8_t pattern_order[256];
// Dynamic arrays for patterns and instruments
struct Pattern *patterns;
struct Instrument *instruments;
// Data buffers
uint8_t **pattern_data;
uint8_t ***sample_data;
} XMFile;
typedef struct {
uint32_t header_length;
uint8_t packing_type;
uint16_t num_rows;
uint16_t data_size;
} Pattern;
typedef struct {
uint32_t size;
char name[23];
uint8_t type;
uint16_t num_samples;
// If num_samples > 0
uint32_t sample_header_size;
uint8_t sample_keymap[96];
uint16_t volume_points[24]; // 12 x/y pairs
uint16_t panning_points[24];
uint8_t num_volume_points;
uint8_t num_panning_points;
uint8_t volume_sustain;
uint8_t volume_loop_start;
uint8_t volume_loop_end;
uint8_t panning_sustain;
uint8_t panning_loop_start;
uint8_t panning_loop_end;
uint8_t volume_type;
uint8_t panning_type;
uint8_t vibrato_type;
uint8_t vibrato_sweep;
uint8_t vibrato_depth;
uint8_t vibrato_rate;
uint16_t volume_fadeout;
uint16_t reserved;
struct Sample *samples;
} Instrument;
typedef struct {
uint32_t length;
uint32_t loop_start;
uint32_t loop_length;
uint8_t volume;
int8_t finetune;
uint8_t type;
uint8_t panning;
int8_t relative_note;
uint8_t reserved;
char name[23];
} Sample;
// Helper to read string
void read_string(FILE *f, char *dest, size_t len) {
fread(dest, 1, len, f);
dest[len] = '\0';
// Trim nulls/spaces
for (int i = len - 1; i >= 0; i--) {
if (dest[i] == 0 || dest[i] == ' ') dest[i] = 0;
else break;
}
}
void read(XMFile *xm, const char *filename) {
FILE *f = fopen(filename, "rb");
if (!f) return;
fseek(f, 0, SEEK_END);
long file_size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t *buffer = malloc(file_size);
fread(buffer, 1, file_size, f);
fclose(f);
int offset = 0;
memcpy(xm->id_text, buffer + offset, 17);
xm->id_text[17] = 0;
offset += 17;
memcpy(xm->module_name, buffer + offset, 20);
xm->module_name[20] = 0;
offset += 20;
xm->separator = buffer[offset++];
memcpy(xm->tracker_name, buffer + offset, 20);
xm->tracker_name[20] = 0;
offset += 20;
xm->version_number = *(uint16_t*)(buffer + offset);
offset += 2;
xm->header_size = *(uint32_t*)(buffer + offset);
uint32_t header_size = xm->header_size;
offset += 4;
xm->song_length = *(uint16_t*)(buffer + offset);
offset += 2;
xm->restart_position = *(uint16_t*)(buffer + offset);
offset += 2;
xm->num_channels = *(uint16_t*)(buffer + offset);
offset += 2;
xm->num_patterns = *(uint16_t*)(buffer + offset);
uint16_t num_patterns = xm->num_patterns;
offset += 2;
xm->num_instruments = *(uint16_t*)(buffer + offset);
uint16_t num_instruments = xm->num_instruments;
offset += 2;
xm->flags = *(uint16_t*)(buffer + offset);
offset += 2;
xm->default_tempo = *(uint16_t*)(buffer + offset);
offset += 2;
xm->default_bpm = *(uint16_t*)(buffer + offset);
offset += 2;
memcpy(xm->pattern_order, buffer + offset, 256);
offset += 256;
offset = 60 + header_size - 4;
xm->patterns = malloc(num_patterns * sizeof(Pattern));
xm->pattern_data = malloc(num_patterns * sizeof(uint8_t*));
for (int p = 0; p < num_patterns; p++) {
xm->patterns[p].header_length = *(uint32_t*)(buffer + offset);
uint32_t pat_header_len = xm->patterns[p].header_length;
offset += 4;
xm->patterns[p].packing_type = buffer[offset++];
xm->patterns[p].num_rows = *(uint16_t*)(buffer + offset);
offset += 2;
xm->patterns[p].data_size = *(uint16_t*)(buffer + offset);
uint16_t pat_data_size = xm->patterns[p].data_size;
offset += pat_header_len - 9;
xm->pattern_data[p] = malloc(pat_data_size);
memcpy(xm->pattern_data[p], buffer + offset, pat_data_size);
offset += pat_data_size;
}
xm->instruments = malloc(num_instruments * sizeof(Instrument));
xm->sample_data = malloc(num_instruments * sizeof(uint8_t**));
for (int i = 0; i < num_instruments; i++) {
xm->instruments[i].size = *(uint32_t*)(buffer + offset);
uint32_t inst_size = xm->instruments[i].size;
offset += 4;
memcpy(xm->instruments[i].name, buffer + offset, 22);
xm->instruments[i].name[22] = 0;
offset += 22;
xm->instruments[i].type = buffer[offset++];
xm->instruments[i].num_samples = *(uint16_t*)(buffer + offset);
uint16_t num_samples = xm->instruments[i].num_samples;
offset += 2;
xm->sample_data[i] = malloc(num_samples * sizeof(uint8_t*));
if (num_samples > 0) {
xm->instruments[i].sample_header_size = *(uint32_t*)(buffer + offset);
offset += 4;
memcpy(xm->instruments[i].sample_keymap, buffer + offset, 96);
offset += 96;
memcpy(xm->instruments[i].volume_points, buffer + offset, 48);
offset += 48;
memcpy(xm->instruments[i].panning_points, buffer + offset, 48);
offset += 48;
xm->instruments[i].num_volume_points = buffer[offset++];
xm->instruments[i].num_panning_points = buffer[offset++];
xm->instruments[i].volume_sustain = buffer[offset++];
xm->instruments[i].volume_loop_start = buffer[offset++];
xm->instruments[i].volume_loop_end = buffer[offset++];
xm->instruments[i].panning_sustain = buffer[offset++];
xm->instruments[i].panning_loop_start = buffer[offset++];
xm->instruments[i].panning_loop_end = buffer[offset++];
xm->instruments[i].volume_type = buffer[offset++];
xm->instruments[i].panning_type = buffer[offset++];
xm->instruments[i].vibrato_type = buffer[offset++];
xm->instruments[i].vibrato_sweep = buffer[offset++];
xm->instruments[i].vibrato_depth = buffer[offset++];
xm->instruments[i].vibrato_rate = buffer[offset++];
xm->instruments[i].volume_fadeout = *(uint16_t*)(buffer + offset);
offset += 2;
xm->instruments[i].reserved = *(uint16_t*)(buffer + offset);
offset += 2;
offset += inst_size - 263;
xm->instruments[i].samples = malloc(num_samples * sizeof(Sample));
for (int s = 0; s < num_samples; s++) {
xm->instruments[i].samples[s].length = *(uint32_t*)(buffer + offset);
uint32_t sample_len = xm->instruments[i].samples[s].length;
offset += 4;
xm->instruments[i].samples[s].loop_start = *(uint32_t*)(buffer + offset);
offset += 4;
xm->instruments[i].samples[s].loop_length = *(uint32_t*)(buffer + offset);
offset += 4;
xm->instruments[i].samples[s].volume = buffer[offset++];
xm->instruments[i].samples[s].finetune = (int8_t)buffer[offset++];
xm->instruments[i].samples[s].type = buffer[offset++];
uint8_t sample_type = xm->instruments[i].samples[s].type;
xm->instruments[i].samples[s].panning = buffer[offset++];
xm->instruments[i].samples[s].relative_note = (int8_t)buffer[offset++];
xm->instruments[i].samples[s].reserved = buffer[offset++];
memcpy(xm->instruments[i].samples[s].name, buffer + offset, 22);
xm->instruments[i].samples[s].name[22] = 0;
offset += 22;
uint32_t byte_len = sample_len * ((sample_type & 0x10) ? 2 : 1);
xm->sample_data[i][s] = malloc(byte_len);
memcpy(xm->sample_data[i][s], buffer + offset, byte_len);
offset += byte_len;
}
} else {
xm->instruments[i].samples = NULL;
}
}
free(buffer);
}
void print_properties(const XMFile *xm) {
printf("ID text: %s\n", xm->id_text);
printf("Module name: %s\n", xm->module_name);
printf("Separator: %u\n", xm->separator);
printf("Tracker name: %s\n", xm->tracker_name);
printf("Version number: %u\n", xm->version_number);
printf("Header size: %u\n", xm->header_size);
printf("Song length: %u\n", xm->song_length);
printf("Restart position: %u\n", xm->restart_position);
printf("Number of channels: %u\n", xm->num_channels);
printf("Number of patterns: %u\n", xm->num_patterns);
printf("Number of instruments: %u\n", xm->num_instruments);
printf("Flags: %u\n", xm->flags);
printf("Default tempo: %u\n", xm->default_tempo);
printf("Default BPM: %u\n", xm->default_bpm);
printf("Pattern order table: ");
for (int i = 0; i < 256; i++) printf("%u ", xm->pattern_order[i]);
printf("\n");
for (int p = 0; p < xm->num_patterns; p++) {
printf("Pattern %d:\n", p);
printf(" Pattern header length: %u\n", xm->patterns[p].header_length);
printf(" Packing type: %u\n", xm->patterns[p].packing_type);
printf(" Number of rows: %u\n", xm->patterns[p].num_rows);
printf(" Packed pattern data size: %u\n", xm->patterns[p].data_size);
}
for (int i = 0; i < xm->num_instruments; i++) {
printf("Instrument %d:\n", i);
printf(" Instrument size: %u\n", xm->instruments[i].size);
printf(" Instrument name: %s\n", xm->instruments[i].name);
printf(" Instrument type: %u\n", xm->instruments[i].type);
printf(" Number of samples: %u\n", xm->instruments[i].num_samples);
if (xm->instruments[i].num_samples > 0) {
printf(" Sample header size: %u\n", xm->instruments[i].sample_header_size);
printf(" Sample number for all notes: ");
for (int k = 0; k < 96; k++) printf("%u ", xm->instruments[i].sample_keymap[k]);
printf("\n");
printf(" Volume envelope points: ");
for (int ep = 0; ep < 24; ep++) printf("%u ", xm->instruments[i].volume_points[ep]);
printf("\n");
printf(" Panning envelope points: ");
for (int ep = 0; ep < 24; ep++) printf("%u ", xm->instruments[i].panning_points[ep]);
printf("\n");
printf(" Number of volume points: %u\n", xm->instruments[i].num_volume_points);
printf(" Number of panning points: %u\n", xm->instruments[i].num_panning_points);
printf(" Volume sustain point: %u\n", xm->instruments[i].volume_sustain);
printf(" Volume loop start point: %u\n", xm->instruments[i].volume_loop_start);
printf(" Volume loop end point: %u\n", xm->instruments[i].volume_loop_end);
printf(" Panning sustain point: %u\n", xm->instruments[i].panning_sustain);
printf(" Panning loop start point: %u\n", xm->instruments[i].panning_loop_start);
printf(" Panning loop end point: %u\n", xm->instruments[i].panning_loop_end);
printf(" Volume type: %u\n", xm->instruments[i].volume_type);
printf(" Panning type: %u\n", xm->instruments[i].panning_type);
printf(" Vibrato type: %u\n", xm->instruments[i].vibrato_type);
printf(" Vibrato sweep: %u\n", xm->instruments[i].vibrato_sweep);
printf(" Vibrato depth: %u\n", xm->instruments[i].vibrato_depth);
printf(" Vibrato rate: %u\n", xm->instruments[i].vibrato_rate);
printf(" Volume fadeout: %u\n", xm->instruments[i].volume_fadeout);
printf(" Reserved: %u\n", xm->instruments[i].reserved);
for (int s = 0; s < xm->instruments[i].num_samples; s++) {
printf(" Sample %d:\n", s);
printf(" Sample length: %u\n", xm->instruments[i].samples[s].length);
printf(" Sample loop start: %u\n", xm->instruments[i].samples[s].loop_start);
printf(" Sample loop length: %u\n", xm->instruments[i].samples[s].loop_length);
printf(" Volume: %u\n", xm->instruments[i].samples[s].volume);
printf(" Finetune: %d\n", xm->instruments[i].samples[s].finetune);
printf(" Sample type: %u\n", xm->instruments[i].samples[s].type);
printf(" Panning: %u\n", xm->instruments[i].samples[s].panning);
printf(" Relative note number: %d\n", xm->instruments[i].samples[s].relative_note);
printf(" Reserved: %u\n", xm->instruments[i].samples[s].reserved);
printf(" Sample name: %s\n", xm->instruments[i].samples[s].name);
}
}
}
}
void write(const XMFile *xm, const char *filename) {
FILE *f = fopen(filename, "wb");
if (!f) return;
fwrite(xm->id_text, 1, 17, f);
fwrite(xm->module_name, 1, 20, f);
fwrite(&xm->separator, 1, 1, f);
fwrite(xm->tracker_name, 1, 20, f);
fwrite(&xm->version_number, 2, 1, f);
fwrite(&xm->header_size, 4, 1, f);
fwrite(&xm->song_length, 2, 1, f);
fwrite(&xm->restart_position, 2, 1, f);
fwrite(&xm->num_channels, 2, 1, f);
fwrite(&xm->num_patterns, 2, 1, f);
fwrite(&xm->num_instruments, 2, 1, f);
fwrite(&xm->flags, 2, 1, f);
fwrite(&xm->default_tempo, 2, 1, f);
fwrite(&xm->default_bpm, 2, 1, f);
fwrite(xm->pattern_order, 1, 256, f);
for (int p = 0; p < xm->num_patterns; p++) {
fwrite(&xm->patterns[p].header_length, 4, 1, f);
fwrite(&xm->patterns[p].packing_type, 1, 1, f);
fwrite(&xm->patterns[p].num_rows, 2, 1, f);
fwrite(&xm->patterns[p].data_size, 2, 1, f);
fwrite(xm->pattern_data[p], 1, xm->patterns[p].data_size, f);
}
for (int i = 0; i < xm->num_instruments; i++) {
fwrite(&xm->instruments[i].size, 4, 1, f);
fwrite(xm->instruments[i].name, 1, 22, f);
fwrite(&xm->instruments[i].type, 1, 1, f);
fwrite(&xm->instruments[i].num_samples, 2, 1, f);
if (xm->instruments[i].num_samples > 0) {
fwrite(&xm->instruments[i].sample_header_size, 4, 1, f);
fwrite(xm->instruments[i].sample_keymap, 1, 96, f);
fwrite(xm->instruments[i].volume_points, 2, 24, f);
fwrite(xm->instruments[i].panning_points, 2, 24, f);
fwrite(&xm->instruments[i].num_volume_points, 1, 1, f);
fwrite(&xm->instruments[i].num_panning_points, 1, 1, f);
fwrite(&xm->instruments[i].volume_sustain, 1, 1, f);
fwrite(&xm->instruments[i].volume_loop_start, 1, 1, f);
fwrite(&xm->instruments[i].volume_loop_end, 1, 1, f);
fwrite(&xm->instruments[i].panning_sustain, 1, 1, f);
fwrite(&xm->instruments[i].panning_loop_start, 1, 1, f);
fwrite(&xm->instruments[i].panning_loop_end, 1, 1, f);
fwrite(&xm->instruments[i].volume_type, 1, 1, f);
fwrite(&xm->instruments[i].panning_type, 1, 1, f);
fwrite(&xm->instruments[i].vibrato_type, 1, 1, f);
fwrite(&xm->instruments[i].vibrato_sweep, 1, 1, f);
fwrite(&xm->instruments[i].vibrato_depth, 1, 1, f);
fwrite(&xm->instruments[i].vibrato_rate, 1, 1, f);
fwrite(&xm->instruments[i].volume_fadeout, 2, 1, f);
fwrite(&xm->instruments[i].reserved, 2, 1, f);
for (int s = 0; s < xm->instruments[i].num_samples; s++) {
fwrite(&xm->instruments[i].samples[s].length, 4, 1, f);
fwrite(&xm->instruments[i].samples[s].loop_start, 4, 1, f);
fwrite(&xm->instruments[i].samples[s].loop_length, 4, 1, f);
fwrite(&xm->instruments[i].samples[s].volume, 1, 1, f);
fwrite(&xm->instruments[i].samples[s].finetune, 1, 1, f);
fwrite(&xm->instruments[i].samples[s].type, 1, 1, f);
fwrite(&xm->instruments[i].samples[s].panning, 1, 1, f);
fwrite(&xm->instruments[i].samples[s].relative_note, 1, 1, f);
fwrite(&xm->instruments[i].samples[s].reserved, 1, 1, f);
fwrite(xm->instruments[i].samples[s].name, 1, 22, f);
uint32_t byte_len = xm->instruments[i].samples[s].length * ((xm->instruments[i].samples[s].type & 0x10) ? 2 : 1);
fwrite(xm->sample_data[i][s], 1, byte_len, f);
}
}
}
fclose(f);
}
// Free memory function omitted for brevity