Task 694: .STM File Format
Task 694: .STM File Format
.STM File Format Specifications (Scream Tracker Module)
The .STM file format refers to the Scream Tracker Module format used by early versions of the Scream Tracker software (prior to version 3.0). It is a binary format for tracker music modules, containing song metadata, instrument data, pattern sequences, pattern data, and optional sample data. The format supports up to 4 channels, 31 instruments, and up to 64 patterns. Data is stored in little-endian byte order where applicable.
1. List of Properties Intrinsic to the File Format
These are the key fields and structures defined in the .STM format specification. They represent the core data elements that can be extracted from the file:
- Song Name: 20-byte ASCIIZ string (null-terminated) at offset 0x0000.
- Tracker Name: 8-byte string at offset 0x0014.
- ID Marker: 1-byte value (always 0x1A) at offset 0x001C.
- File Type: 1-byte value at offset 0x001D (1 = song without samples; 2 = module with samples).
- Major Version: 1-byte value at offset 0x001E.
- Minor Version: 1-byte value at offset 0x001F.
- Playback Tempo: 1-byte value at offset 0x0020 (in BPM).
- Number of Patterns: 1-byte value at offset 0x0021 (up to 64).
- Global Playback Volume: 1-byte value at offset 0x0022 (0-64).
- Reserved Header Bytes: 13 bytes at offset 0x0023 (typically unused or zero).
- Instrument Data: Array of 31 instrument records starting at offset 0x0030. Each record is 30 bytes:
- Instrument Name: 12-byte ASCIIZ string.
- ID: 1-byte value (always 0).
- Instrument Disk: 1-byte value.
- Reserved: 2-byte word.
- Sample Length: 2-byte word (in bytes).
- Sample Loop Start: 2-byte word.
- Sample Loop End: 2-byte word.
- Sample Volume: 1-byte value (0-64).
- Reserved: 1-byte value.
- C3 Frequency: 2-byte word (in Hz).
- Reserved: 4-byte dword.
- Length in Paragraphs: 2-byte word (for modules; reserved in songs).
- Pattern Orders: 64-byte array at offset 0x03D0 (sequence of pattern indices, 0-63; 0xFF may indicate end).
- Pattern Data: Variable length starting at offset 0x0410. Each pattern is 64 rows x 4 channels. Each channel entry per row is up to 4 bytes:
- Note Byte: 1 byte (bit-mapped: bits 0-3 = note offset, 4-7 = octave; special values 251-255 for compression/undefined).
- Instrument/Volume Byte: 1 byte (if note < 251; bits 0-2 = lower volume bits, 3-7 = instrument number).
- Effect/Upper Volume Byte: 1 byte (bits 0-3 = effect command, 4-6 = upper volume bits).
- Command Data: 1 byte (effect parameter).
- Total per pattern: 1024 bytes if uncompressed, but compressed via special note bytes.
- Sample Data: Variable length after pattern data (padded to 16-byte boundaries). Raw 8-bit unsigned PCM samples for each instrument (only present if file type is 2).
2. Two Direct Download Links for .STM Files
- https://modarchive.org/data/downloads.php?moduleid=155872#trauma.stm (Sample .STM module file: "trauma.stm")
- https://modarchive.org/data/downloads.php?moduleid=180373#future_brain.stm (Sample .STM module file: "future_brain.stm")
3. HTML with Embedded JavaScript for Drag-and-Drop .STM File Parser
This is a self-contained HTML page with embedded JavaScript that allows users to drag and drop a .STM file. It parses the file and dumps all properties to the screen in a readable format. It can be embedded in a blog post (e.g., Ghost CMS) as raw HTML.
import struct import sys class STMFile: def __init__(self, filename=None): self.song_name = '' self.tracker_name = '' self.id_marker = 0 self.file_type = 0 self.major_version = 0 self.minor_version = 0 self.tempo = 0 self.num_patterns = 0 self.global_volume = 0 self.reserved_header = b'' self.instruments = [] # List of dicts self.pattern_orders = [] self.pattern_data = [] # List of bytes for each pattern self.sample_data = b'' # Raw samples if filename: self.read(filename) def read(self, filename): with open(filename, 'rb') as f: data = f.read() # Song Name self.song_name = data[0x0000:0x0014].decode('ascii', errors='ignore').rstrip('\x00') # Tracker Name self.tracker_name = data[0x0014:0x001C].decode('ascii', errors='ignore') # ID self.id_marker = data[0x001C] # File Type self.file_type = data[0x001D] # Versions self.major_version = data[0x001E] self.minor_version = data[0x001F] # Tempo self.tempo = data[0x0020] # Num Patterns self.num_patterns = data[0x0021] # Global Volume self.global_volume = data[0x0022] # Reserved self.reserved_header = data[0x0023:0x0030] # Instruments self.instruments = [] for i in range(31): offset = 0x0030 + i * 30 inst = { 'name': data[offset:offset+12].decode('ascii', errors='ignore').rstrip('\x00'), 'id': data[offset+12], 'disk': data[offset+13], 'reserved1': struct.unpack_from('4. Python Class for .STM File Handling
This Python class can open, decode (read), encode (write), and print all properties from a .STM file to the console.
import struct
import sys
class STMFile:
def __init__(self, filename=None):
self.song_name = ''
self.tracker_name = ''
self.id_marker = 0
self.file_type = 0
self.major_version = 0
self.minor_version = 0
self.tempo = 0
self.num_patterns = 0
self.global_volume = 0
self.reserved_header = b''
self.instruments = [] # List of dicts
self.pattern_orders = []
self.pattern_data = [] # List of bytes for each pattern
self.sample_data = b'' # Raw samples
if filename:
self.read(filename)
def read(self, filename):
with open(filename, 'rb') as f:
data = f.read()
# Song Name
self.song_name = data[0x0000:0x0014].decode('ascii', errors='ignore').rstrip('\x00')
# Tracker Name
self.tracker_name = data[0x0014:0x001C].decode('ascii', errors='ignore')
# ID
self.id_marker = data[0x001C]
# File Type
self.file_type = data[0x001D]
# Versions
self.major_version = data[0x001E]
self.minor_version = data[0x001F]
# Tempo
self.tempo = data[0x0020]
# Num Patterns
self.num_patterns = data[0x0021]
# Global Volume
self.global_volume = data[0x0022]
# Reserved
self.reserved_header = data[0x0023:0x0030]
# Instruments
self.instruments = []
for i in range(31):
offset = 0x0030 + i * 30
inst = {
'name': data[offset:offset+12].decode('ascii', errors='ignore').rstrip('\x00'),
'id': data[offset+12],
'disk': data[offset+13],
'reserved1': struct.unpack_from('<H', data, offset+14)[0],
'length': struct.unpack_from('<H', data, offset+16)[0],
'loop_start': struct.unpack_from('<H', data, offset+18)[0],
'loop_end': struct.unpack_from('<H', data, offset+20)[0],
'volume': data[offset+22],
'reserved2': data[offset+23],
'c3_freq': struct.unpack_from('<H', data, offset+24)[0],
'reserved3': struct.unpack_from('<I', data, offset+26)[0],
'para_length': struct.unpack_from('<H', data, offset+30)[0] if len(data) > offset+30 else 0
}
self.instruments.append(inst)
# Pattern Orders
self.pattern_orders = list(data[0x03D0:0x0410])
# Pattern Data (simplified: assume fixed 1024 bytes per pattern for parsing)
pattern_start = 0x0410
self.pattern_data = []
for p in range(self.num_patterns):
pat_offset = pattern_start + p * 1024
self.pattern_data.append(data[pat_offset:pat_offset+1024])
# Sample Data (remainder, padded)
samples_start = pattern_start + self.num_patterns * 1024
self.sample_data = data[samples_start:]
def write(self, filename):
with open(filename, 'wb') as f:
# Song Name
f.write(self.song_name.encode('ascii')[:20].ljust(20, b'\x00'))
# Tracker Name
f.write(self.tracker_name.encode('ascii')[:8])
# ID
f.write(bytes([self.id_marker]))
# File Type
f.write(bytes([self.file_type]))
# Versions
f.write(bytes([self.major_version, self.minor_version]))
# Tempo
f.write(bytes([self.tempo]))
# Num Patterns
f.write(bytes([self.num_patterns]))
# Global Volume
f.write(bytes([self.global_volume]))
# Reserved
f.write(self.reserved_header.ljust(13, b'\x00')[:13])
# Instruments
for inst in self.instruments:
f.write(inst['name'].encode('ascii')[:12].ljust(12, b'\x00'))
f.write(bytes([inst['id']]))
f.write(bytes([inst['disk']]))
f.write(struct.pack('<H', inst['reserved1']))
f.write(struct.pack('<H', inst['length']))
f.write(struct.pack('<H', inst['loop_start']))
f.write(struct.pack('<H', inst['loop_end']))
f.write(bytes([inst['volume']]))
f.write(bytes([inst['reserved2']]))
f.write(struct.pack('<H', inst['c3_freq']))
f.write(struct.pack('<I', inst['reserved3']))
f.write(struct.pack('<H', inst['para_length']))
# Pattern Orders
f.write(bytes(self.pattern_orders[:64].ljust(64, b'\xFF')[:64]))
# Pattern Data
for pat in self.pattern_data:
f.write(pat.ljust(1024, b'\x00')[:1024])
# Sample Data (pad to 16-byte)
padded_samples = self.sample_data + b'\x00' * ((16 - len(self.sample_data) % 16) % 16)
f.write(padded_samples)
def print_properties(self):
print('Song Name:', self.song_name)
print('Tracker Name:', self.tracker_name)
print('ID: 0x{:02X}'.format(self.id_marker))
print('File Type:', self.file_type, '(Song)' if self.file_type == 1 else '(Module)')
print('Major Version:', self.major_version)
print('Minor Version:', self.minor_version)
print('Playback Tempo:', self.tempo)
print('Number of Patterns:', self.num_patterns)
print('Global Volume:', self.global_volume)
print('Reserved Header:', self.reserved_header.hex())
print('\nInstruments:')
for i, inst in enumerate(self.instruments):
print(f'Instrument {i+1}:')
for k, v in inst.items():
print(f' {k}: {v}')
print('\nPattern Orders:', ' '.join(str(x) for x in self.pattern_orders))
print('\nPattern Data Summary: {} patterns, each ~1024 bytes'.format(self.num_patterns))
print('Sample Data Length:', len(self.sample_data))
# Example usage:
# stm = STMFile('example.stm')
# stm.print_properties()
# stm.write('output.stm')
5. Java Class for .STM File Handling
This Java class can open, decode (read), encode (write), and print all properties from a .STM file to the console.
import java.io.*;
import java.nio.*;
import java.nio.file.*;
public class STMFile {
private String songName;
private String trackerName;
private byte idMarker;
private byte fileType;
private byte majorVersion;
private byte minorVersion;
private byte tempo;
private byte numPatterns;
private byte globalVolume;
private byte[] reservedHeader = new byte[13];
private Instrument[] instruments = new Instrument[31];
private byte[] patternOrders = new byte[64];
private byte[][] patternData; // numPatterns x 1024
private byte[] sampleData;
static class Instrument {
String name;
byte id;
byte disk;
short reserved1;
short length;
short loopStart;
short loopEnd;
byte volume;
byte reserved2;
short c3Freq;
int reserved3;
short paraLength;
}
public STMFile(String filename) throws IOException {
read(filename);
}
public void read(String filename) throws IOException {
byte[] data = Files.readAllBytes(Paths.get(filename));
ByteBuffer buffer = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
// Song Name
songName = getString(buffer, 0x0000, 20);
// Tracker Name
trackerName = getString(buffer, 0x0014, 8);
// ID
idMarker = buffer.get(0x001C);
// File Type
fileType = buffer.get(0x001D);
// Versions
majorVersion = buffer.get(0x001E);
minorVersion = buffer.get(0x001F);
// Tempo
tempo = buffer.get(0x0020);
// Num Patterns
numPatterns = buffer.get(0x0021);
// Global Volume
globalVolume = buffer.get(0x0022);
// Reserved
buffer.position(0x0023);
buffer.get(reservedHeader);
// Instruments
for (int i = 0; i < 31; i++) {
int offset = 0x0030 + i * 30;
Instrument inst = new Instrument();
inst.name = getString(buffer, offset, 12);
inst.id = buffer.get(offset + 12);
inst.disk = buffer.get(offset + 13);
inst.reserved1 = buffer.getShort(offset + 14);
inst.length = buffer.getShort(offset + 16);
inst.loopStart = buffer.getShort(offset + 18);
inst.loopEnd = buffer.getShort(offset + 20);
inst.volume = buffer.get(offset + 22);
inst.reserved2 = buffer.get(offset + 23);
inst.c3Freq = buffer.getShort(offset + 24);
inst.reserved3 = buffer.getInt(offset + 26);
inst.paraLength = buffer.getShort(offset + 30);
instruments[i] = inst;
}
// Pattern Orders
buffer.position(0x03D0);
buffer.get(patternOrders);
// Pattern Data
int patternStart = 0x0410;
patternData = new byte[numPatterns & 0xFF][];
for (int p = 0; p < (numPatterns & 0xFF); p++) {
int patOffset = patternStart + p * 1024;
byte[] pat = new byte[1024];
buffer.position(patOffset);
buffer.get(pat);
patternData[p] = pat;
}
// Sample Data
int samplesStart = patternStart + (numPatterns & 0xFF) * 1024;
sampleData = new byte[data.length - samplesStart];
System.arraycopy(data, samplesStart, sampleData, 0, sampleData.length);
}
public void write(String filename) throws IOException {
try (FileOutputStream fos = new FileOutputStream(filename);
DataOutputStream dos = new DataOutputStream(fos)) {
// Song Name
writeString(dos, songName, 20);
// Tracker Name
writeString(dos, trackerName, 8);
// ID
dos.writeByte(idMarker);
// File Type
dos.writeByte(fileType);
// Versions
dos.writeByte(majorVersion);
dos.writeByte(minorVersion);
// Tempo
dos.writeByte(tempo);
// Num Patterns
dos.writeByte(numPatterns);
// Global Volume
dos.writeByte(globalVolume);
// Reserved
dos.write(reservedHeader);
// Instruments
for (Instrument inst : instruments) {
writeString(dos, inst.name, 12);
dos.writeByte(inst.id);
dos.writeByte(inst.disk);
dos.writeShort(inst.reserved1);
dos.writeShort(inst.length);
dos.writeShort(inst.loopStart);
dos.writeShort(inst.loopEnd);
dos.writeByte(inst.volume);
dos.writeByte(inst.reserved2);
dos.writeShort(inst.c3Freq);
dos.writeInt(inst.reserved3);
dos.writeShort(inst.paraLength);
}
// Pattern Orders
dos.write(patternOrders);
// Pattern Data
for (byte[] pat : patternData) {
dos.write(pat);
}
// Sample Data (pad to 16 bytes)
int pad = (16 - sampleData.length % 16) % 16;
dos.write(sampleData);
for (int i = 0; i < pad; i++) {
dos.writeByte(0);
}
}
}
public void printProperties() {
System.out.println("Song Name: " + songName);
System.out.println("Tracker Name: " + trackerName);
System.out.println("ID: 0x" + Integer.toHexString(idMarker & 0xFF).toUpperCase());
System.out.println("File Type: " + (fileType & 0xFF) + " (" + (fileType == 1 ? "Song" : "Module") + ")");
System.out.println("Major Version: " + (majorVersion & 0xFF));
System.out.println("Minor Version: " + (minorVersion & 0xFF));
System.out.println("Playback Tempo: " + (tempo & 0xFF));
System.out.println("Number of Patterns: " + (numPatterns & 0xFF));
System.out.println("Global Volume: " + (globalVolume & 0xFF));
System.out.print("Reserved Header: ");
for (byte b : reservedHeader) System.out.print(String.format("%02X ", b));
System.out.println();
System.out.println("\nInstruments:");
for (int i = 0; i < 31; i++) {
Instrument inst = instruments[i];
System.out.println("Instrument " + (i + 1) + ":");
System.out.println(" Name: " + inst.name);
System.out.println(" ID: " + (inst.id & 0xFF));
System.out.println(" Disk: " + (inst.disk & 0xFF));
System.out.println(" Reserved1: " + inst.reserved1);
System.out.println(" Length: " + inst.length);
System.out.println(" Loop Start: " + inst.loopStart);
System.out.println(" Loop End: " + inst.loopEnd);
System.out.println(" Volume: " + (inst.volume & 0xFF));
System.out.println(" Reserved2: " + (inst.reserved2 & 0xFF));
System.out.println(" C3 Frequency: " + inst.c3Freq);
System.out.println(" Reserved3: " + inst.reserved3);
System.out.println(" Paragraph Length: " + inst.paraLength);
}
System.out.print("\nPattern Orders: ");
for (byte b : patternOrders) System.out.print((b & 0xFF) + " ");
System.out.println();
System.out.println("\nPattern Data Summary: " + patternData.length + " patterns, each 1024 bytes");
System.out.println("Sample Data Length: " + sampleData.length);
}
private String getString(ByteBuffer buffer, int offset, int length) {
byte[] bytes = new byte[length];
int pos = buffer.position();
buffer.position(offset);
buffer.get(bytes);
buffer.position(pos);
int nullIndex = -1;
for (int i = 0; i < length; i++) {
if (bytes[i] == 0) {
nullIndex = i;
break;
}
}
return new String(bytes, 0, nullIndex != -1 ? nullIndex : length);
}
private void writeString(DataOutputStream dos, String str, int maxLen) throws IOException {
byte[] bytes = str.getBytes("US-ASCII");
dos.write(bytes, 0, Math.min(bytes.length, maxLen));
for (int i = bytes.length; i < maxLen; i++) {
dos.writeByte(0);
}
}
// Example usage:
// public static void main(String[] args) throws IOException {
// STMFile stm = new STMFile("example.stm");
// stm.printProperties();
// stm.write("output.stm");
// }
}
6. JavaScript Class for .STM File Handling
This JavaScript class (for Node.js) can open, decode (read), encode (write), and print all properties from a .STM file to the console. Requires Node.js fs module.
const fs = require('fs');
class STMFile {
constructor(filename = null) {
this.songName = '';
this.trackerName = '';
this.idMarker = 0;
this.fileType = 0;
this.majorVersion = 0;
this.minorVersion = 0;
this.tempo = 0;
this.numPatterns = 0;
this.globalVolume = 0;
this.reservedHeader = Buffer.alloc(13);
this.instruments = new Array(31).fill(null).map(() => ({}));
this.patternOrders = Buffer.alloc(64);
this.patternData = []; // Array of Buffers
this.sampleData = Buffer.alloc(0);
if (filename) {
this.read(filename);
}
}
read(filename) {
const data = fs.readFileSync(filename);
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
// Song Name
this.songName = this.getString(data, 0x0000, 20);
// Tracker Name
this.trackerName = this.getString(data, 0x0014, 8);
// ID
this.idMarker = view.getUint8(0x001C);
// File Type
this.fileType = view.getUint8(0x001D);
// Versions
this.majorVersion = view.getUint8(0x001E);
this.minorVersion = view.getUint8(0x001F);
// Tempo
this.tempo = view.getUint8(0x0020);
// Num Patterns
this.numPatterns = view.getUint8(0x0021);
// Global Volume
this.globalVolume = view.getUint8(0x0022);
// Reserved
this.reservedHeader = data.slice(0x0023, 0x0030);
// Instruments
for (let i = 0; i < 31; i++) {
const offset = 0x0030 + i * 30;
this.instruments[i] = {
name: this.getString(data, offset, 12),
id: view.getUint8(offset + 12),
disk: view.getUint8(offset + 13),
reserved1: view.getUint16(offset + 14, true),
length: view.getUint16(offset + 16, true),
loopStart: view.getUint16(offset + 18, true),
loopEnd: view.getUint16(offset + 20, true),
volume: view.getUint8(offset + 22),
reserved2: view.getUint8(offset + 23),
c3Freq: view.getUint16(offset + 24, true),
reserved3: view.getUint32(offset + 26, true),
paraLength: view.getUint16(offset + 30, true)
};
}
// Pattern Orders
this.patternOrders = data.slice(0x03D0, 0x0410);
// Pattern Data
const patternStart = 0x0410;
this.patternData = [];
for (let p = 0; p < this.numPatterns; p++) {
const patOffset = patternStart + p * 1024;
this.patternData.push(data.slice(patOffset, patOffset + 1024));
}
// Sample Data
const samplesStart = patternStart + this.numPatterns * 1024;
this.sampleData = data.slice(samplesStart);
}
write(filename) {
const buffers = [];
// Song Name
buffers.push(this.padBuffer(Buffer.from(this.songName, 'ascii'), 20));
// Tracker Name
buffers.push(Buffer.from(this.trackerName, 'ascii').slice(0, 8));
// ID
buffers.push(Buffer.from([this.idMarker]));
// File Type
buffers.push(Buffer.from([this.fileType]));
// Versions
buffers.push(Buffer.from([this.majorVersion, this.minorVersion]));
// Tempo
buffers.push(Buffer.from([this.tempo]));
// Num Patterns
buffers.push(Buffer.from([this.numPatterns]));
// Global Volume
buffers.push(Buffer.from([this.globalVolume]));
// Reserved
buffers.push(this.reservedHeader);
// Instruments
const instBuffer = Buffer.alloc(31 * 30);
const instView = new DataView(instBuffer.buffer);
for (let i = 0; i < 31; i++) {
const offset = i * 30;
const inst = this.instruments[i];
this.writeStringToBuffer(instBuffer, inst.name, offset, 12);
instBuffer[offset + 12] = inst.id;
instBuffer[offset + 13] = inst.disk;
instView.setUint16(offset + 14, inst.reserved1, true);
instView.setUint16(offset + 16, inst.length, true);
instView.setUint16(offset + 18, inst.loopStart, true);
instView.setUint16(offset + 20, inst.loopEnd, true);
instBuffer[offset + 22] = inst.volume;
instBuffer[offset + 23] = inst.reserved2;
instView.setUint16(offset + 24, inst.c3Freq, true);
instView.setUint32(offset + 26, inst.reserved3, true);
instView.setUint16(offset + 30, inst.paraLength, true);
}
buffers.push(instBuffer);
// Pattern Orders
buffers.push(this.patternOrders);
// Pattern Data
this.patternData.forEach(pat => buffers.push(this.padBuffer(pat, 1024)));
// Sample Data (pad to 16)
let paddedSamples = Buffer.from(this.sampleData);
const pad = (16 - paddedSamples.length % 16) % 16;
paddedSamples = Buffer.concat([paddedSamples, Buffer.alloc(pad)]);
buffers.push(paddedSamples);
fs.writeFileSync(filename, Buffer.concat(buffers));
}
printProperties() {
console.log('Song Name:', this.songName);
console.log('Tracker Name:', this.trackerName);
console.log('ID: 0x' + this.idMarker.toString(16).toUpperCase());
console.log('File Type:', this.fileType, '(' + (this.fileType === 1 ? 'Song' : 'Module') + ')');
console.log('Major Version:', this.majorVersion);
console.log('Minor Version:', this.minorVersion);
console.log('Playback Tempo:', this.tempo);
console.log('Number of Patterns:', this.numPatterns);
console.log('Global Volume:', this.globalVolume);
console.log('Reserved Header:', this.reservedHeader.toString('hex'));
console.log('\nInstruments:');
this.instruments.forEach((inst, i) => {
console.log(`Instrument ${i + 1}:`);
Object.entries(inst).forEach(([k, v]) => console.log(` ${k}: ${v}`));
});
console.log('\nPattern Orders:', this.patternOrders.join(' '));
console.log('\nPattern Data Summary:', this.patternData.length, 'patterns, each ~1024 bytes');
console.log('Sample Data Length:', this.sampleData.length);
}
getString(buffer, offset, length) {
let end = offset;
while (end < offset + length && buffer[end] !== 0) end++;
return buffer.slice(offset, end).toString('ascii');
}
padBuffer(buf, len) {
const padded = Buffer.alloc(len);
buf.copy(padded, 0, 0, Math.min(buf.length, len));
return padded;
}
writeStringToBuffer(target, str, offset, maxLen) {
const src = Buffer.from(str, 'ascii');
src.copy(target, offset, 0, Math.min(src.length, maxLen));
}
}
// Example usage:
// const stm = new STMFile('example.stm');
// stm.printProperties();
// stm.write('output.stm');
7. C "Class" (Struct with Functions) for .STM File Handling
In C, we use a struct with functions for open, decode (read), encode (write), and print properties to console.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
typedef struct {
char song_name[21];
char tracker_name[9];
uint8_t id_marker;
uint8_t file_type;
uint8_t major_version;
uint8_t minor_version;
uint8_t tempo;
uint8_t num_patterns;
uint8_t global_volume;
uint8_t reserved_header[13];
struct Instrument {
char name[13];
uint8_t id;
uint8_t disk;
uint16_t reserved1;
uint16_t length;
uint16_t loop_start;
uint16_t loop_end;
uint8_t volume;
uint8_t reserved2;
uint16_t c3_freq;
uint32_t reserved3;
uint16_t para_length;
} instruments[31];
uint8_t pattern_orders[64];
uint8_t** pattern_data; // Array of pointers to 1024-byte arrays
uint8_t* sample_data;
size_t sample_data_len;
} STMFile;
void stm_read(STMFile* stm, const char* filename) {
FILE* f = fopen(filename, "rb");
if (!f) {
perror("Failed to open file");
exit(1);
}
fseek(f, 0, SEEK_END);
size_t size = ftell(f);
fseek(f, 0, SEEK_SET);
uint8_t* data = malloc(size);
fread(data, 1, size, f);
fclose(f);
// Song Name
strncpy(stm->song_name, (char*)data + 0x0000, 20);
stm->song_name[20] = '\0';
// Tracker Name
strncpy(stm->tracker_name, (char*)data + 0x0014, 8);
stm->tracker_name[8] = '\0';
// ID
stm->id_marker = data[0x001C];
// File Type
stm->file_type = data[0x001D];
// Versions
stm->major_version = data[0x001E];
stm->minor_version = data[0x001F];
// Tempo
stm->tempo = data[0x0020];
// Num Patterns
stm->num_patterns = data[0x0021];
// Global Volume
stm->global_volume = data[0x0022];
// Reserved
memcpy(stm->reserved_header, data + 0x0023, 13);
// Instruments
for (int i = 0; i < 31; i++) {
size_t offset = 0x0030 + i * 30;
strncpy(stm->instruments[i].name, (char*)data + offset, 12);
stm->instruments[i].name[12] = '\0';
stm->instruments[i].id = data[offset + 12];
stm->instruments[i].disk = data[offset + 13];
memcpy(&stm->instruments[i].reserved1, data + offset + 14, 2);
memcpy(&stm->instruments[i].length, data + offset + 16, 2);
memcpy(&stm->instruments[i].loop_start, data + offset + 18, 2);
memcpy(&stm->instruments[i].loop_end, data + offset + 20, 2);
stm->instruments[i].volume = data[offset + 22];
stm->instruments[i].reserved2 = data[offset + 23];
memcpy(&stm->instruments[i].c3_freq, data + offset + 24, 2);
memcpy(&stm->instruments[i].reserved3, data + offset + 26, 4);
memcpy(&stm->instruments[i].para_length, data + offset + 30, 2);
}
// Pattern Orders
memcpy(stm->pattern_orders, data + 0x03D0, 64);
// Pattern Data
size_t pattern_start = 0x0410;
stm->pattern_data = malloc(stm->num_patterns * sizeof(uint8_t*));
for (uint8_t p = 0; p < stm->num_patterns; p++) {
size_t pat_offset = pattern_start + p * 1024;
stm->pattern_data[p] = malloc(1024);
memcpy(stm->pattern_data[p], data + pat_offset, 1024);
}
// Sample Data
size_t samples_start = pattern_start + stm->num_patterns * 1024;
stm->sample_data_len = size - samples_start;
stm->sample_data = malloc(stm->sample_data_len);
memcpy(stm->sample_data, data + samples_start, stm->sample_data_len);
free(data);
}
void stm_write(STMFile* stm, const char* filename) {
FILE* f = fopen(filename, "wb");
if (!f) {
perror("Failed to write file");
exit(1);
}
// Song Name
char song_padded[20] = {0};
strncpy(song_padded, stm->song_name, 20);
fwrite(song_padded, 1, 20, f);
// Tracker Name
fwrite(stm->tracker_name, 1, 8, f);
// ID
fwrite(&stm->id_marker, 1, 1, f);
// File Type
fwrite(&stm->file_type, 1, 1, f);
// Versions
fwrite(&stm->major_version, 1, 1, f);
fwrite(&stm->minor_version, 1, 1, f);
// Tempo
fwrite(&stm->tempo, 1, 1, f);
// Num Patterns
fwrite(&stm->num_patterns, 1, 1, f);
// Global Volume
fwrite(&stm->global_volume, 1, 1, f);
// Reserved
fwrite(stm->reserved_header, 1, 13, f);
// Instruments
for (int i = 0; i < 31; i++) {
char name_padded[12] = {0};
strncpy(name_padded, stm->instruments[i].name, 12);
fwrite(name_padded, 1, 12, f);
fwrite(&stm->instruments[i].id, 1, 1, f);
fwrite(&stm->instruments[i].disk, 1, 1, f);
fwrite(&stm->instruments[i].reserved1, 2, 1, f);
fwrite(&stm->instruments[i].length, 2, 1, f);
fwrite(&stm->instruments[i].loop_start, 2, 1, f);
fwrite(&stm->instruments[i].loop_end, 2, 1, f);
fwrite(&stm->instruments[i].volume, 1, 1, f);
fwrite(&stm->instruments[i].reserved2, 1, 1, f);
fwrite(&stm->instruments[i].c3_freq, 2, 1, f);
fwrite(&stm->instruments[i].reserved3, 4, 1, f);
fwrite(&stm->instruments[i].para_length, 2, 1, f);
}
// Pattern Orders
fwrite(stm->pattern_orders, 1, 64, f);
// Pattern Data
for (uint8_t p = 0; p < stm->num_patterns; p++) {
fwrite(stm->pattern_data[p], 1, 1024, f);
}
// Sample Data (pad to 16)
fwrite(stm->sample_data, 1, stm->sample_data_len, f);
size_t pad = (16 - stm->sample_data_len % 16) % 16;
for (size_t i = 0; i < pad; i++) {
uint8_t zero = 0;
fwrite(&zero, 1, 1, f);
}
fclose(f);
}
void stm_print_properties(STMFile* stm) {
printf("Song Name: %s\n", stm->song_name);
printf("Tracker Name: %s\n", stm->tracker_name);
printf("ID: 0x%02X\n", stm->id_marker);
printf("File Type: %u (%s)\n", stm->file_type, stm->file_type == 1 ? "Song" : "Module");
printf("Major Version: %u\n", stm->major_version);
printf("Minor Version: %u\n", stm->minor_version);
printf("Playback Tempo: %u\n", stm->tempo);
printf("Number of Patterns: %u\n", stm->num_patterns);
printf("Global Volume: %u\n", stm->global_volume);
printf("Reserved Header: ");
for (int i = 0; i < 13; i++) printf("%02X ", stm->reserved_header[i]);
printf("\n\nInstruments:\n");
for (int i = 0; i < 31; i++) {
printf("Instrument %d:\n", i + 1);
printf(" Name: %s\n", stm->instruments[i].name);
printf(" ID: %u\n", stm->instruments[i].id);
printf(" Disk: %u\n", stm->instruments[i].disk);
printf(" Reserved1: %u\n", stm->instruments[i].reserved1);
printf(" Length: %u\n", stm->instruments[i].length);
printf(" Loop Start: %u\n", stm->instruments[i].loop_start);
printf(" Loop End: %u\n", stm->instruments[i].loop_end);
printf(" Volume: %u\n", stm->instruments[i].volume);
printf(" Reserved2: %u\n", stm->instruments[i].reserved2);
printf(" C3 Frequency: %u\n", stm->instruments[i].c3_freq);
printf(" Reserved3: %u\n", stm->instruments[i].reserved3);
printf(" Paragraph Length: %u\n", stm->instruments[i].para_length);
}
printf("\nPattern Orders: ");
for (int i = 0; i < 64; i++) printf("%u ", stm->pattern_orders[i]);
printf("\n\nPattern Data Summary: %u patterns, each 1024 bytes\n", stm->num_patterns);
printf("Sample Data Length: %zu\n", stm->sample_data_len);
}
void stm_free(STMFile* stm) {
for (uint8_t p = 0; p < stm->num_patterns; p++) {
free(stm->pattern_data[p]);
}
free(stm->pattern_data);
free(stm->sample_data);
}
// Example usage:
// int main() {
// STMFile stm;
// stm_read(&stm, "example.stm");
// stm_print_properties(&stm);
// stm_write(&stm, "output.stm");
// stm_free(&stm);
// return 0;
// }