- Assist in reverse engineering the OP-XY
.xyproject files. - Preserve the originals by moving them into
src/and never modifying that directory. - Use separate working copies for any experiments or tooling.
- The
.xybinaries share a big-endian signaturedd cc bb aa 09 13 03 86, which likely serves as the container magic before a series of chunk descriptors (OP-Z carries a similar design). - The first ~0x200 bytes form a structured header with version-like integers and float encodings (tempo/swing/global FX candidates). These fields differ per project in ways that line up with expected tempo/swing ranges.
- Sample/library metadata begins near offset
0x240and repeats a 12-byte big-endian prologue (>6H) followed by two null-terminated strings (library folder + file name) and at least 8 bytes of additional numeric metadata. Field distributions cluster by instrument family (drum, hihat, ensemble), reinforcing that tracks later reference this table by index. - We now see explicit chunk headers: 24-byte descriptors where
size1 == size2(often0x0540),size3is the payload length,chunk_typefalls into{0x02f8, 0x06f8, 0x0af8, 0x0ef8}(and occasionally0x050100), and flags frequently equal0xffff003f. Examples:ask why2.xy: chunk0x0ef8at0x025c, additional0x0ef8at0x3920, and0x0ef8with a different field6 at0x41b4.agent2.xy: chunk0x0ef8at0x7af0and chunk0x0af8at0x8bb8, both withsize1=size2=0x540.
- Created
scripts/analyze_xy.pyto dump header words, enumerate sample records, and heuristically list the chunk headers. This tooling confirms 201–265 sample entries per project (matching factory content expectations) and surfaces per-field counters to guide flag decoding. - Added
scripts/diff_xy.pywhich slices.xyfiles on the0x00000AF8 FFFF003Fpattern markers and compares each segment in isolation. Usage:
The tool highlights differing bytes with contextual hexdumps even when earlier chunks shift, which simplifies validating trig additions/removals and ring-rotation experiments.python scripts/diff_xy.py path/to/base.xy path/to/other.xy --segments 0 1 - Track blocks (e.g., at
agent2.xy:0x5000) begin just after a chunk header with long stretches of0xff00masks, a track name string (bass, etc.), a patch alias (/vogel), and arrays of 32-bit big-endian parameters—consistent with the 16-track layout hypothesis. - Track pattern payloads (
0x0af8,len ~0x160) show a repeating structure:0x0000 0x0af8marker,0xffff 0x003fflags, a grid of alternating0x0000ff00 / 0x00ff0000 / 0xff0000ffwords (likely trig/step bitmasks across pages), followed by a per-pattern header (0x9b01preset ID,0x10f0pattern length, counts like0x0501,0x020a,0x0201,0x0102). The tail section contains parameter values (e.g.,0x00e8ffff,0x017f0000,0x1140cdcc,0x00014000) plausibly matching per-track envelopes, filters, and FX sends. Marker offsets (0x00000af8) repeat at ~300-byte intervals, clearly segmenting the up-to-9 pattern slots per track (unused patterns collapse to short 110-byte stubs with only preset/slot references). - Larger
0x0af8chunks (size1=0x4761,len=0x358–0x375) begin with an inline length (e.g.,0x00000375) and pack arrays of 0x500a/0x50xx words that resemble per-track pattern pointers plus mix values—consistent with the 99-scene table. The same chunk includes what appears to be a second segment (duplicated header) suggesting multiple tables (scenes + song chain) are stored back-to-back. 0x00000af8/0xffff003fsignatures occur throughout the large track payloads, enabling deterministic slicing of each pattern. All used patterns inagent2.xyshowed lengths between 347–371 bytes; unused slots degrade to 110-byte metadata blocks, which supports the idea that the pattern structure is sparse (only non-empty sections are serialized).- Project headers (offsets 0x08–0x18) hold aggregate sizes that match the string-table chunks (
0x0ef8/0x06f8) plus small integers (e.g.,0x0118,0x011e,0x0100) aligning with tempo/time-signature/groove settings. Flags like0xfdfe01ff/0x0000ff00mirror bitmasks seen later in the project-scene chunks, reinforcing that the header keeps the global defaults. - Step-component roster census on
comp2.xy(track 1/pattern 1): multiply on step 1, probability on step 5, jump on step 9, and the pair portamento/bend on step 13. The latter three share the “0x2700” marker variant that is byte-swapped compared to the canonical0x0027records. Those entries point into the same parameter ring and reuse the shared payload block (payload@0x40). - Step-component removal behaves like a ring buffer update rather than a hard delete. Removing the Step 1 multiply component on-device (creating
comp2b.xy) advanced the component-table head by0x200, shifted subsequent entries one slot “forward”, and rotated the 0x15-byte parameter payload by 2 bytes. The0x27record and payload remain intact; the slot’s step binding (w1) is cleared and the slot-tag (w3) flips from0x3b00(multiply) to an “unassigned” tag (0x4000). The pattern header near0x170also has its component-count byte zeroed. Attempting to zero these structures manually (our earliercomp3/comp4) caused loader assertion failures (serialize_latest.cpp:90 num_patterns > 0) because the ring pointers no longer lined up. - Removing the Step 5 probability component (
comp2c.xy) shows a more complex rotation. The firmware re-tagged every table entry and rewound the parameter ring so the probability slot’s data no longer leads, yet the0x27probability record persists. The slot roster reordered, the head pointer stayed constant, and the pattern metadata (0x0170onwards) flipped several bitmasks (0xff↔0x00, new0x9b01preset marker). Interpretation: the slot list is a circular queue; removal advances the head to the next active slot and copies default masks into the freed slot rather than deleting the record. Offline tools must replicate this rotation exactly to avoid corrupting the pattern index. - Removing the Step 9 jump together with one of the Step 13 (portamento/bend) components on-device (
comp2d.xy) triggered a larger reshuffle: the roster entries now start with the same multiply slot but subsequent entries carry new type IDs (0x3700,0x0132, …) and different step/page bindings (e.g. the shared slot now points at step 8/page 5). The 0x15-byte ring rotated again (0x0004…→0x0404…), offsets of all three0x27records shifted earlier by one byte, and the pattern header/component mask block (0x170&0x2560–0x25c0) swapped its alternating0xff/0x00pairs. The shared 0x27 record at0x18eais still present but its lead-in bytes compacted (00 00 00 27→00 00 27 5c), indicating the firmware slides structure boundaries rather than deleting the record. Overall length shrank by one byte, so downstream offsets change after each firmware edit. - Draft removal procedure (from Step 1 experiment): to clear a component safely we must (1) leave the
0x27record untouched, (2) advance the component-table head (w0 += 0x200) and rotate the eight 4-word entries so the freed slot moves to the tail, (3) rotate the 0x15-byte parameter payload by two bytes so the old component data falls behind the new head, and (4) zero the component-count byte(s) in the pattern header (0x0170region). Anything less leaves dangling pointers and crashes the loader. Need to generalise this to arbitrary slots—comp2cindicates that removing a mid-list entry triggers a full reordering rather than a single-slot rotation. We now detect both0x0027and0x2700markers in code to cover these variants.
- Project hierarchy from the OP-XY guide:
Project → Songs → Scenes → Patterns → Tracks → Steps, with 16 total tracks (8 instrument, 8 auxiliary), 9 patterns per track, up to 99 scenes per project, and 9–14 songs per project depending on firmware revision. Patterns cap at 64 bars / 4 pages and 120 notes, with 14 step components available and 1920 PPQN timing. - Scene data stores per-track pattern index, volume, and mute; scene duration equals the longest pattern unless overridden by the project setting introduced in firmware 1.1.0.
- Track types imply distinct data payloads: instrument tracks carry engine/sampler settings, envelopes, FX sends; auxiliary tracks hold brain routing (scale/key + track targets), punch-in FX states, external MIDI (channel/program/CCs), CV mappings, tape/FX send automation, etc.
- Sample management limits: WAV/AIFF up to 20 s per recording, drum sampler exposes 24 one-shot slots, multisampler allows 24 zones. Device storage is arranged under
projects/,samples/, andpresets/; backups live alongside project files. - Expect project/global config blocks for tempo, groove (10 styles), time signature options (3/4, 4/4, 5/4, 6/8, 7/8, 12/8), voice allocation (24 voices), and master EQ/saturator/compressor.
- Firmware 1.1.0 (2025-10-15) introduced a scene-length policy toggle plus sampler parameter locks, implying future
.xyvariants may encode additional flags or blocks—parser should accommodate optional sections. - Chunk census (flags
0xffff003f):0x0ef8(size10x540, len0xd40or0x8ae): appears near header and throughout the string-table region; wraps blocks of sample path/name pairs (e.g.,agent2.xy:0x025d,0x4fff). Likely “library index” segments feeding the track sampler references.0x06f8(size10xff7f, len0xd40): additional sample-table segments (e.g.,agent2.xy:0x509) containing consecutivecontent/samples/...entries.0x0af8splits into three subtypes:- size1
0x0160, len0x8ae: per-track preset blocks. They carry the sample path + alias plus a 0x150-ish byte parameter bank; markers like0x9b01identify the preset ID that tracks reference from pattern slots. - size1
0xffff017f, len0xff00: instrument-track payloads. Each chunk contains up to nine pattern segments, delimited by0x0000 0x0af8headers. Within each segment we see:0xffff003fflag, followed by alternating0x0000ff00 / 0x00ff0000 / 0xff0000ffwords that act as the 64-step trig/component bitmasks across pages.- A consistent pattern header (
0x9b01preset,0x10f0length) and slot counters like0x0501,0x020a,0x0201,0x0102. - A trailing parameter array (
0x00e8ffff,0x017f0000,0x1140cdcc,0x00014000, etc.) that aligns with per-pattern filter/envelope/FX settings. Unused slots shrink to a 110-byte stub that retains only preset IDs and default masks.
- size1
0x4761, len0x358–0x375: mix/scene tables. Each begins with0x00000af8 ffff003f 00000375and a body of 59 fixed 15-byte records (five 3-byte fields). Those fields vary with scenes (e.g.,[0x8f22, 0x00ae, 0x4700, 0xff7f, 0x00ff]for the first scene and0x00fftriplets for muted/default tracks). Extra arrays in the remainder of the chunk (repeating0x500a0700,0x0501, etc.) look like per-track level/mix data and probably host the song chain.
- size1
0x02f8(size1 ≈0xe50b, len0xd40): containssnapshot/...strings in agent/basby/curio dumps, suggesting project-history slots.
- Pattern trig block (validated in
step05.xy,step01050913.xy,step03_note.xy,step_ascvel_soft.xy,step05nudge.xy,step05nudgeleft.xy):- Lives directly ahead of the parameter words at offset
0x7a4in our test projects. - Byte layout (little-endian):
- Bytes 0–2: signature
00 00 25. - Byte 3: trig count (e.g.
0x01for a single hit,0x04for four hits). - For each trig we observe two 3-byte triplets:
- Tick position (two little-end bytes + pad). Examples:
80 07 00→ 0x0780 ticks (step 5),C0 03 00→ 0x03C0 ticks (step 3),00 0F 00→ 0x0F00 ticks (step 9),80 16 00→ 0x1680 ticks (step 13). Nudging step 5 fully left/right produced0x0690(−240 ticks) and0x086F(+239 ticks), so micro timing lives in the same field with roughly ±240 ticks of swing around the 480‑tick grid. - Note ID + velocity: two little-end bytes for the note (e.g.
0x0135,0x0136) and one byte for velocity (confirmed by tests at 20/40/80/120).
- Tick position (two little-end bytes + pad). Examples:
- Bytes 0–2: signature
- After all trig triplets, the familiar pattern parameter block (
03ff00fc,00000508, etc.) remains unchanged. - Gate length bytes: in
len.xy(steps 1/5/9/13 stretched to 1–4 steps) the 24-bit little-end field starting atoffset+4grows with the gate—e.g.00 C0 03→ 0x03C000 (960 ticks ≈ 2 steps),00 A0 05→ 0x05A000 (1424 ticks due to clip near the pattern end),00 DC 05→ 0x05DC00 (≈1500 ticks). A full 16-step gate in16len.xyyields03 1E 00, which shifts down to 0x001E ticks (=30) once the triads repeat, showing the 24-bit value carries both the raw gate in ticks and redundant bytes at the tail; more tests are needed to decode the exact packing, but the length clearly scales with the 24-bit field. - Gate length bytes: in
len.xy(steps 1/5/9/13 stretched to 1–4 steps) the 24-bit little-end field starting atoffset+4grows with the gate—e.g.00 C0 03→ 0x03C000 (960 ticks ≈ 2 steps),00 A0 05→ 0x05A000 (1440 ticks ≈ 3 steps),00 DC 05→ 0x05DC00 (≈1500 ticks capped by the pattern end). Default hits show02 F0 00, suggesting a base gate of ~240 ticks even before stretching.
- Lives directly ahead of the parameter words at offset
- Sample-record schema (
>6H+ trailer):field0tags resource type:0x0002drum family,0x0200aux/performance,0x0000shaker alt,0xB41F/0x01E1Fsynth presets.field3aligns with root key / keyboard zone (e.g., 256 for drums at C4, 60 for synth middle C, 512 for closed/open hihat).field4behaves as a category mask (clustered per drum subgroup; synth presets use0x8000/0x6400style values).- Trailer stores two 32‑bit pointers/IDs: drum/percussion groups share paired IDs (suggesting left/right sample slots), while factory synth presets use
0xFF0000FFsentinel IDs.
- Quantify how the 0x0ef8 / 0x06f8 string-table chunks cover the full sample catalogue and document how track payloads point back into those segment-local indices.
- Reverse-map the project header words to documented settings (tempo, groove, time signature, voice allocation, scene-length policy) by comparing multiple
.xyfiles and the device UI. - Decode the large
0x0af8(len0xff00) payloads into their 9 pattern slots and 64-step grids, identifying the per-step bitfields for trigs, components, and parameter locks. - Locate the scene (99) and song (9/14) tables—likely within the mid-sized
0x0af8blocks (len0x358–0x375)—and describe the per-track pattern index plus mute/volume encoding. - Validate interpretations with controlled device experiments (toggling a single scene mute, changing groove, adding songs) once the parser extracts these sections.
- Automation PoC runbook for MIDI-driven project generation is captured in
docs/automation_poc.md, with the executable flow inscripts/automation_poc_run.py.
- The per-track synth expression knobs (M1–M4) live in the short header that precedes the pattern payload. When every knob is zero the block is omitted entirely.
- Each knob is serialized as four little-endian bytes: three bytes of 24-bit fixed-point magnitude (0x000000 = far left, 0xFFFFFF = far right), followed by an identifier byte. Teenage Engineering reuses identifier codes across knobs (e.g. both mid knobs carry
0x47, while the fourth uses0x04). - Example (
param2.xy, track 1):dc ff ff 01 7f ff 80 47 41 ff 80 47 41 00 00 04translates to:- M1 =
0xFFFFDC→ ~0.999998 (hard right) - M2 =
0x80FF7F→ ~0.504 (midway) - M3 =
0x80FF41→ ~0.504 (midway) - M4 =
0x000041→ ~0.000004 (effectively zero)
- M1 =
- Editing those 24-bit values in-place directly controls the M1–M4 positions without touching the larger pattern payload.
- Untested theory (based on
adsr.xy→adsr2.xy, track 3): the M2 amp/filter ADSR sliders serialize as the same 24-bit-plus-ID tuples, appended right after the M1 block inside the track’s large0x0af8payload. Maxing amp ADSR and zeroing filter ADSR pushed twelve new records into the list at0x2450, each tagged by stage IDs (amp attack0x99, decay split across0x02/0x0a, sustain0x26/0x7f, release0x19; filter stages surfaced as0x40/0x9a). Older values remain in place, so the device appears to treat the table as a ring buffer and consumes the most recent entry per ID. - Firmware likely maintains an undo ring for these parameter tables. Each slider tweak appends a fresh tuple instead of overwriting the current value, which keeps historical entries available for undo/redo. Because the deserializer expects the ring structure intact (see
serialize_latest.cpp:90 num_patterns > 0crash when we clobbered existing records), offline tools must append new ADSR entries—or perform the same head/tail rotation the device does—rather than editing bytes in place. - Track 4 experiment (
adsr2.xy→adsr2b1.xy) shows how the device serializes injected ADSR edits. Each track carries two contiguous(value24,id8)tables ahead of its0x00000af8 ffff003fpattern payload: the amp envelope block begins atsrc/adsr2b1.xy:0x2050, and the filter envelope block starts at0x21c0. New slider moves are prepended to these blocks, so the first tuple with a given ID is always the current value while the older history trails behind. Example deltas:- Amp release (ID
0x00) shifted from0x017f00(src/adsr2.xy:0x2050) to a fresh head entry0x0000ffatsrc/adsr2b1.xy:0x2054, leaving the older value a few slots deeper. - Filter release (ID
0xff) moved from0x3f0000(src/adsr2.xy:0x21fc) to0xff0000atsrc/adsr2b1.xy:0x21c4. The amp block also received matching0xff0000entries because the history queue records every intermediate tweak. - The device wrote 11 new tuples for these three knob moves, pushing the first pattern marker from
0x20a4to0x20d0(+0x2c bytes). Chunk size fields inside the surrounding track descriptor grow by 4 bytes per tuple, so offline injectors must bump both the amp/filter blocks and the enclosing0x0af8length metadata when splicing in new records. - Practical edit recipe: locate the desired block, insert your new 4-byte
(value,id)tuple(s) immediately before the existing head entry, adjust the chunk’s serialized size counters by4 × tuples_added, and leave the historical records untouched. This mirrors the firmware’s undo-friendly ring and avoids the loader assert hit we saw with in-place edits.
- Amp release (ID
55(Shift) stays latched until a release event is found. For scripted runs, finish by pressing Shift on-device to clear the modifier.- Project quick-iteration macro (
0 → 7 → 6on CC#106, channel 1): open Project menu, trigger the current "save" shortcut (mapped to M2), then hit M1 to start a new project slot. Handy for smoke-testing edited.xyfiles quickly. - CC#106 value 55 (Shift) cannot be released over MIDI; once sent, Shift stays held until the physical key is pressed again, limiting its automation utility.
- Pulse
- Hold
- Multiply
- Velocity
- Up
- Down
- Random
- Portamento
- Bend
- Tonality
- Jump
- Parameter
- Component
- Trigger
The black-key digits map to CC#106 values 27,29,31,34,36,39,41,43,46,48 (digits 1–0 respectively) and set the component config numeric value (max digit = 9).