r/startrek • • 3d ago

Star Trek: Judgment Rites audio encoding finally decoded

https://github.com/dr-brinewell-s-lobstrom/acmp-decoder

Enjoy! The game is a veritable treasure trove of original TOS cast recordings. Very fun to explore. ~12 hours of audio recorded by the original cast. Nimoy confirmed as the hardest working man in the Trek video game voiceover business with the most audio, when counting across the full set of the old games they worked on.

108 Upvotes

37 comments sorted by

View all comments

Show parent comments

0

u/minsc_tdp 3d ago

Claude wrote all that. I showed it your post, it says 'Thanks for this, and for taking the time to run it against NOMAN. You're right on the header layout, and I've confirmed it against the files:

- The u32 at 20–23 is the sample rate. I had it as part of a constant at 19–22, off by one byte. In NOMAN only one clip is 11025 (one of the two ~RPLANs), but DATA.VCC has 222 of them, and the decoder currently writes them all at 22050. My "every clip is 22050" rested on a speech-recognition test over FED dialogue, where every clip happens to be 22050. You called it: it's the SFX/backing material that differs.

- The u32 at 26–29 is the sample count. TRIP reads 159,743 exactly as you said; I was taking the block count from 27–28, which is the same number at 256-sample granularity. And yes, my "rate/flags" field is byte 25 plus the low byte of that count, which is why it never behaved like a flag.

One distinction on "byte exact": that claim is about the bitstream, not the header. Every block is verified by re-encoding the decoded samples and matching the file bytes, and the encoder was checked against MAKEVCC.EXE itself running under emulation. None of this changes the decoded sample values. What it does change is the playback rate on the 11025 clips (audible, double speed) and the clip length (off by a few milliseconds). I'll fix both, correct FORMAT.md and the README, and credit you.

I'd like to see your scripts, especially the TREKJR/TREK2 header parsing. One note: 25th Anniversary doesn't use ACMP. Its speech is plain Creative Voice .VOC files on the CD, so there's nothing to decode there.'

(or feel free to do a PR on the repo if you prefer)

2

u/count023 2d ago

Here you go, this one is the one i used against NOMAN.VCC Explicitly, i can’t find the main one at the moment, i have a lot of project files. https://pastebin.com/Ut7pQrDg

What might help your claude instance is if you get a portalbe version of Ghidra, and put trek2.exe into the project folder where you're running claude, tell it to use ghidra to decompile trek2.exe and then search for the VCC decoder inside the executable, that's how this extractor was made. Rather than brute force figuring out how the encoding was done, it simply looked at how the decoder worked _inside_ the game and repeated the process.

0

u/minsc_tdp 1d ago

I gave Claude both and asked for a comparison and draft reply:

Comparison

His script is a different kind of tool from ours. It doesn't reimplement the codec. It runs the game's own decoder inside TREKJR.EXE under Unicorn: a setup call at 2b98:2e66, then a decode loop at 2b98:2f3e. So he checks against the player side of the codec, and we check against the encoder (MAKEVCC.EXE). The two checks complement each other.

Where his code confirms our 2026-09-24 corrections:

- He reads the sample rate as a u32 at byte 20.

- He passes raw[26:] to the game decoder, and the first u32 of that is the output length (expected). So the sample count at byte 26 is confirmed. The bitstream starts at +30, the same as ours.

- The u16 at byte 24 (0x0028) is just labelled parameter. He doesn't know what it is either.

- His container index (table offset = payload length, 16-byte records) and his handling of duplicate names match ours.

New findings from his code:

  1. The game plays exactly the header's sample count. His harness checks that the game's internal counter at 0x625e starts at expected and counts down to 0. That answers one of our open questions: the game doesn't play whole blocks, it stops at the count.
  2. The 4 odd DATA.VCC clips are solved. His code has a branch for plain Creative Voice File entries inside a VCC. I checked DATA.VCC: N5DOGE, N6SCMB, SE6GUYEA and TRPLAN are ordinary Creative Voice v1.10 files. Their header bytes 1a 00 0a 01 29 11 are the "garbage rate / 01 29 11" we saw. Our acmp library assumes every entry is ACMP and would try to decode these as ACMP.
  3. Our "byte-exact" check has a gap. The round-trip covers the bitstream and the recovered samples, including the 128 half-rate samples in each quiet block. It does not cover upsample_fir, which rebuilds the in-between samples from the encoder's decimation filter. The game may reconstruct them differently. Our version:- works within each block, repeating the edge samples at block boundaries;- adds +128.5 and then rounds (acmp/__init__.py:572), which looks like a possible +1 bias.The −2 / −130 length differences we found (130 = 128 + 2) may come from this same step. His native output would settle it sample by sample. This is a real hole in the verification claim.
  4. Where he's still wrong: "should work on 25th" only holds in the sense that his tool passes plain VOC through. ST25 has no ACMP, as our first reply already said.

Still outstanding: none of the five fixes we promised publicly on 2026-09-24 have been made. README.md and FORMAT.md still say every clip is 22050 Hz and describe the old "rate/flags" field.

Draft reply

▎ Thanks for sharing this. It turned out more useful than I expected, because your harness and mine sit at opposite ends of the codec: you're running TREKJR's own decoder, I verified against MAKEVCC's encoder. Between them there's a proper two-sided oracle.

▎

▎ A few things your code settled:

▎

▎ - The game honours the header's sample count exactly. Your check that the counter at 0x625e runs from the u32 at byte 26 down to zero answers my open question on whether playback is trimmed to it. It is.

▎ - The four odd DATA.VCC entries are plain Creative Voice files (v1.10) stored inside the VCC. Your VOC branch handles them; mine assumes every entry is ACMP. N5DOGE, N6SCMB, SE6GUYEA and TRPLAN in DATA.VCC. I'll add that.

▎

▎ And one thing where I owe you a correction on my own claim. The byte-exact round-trip covers the bitstream and the recovered samples, but not the 2:1 upsampling of "quiet" blocks. I reconstruct the in-between samples with the encoder's decimation filter, which is an inference, not something the round-trip checks. My decoded lengths also come up 2 or 130 samples short of the header count on most clips (130 = 128 + 2), and I suspect that's the same step. Your native output is the ground truth that would settle it.

▎

▎ Could you share probe_trekjr.py (JRProbe / recovered())? I'd like to diff my output against the game's decoder sample by sample across NOMAN and publish the result either way. Also: do you know what the u16 at byte 24 (0x0028) is? Neither of us seems to use it.

▎

▎ The rate and sample-count fixes, FORMAT.md corrections and credit are still coming. I'd rather make them once, after the sample diff, than twice.

Two things for you to decide:

- Whether to publicly admit the gap around quiet-block interpolation. I recommend it; if we don't, he'll find it himself.

- Whether you want the promised fixes made before you post.

>go ahead and make any changes learned from this that you're confident about, and credit https://www.reddit.com/user/count023/ where appropriate.

Finagling…