bbbradsmith/nsfplay

Documenting Loop Detection

eatnumber1 opened this issue · 1 comments

I've been working on deciphering how nsfplay's loop detection works. It's not documented, and until I started poking around I'd never seen it work.

This is mostly some disconnected notes from my code diving / debugging sessions with it, but I'm dropping it here because as far as I can tell this is the closest to documentation that's available.

To enable detection, set AUTO_DETECT=1 in in_yansf.ini. "Enable playtime detection" in the settings does this.

There's two detectors: BasicDetector (default) and NESDetectorEX (DETECT_ALT=1, or uncheck "Use the default loop detection algorithm" in the settings).

I didn't dive too deeply into NESDetectorEX, but it seems to run BasicDetector over every channel separately (e.g. DPCM, SQR0, SQR1, etc.), then joins the results. Everything below is about BasicDetector.

It uses a sliding window as configured by DETECT_TIME (default 30s). Units are milliseconds. The maximum window is 16 KiB in size, not configurable. How much time this is depends on the playback speed, but there's a fixed maximum DETECT_TIME due to this.

Every DETECT_INT milliseconds (no setting in the UI for this, default 5s), the detector matches against the last DETECT_TIME milliseconds of audio. If it is identical, it is considered a loop.

Detection doesn't even start until a DETECT_TIME period has passed. After one DETECT_TIME has passed, on the next detection call (as controlled by DETECT_INT), a loop is attempted to be detected. I think there's a bug however, as it seems that the immediate next detection call is skipped (so you've actually got to wait two DETECT_INT periods after the buffer is full).

If a loop is detected however, the time that the loop occurred is not returned. Instead, the loop end time seems to be shifted forward to the time the loop detector that detected the loop ran.

Here's an example:

Config:

AUTO_DETECT=1
DETECT_ALT=0
DETECT_TIME=10000
DETECT_INT=7000

Song is mod_depth_mod.zip, it has a loop at about T+7s.

Behavior:
At T+21s a loop detected to occur at T+12s. The real loop occurred at T+7s, but when the detector ran the first time at T+7s, a DETECT_TIME period hadn't passed, and when it ran again at T+14s, I think it's a bug. When it ran a third time at T+21s it only looked back as far as T+1s (I suspect an off-by-one error between the T+12s detection time and the T+1s lookback max). It then saw that T+2s to T+12s was a loop, so it set the loop end time to T+12s.

The result of this behavior is the following limitations:

  1. Loops are not detectable without playing at a minimum two full iterations of DETECT_TIME.
    1. If DETECT_TIME is shorter than the song's actual loop time, no loop will be found
    2. If DETECT_TIME is longer than the song's actual loop time, more than two loops of the real song will need to be played for detection to occur.
  2. Loop start and end times are influenced more by the DETECT_INT and DETECT_WINDOW setting than they are actually the first time the song looped.

Based on all this, I can't really come up with a way to configure the loop detector such that it will find the earliest possible time a song loops, even for songs I know have a loop within a fixed time bound. The only reasonable use I can come up with for the loop detector as-is is as a heuristic way to stop a relatively short looping song from looping so many times it gets annoying (which, not-so-coincidentally, also seems to be the intended use of it).

I've never had good results with the loop detector, and I've never really figured out quite how it works. I think in general this is better addressed with NSF2/NSFe metadata giving track lengths, but my long term plan was to leave this as-is, and write a new loop detector for NSFPlay 3 eventually.