No, we're not doing the same thing two different ways. Here's the full explanation of what the final solution comprises of:
Each full chunk is comprised of 10 packets. So, at each chunk, at offset 0x38, there's 10 Int32s. A 40-byte (10x4) table that tells the XAudio2 engine how much information in bytes each packet corresponds to after it is decoded (the WMAv2 information needs to be decoded to PCM or something else before it's fed into the DirectSound output). When you encode an audio file into xWMA, it's essentially a RIFF-type file comprised of three areas:
"fmt " - Header containing some information about the file format
"dpds" - Int32 table containing the accumulated data length after each packet is decoded (so, say each packet was 40 bytes after decoding, this table looks like {40, 80, 120, 160, 200, 240, etc.}.
"data" - The WMAv2 encoded data.
The dpds table is important, because it is essential for seeking. You can't properly seek inside an xWMA file without that information, nor do you know how big the file is after it is decoded.
So, how does that translate to each chunk inside the BIN files? There's two important areas.
As we said, each chunk contains 10 packets. So the 40-byte area between 0x38 and 0x5F is always the partial dpds table. The Int32 at 0x14 is the total bytes after decoding, essentially how many bytes of PCM this chunk will correspond to after it is decoded. This should ALWAYS be the same as the last (10th) entry of the dpds table for that chunk. This is actually redundant information. If we know how much data we have after decoding the 10 packets of the chunk, we know its total decoded size. That's why 0x14-0x17 and 0x5C-0x5F is always the same Int32 in the default files.
So, here's what I'm doing each time I encode an MP3 or WAV to an xWMA:
- Code: Select all
_dpdsTable = new List<Int32>();
var dpdsPos = findArrayInStream(userSongData, XwmaDpdsHeader, 0) + 8;
userSongData.Position = dpdsPos;
var buf = new byte[4];
while (true)
{
userSongData.Read(buf, 0, 4);
var dec = BitConverter.ToInt32(buf, 0);
if (dec == 1635017060) // buf == data
{
break;
}
_dpdsTable.Add(dec);
}
Essentially, I read the whole dpds table from the xWMA file. So, we need to copy that table to the BIN file for each chunk. First of all, the song the user has selected will probably be shorter than the original song. So, it's dpds table will be shorter than the one inside the BIN file for that song. What I do to overcome that is copy the last entry of the dpds table enough times to match the ChunkCount*10 of the original song. So if the user's song had 438 packets while the song inside the BIN file is 58 chunks long (so 580 packets), I add enough entries to the dpds table of the user's song to get it up to 580 packets. However, all entries I add are the same as the last one, so the game essentially sees that all packets after 438 hold NO DATA. This means that the game skips instantly to the next song, since it realizes there's no more data to decode even though there is some in the data section (but it's dummy zeros.
Similarly to how each song resets the Chunk ID to 0, each chunk resets its first dpds entry. So, let's say that the 10th packet of the Chunk 0 had 4000 in the dpds table, which means that essentially, after the 10th packet is decoded, we'll have 4000 bytes of PCM data. In the xWMA file, the 11th packet will have something bigger in the entry of the dpds table, for example 4385. However, since we've started a new chunk in the BIN file, we need to start from 0. Not really 0, but count how many bytes will have been decoded since the last chunk's last packet. So 4385 - 4000 = 385, so we copy that into Chunk 1 Packet 0 in the dpds table inside the BIN file, and continue likewise.
Here's the code that copies the xWMA file inside the BIN file properly, using my trick to copy the DPDS table and your trick to make sure the chunk's total decoded size matches the chunk's 10th packet accumulated decoded size:
- Code: Select all
var songToReplace = (Song) dgSongs.SelectedItem;
bw.BaseStream.Position = _curChunkOffsets[songToReplace.FirstChunkID];
var lastDpdsEntry = _dpdsTable.Last();
for (var i = _dpdsTable.Count; i < songToReplace.ChunkCount * 10; i++)
{
_dpdsTable.Add(lastDpdsEntry);
}
var bytesWritten = 0;
for (var i = songToReplace.FirstChunkID; i <= songToReplace.LastChunkID; i++)
{
var buf = new byte[ChunkBufferSize];
Array.Clear(buf, 0, buf.Length);
bw.BaseStream.Position += 56;
for (var j = 0; j < 10; j++)
{
var curIndex = (i - songToReplace.FirstChunkID) * 10 + j;
var toWrite = _dpdsTable[curIndex];
if (curIndex > 9)
{
toWrite -= _dpdsTable[(curIndex / 10) * 10 - 1];
}
bw.Write(toWrite);
if (curIndex % 10 == 9)
{
bw.BaseStream.Position -= 76;
bw.Write(toWrite);
bw.BaseStream.Position += 72;
}
}
var chunkLength = (int) getChunkLength(i);
if (bytesWritten == lengthToWrite)
{
bw.Write(buf, 0, chunkLength);
bw.Flush();
continue;
}
if (bytesWritten + chunkLength > lengthToWrite)
{
var newChunkLength = (int)(lengthToWrite - bytesWritten);
ms.Read(buf, 0, newChunkLength);
bytesWritten += newChunkLength;
}
else
{
ms.Read(buf, 0, chunkLength);
bytesWritten += chunkLength;
}
bw.Write(buf, 0, chunkLength);
bw.Flush();
}
Example:In the below image there's the values in the xWMA's DPDS tables and how they should be copied in the 40-byte BIN DPDS tables per chunk. The highlighted value which is
each chunk's 10th packet accumulated decoded data size needs to be also copied to the
chunk's total accumulated decoded data size at offset 0x14.
What your solution lacked: You're just copying the chunk's total decoded size (and part of it, just 2 bytes instead of 4) into the 10th packet's accumulated decoded size. This is not enough, since the original chunk when decoded doesn't have the same amount of data as the chunk of the user's file when decoded. We need the user's chunk's 10th packet accumulated data to replace the chunk's full decoded size like I explained above. So, after copying part of the dpds table to 0x38-0x5F, we then need to copy 0x5C-0x5F to 0x14-0x17, not the other way around.
What this means for the future: With this theory in place, we can essentially rebuild the jukeboxmusic.bin from scratch with any kind of song and length we want, hopefully. This is going to be my next project, and I hope my theory works.