Created
November 2, 2025 17:07
-
-
Save kmorrill/da5c575cf0ca8da5ea9537ba7275462d to your computer and use it in GitHub Desktop.
Extract the sample/library paths embedded in OP-XY project files.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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)) |
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
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?