What does this page do?

Takes a disc image file of Sly 2 or Sly 3 (including some of the demos), and lets you listen to (and download, raw or rendered to wav) all of its streamed music and ambient tracks.

    Update 06/04/25: When a disc is loaded, you can make a .zip file containing all tracks which are currently listed on the select box (based on the filters applied with checkboxes)

    Because this operation takes a lot of memory and CPU, there is a maximum limit on how many tracks you can put in a single zip file (and the page will tell you if there are too many)

    If you have CPU to spare, you can bypass this limit by writing and running the phrase option_noZipLimit=true in your browser's console (see developer tools on MDN)

    Update 18/01/26: I restructured the audio processing so that it's more complex but faster.

    I added a playing bar so you can jump at any point in the track.

    Exported wav files now contain embedded loop points readable by some audio editors and DAWs (notably Wavosaur and Reaper, but not Audacity).

    Update 22/01/26: Remade the page entirely to use ES6 modules. No change in functionality, but hopefully friendlier to archival sites that modify javascript files.

    I will soon start working on a separate page that lets you insert your own music and SFX tracks in a Sly 2/3 disc, within the space limiations of existing audio tracks.

Doesn't it take long to upload a whole disc image?

No because you're not uploading anything anywhere. This page does all the work locally, in your browser (preferably Firefox), without relying on a server after it's loaded.
It's a bit like vgmstream's web player, but in pure javascript.

    Update 06/04/25: However, it will take a lot of time (and memory) to make a single .zip file that contains every audio track on the disc, if you choose to do so.

Why does this page only work with some discs?

Because this page is made for a specific format used in Sly 2 and 3, and I had to manually find and classify each track. These games in particular have their entire file structure hidden, so searching for them is not as easy as opening a folder and reading a name.

Can I play the raw files that I get from this page in vgmstream?

Yes if you put this file in the same directory and rename it ".kpv.txth" (dots are important).
Read more about vgmstream's TXTH files on their github



So what does the page do exactly? (for programmers)

  • Given an .iso file, reads its ID code
      (slyDiscMusic_ioV2.js, function Ps2DiscFile.discIdPromise).
  • If the ID is recognized
      (if it appears in the table knownIDs at the end of slyDiscMusic_data.js),
      shows you a list of all known tracks from that disc, and a few filter options
  • When you select a track, it starts playing
      (slyDiscMusic_playV2.js, function SlyAudioContext.playTrack)
      using the AudioWorklet defined in psxDecoderWorkletV2.js
  • While a track is playing, you can see some information about the track in a table,
      download the track in its original raw format (tentatively named ".kpv"),
      download the leftover (mirrored) data until the next multiple of 0x10000,
      or render it to wav (streamBlockNodesV2.js, function WavSink.render).
    Like above, any downloaded files are created on the spot by your browser (preferably Firefox).
  • If the track has several layers, you can switch between them during playback using the slider under the selection list.

Can this page recognize other files?

Yes (see slyDiscMusic_ioV2.js, function loadFileDataPromise)

  • If you open a file starting with " KPV" (like the results of the Download Raw File button), the page will play them.
  • If you open a .bin image, the page will explain why it isn't supported directly, but let you convert it to .iso

If I don't want to excavate it from your code, what's the format of these files?

  • Header: 0x800 bytes (1 entire disc sector) (see KpvToPsx.decodeFirstSector in streamBlockNodesV2.js):
    Offset Format Value Notes
    0x000 ascii string " KPV" (0x20 0x4B 0x50 0x56)
    0x004 Uint32 LE length and loopEnd: number of used bytes per channel each channel occupies an integer number of 0x800 sectors, so this value should be accordingly rounded up to get the actual file size
    0x008 Uint32 LE unknown (sector size?): always 0x800
    0x00C Uint32 LE interleave*2: double of channel interleave Usually 0x2000, meaning that channels alternate every 0x1000 bytes.
    0x010 Uint32 LE sampleRate
    0x014 Uint32 LE channels
    0x018 byte⁠[channels]⁠[4] channel mapping see below
    0x7FC Uint32 LE loopStart: byte offset of loop start point in each channel this single value is all the way to the end of the disc sector, separate from the rest of the header
  • Channel mapping: each channel is defined using 4 bytes, structured like this:
    Byte 0 Byte 1 Uint16 LE
    0x7F Layer number 0x0000 for Mono layers,
    0x005A and 0x010E to couple 2 channels into a Stereo layer
  • Audio data: PSX ADPCM (decoding function PsxToPcm.processLine in streamBlockNodesV2.js, based on vgmstream implementation)
    Each row of 16 bytes decodes to 28 samples.
    Byte 0 Byte 1 Bytes 2...15
    Low Nibble: shift
    High Nibble: coef
    Flags
    (unused)
    28 signed nibbles (low before high): minisamples

    new sample =  minisample<<(12-shift)
      + prev[0]*coefTable[0][coef]
      + prev[1]*coefTable[1][coef]
    where "coefTable" is the same as the one given in vgmstream,"prev[n]" are the (n+1)th past output samples that you must keep track of when decoding.
    Each disc sector (0x800 bytes) contains 128 rows = 3584 samples;
    The final sector of each channel is padded to the end with rows of silence (0xC0 00 00 00 ...).

What's the deal with duplicate tracks?

It looks like Sly 2/3 associate every room to a reasonably small section of the disc, so that elements of the room can be loaded relatively quickly.
As a result, if separate indoor rooms share the same music or SFX, that music track needs to be duplicated, so that it exists near the data of both rooms.

Sly 3 pushed this idea to an unfeasible length:
In every room where the Guru can enter, he can transform or ride enemies (both actions are accompanied by their own music).
And in almost every room, Bentley can activate the Grapple Cam, which also has its own music.
As a result, these 3 music tracks are duplicated over and over and over and over and over and over and over again.
Respectively: 31 times (Guru Transforms), 9 times (Guru Rides), and 38 times (Grapple Cam).
The combined length of these tracks on disc, without loops, is probably longer than the time I have heard them in the game, especially "Guru Transforms".
And that's why these 3 get the special derogatory moniker of "frequent duplicates".

What about that "leftover" / "mirrored" data?

It looks like every file in Sly 2/3 is stored in groups of 32 sectors (treating the disc as if it were structured in "macrosectors" of 0x10000 bytes each).
When a file ends before completely filling a "macrosector", the remaining space is filled by repeating the beginning of the same file (instead of being left blank).

In some demos, that space may be filled with the beginning of a different file, so there's a slyght chance this leftover data may be of interest to passionate Sly dataminers.
Without doing any analysis myself, I'm putting up the option to download this leftover data for anyone else to use or ignore.