Task 618: .ROL File Format

Task 618: .ROL File Format

Here is a list of all the properties (fields and structures) intrinsic to the .ROL file format, based on its binary structure. These define the file's layout, data types, and organization. The format is little-endian, binary, with a fixed header followed by 45 variable-length tracks. It requires a separate .BNK file for instruments but the .ROL itself contains song data. Properties are grouped by section for clarity:

Header Properties (fixed at start, total 182 bytes):

  • majorVersion: UINT16LE (major version, usually 0)
  • minorVersion: UINT16LE (minor version, usually 4)
  • signature: char[40] (null-terminated string, usually "\roll\default")
  • tickBeat: UINT16LE (ticks per beat, used for timing)
  • beatMeasure: UINT16LE (beats per measure, for display)
  • scaleY: UINT16LE (editing scale Y-axis)
  • scaleX: UINT16LE (editing scale X-axis)
  • reserved: BYTE (unused, 0)
  • isMelodic: UINT8 (0 = percussive mode, 1 = melodic mode)
  • counters: UINT16LE[45] (array of 45 counters: indices 0-10 = cTicks[11] for voice total ticks; 11-21 = cTimbreEvents[11]; 22-32 = cVolumeEvents[11]; 33-43 = cPitchEvents[11]; 44 = cTempoEvents)
  • filler: BYTE[38] (padding, all 0)

Tempo Track Properties (Track 0, variable length):

  • trackName: char[15] (null-terminated, usually " Tempo")
  • basicTempo: Single (float32 LE, beats per minute)
  • nEvents: UINT16LE (number of tempo events, equals counters[44])
  • tempoEvents: Array of E_TEMPO[nEvents], where each E_TEMPO is:
  • atTick: UINT16LE (absolute tick time)
  • multiplier: Single (float32 LE, tempo multiplier 0.01-10.0)

Voice Tracks Properties (11 tracks, one per voice 0-10, variable length):

  • trackName: char[15] (null-terminated, " Voix ##" where ## is 00-10)
  • nTicks: UINT16LE (total ticks for track, equals counters[voice_index])
  • noteEvents: Array of E_NOTE (variable count until sum of durations >= nTicks), where each E_NOTE is:
  • note: UINT16LE (0 = off, 12-107 = on, MIDI-like)
  • duration: UINT16LE (ticks)

Timbre Tracks Properties (11 tracks, one per voice 0-10, variable length):

  • trackName: char[15] (null-terminated, " Timbre ##" where ## is 00-10)
  • nEvents: UINT16LE (number of events, equals counters[11 + voice_index])
  • timbreEvents: Array of E_TIMBRE[nEvents], where each E_TIMBRE is:
  • atTick: UINT16LE (absolute tick time)
  • instName: char[9] (null-terminated instrument name from .BNK)
  • filler: BYTE (padding, 0)
  • unknown: UINT16LE (often instrument index)

Volume Tracks Properties (11 tracks, one per voice 0-10, variable length):

  • trackName: char[15] (null-terminated, " Volume ##" where ## is 00-10)
  • nEvents: UINT16LE (number of events, equals counters[22 + voice_index])
  • volumeEvents: Array of E_VOLUME[nEvents], where each E_VOLUME is:
  • atTick: UINT16LE (absolute tick time)
  • volume: Single (float32 LE, multiplier 0.0-1.0)

Pitch Tracks Properties (11 tracks, one per voice 0-10, variable length):

  • trackName: char[15] (null-terminated, " Pitch ##" where ## is 00-10)
  • nEvents: UINT16LE (number of events, equals counters[33 + voice_index])
  • pitchEvents: Array of E_PITCH[nEvents], where each E_PITCH is:
  • atTick: UINT16LE (absolute tick time)
  • pitch: Single (float32 LE, variation 0.0-2.0)

Additional Intrinsic Properties:

  • Total tracks: Always 45 (1 tempo + 4*11 for voices)
  • Mode-dependent: In percussive mode (isMelodic=0), channels 6-10 are percussive instruments with specific mappings and pitch adjustments relative to channel 8 (tom-tom)
  • Endianness: Little-endian for all multi-byte values
  • Floating-point: IEEE 754 single-precision (4 bytes)
  • No compression or encryption
  • File size: Variable, header 182 bytes + sum of track sizes

Two direct download links for .ROL files:

Here is the HTML with embedded JavaScript for a simple page (embeddable in a Ghost blog or similar) that allows drag-and-drop of a .ROL file and dumps all properties to the screen (in a pre-formatted text area):

ROL File Parser
Drag and drop .ROL file here
  1. Here is a Python class that can open, decode (read), print, and write .ROL files based on the properties:
import struct
import os

class ROLFile:
    def __init__(self, filename=None):
        self.majorVersion = 0
        self.minorVersion = 4
        self.signature = b'\\roll\\default\0'
        self.tickBeat = 120
        self.beatMeasure = 4
        self.scaleY = 0
        self.scaleX = 0
        self.reserved = 0
        self.isMelodic = 1
        self.counters = [0] * 45
        self.filler = b'\0' * 38
        # Tempo track
        self.tempo_trackName = b' Tempo\0'
        self.basicTempo = 120.0
        self.tempoEvents = []  # list of (atTick, multiplier)
        # Voice tracks: list of dicts for each voice 0-10
        self.voices = [{} for _ in range(11)]
        for i in range(11):
            self.voices[i]['voice_trackName'] = f' Voix {i:02}\0'.encode()
            self.voices[i]['nTicks'] = 0
            self.voices[i]['noteEvents'] = []  # list of (note, duration)
            self.voices[i]['timbre_trackName'] = f' Timbre {i:02}\0'.encode()
            self.voices[i]['timbreEvents'] = []  # list of (atTick, instName, filler, unknown)
            self.voices[i]['volume_trackName'] = f' Volume {i:02}\0'.encode()
            self.voices[i]['volumeEvents'] = []  # list of (atTick, volume)
            self.voices[i]['pitch_trackName'] = f' Pitch {i:02}\0'.encode()
            self.voices[i]['pitchEvents'] = []  # list of (atTick, pitch)
        if filename:
            self.read(filename)

    def read(self, filename):
        with open(filename, 'rb') as f:
            data = f.read()
        offset = 0
        (self.majorVersion,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        (self.minorVersion,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        self.signature = data[offset:offset+40]; offset += 40
        (self.tickBeat,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        (self.beatMeasure,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        (self.scaleY,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        (self.scaleX,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        (self.reserved,) = struct.unpack('B', data[offset:offset+1]); offset += 1
        (self.isMelodic,) = struct.unpack('B', data[offset:offset+1]); offset += 1
        for i in range(45):
            (self.counters[i],) = struct.unpack('<H', data[offset:offset+2]); offset += 2
        self.filler = data[offset:offset+38]; offset += 38
        # Tempo
        self.tempo_trackName = data[offset:offset+15]; offset += 15
        (self.basicTempo,) = struct.unpack('<f', data[offset:offset+4]); offset += 4
        nTempoEvents = self.counters[44]
        (nEvents,) = struct.unpack('<H', data[offset:offset+2]); offset += 2  # should match
        self.tempoEvents = []
        for _ in range(nTempoEvents):
            (atTick,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
            (multiplier,) = struct.unpack('<f', data[offset:offset+4]); offset += 4
            self.tempoEvents.append((atTick, multiplier))
        # Voices
        for i in range(11):
            # Voice
            self.voices[i]['voice_trackName'] = data[offset:offset+15]; offset += 15
            (self.voices[i]['nTicks'],) = struct.unpack('<H', data[offset:offset+2]); offset += 2
            self.voices[i]['noteEvents'] = []
            sum_dur = 0
            while sum_dur < self.voices[i]['nTicks']:
                (note,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                (duration,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                self.voices[i]['noteEvents'].append((note, duration))
                sum_dur += duration
                if sum_dur >= self.voices[i]['nTicks']: break
            # Timbre
            self.voices[i]['timbre_trackName'] = data[offset:offset+15]; offset += 15
            nTimbreEvents = self.counters[11 + i]
            (nEvents,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
            self.voices[i]['timbreEvents'] = []
            for _ in range(nTimbreEvents):
                (atTick,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                instName = data[offset:offset+9]; offset += 9
                (filler,) = struct.unpack('B', data[offset:offset+1]); offset += 1
                (unknown,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                self.voices[i]['timbreEvents'].append((atTick, instName, filler, unknown))
            # Volume
            self.voices[i]['volume_trackName'] = data[offset:offset+15]; offset += 15
            nVolumeEvents = self.counters[22 + i]
            (nEvents,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
            self.voices[i]['volumeEvents'] = []
            for _ in range(nVolumeEvents):
                (atTick,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                (volume,) = struct.unpack('<f', data[offset:offset+4]); offset += 4
                self.voices[i]['volumeEvents'].append((atTick, volume))
            # Pitch
            self.voices[i]['pitch_trackName'] = data[offset:offset+15]; offset += 15
            nPitchEvents = self.counters[33 + i]
            (nEvents,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
            self.voices[i]['pitchEvents'] = []
            for _ in range(nPitchEvents):
                (atTick,) = struct.unpack('<H', data[offset:offset+2]); offset += 2
                (pitch,) = struct.unpack('<f', data[offset:offset+4]); offset += 4
                self.voices[i]['pitchEvents'].append((atTick, pitch))
        # Update counters based on read data
        self.counters[44] = len(self.tempoEvents)
        for i in range(11):
            self.counters[i] = self.voices[i]['nTicks']
            self.counters[11 + i] = len(self.voices[i]['timbreEvents'])
            self.counters[22 + i] = len(self.voices[i]['volumeEvents'])
            self.counters[33 + i] = len(self.voices[i]['pitchEvents'])

    def print_properties(self):
        print('Header:')
        print(f'majorVersion: {self.majorVersion}')
        print(f'minorVersion: {self.minorVersion}')
        print(f'signature: {self.signature.decode(errors="ignore")}')
        print(f'tickBeat: {self.tickBeat}')
        print(f'beatMeasure: {self.beatMeasure}')
        print(f'scaleY: {self.scaleY}')
        print(f'scaleX: {self.scaleX}')
        print(f'reserved: {self.reserved}')
        print(f'isMelodic: {self.isMelodic}')
        print(f'counters: {self.counters}')
        print(f'filler: (38 bytes of padding)')
        print('\nTempo Track:')
        print(f'trackName: {self.tempo_trackName.decode(errors="ignore")}')
        print(f'basicTempo: {self.basicTempo}')
        print(f'nEvents: {len(self.tempoEvents)}')
        print('tempoEvents:')
        for evt in self.tempoEvents:
            print(f'  - atTick: {evt[0]}, multiplier: {evt[1]}')
        for i in range(11):
            print(f'\nVoice Track {i}:')
            print(f'trackName: {self.voices[i]["voice_trackName"].decode(errors="ignore")}')
            print(f'nTicks: {self.voices[i]["nTicks"]}')
            print('noteEvents:')
            for evt in self.voices[i]['noteEvents']:
                print(f'  - note: {evt[0]}, duration: {evt[1]}')
            print(f'\nTimbre Track {i}:')
            print(f'trackName: {self.voices[i]["timbre_trackName"].decode(errors="ignore")}')
            print(f'nEvents: {len(self.voices[i]["timbreEvents"])}')
            print('timbreEvents:')
            for evt in self.voices[i]['timbreEvents']:
                print(f'  - atTick: {evt[0]}, instName: {evt[1].decode(errors="ignore")}, filler: {evt[2]}, unknown: {evt[3]}')
            print(f'\nVolume Track {i}:')
            print(f'trackName: {self.voices[i]["volume_trackName"].decode(errors="ignore")}')
            print(f'nEvents: {len(self.voices[i]["volumeEvents"])}')
            print('volumeEvents:')
            for evt in self.voices[i]['volumeEvents']:
                print(f'  - atTick: {evt[0]}, volume: {evt[1]}')
            print(f'\nPitch Track {i}:')
            print(f'trackName: {self.voices[i]["pitch_trackName"].decode(errors="ignore")}')
            print(f'nEvents: {len(self.voices[i]["pitchEvents"])}')
            print('pitchEvents:')
            for evt in self.voices[i]['pitchEvents']:
                print(f'  - atTick: {evt[0]}, pitch: {evt[1]}')

    def write(self, filename):
        with open(filename, 'wb') as f:
            # Header
            f.write(struct.pack('<H', self.majorVersion))
            f.write(struct.pack('<H', self.minorVersion))
            sig = self.signature.ljust(40, b'\0')
            f.write(sig)
            f.write(struct.pack('<H', self.tickBeat))
            f.write(struct.pack('<H', self.beatMeasure))
            f.write(struct.pack('<H', self.scaleY))
            f.write(struct.pack('<H', self.scaleX))
            f.write(struct.pack('B', self.reserved))
            f.write(struct.pack('B', self.isMelodic))
            for c in self.counters:
                f.write(struct.pack('<H', c))
            f.write(self.filler)
            # Tempo
            tname = self.tempo_trackName.ljust(15, b'\0')
            f.write(tname)
            f.write(struct.pack('<f', self.basicTempo))
            f.write(struct.pack('<H', len(self.tempoEvents)))
            for evt in self.tempoEvents:
                f.write(struct.pack('<H', evt[0]))
                f.write(struct.pack('<f', evt[1]))
            # Voices
            for i in range(11):
                # Voice
                vname = self.voices[i]['voice_trackName'].ljust(15, b'\0')
                f.write(vname)
                f.write(struct.pack('<H', self.voices[i]['nTicks']))
                for evt in self.voices[i]['noteEvents']:
                    f.write(struct.pack('<H', evt[0]))
                    f.write(struct.pack('<H', evt[1]))
                # Timbre
                tname = self.voices[i]['timbre_trackName'].ljust(15, b'\0')
                f.write(tname)
                f.write(struct.pack('<H', len(self.voices[i]['timbreEvents'])))
                for evt in self.voices[i]['timbreEvents']:
                    f.write(struct.pack('<H', evt[0]))
                    iname = evt[1].ljust(9, b'\0')
                    f.write(iname)
                    f.write(struct.pack('B', evt[2]))
                    f.write(struct.pack('<H', evt[3]))
                # Volume
                vname = self.voices[i]['volume_trackName'].ljust(15, b'\0')
                f.write(vname)
                f.write(struct.pack('<H', len(self.voices[i]['volumeEvents'])))
                for evt in self.voices[i]['volumeEvents']:
                    f.write(struct.pack('<H', evt[0]))
                    f.write(struct.pack('<f', evt[1]))
                # Pitch
                pname = self.voices[i]['pitch_trackName'].ljust(15, b'\0')
                f.write(pname)
                f.write(struct.pack('<H', len(self.voices[i]['pitchEvents'])))
                for evt in self.voices[i]['pitchEvents']:
                    f.write(struct.pack('<H', evt[0]))
                    f.write(struct.pack('<f', evt[1]))
  1. Here is a Java class that can open, decode (read), print, and write .ROL files based on the properties:
import java.io.*;
import java.nio.*;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.util.*;

public class RolFile {
    private short majorVersion = 0;
    private short minorVersion = 4;
    private String signature = "\\roll\\default";
    private short tickBeat = 120;
    private short beatMeasure = 4;
    private short scaleY = 0;
    private short scaleX = 0;
    private byte reserved = 0;
    private byte isMelodic = 1;
    private short[] counters = new short[45];
    private byte[] filler = new byte[38];
    // Tempo
    private String tempo_trackName = " Tempo";
    private float basicTempo = 120.0f;
    private List<float[]> tempoEvents = new ArrayList<>(); // atTick (short), multiplier (float)
    // Voices: array of maps for simplicity
    private Map<String, Object>[] voices = new HashMap[11];

    public RolFile(String filename) throws IOException {
        for (int i = 0; i < 11; i++) {
            voices[i] = new HashMap<>();
            voices[i].put("voice_trackName", String.format(" Voix %02d", i));
            voices[i].put("nTicks", (short) 0);
            voices[i].put("noteEvents", new ArrayList<short[]>()); // note, duration
            voices[i].put("timbre_trackName", String.format(" Timbre %02d", i));
            voices[i].put("timbreEvents", new ArrayList<Object[]>()); // atTick (short), instName (String), filler (byte), unknown (short)
            voices[i].put("volume_trackName", String.format(" Volume %02d", i));
            voices[i].put("volumeEvents", new ArrayList<float[]>()); // atTick (short), volume (float)
            voices[i].put("pitch_trackName", String.format(" Pitch %02d", i));
            voices[i].put("pitchEvents", new ArrayList<float[]>()); // atTick (short), pitch (float)
        }
        if (filename != null) {
            read(filename);
        }
    }

    public void read(String filename) throws IOException {
        byte[] data = Files.readAllBytes(Paths.get(filename));
        ByteBuffer bb = ByteBuffer.wrap(data).order(ByteOrder.LITTLE_ENDIAN);
        majorVersion = bb.getShort();
        minorVersion = bb.getShort();
        byte[] sigBytes = new byte[40];
        bb.get(sigBytes);
        signature = new String(sigBytes, "UTF-8").replace("\0", "");
        tickBeat = bb.getShort();
        beatMeasure = bb.getShort();
        scaleY = bb.getShort();
        scaleX = bb.getShort();
        reserved = bb.get();
        isMelodic = bb.get();
        for (int i = 0; i < 45; i++) {
            counters[i] = bb.getShort();
        }
        bb.get(filler);
        // Tempo
        byte[] nameBytes = new byte[15];
        bb.get(nameBytes);
        tempo_trackName = new String(nameBytes, "UTF-8").replace("\0", "");
        basicTempo = bb.getFloat();
        short nTempoEvents = bb.getShort();
        tempoEvents.clear();
        for (int i = 0; i < nTempoEvents; i++) {
            short atTick = bb.getShort();
            float multiplier = bb.getFloat();
            tempoEvents.add(new float[]{atTick, multiplier});
        }
        // Voices
        for (int v = 0; v < 11; v++) {
            // Voice
            bb.get(nameBytes);
            voices[v].put("voice_trackName", new String(nameBytes, "UTF-8").replace("\0", ""));
            short nTicks = bb.getShort();
            voices[v].put("nTicks", nTicks);
            List<short[]> noteEvents = (List<short[]>) voices[v].get("noteEvents");
            noteEvents.clear();
            int sumDur = 0;
            while (sumDur < nTicks && bb.hasRemaining()) {
                short note = bb.getShort();
                short dur = bb.getShort();
                noteEvents.add(new short[]{note, dur});
                sumDur += dur;
                if (sumDur >= nTicks) break;
            }
            // Timbre
            bb.get(nameBytes);
            voices[v].put("timbre_trackName", new String(nameBytes, "UTF-8").replace("\0", ""));
            short nTimbre = bb.getShort();
            List<Object[]> timbreEvents = (List<Object[]>) voices[v].get("timbreEvents");
            timbreEvents.clear();
            for (int i = 0; i < nTimbre; i++) {
                short atTick = bb.getShort();
                byte[] instBytes = new byte[9];
                bb.get(instBytes);
                String instName = new String(instBytes, "UTF-8").replace("\0", "");
                byte fill = bb.get();
                short unk = bb.getShort();
                timbreEvents.add(new Object[]{atTick, instName, fill, unk});
            }
            // Volume
            bb.get(nameBytes);
            voices[v].put("volume_trackName", new String(nameBytes, "UTF-8").replace("\0", ""));
            short nVol = bb.getShort();
            List<float[]> volEvents = (List<float[]>) voices[v].get("volumeEvents");
            volEvents.clear();
            for (int i = 0; i < nVol; i++) {
                short atTick = bb.getShort();
                float vol = bb.getFloat();
                volEvents.add(new float[]{atTick, vol});
            }
            // Pitch
            bb.get(nameBytes);
            voices[v].put("pitch_trackName", new String(nameBytes, "UTF-8").replace("\0", ""));
            short nPitch = bb.getShort();
            List<float[]> pitchEvents = (List<float[]>) voices[v].get("pitchEvents");
            pitchEvents.clear();
            for (int i = 0; i < nPitch; i++) {
                short atTick = bb.getShort();
                float p = bb.getFloat();
                pitchEvents.add(new float[]{atTick, p});
            }
        }
        // Update counters
        counters[44] = (short) tempoEvents.size();
        for (int i = 0; i < 11; i++) {
            counters[i] = (short) voices[i].get("nTicks");
            counters[11 + i] = (short) ((List) voices[i].get("timbreEvents")).size();
            counters[22 + i] = (short) ((List) voices[i].get("volumeEvents")).size();
            counters[33 + i] = (short) ((List) voices[i].get("pitchEvents")).size();
        }
    }

    public void printProperties() {
        System.out.println("Header:");
        System.out.println("majorVersion: " + majorVersion);
        System.out.println("minorVersion: " + minorVersion);
        System.out.println("signature: " + signature);
        System.out.println("tickBeat: " + tickBeat);
        System.out.println("beatMeasure: " + beatMeasure);
        System.out.println("scaleY: " + scaleY);
        System.out.println("scaleX: " + scaleX);
        System.out.println("reserved: " + reserved);
        System.out.println("isMelodic: " + isMelodic);
        System.out.println("counters: " + Arrays.toString(counters));
        System.out.println("filler: (38 bytes of padding)");
        System.out.println("\nTempo Track:");
        System.out.println("trackName: " + tempo_trackName);
        System.out.println("basicTempo: " + basicTempo);
        System.out.println("nEvents: " + tempoEvents.size());
        System.out.println("tempoEvents:");
        for (float[] evt : tempoEvents) {
            System.out.println("  - atTick: " + (short) evt[0] + ", multiplier: " + evt[1]);
        }
        for (int i = 0; i < 11; i++) {
            System.out.println("\nVoice Track " + i + ":");
            System.out.println("trackName: " + voices[i].get("voice_trackName"));
            System.out.println("nTicks: " + voices[i].get("nTicks"));
            System.out.println("noteEvents:");
            for (short[] evt : (List<short[]>) voices[i].get("noteEvents")) {
                System.out.println("  - note: " + evt[0] + ", duration: " + evt[1]);
            }
            System.out.println("\nTimbre Track " + i + ":");
            System.out.println("trackName: " + voices[i].get("timbre_trackName"));
            System.out.println("nEvents: " + ((List) voices[i].get("timbreEvents")).size());
            System.out.println("timbreEvents:");
            for (Object[] evt : (List<Object[]>) voices[i].get("timbreEvents")) {
                System.out.println("  - atTick: " + evt[0] + ", instName: " + evt[1] + ", filler: " + evt[2] + ", unknown: " + evt[3]);
            }
            System.out.println("\nVolume Track " + i + ":");
            System.out.println("trackName: " + voices[i].get("volume_trackName"));
            System.out.println("nEvents: " + ((List) voices[i].get("volumeEvents")).size());
            System.out.println("volumeEvents:");
            for (float[] evt : (List<float[]>) voices[i].get("volumeEvents")) {
                System.out.println("  - atTick: " + (short) evt[0] + ", volume: " + evt[1]);
            }
            System.out.println("\nPitch Track " + i + ":");
            System.out.println("trackName: " + voices[i].get("pitch_trackName"));
            System.out.println("nEvents: " + ((List) voices[i].get("pitchEvents")).size());
            System.out.println("pitchEvents:");
            for (float[] evt : (List<float[]>) voices[i].get("pitchEvents")) {
                System.out.println("  - atTick: " + (short) evt[0] + ", pitch: " + evt[1]);
            }
        }
    }

    public void write(String filename) throws IOException {
        ByteBuffer bb = ByteBuffer.allocate(1024 * 1024).order(ByteOrder.LITTLE_ENDIAN); // large enough
        // Header
        bb.putShort(majorVersion);
        bb.putShort(minorVersion);
        byte[] sigBytes = (signature + "\0").getBytes("UTF-8");
        byte[] paddedSig = Arrays.copyOf(sigBytes, 40);
        bb.put(paddedSig);
        bb.putShort(tickBeat);
        bb.putShort(beatMeasure);
        bb.putShort(scaleY);
        bb.putShort(scaleX);
        bb.put(reserved);
        bb.put(isMelodic);
        for (short c : counters) {
            bb.putShort(c);
        }
        bb.put(filler);
        // Tempo
        byte[] nameBytes = (tempo_trackName + "\0").getBytes("UTF-8");
        byte[] paddedName = Arrays.copyOf(nameBytes, 15);
        bb.put(paddedName);
        bb.putFloat(basicTempo);
        bb.putShort((short) tempoEvents.size());
        for (float[] evt : tempoEvents) {
            bb.putShort((short) evt[0]);
            bb.putFloat(evt[1]);
        }
        // Voices
        for (int v = 0; v < 11; v++) {
            // Voice
            nameBytes = ((String) voices[v].get("voice_trackName") + "\0").getBytes("UTF-8");
            paddedName = Arrays.copyOf(nameBytes, 15);
            bb.put(paddedName);
            bb.putShort((short) voices[v].get("nTicks"));
            for (short[] evt : (List<short[]>) voices[v].get("noteEvents")) {
                bb.putShort(evt[0]);
                bb.putShort(evt[1]);
            }
            // Timbre
            nameBytes = ((String) voices[v].get("timbre_trackName") + "\0").getBytes("UTF-8");
            paddedName = Arrays.copyOf(nameBytes, 15);
            bb.put(paddedName);
            bb.putShort((short) ((List) voices[v].get("timbreEvents")).size());
            for (Object[] evt : (List<Object[]>) voices[v].get("timbreEvents")) {
                bb.putShort((short) evt[0]);
                byte[] instBytes = ((String) evt[1] + "\0").getBytes("UTF-8");
                byte[] paddedInst = Arrays.copyOf(instBytes, 9);
                bb.put(paddedInst);
                bb.put((byte) evt[2]);
                bb.putShort((short) evt[3]);
            }
            // Volume
            nameBytes = ((String) voices[v].get("volume_trackName") + "\0").getBytes("UTF-8");
            paddedName = Arrays.copyOf(nameBytes, 15);
            bb.put(paddedName);
            bb.putShort((short) ((List) voices[v].get("volumeEvents")).size());
            for (float[] evt : (List<float[]>) voices[v].get("volumeEvents")) {
                bb.putShort((short) evt[0]);
                bb.putFloat(evt[1]);
            }
            // Pitch
            nameBytes = ((String) voices[v].get("pitch_trackName") + "\0").getBytes("UTF-8");
            paddedName = Arrays.copyOf(nameBytes, 15);
            bb.put(paddedName);
            bb.putShort((short) ((List) voices[v].get("pitchEvents")).size());
            for (float[] evt : (List<float[]>) voices[v].get("pitchEvents")) {
                bb.putShort((short) evt[0]);
                bb.putFloat(evt[1]);
            }
        }
        try (FileChannel fc = new RandomAccessFile(filename, "rw").getChannel()) {
            bb.flip();
            fc.write(bb);
        }
    }
}
  1. Here is a JavaScript class (for Node.js, using fs) that can open, decode (read), print, and write .ROL files based on the properties:
const fs = require('fs');

class ROLFile {
    constructor(filename = null) {
        this.majorVersion = 0;
        this.minorVersion = 4;
        this.signature = '\\roll\\default';
        this.tickBeat = 120;
        this.beatMeasure = 4;
        this.scaleY = 0;
        this.scaleX = 0;
        this.reserved = 0;
        this.isMelodic = 1;
        this.counters = new Array(45).fill(0);
        this.filler = new Uint8Array(38);
        // Tempo
        this.tempo_trackName = ' Tempo';
        this.basicTempo = 120.0;
        this.tempoEvents = []; // array of {atTick, multiplier}
        // Voices
        this.voices = [];
        for (let i = 0; i < 11; i++) {
            this.voices.push({
                voice_trackName: ` Voix ${i.toString().padStart(2, '0')}`,
                nTicks: 0,
                noteEvents: [], // array of {note, duration}
                timbre_trackName: ` Timbre ${i.toString().padStart(2, '0')}`,
                timbreEvents: [], // array of {atTick, instName, filler, unknown}
                volume_trackName: ` Volume ${i.toString().padStart(2, '0')}`,
                volumeEvents: [], // array of {atTick, volume}
                pitch_trackName: ` Pitch ${i.toString().padStart(2, '0')}`,
                pitchEvents: [] // array of {atTick, pitch}
            });
        }
        if (filename) {
            this.read(filename);
        }
    }

    read(filename) {
        const buffer = fs.readFileSync(filename);
        const view = new DataView(buffer.buffer);
        let offset = 0;
        this.majorVersion = view.getUint16(offset, true); offset += 2;
        this.minorVersion = view.getUint16(offset, true); offset += 2;
        let sig = '';
        for (let i = 0; i < 40; i++) {
            const char = view.getUint8(offset++);
            if (char === 0) break;
            sig += String.fromCharCode(char);
        }
        this.signature = sig;
        this.tickBeat = view.getUint16(offset, true); offset += 2;
        this.beatMeasure = view.getUint16(offset, true); offset += 2;
        this.scaleY = view.getUint16(offset, true); offset += 2;
        this.scaleX = view.getUint16(offset, true); offset += 2;
        this.reserved = view.getUint8(offset++); 
        this.isMelodic = view.getUint8(offset++);
        for (let i = 0; i < 45; i++) {
            this.counters[i] = view.getUint16(offset, true); offset += 2;
        }
        for (let i = 0; i < 38; i++) {
            this.filler[i] = view.getUint8(offset++);
        }
        // Tempo
        let tname = '';
        for (let i = 0; i < 15; i++) {
            const char = view.getUint8(offset++);
            if (char !== 0) tname += String.fromCharCode(char);
        }
        this.tempo_trackName = tname;
        this.basicTempo = view.getFloat32(offset, true); offset += 4;
        const nTempo = view.getUint16(offset, true); offset += 2;
        this.tempoEvents = [];
        for (let i = 0; i < nTempo; i++) {
            const atTick = view.getUint16(offset, true); offset += 2;
            const mult = view.getFloat32(offset, true); offset += 4;
            this.tempoEvents.push({atTick, mult});
        }
        // Voices
        for (let v = 0; v < 11; v++) {
            // Voice
            let vname = '';
            for (let i = 0; i < 15; i++) {
                const char = view.getUint8(offset++);
                if (char !== 0) vname += String.fromCharCode(char);
            }
            this.voices[v].voice_trackName = vname;
            this.voices[v].nTicks = view.getUint16(offset, true); offset += 2;
            this.voices[v].noteEvents = [];
            let sumDur = 0;
            while (sumDur < this.voices[v].nTicks) {
                const note = view.getUint16(offset, true); offset += 2;
                const dur = view.getUint16(offset, true); offset += 2;
                this.voices[v].noteEvents.push({note, dur});
                sumDur += dur;
                if (sumDur >= this.voices[v].nTicks) break;
            }
            // Timbre
            let timname = '';
            for (let i = 0; i < 15; i++) {
                const char = view.getUint8(offset++);
                if (char !== 0) timname += String.fromCharCode(char);
            }
            this.voices[v].timbre_trackName = timname;
            const nTim = view.getUint16(offset, true); offset += 2;
            this.voices[v].timbreEvents = [];
            for (let i = 0; i < nTim; i++) {
                const atTick = view.getUint16(offset, true); offset += 2;
                let inst = '';
                for (let j = 0; j < 9; j++) {
                    const char = view.getUint8(offset++);
                    if (char === 0) break;
                    inst += String.fromCharCode(char);
                }
                const fill = view.getUint8(offset++);
                const unk = view.getUint16(offset, true); offset += 2;
                this.voices[v].timbreEvents.push({atTick, inst, fill, unk});
            }
            // Volume
            let volname = '';
            for (let i = 0; i < 15; i++) {
                const char = view.getUint8(offset++);
                if (char !== 0) volname += String.fromCharCode(char);
            }
            this.voices[v].volume_trackName = volname;
            const nVol = view.getUint16(offset, true); offset += 2;
            this.voices[v].volumeEvents = [];
            for (let i = 0; i < nVol; i++) {
                const atTick = view.getUint16(offset, true); offset += 2;
                const vol = view.getFloat32(offset, true); offset += 4;
                this.voices[v].volumeEvents.push({atTick, vol});
            }
            // Pitch
            let pname = '';
            for (let i = 0; i < 15; i++) {
                const char = view.getUint8(offset++);
                if (char !== 0) pname += String.fromCharCode(char);
            }
            this.voices[v].pitch_trackName = pname;
            const nPitch = view.getUint16(offset, true); offset += 2;
            this.voices[v].pitchEvents = [];
            for (let i = 0; i < nPitch; i++) {
                const atTick = view.getUint16(offset, true); offset += 2;
                const p = view.getFloat32(offset, true); offset += 4;
                this.voices[v].pitchEvents.push({atTick, p});
            }
        }
        // Update counters
        this.counters[44] = this.tempoEvents.length;
        for (let i = 0; i < 11; i++) {
            this.counters[i] = this.voices[i].nTicks;
            this.counters[11 + i] = this.voices[i].timbreEvents.length;
            this.counters[22 + i] = this.voices[i].volumeEvents.length;
            this.counters[33 + i] = this.voices[i].pitchEvents.length;
        }
    }

    printProperties() {
        console.log('Header:');
        console.log(`majorVersion: ${this.majorVersion}`);
        console.log(`minorVersion: ${this.minorVersion}`);
        console.log(`signature: ${this.signature}`);
        console.log(`tickBeat: ${this.tickBeat}`);
        console.log(`beatMeasure: ${this.beatMeasure}`);
        console.log(`scaleY: ${this.scaleY}`);
        console.log(`scaleX: ${this.scaleX}`);
        console.log(`reserved: ${this.reserved}`);
        console.log(`isMelodic: ${this.isMelodic}`);
        console.log(`counters: [${this.counters.join(', ')}]`);
        console.log('filler: (38 bytes of padding)');
        console.log('\nTempo Track:');
        console.log(`trackName: ${this.tempo_trackName}`);
        console.log(`basicTempo: ${this.basicTempo}`);
        console.log(`nEvents: ${this.tempoEvents.length}`);
        console.log('tempoEvents:');
        this.tempoEvents.forEach(evt => {
            console.log(`  - atTick: ${evt.atTick}, multiplier: ${evt.mult}`);
        });
        for (let i = 0; i < 11; i++) {
            console.log(`\nVoice Track ${i}:`);
            console.log(`trackName: ${this.voices[i].voice_trackName}`);
            console.log(`nTicks: ${this.voices[i].nTicks}`);
            console.log('noteEvents:');
            this.voices[i].noteEvents.forEach(evt => {
                console.log(`  - note: ${evt.note}, duration: ${evt.dur}`);
            });
            console.log(`\nTimbre Track ${i}:`);
            console.log(`trackName: ${this.voices[i].timbre_trackName}`);
            console.log(`nEvents: ${this.voices[i].timbreEvents.length}`);
            console.log('timbreEvents:');
            this.voices[i].timbreEvents.forEach(evt => {
                console.log(`  - atTick: ${evt.atTick}, instName: ${evt.inst}, filler: ${evt.fill}, unknown: ${evt.unk}`);
            });
            console.log(`\nVolume Track ${i}:`);
            console.log(`trackName: ${this.voices[i].volume_trackName}`);
            console.log(`nEvents: ${this.voices[i].volumeEvents.length}`);
            console.log('volumeEvents:');
            this.voices[i].volumeEvents.forEach(evt => {
                console.log(`  - atTick: ${evt.atTick}, volume: ${evt.vol}`);
            });
            console.log(`\nPitch Track ${i}:`);
            console.log(`trackName: ${this.voices[i].pitch_trackName}`);
            console.log(`nEvents: ${this.voices[i].pitchEvents.length}`);
            console.log('pitchEvents:');
            this.voices[i].pitchEvents.forEach(evt => {
                console.log(`  - atTick: ${evt.atTick}, pitch: ${evt.p}`);
            });
        }
    }

    write(filename) {
        const buffer = new ArrayBuffer(1024 * 1024); // large enough
        const view = new DataView(buffer);
        let offset = 0;
        // Header
        view.setUint16(offset, this.majorVersion, true); offset += 2;
        view.setUint16(offset, this.minorVersion, true); offset += 2;
        const sigBytes = new TextEncoder().encode(this.signature + '\0');
        for (let i = 0; i < 40; i++) {
            view.setUint8(offset++, i < sigBytes.length ? sigBytes[i] : 0);
        }
        view.setUint16(offset, this.tickBeat, true); offset += 2;
        view.setUint16(offset, this.beatMeasure, true); offset += 2;
        view.setUint16(offset, this.scaleY, true); offset += 2;
        view.setUint16(offset, this.scaleX, true); offset += 2;
        view.setUint8(offset++, this.reserved);
        view.setUint8(offset++, this.isMelodic);
        this.counters.forEach(c => {
            view.setUint16(offset, c, true); offset += 2;
        });
        this.filler.forEach(b => {
            view.setUint8(offset++, b);
        });
        // Tempo
        const tnameBytes = new TextEncoder().encode(this.tempo_trackName + '\0');
        for (let i = 0; i < 15; i++) {
            view.setUint8(offset++, i < tnameBytes.length ? tnameBytes[i] : 0);
        }
        view.setFloat32(offset, this.basicTempo, true); offset += 4;
        view.setUint16(offset, this.tempoEvents.length, true); offset += 2;
        this.tempoEvents.forEach(evt => {
            view.setUint16(offset, evt.atTick, true); offset += 2;
            view.setFloat32(offset, evt.mult, true); offset += 4;
        });
        // Voices
        for (let v = 0; v < 11; v++) {
            // Voice
            const vnameBytes = new TextEncoder().encode(this.voices[v].voice_trackName + '\0');
            for (let i = 0; i < 15; i++) {
                view.setUint8(offset++, i < vnameBytes.length ? vnameBytes[i] : 0);
            }
            view.setUint16(offset, this.voices[v].nTicks, true); offset += 2;
            this.voices[v].noteEvents.forEach(evt => {
                view.setUint16(offset, evt.note, true); offset += 2;
                view.setUint16(offset, evt.dur, true); offset += 2;
            });
            // Timbre
            const timnameBytes = new TextEncoder().encode(this.voices[v].timbre_trackName + '\0');
            for (let i = 0; i < 15; i++) {
                view.setUint8(offset++, i < timnameBytes.length ? timnameBytes[i] : 0);
            }
            view.setUint16(offset, this.voices[v].timbreEvents.length, true); offset += 2;
            this.voices[v].timbreEvents.forEach(evt => {
                view.setUint16(offset, evt.atTick, true); offset += 2;
                const instBytes = new TextEncoder().encode(evt.inst + '\0');
                for (let j = 0; j < 9; j++) {
                    view.setUint8(offset++, j < instBytes.length ? instBytes[j] : 0);
                }
                view.setUint8(offset++, evt.fill);
                view.setUint16(offset, evt.unk, true); offset += 2;
            });
            // Volume
            const volnameBytes = new TextEncoder().encode(this.voices[v].volume_trackName + '\0');
            for (let i = 0; i < 15; i++) {
                view.setUint8(offset++, i < volnameBytes.length ? volnameBytes[i] : 0);
            }
            view.setUint16(offset, this.voices[v].volumeEvents.length, true); offset += 2;
            this.voices[v].volumeEvents.forEach(evt => {
                view.setUint16(offset, evt.atTick, true); offset += 2;
                view.setFloat32(offset, evt.vol, true); offset += 4;
            });
            // Pitch
            const pnameBytes = new TextEncoder().encode(this.voices[v].pitch_trackName + '\0');
            for (let i = 0; i < 15; i++) {
                view.setUint8(offset++, i < pnameBytes.length ? pnameBytes[i] : 0);
            }
            view.setUint16(offset, this.voices[v].pitchEvents.length, true); offset += 2;
            this.voices[v].pitchEvents.forEach(evt => {
                view.setUint16(offset, evt.atTick, true); offset += 2;
                view.setFloat32(offset, evt.p, true); offset += 4;
            });
        }
        fs.writeFileSync(filename, new Uint8Array(buffer, 0, offset));
    }
}
  1. Here is a C++ class that can open, decode (read), print, and write .ROL files based on the properties:
#include <iostream>
#include <fstream>
#include <vector>
#include <string>
#include <cstring>
#include <iomanip>

struct TempoEvent {
    uint16_t atTick;
    float multiplier;
};

struct NoteEvent {
    uint16_t note;
    uint16_t duration;
};

struct TimbreEvent {
    uint16_t atTick;
    char instName[10]; // null-terminated
    uint8_t filler;
    uint16_t unknown;
};

struct VolumeEvent {
    uint16_t atTick;
    float volume;
};

struct PitchEvent {
    uint16_t atTick;
    float pitch;
};

struct Voice {
    char voice_trackName[16]; // null-terminated
    uint16_t nTicks;
    std::vector<NoteEvent> noteEvents;
    char timbre_trackName[16];
    std::vector<TimbreEvent> timbreEvents;
    char volume_trackName[16];
    std::vector<VolumeEvent> volumeEvents;
    char pitch_trackName[16];
    std::vector<PitchEvent> pitchEvents;
};

class ROLFile {
public:
    uint16_t majorVersion = 0;
    uint16_t minorVersion = 4;
    char signature[41] = "\\roll\\default"; // null-terminated
    uint16_t tickBeat = 120;
    uint16_t beatMeasure = 4;
    uint16_t scaleY = 0;
    uint16_t scaleX = 0;
    uint8_t reserved = 0;
    uint8_t isMelodic = 1;
    uint16_t counters[45] = {0};
    uint8_t filler[38] = {0};
    // Tempo
    char tempo_trackName[16] = " Tempo";
    float basicTempo = 120.0f;
    std::vector<TempoEvent> tempoEvents;
    // Voices
    Voice voices[11];

    ROLFile(const std::string& filename = "") {
        for (int i = 0; i < 11; ++i) {
            sprintf(voices[i].voice_trackName, " Voix %02d", i);
            voices[i].nTicks = 0;
            sprintf(voices[i].timbre_trackName, " Timbre %02d", i);
            sprintf(voices[i].volume_trackName, " Volume %02d", i);
            sprintf(voices[i].pitch_trackName, " Pitch %02d", i);
        }
        if (!filename.empty()) {
            read(filename);
        }
    }

    void read(const std::string& filename) {
        std::ifstream file(filename, std::ios::binary | std::ios::ate);
        if (!file) return;
        size_t size = file.tellg();
        file.seekg(0);
        std::vector<uint8_t> data(size);
        file.read(reinterpret_cast<char*>(data.data()), size);
        size_t offset = 0;

        majorVersion = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        minorVersion = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        memcpy(signature, &data[offset], 40); signature[40] = '\0'; offset += 40;
        tickBeat = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        beatMeasure = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        scaleY = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        scaleX = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        reserved = data[offset++];
        isMelodic = data[offset++];
        for (int i = 0; i < 45; ++i) {
            counters[i] = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        }
        memcpy(filler, &data[offset], 38); offset += 38;
        // Tempo
        memcpy(tempo_trackName, &data[offset], 15); tempo_trackName[15] = '\0'; offset += 15;
        basicTempo = *reinterpret_cast<float*>(&data[offset]); offset += 4;
        uint16_t nTempo = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
        tempoEvents.clear();
        for (uint16_t i = 0; i < nTempo; ++i) {
            TempoEvent evt;
            evt.atTick = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
            evt.multiplier = *reinterpret_cast<float*>(&data[offset]); offset += 4;
            tempoEvents.push_back(evt);
        }
        // Voices
        for (int v = 0; v < 11; ++v) {
            // Voice
            memcpy(voices[v].voice_trackName, &data[offset], 15); voices[v].voice_trackName[15] = '\0'; offset += 15;
            voices[v].nTicks = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
            voices[v].noteEvents.clear();
            uint32_t sumDur = 0;
            while (sumDur < voices[v].nTicks && offset < size) {
                NoteEvent evt;
                evt.note = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                evt.duration = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                voices[v].noteEvents.push_back(evt);
                sumDur += evt.duration;
                if (sumDur >= voices[v].nTicks) break;
            }
            // Timbre
            memcpy(voices[v].timbre_trackName, &data[offset], 15); voices[v].timbre_trackName[15] = '\0'; offset += 15;
            uint16_t nTim = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
            voices[v].timbreEvents.clear();
            for (uint16_t i = 0; i < nTim; ++i) {
                TimbreEvent evt;
                evt.atTick = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                memcpy(evt.instName, &data[offset], 9); evt.instName[9] = '\0'; offset += 9;
                evt.filler = data[offset++];
                evt.unknown = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                voices[v].timbreEvents.push_back(evt);
            }
            // Volume
            memcpy(voices[v].volume_trackName, &data[offset], 15); voices[v].volume_trackName[15] = '\0'; offset += 15;
            uint16_t nVol = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
            voices[v].volumeEvents.clear();
            for (uint16_t i = 0; i < nVol; ++i) {
                VolumeEvent evt;
                evt.atTick = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                evt.volume = *reinterpret_cast<float*>(&data[offset]); offset += 4;
                voices[v].volumeEvents.push_back(evt);
            }
            // Pitch
            memcpy(voices[v].pitch_trackName, &data[offset], 15); voices[v].pitch_trackName[15] = '\0'; offset += 15;
            uint16_t nPitch = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
            voices[v].pitchEvents.clear();
            for (uint16_t i = 0; i < nPitch; ++i) {
                PitchEvent evt;
                evt.atTick = *reinterpret_cast<uint16_t*>(&data[offset]); offset += 2;
                evt.pitch = *reinterpret_cast<float*>(&data[offset]); offset += 4;
                voices[v].pitchEvents.push_back(evt);
            }
        }
        // Update counters
        counters[44] = tempoEvents.size();
        for (int i = 0; i < 11; ++i) {
            counters[i] = voices[i].nTicks;
            counters[11 + i] = voices[i].timbreEvents.size();
            counters[22 + i] = voices[i].volumeEvents.size();
            counters[33 + i] = voices[i].pitchEvents.size();
        }
    }

    void printProperties() const {
        std::cout << "Header:" << std::endl;
        std::cout << "majorVersion: " << majorVersion << std::endl;
        std::cout << "minorVersion: " << minorVersion << std::endl;
        std::cout << "signature: " << signature << std::endl;
        std::cout << "tickBeat: " << tickBeat << std::endl;
        std::cout << "beatMeasure: " << beatMeasure << std::endl;
        std::cout << "scaleY: " << scaleY << std::endl;
        std::cout << "scaleX: " << scaleX << std::endl;
        std::cout << "reserved: " << static_cast<int>(reserved) << std::endl;
        std::cout << "isMelodic: " << static_cast<int>(isMelodic) << std::endl;
        std::cout << "counters: [";
        for (int i = 0; i < 45; ++i) {
            std::cout << counters[i] << (i < 44 ? ", " : "");
        }
        std::cout << "]" << std::endl;
        std::cout << "filler: (38 bytes of padding)" << std::endl;
        std::cout << "\nTempo Track:" << std::endl;
        std::cout << "trackName: " << tempo_trackName << std::endl;
        std::cout << "basicTempo: " << basicTempo << std::endl;
        std::cout << "nEvents: " << tempoEvents.size() << std::endl;
        std::cout << "tempoEvents:" << std::endl;
        for (const auto& evt : tempoEvents) {
            std::cout << "  - atTick: " << evt.atTick << ", multiplier: " << evt.multiplier << std::endl;
        }
        for (int i = 0; i < 11; ++i) {
            std::cout << "\nVoice Track " << i << ":" << std::endl;
            std::cout << "trackName: " << voices[i].voice_trackName << std::endl;
            std::cout << "nTicks: " << voices[i].nTicks << std::endl;
            std::cout << "noteEvents:" << std::endl;
            for (const auto& evt : voices[i].noteEvents) {
                std::cout << "  - note: " << evt.note << ", duration: " << evt.duration << std::endl;
            }
            std::cout << "\nTimbre Track " << i << ":" << std::endl;
            std::cout << "trackName: " << voices[i].timbre_trackName << std::endl;
            std::cout << "nEvents: " << voices[i].timbreEvents.size() << std::endl;
            std::cout << "timbreEvents:" << std::endl;
            for (const auto& evt : voices[i].timbreEvents) {
                std::cout << "  - atTick: " << evt.atTick << ", instName: " << evt.instName << ", filler: " << static_cast<int>(evt.filler) << ", unknown: " << evt.unknown << std::endl;
            }
            std::cout << "\nVolume Track " << i << ":" << std::endl;
            std::cout << "trackName: " << voices[i].volume_trackName << std::endl;
            std::cout << "nEvents: " << voices[i].volumeEvents.size() << std::endl;
            std::cout << "volumeEvents:" << std::endl;
            for (const auto& evt : voices[i].volumeEvents) {
                std::cout << "  - atTick: " << evt.atTick << ", volume: " << evt.volume << std::endl;
            }
            std::cout << "\nPitch Track " << i << ":" << std::endl;
            std::cout << "trackName: " << voices[i].pitch_trackName << std::endl;
            std::cout << "nEvents: " << voices[i].pitchEvents.size() << std::endl;
            std::cout << "pitchEvents:" << std::endl;
            for (const auto& evt : voices[i].pitchEvents) {
                std::cout << "  - atTick: " << evt.atTick << ", pitch: " << evt.pitch << std::endl;
            }
        }
    }

    void write(const std::string& filename) const {
        std::ofstream file(filename, std::ios::binary);
        if (!file) return;
        // Header
        file.write(reinterpret_cast<const char*>(&majorVersion), 2);
        file.write(reinterpret_cast<const char*>(&minorVersion), 2);
        char sigPadded[40];
        memset(sigPadded, 0, 40);
        strcpy(sigPadded, signature);
        file.write(sigPadded, 40);
        file.write(reinterpret_cast<const char*>(&tickBeat), 2);
        file.write(reinterpret_cast<const char*>(&beatMeasure), 2);
        file.write(reinterpret_cast<const char*>(&scaleY), 2);
        file.write(reinterpret_cast<const char*>(&scaleX), 2);
        file.write(reinterpret_cast<const char*>(&reserved), 1);
        file.write(reinterpret_cast<const char*>(&isMelodic), 1);
        for (int i = 0; i < 45; ++i) {
            file.write(reinterpret_cast<const char*>(&counters[i]), 2);
        }
        file.write(reinterpret_cast<const char*>(filler), 38);
        // Tempo
        char namePadded[15];
        memset(namePadded, 0, 15);
        strcpy(namePadded, tempo_trackName);
        file.write(namePadded, 15);
        file.write(reinterpret_cast<const char*>(&basicTempo), 4);
        uint16_t nTempo = tempoEvents.size();
        file.write(reinterpret_cast<const char*>(&nTempo), 2);
        for (const auto& evt : tempoEvents) {
            file.write(reinterpret_cast<const char*>(&evt.atTick), 2);
            file.write(reinterpret_cast<const char*>(&evt.multiplier), 4);
        }
        // Voices
        for (int v = 0; v < 11; ++v) {
            // Voice
            memset(namePadded, 0, 15);
            strcpy(namePadded, voices[v].voice_trackName);
            file.write(namePadded, 15);
            file.write(reinterpret_cast<const char*>(&voices[v].nTicks), 2);
            for (const auto& evt : voices[v].noteEvents) {
                file.write(reinterpret_cast<const char*>(&evt.note), 2);
                file.write(reinterpret_cast<const char*>(&evt.duration), 2);
            }
            // Timbre
            memset(namePadded, 0, 15);
            strcpy(namePadded, voices[v].timbre_trackName);
            file.write(namePadded, 15);
            uint16_t nTim = voices[v].timbreEvents.size();
            file.write(reinterpret_cast<const char*>(&nTim), 2);
            for (const auto& evt : voices[v].timbreEvents) {
                file.write(reinterpret_cast<const char*>(&evt.atTick), 2);
                char instPadded[9];
                memset(instPadded, 0, 9);
                strcpy(instPadded, evt.instName);
                file.write(instPadded, 9);
                file.write(reinterpret_cast<const char*>(&evt.filler), 1);
                file.write(reinterpret_cast<const char*>(&evt.unknown), 2);
            }
            // Volume
            memset(namePadded, 0, 15);
            strcpy(namePadded, voices[v].volume_trackName);
            file.write(namePadded, 15);
            uint16_t nVol = voices[v].volumeEvents.size();
            file.write(reinterpret_cast<const char*>(&nVol), 2);
            for (const auto& evt : voices[v].volumeEvents) {
                file.write(reinterpret_cast<const char*>(&evt.atTick), 2);
                file.write(reinterpret_cast<const char*>(&evt.volume), 4);
            }
            // Pitch
            memset(namePadded, 0, 15);
            strcpy(namePadded, voices[v].pitch_trackName);
            file.write(namePadded, 15);
            uint16_t nPitch = voices[v].pitchEvents.size();
            file.write(reinterpret_cast<const char*>(&nPitch), 2);
            for (const auto& evt : voices[v].pitchEvents) {
                file.write(reinterpret_cast<const char*>(&evt.atTick), 2);
                file.write(reinterpret_cast<const char*>(&evt.pitch), 4);
            }
        }
    }
};