Skip to content

fix(libSceNgs2.native): parse ATRAC9 smpl loops as the module does - #9327

Merged
oneandonlydean merged 16 commits into
boykopovar:mainfrom
MarrasFederico:feat/ngs2-atrac9-module-loops
Oct 23, 2026
Merged

oneandonlydean merged 16 commits into
boykopovar:mainfrom
MarrasFederico:feat/ngs2-atrac9-module-loops

Conversation

@MarrasFederico

@MarrasFederico MarrasFederico commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

What

sceNgs2ParseWaveformData built ATRAC9 loop blocks with AnyPS5's own rules from #1301. Its debt entry recorded those rules as "unverified on hardware". The parser now follows prosper's reading of libSceNgs2.native.sprx, the same rules #9149 applies to PCM:

What changes for titles:

  • A positive play count no longer limits the repeats.
  • Two loops, ping-pong, backward and fractional loops no longer throw.
  • Ranges the module accepts no longer return INVALID_WAVEFORM_DATA.
  • The block layout itself is unchanged: the module's skip already includes a frame before the loop for decoder history, as the old ranges did, so the rendered audio stays bit-exact.
  • For PPSA03747's chunk (start 256, inclusive end 299224, delay 256, 298969 samples), the result is one block over the whole waveform repeating forever.

What still throws (in the rewritten debt entry):

  • an end past the last sample, where the module's tail count wraps;
  • a smpl chunk too short for its count or first loop, where the module reads the bytes after it;
  • a looped file at a sampling rate the module's frame-size table lacks.

This builds on #9155 (the block mapping) and #9143 (the delay). The branch merges both.

Evidence: read, not measured. Every "as the module does" in this PR comes from a third-party reading of libSceNgs2.native.sprx, prosper's, done from the disassembly (parse core at file offset 0x10f50, block list 0x11a20, CalcWaveformBlock 0x12060). Nobody here has checked it against the module, and nothing was run on a console. That includes:

  • the frame-size table at rodata 0x4e520, which has a frame size only for rate indices 1, 4 and 7, so the other rates throw;
  • the divide by zero the module performs for the other indices;
  • the loop handling: only the first loop's start and inclusive end are read, less the decoder delay, and type, fraction, play count and later loops are ignored;
  • the one-frame decoder delay, whatever the fact chunk says;
  • the data handling: the module does not bound blocks by the data chunk.

The two new throws, for a smpl start inside the decoder delay and for a loop block past the data chunk, cover cases where that reading shows the module wrapping or not bounding, so what it would then decode is unknown.

Tested

Ubuntu 26.04.1, kernel 7.0.0-38, GCC 15.2.0, Release, full build with BUILD_TESTING=ON. Windows was not compiled. No title was run.

Checklist

@github-actions

github-actions Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Other open pull requests touch the same code. Merging one will break the other, or both implement the same thing. If this one builds on them, list them in Depends on.

Pull request Conflicting files (lines on main) Same exports Same new files
PR 5548 docs/dev/debt/libSceNgs2.native.md L11-12

Some comparisons could not be completed because a pull request conflicts with the base branch. Review these shared files manually before concluding that there is no overlap.

Pull request Shared files not compared
PR 5797 core/libs/prx/libSceNgs2.native/src/Atrac9.cpp
docs/dev/debt/libSceNgs2.native.md

@ycbuzi

ycbuzi commented Oct 21, 2026

Copy link
Copy Markdown
Contributor

Windows run of 69b21ad merged onto main 0c6a5bd (9e1a513f43, clean), the platform the description lists as not tested. Windows 11 Home 10.0.26200, MinGW-w64 GCC 15.2.0 (ucrt-posix-seh r7), Release, BUILD_TESTING=ON.

Built libSceNgs2.native and ngs2_atrac9_tests with no errors or warnings. ctest --repeat until-fail:2 on ngs2_atrac9: passes on both repeats. The head carries the PRs below it in its stack (#9155, #9145, #9143) too. Not run: a title.

@oneandonlydean oneandonlydean self-assigned this Oct 22, 2026
@oneandonlydean

Copy link
Copy Markdown
Collaborator

@MarrasFederico thanks for the VAG blocks and the module-style ATRAC9 mapping. I read the diff on be6d4f2, and it builds against current main. Before we can take it, there are a few behaviour changes I need either fixed or backed by evidence:

  1. Most ATRAC9 sample rates now throw. ATRAC9_FRAME_SAMPLES only has entries for rate indices 1, 4 and 7, so Atrac9Block throws for the other 13, including 44.1 kHz (index 6); the new test even expects that for 0, 2, 3, 5, 6, 8 and 15. On main, sceNgs2CalcWaveformBlock and looped sceNgs2ParseWaveformData handle every rate CodecInfo accepts, because they use codec.frameSamples. This PR turns those into throws, and for parsing only when the file has a loop. Could you take the frame size from CodecInfo (or the full 16-entry table) instead? If the module really rejects those rates, please show where.
  2. Blocks are no longer bounded by the data chunk. AddAtrac9Range returned SCE_NGS2_ERROR_INVALID_WAVEFORM_DATA when a block's offset/size fell outside data_size. AddAtrac9Block has no check, and with the extra leading frame (lead = frameSamples + pos), a loop or tail that ends near num_samples can map past the data, so the decoder reads past the waveform. Please keep that check, or show that the module accepts such a block and what it reads.
  3. Loop start before the delay wraps. begin = smpl start - num_delay_samples is uint32_t. A loop that starts before the delay becomes about 4e9; then end <= begin returns OK with that value in loop_begin_position. That needs an explicit reject or clamp.
  4. Loop fields that used to throw are now ignored. The play count (loop + 20) is dropped (always UINT32_MAX), and so are the loop type, the fraction, and every loop after the first. A ping-pong loop now plays forward, and a loop with a finite play count plays forever. Could you keep the "not implemented" throws for what isn't modelled, or show that the module ignores these fields?
  5. num_delay_samples now ignores the fact chunk. It's codec.frameSamples for every file instead of fact + 4. If that's what the module does, a test with a fact delay different from one frame, plus a pointer to where you saw it, would settle it.

For all of these, what's the evidence behind "as the module does": disassembly offsets, or results from a console run? A note in the PR body would be enough.

@MarrasFederico

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks for the careful read. Where it says "as the module does" it's prosper's reading of libSceNgs2.native.sprx. That's disassembly at file offsets (parse core 0x10f50, CalcWaveformBlock 0x12060, block list 0x11a20), marked "CONFIDENCE: HIGH … read from the disassembly and exercised by test_ngs2_waveform". I haven't run any of this on a console. Point by point, on the new head:

  1. Rates. The module has its own frame-size table, not libatrac9's CodecInfo: rodata 0x4e520 is {0, 64, 0, 0, 128, 0, 0, 256, 0, …}, so only indices 1, 4 and 7 (12, 24, 48 kHz) have a frame size. Both the parser (L228-L230) and CalcWaveformBlock read it, and for the other indices the module divides by zero. A 44.1 kHz ATRAC9 file can't be parsed or mapped by the module, so I kept the throw (it comes from feat(libSceNgs2.native): map ATRAC9 waveform blocks as the module does in sceNgs2CalcWaveformBlock #9155) and didn't answer with CodecInfo values the module never computes. If you'd still rather keep main's mapping for those rates as a known divergence, I'll switch.
  2. Blocks past data. The module doesn't bound them: it ignores calc_block's result and keeps the declared data size unclamped. What its decoder reads then isn't known, so I didn't bring back an INVALID_WAVEFORM_DATA the module doesn't return. AddAtrac9Block now throws when a loop block would reach past the data chunk. The new test case is a fact sample count one past the last superframe: it throws, and the count one below still parses into three blocks.
  3. Loop start before the delay. The module subtracts unsigned (L255-L256). A smpl start below the decoder delay now throws (tested with 0 and 255; 256 still gives position 0), so no wrapped position reaches loop_begin_position.
  4. Ignored loop fields. The module reads only the loop count at +0x1c and the first loop's start and end at +0x2c and +0x30 (L249-L257), and marks the loop block 0xffffffff. So type, fraction, play count and later loops don't reach it, and every loop plays forward forever. I kept that. TestMalformedLoops sets the count to 2, the type to 1 and 2, a fraction and a play count of 5, and checks the blocks don't change.
  5. Delay. For ATRAC9 the module sets the delay to one frame (L230) and reads fact's delay word only for type 0x43 (L244). That's fix(libSceNgs2.native): report one ATRAC9 frame as the parsed decoder delay #9143's change. TestParse sets the fact delay to 0, 1, 900 and 0xffffffff and expects 256 each time, as prosper's own test does for 900 and 0.

I also merged current main and the updated #9143 and #9155. #8528's frame-info test expected the old one-superframe block from CalcWaveformBlock, so #9155 now expects the module's block (two superframes for 1024 samples from 0, the delay frame included) and checks sceNgs2GetWaveformFrameInfo's rejections on their own. All 13 NGS2 tests pass. Removing either new throw, or letting a start equal to the delay throw, fails ngs2_atrac9. The debt entry now lists the new throws and cites these lines.

@oneandonlydean

Copy link
Copy Markdown
Collaborator

@MarrasFederico on the rate question: keep the throw for rate indices other than 1, 4 and 7. The module's own table has no frame size for them, so a 44.1 kHz file can't be parsed or mapped by the real module either, and keeping main's wider mapping would preserve behaviour the module doesn't have.

That rests on the third-party reading of the table at rodata 0x4e520 and of the divide by zero, which nobody here has checked against the module. Please say in the PR body that those two statements, and the loop, delay and data handling, come from that reading and not from a console run, so a later reader can tell what was measured from what was read.

The other four points are covered in your comment; they need a read of the new head, which is a separate step.

@MarrasFederico

Copy link
Copy Markdown
Contributor Author

@oneandonlydean thanks, I'm keeping the throw. The PR body now has an "Evidence: read, not measured" section. It lists the table at rodata 0x4e520, the divide by zero, the loop and delay handling, and the unbounded data handling, and says that each comes from prosper's reading of the disassembly, not from a check against the module or a console run. It also says why the two new throws cover the cases that reading leaves open. The code on the head (d82faf18aa) is unchanged.

@oneandonlydean
oneandonlydean merged commit 6ee3872 into boykopovar:main Oct 23, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants