Skip to content

Instantly share code, notes, and snippets.

@kmorrill
Created November 2, 2025 17:07
Show Gist options
  • Select an option

  • Save kmorrill/da5c575cf0ca8da5ea9537ba7275462d to your computer and use it in GitHub Desktop.

Select an option

Save kmorrill/da5c575cf0ca8da5ea9537ba7275462d to your computer and use it in GitHub Desktop.
Extract the sample/library paths embedded in OP-XY project files.
#!/usr/bin/env python3
"""
Extract the sample/library paths embedded in OP-XY project files.
Teenage Engineering stores each record as:
* 12-byte big-endian prologue (`>6H`)
* null-terminated folder string (e.g. `content/samples/...`)
* null-terminated optional filename (often blank)
* 8-byte trailer of metadata/IDs
This script walks those records and prints each unique folder+filename pair.
"""
from __future__ import annotations
import struct
import sys
from pathlib import Path
from typing import Iterable, Iterator, Tuple
PROLOGUE_LEN = 12
SEARCH_TERMS = (b"content/", b"strings/")
def _find_next_term(data: bytes, start: int) -> int:
hits = (data.find(term, start) for term in SEARCH_TERMS)
hits = [pos for pos in hits if pos != -1]
return min(hits) if hits else -1
def _read_c_string(data: bytes, pos: int) -> Tuple[str, int]:
end = data.find(b"\x00", pos)
if end == -1:
raise ValueError(f"unterminated string starting at {pos:#x}")
return data[pos:end].decode("utf-8", "replace"), end + 1
def iter_sample_records(data: bytes) -> Iterator[Tuple[int, Tuple[int, ...], str]]:
"""
Yield `(offset, header_fields, display_path)` for each sample/library entry.
`display_path` concatenates the folder + filename strings so callers do not
need to worry about double-colon formatting (`foo/ :: bar.wav` cases).
"""
idx = _find_next_term(data, 0)
while idx != -1:
header = idx - PROLOGUE_LEN
fields = struct.unpack_from(">6H", data, header)
folder, nxt = _read_c_string(data, idx)
name, end = _read_c_string(data, nxt)
display = f"{folder}{name}" if name else folder
trailer_len = 8 # retained for future use; skip past metadata bytes
yield header, fields, display
idx = _find_next_term(data, end + trailer_len)
def extract_sample_paths(path: Path) -> Iterable[str]:
data = path.read_bytes()
seen = set()
for _, _, display in iter_sample_records(data):
if display not in seen:
seen.add(display)
yield display
def main(argv: list[str]) -> int:
if len(argv) != 2:
prog = Path(argv[0]).name
print(f"usage: {prog} path/to/project.xy", file=sys.stderr)
return 2
project = Path(argv[1])
for entry in extract_sample_paths(project):
print(entry)
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))
@natebluewizard
Copy link

I tried running this on a project file that is loaded with samples but it only seemed to return a couple samples using the Sampler engine but nothing using the drum engines on the first 3 tracks. Would it help if i passed a link to the .xy file?

@natebluewizard
Copy link

natebluewizard commented Nov 4, 2025

Thank you for going down this road!! Happy to help however I possibly can!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment