//--------------------------------------------------------------------------------------
// File: xwbdump.cpp
//
// XACT3 wave bank file content examination utility
//
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//--------------------------------------------------------------------------------------
#include
#include
#include
#include
#include
#include
#include
//---------------------------------------------------------------------------------
namespace
{
struct handle_closer { void operator()(HANDLE h) { if (h) CloseHandle(h); } };
using ScopedHandle = std::unique_ptr;
inline HANDLE safe_handle(HANDLE h) { return (h == INVALID_HANDLE_VALUE) ? nullptr : h; }
}
//--------------------------------------------------------------------------------------
#pragma pack(push, 1)
#pragma warning( disable : 4201 4203 )
namespace WaveBank
{
constexpr size_t DVD_SECTOR_SIZE = 2048;
// Advanced format (4K native) disk
constexpr size_t ALIGNMENT_ADVANCED_FORMAT = 4096;
constexpr size_t ALIGNMENT_MIN = 4;
constexpr size_t ALIGNMENT_DVD = DVD_SECTOR_SIZE;
constexpr size_t MAX_COMPACT_DATA_SEGMENT_SIZE = 0x001FFFFF;
struct REGION
{
uint32_t dwOffset; // Region offset, in bytes.
uint32_t dwLength; // Region length, in bytes.
void BigEndian()
{
dwOffset = _byteswap_ulong(dwOffset);
dwLength = _byteswap_ulong(dwLength);
}
};
struct SAMPLEREGION
{
uint32_t dwStartSample; // Start sample for the region.
uint32_t dwTotalSamples; // Region length in samples.
void BigEndian()
{
dwStartSample = _byteswap_ulong(dwStartSample);
dwTotalSamples = _byteswap_ulong(dwTotalSamples);
}
};
struct HEADER
{
static constexpr uint32_t SIGNATURE = MAKEFOURCC('W', 'B', 'N', 'D');
static constexpr uint32_t BE_SIGNATURE = MAKEFOURCC('D', 'N', 'B', 'W');
static constexpr uint32_t VERSION = 44;
enum SEGIDX
{
SEGIDX_BANKDATA = 0, // Bank data
SEGIDX_ENTRYMETADATA, // Entry meta-data
SEGIDX_SEEKTABLES, // Storage for seek tables for the encoded waves.
SEGIDX_ENTRYNAMES, // Entry friendly names
SEGIDX_ENTRYWAVEDATA, // Entry wave data
SEGIDX_COUNT
};
uint32_t dwSignature; // File signature
uint32_t dwVersion; // Version of the tool that created the file
uint32_t dwHeaderVersion; // Version of the file format
REGION Segments[SEGIDX_COUNT]; // Segment lookup table
void BigEndian()
{
// Leave dwSignature alone as indicator of BE vs. LE
dwVersion = _byteswap_ulong(dwVersion);
dwHeaderVersion = _byteswap_ulong(dwHeaderVersion);
for (size_t j = 0; j > 5;
if (dwBytesPerSecIndex wNumCoef = 7; /* MSADPCM_NUM_COEFFICIENTS */
static ADPCMCOEFSET aCoef[7] = { { 256, 0}, {512, -256}, {0,0}, {192,64}, {240,0}, {460, -208}, {392,-232} };
memcpy(&fmt->aCoef, aCoef, sizeof(aCoef));
}
};
struct BANKDATA
{
static constexpr size_t BANKNAME_LENGTH = 64;
static constexpr uint32_t TYPE_BUFFER = 0x00000000;
static constexpr uint32_t TYPE_STREAMING = 0x00000001;
static constexpr uint32_t TYPE_MASK = 0x00000001;
static constexpr uint32_t FLAGS_ENTRYNAMES = 0x00010000;
static constexpr uint32_t FLAGS_COMPACT = 0x00020000;
static constexpr uint32_t FLAGS_SYNC_DISABLED = 0x00040000;
static constexpr uint32_t FLAGS_SEEKTABLES = 0x00080000;
static constexpr uint32_t FLAGS_MASK = 0x000F0000;
uint32_t dwFlags; // Bank flags
uint32_t dwEntryCount; // Number of entries in the bank
char szBankName[BANKNAME_LENGTH]; // Bank friendly name
uint32_t dwEntryMetaDataElementSize; // Size of each entry meta-data element, in bytes
uint32_t dwEntryNameElementSize; // Size of each entry name element, in bytes
uint32_t dwAlignment; // Entry alignment, in bytes
MINIWAVEFORMAT CompactFormat; // Format data for compact bank
FILETIME BuildTime; // Build timestamp
void BigEndian()
{
dwFlags = _byteswap_ulong(dwFlags);
dwEntryCount = _byteswap_ulong(dwEntryCount);
dwEntryMetaDataElementSize = _byteswap_ulong(dwEntryMetaDataElementSize);
dwEntryNameElementSize = _byteswap_ulong(dwEntryNameElementSize);
dwAlignment = _byteswap_ulong(dwAlignment);
CompactFormat.BigEndian();
BuildTime.dwLowDateTime = _byteswap_ulong(BuildTime.dwLowDateTime);
BuildTime.dwHighDateTime = _byteswap_ulong(BuildTime.dwHighDateTime);
}
};
struct ENTRY
{
static constexpr uint32_t FLAGS_READAHEAD = 0x00000001; // Enable stream read-ahead
static constexpr uint32_t FLAGS_LOOPCACHE = 0x00000002; // One or more looping sounds use this wave
static constexpr uint32_t FLAGS_REMOVELOOPTAIL = 0x00000004;// Remove data after the end of the loop region
static constexpr uint32_t FLAGS_IGNORELOOP = 0x00000008; // Used internally when the loop region can't be used
static constexpr uint32_t FLAGS_MASK = 0x00000008;
union
{
struct
{
// Entry flags
uint32_t dwFlags : 4;
// Duration of the wave, in units of one sample.
// For instance, a ten second long wave sampled
// at 48KHz would have a duration of 480,000.
// This value is not affected by the number of
// channels, the number of bits per sample, or the
// compression format of the wave.
uint32_t Duration : 28;
};
uint32_t dwFlagsAndDuration;
};
MINIWAVEFORMAT Format; // Entry format.
REGION PlayRegion; // Region within the wave data segment that contains this entry.
SAMPLEREGION LoopRegion; // Region within the wave data (in samples) that should loop.
void BigEndian()
{
dwFlagsAndDuration = _byteswap_ulong(dwFlagsAndDuration);
Format.BigEndian();
PlayRegion.BigEndian();
LoopRegion.BigEndian();
}
};
struct ENTRYCOMPACT
{
uint32_t dwOffset : 21; // Data offset, in multiplies of the bank alignment
uint32_t dwLengthDeviation : 11; // Data length deviation, in bytes
void BigEndian()
{
*reinterpret_cast(this) = _byteswap_ulong(*reinterpret_cast(this));
}
};
};
#pragma pack(pop)
static_assert(sizeof(WaveBank::REGION) == 8, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::SAMPLEREGION) == 8, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::HEADER) == 52, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::ENTRY) == 24, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::MINIWAVEFORMAT) == 4, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::ENTRY) == 24, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::ENTRYCOMPACT) == 4, "Mismatch with xact3wb.h");
static_assert(sizeof(WaveBank::BANKDATA) == 96, "Mismatch with xact3wb.h");
using namespace WaveBank;
namespace
{
uint32_t GetDuration(uint32_t length, const MINIWAVEFORMAT* miniFmt, const uint32_t* seekTable)
{
switch (miniFmt->wFormatTag)
{
case MINIWAVEFORMAT::TAG_ADPCM:
{
uint32_t duration = (length / miniFmt->BlockAlign()) * miniFmt->AdpcmSamplesPerBlock();
uint32_t partial = length % miniFmt->BlockAlign();
if (partial)
{
if (partial >= (7 * miniFmt->nChannels))
duration += (partial * 2 / miniFmt->nChannels - 12);
}
return duration;
}
case MINIWAVEFORMAT::TAG_WMA:
if (seekTable)
{
uint32_t seekCount = *seekTable;
if (seekCount > 0)
{
return seekTable[seekCount] / uint32_t(2 * miniFmt->nChannels);
}
}
return 0;
case MINIWAVEFORMAT::TAG_XMA:
if (seekTable)
{
uint32_t seekCount = *seekTable;
if (seekCount > 0)
{
return seekTable[seekCount];
}
}
return 0;
default:
return (length * 8) / (miniFmt->BitsPerSample() * miniFmt->nChannels);
}
}
}
//--------------------------------------------------------------------------------------
int wmain(int argc, wchar_t* argv[], wchar_t* envp[])
{
UNREFERENCED_PARAMETER(envp);
if (argc 2)
{
wprintf(L"Usage: xwbdump \n");
return 0;
}
#if (_WIN32_WINNT >= 0x0602 /*_WIN32_WINNT_WIN8*/)
ScopedHandle hFile(safe_handle(CreateFile2(argv[1],
GENERIC_READ,
FILE_SHARE_READ,
OPEN_EXISTING,
nullptr)));
#else
ScopedHandle hFile(safe_handle(CreateFileW(argv[1],
GENERIC_READ,
FILE_SHARE_READ,
nullptr,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
nullptr)));
#endif
if (!hFile)
{
wprintf(L"ERROR: Failed to open wavebank - %ls\n", argv[1]);
return 1;
}
// Dump Header
HEADER header = {};
DWORD bytes;
if (!ReadFile(hFile.get(), &header, sizeof(header), &bytes, nullptr)
|| bytes != sizeof(header))
{
wprintf(L"ERROR: File too small for valid wavebank\n");
return 1;
}
if (header.dwSignature != HEADER::SIGNATURE && header.dwSignature != HEADER::BE_SIGNATURE)
{
wprintf(L"ERROR: File is not a wavebank - %ls\n", argv[1]);
return 1;
}
bool be = (header.dwSignature == HEADER::BE_SIGNATURE) ? true : false;
if (be)
{
header.BigEndian();
}
if (header.dwHeaderVersion (header.Segments[HEADER::SEGIDX_BANKDATA].dwOffset), nullptr, SEEK_SET) == INVALID_SET_FILE_POINTER)
{
wprintf(L"ERROR: Failed to seek to bank data %u\n", header.Segments[HEADER::SEGIDX_BANKDATA].dwOffset);
return 1;
}
if (!ReadFile(hFile.get(), &bank, sizeof(bank), &bytes, nullptr)
|| bytes != sizeof(bank))
{
wprintf(L"ERROR: Failed reading bank data\n");
return 1;
}
if (be)
bank.BigEndian();
wprintf(L"Bank Data:\n\tFlags %08X\n", bank.dwFlags);
wprintf(L"\t\t%ls\n", (bank.dwFlags & BANKDATA::TYPE_STREAMING) ? L"Streaming" : L"In-memory");
if (bank.dwFlags & BANKDATA::FLAGS_ENTRYNAMES)
{
wprintf(L"\t\tFLAGS_ENTRYNAMES\n");
}
if (bank.dwFlags & BANKDATA::FLAGS_COMPACT)
{
wprintf(L"\t\tFLAGS_COMPACT\n");
if (!(bank.dwFlags & BANKDATA::TYPE_STREAMING))
{
wprintf(L"WARNING: XACT only supports streaming with compact wavebanks\n");
}
}
if (bank.dwFlags & BANKDATA::FLAGS_SYNC_DISABLED)
{
wprintf(L"\t\tFLAGS_SYNC_DISABLED\n");
}
if (bank.dwFlags & BANKDATA::FLAGS_SEEKTABLES)
{
wprintf(L"\t\tFLAGS_SEEKTABLES\n");
}
if (*bank.szBankName)
wprintf(L"\tName \"%hs\"\n", bank.szBankName);
const wchar_t* aligndesc = L"";
if (bank.dwFlags & BANKDATA::TYPE_STREAMING)
{
switch(bank.dwAlignment)
{
case DVD_SECTOR_SIZE: aligndesc = L" (DVD/HDD)"; break;
case ALIGNMENT_ADVANCED_FORMAT: aligndesc = L" (4Kn)"; break;
}
}
wprintf(L"\tEntry metadata size %u\n\tEntry name element size %u\n\tEntry alignment %u%ls\n",
bank.dwEntryMetaDataElementSize, bank.dwEntryNameElementSize, bank.dwAlignment, aligndesc);
if (bank.dwFlags & BANKDATA::FLAGS_COMPACT)
{
if (bank.dwEntryMetaDataElementSize != sizeof(ENTRYCOMPACT))
{
wprintf(L"ERROR: Compact banks expect a metadata element size of %zu\n", sizeof(ENTRYCOMPACT));
return 1;
}
}
else
{
if (bank.dwEntryMetaDataElementSize != sizeof(ENTRY))
{
wprintf(L"ERROR: Banks expect a metadata element size of %zu\n", sizeof(ENTRY));
return 1;
}
}
if (bank.dwFlags & BANKDATA::TYPE_STREAMING)
{
if (bank.dwAlignment ALIGNMENT_DVD))
{
wprintf(L"WARNING: XACT expects alignment to be in the range %zu...%zu \n", ALIGNMENT_MIN, ALIGNMENT_DVD);
}
SYSTEMTIME st;
if (FileTimeToSystemTime(&bank.BuildTime, &st))
{
SYSTEMTIME lst;
if (SystemTimeToTzSpecificLocalTime(nullptr, &st, &lst))
{
wchar_t szLocalDate[256] = {};
GetDateFormatW(LOCALE_USER_DEFAULT, DATE_LONGDATE, &lst, nullptr, szLocalDate, 256);
wchar_t szLocalTime[256] = {};
GetTimeFormatW(LOCALE_USER_DEFAULT, 0, &lst, nullptr, szLocalTime, 256);
wprintf(L"\tBuild time: %ls, %ls\n", szLocalDate, szLocalTime);
}
}
if (!bank.dwEntryCount)
{
wprintf(L"NOTE: Empty wave bank\n");
return 0;
}
// Entry Names
DWORD metadataBytes = header.Segments[HEADER::SEGIDX_ENTRYMETADATA].dwLength;
if (metadataBytes != (bank.dwEntryMetaDataElementSize * bank.dwEntryCount))
{
wprintf(L"ERROR: Mismatch in entries %u and metadata size %lu\n", bank.dwEntryCount, metadataBytes);
return 1;
}
wprintf(L"%u entries in wave bank:\n", bank.dwEntryCount);
std::unique_ptr entryNames;
DWORD namesBytes = header.Segments[HEADER::SEGIDX_ENTRYNAMES].dwLength;
if (namesBytes > 0)
{
if (namesBytes != (bank.dwEntryNameElementSize * bank.dwEntryCount))
{
wprintf(L"ERROR: Mismatch in entries %u and entry names size %lu\n", bank.dwEntryCount, namesBytes);
}
else
{
entryNames.reset(new char[namesBytes]);
if (SetFilePointer(hFile.get(), static_cast(header.Segments[HEADER::SEGIDX_ENTRYNAMES].dwOffset), nullptr, SEEK_SET) == INVALID_SET_FILE_POINTER)
{
wprintf(L"ERROR: Failed to seek to entry names data %u\n", header.Segments[HEADER::SEGIDX_ENTRYNAMES].dwOffset);
return 1;
}
if (!ReadFile(hFile.get(), entryNames.get(), namesBytes, &bytes, nullptr)
|| namesBytes != bytes)
{
wprintf(L"ERROR: Failed reading entry names\n");
return 1;
}
}
}
// Seek tables
std::unique_ptr seekTables;
DWORD seekLen = header.Segments[HEADER::SEGIDX_SEEKTABLES].dwLength;
if (seekLen > 0)
{
if (seekLen (header.Segments[HEADER::SEGIDX_SEEKTABLES].dwOffset), nullptr, SEEK_SET) == INVALID_SET_FILE_POINTER)
{
wprintf(L"ERROR: Failed to seek to seek tables %u\n", header.Segments[HEADER::SEGIDX_SEEKTABLES].dwOffset);
return 1;
}
if (!ReadFile(hFile.get(), seekTables.get(), seekLen, &bytes, nullptr)
|| seekLen != bytes)
{
wprintf(L"ERROR: Failed reading seek tables\n");
return 1;
}
if (be)
{
for (size_t j = 0; j entries;
if (bank.dwFlags & BANKDATA::FLAGS_COMPACT)
{
entries.reset(reinterpret_cast(new ENTRYCOMPACT[bank.dwEntryCount]));
}
else
{
entries.reset(reinterpret_cast(new ENTRY[bank.dwEntryCount]));
}
if (SetFilePointer(hFile.get(), static_cast(header.Segments[HEADER::SEGIDX_ENTRYMETADATA].dwOffset), nullptr, SEEK_SET) == INVALID_SET_FILE_POINTER)
{
wprintf(L"ERROR: Failed to seek to entry metadata data %u\n", header.Segments[HEADER::SEGIDX_ENTRYMETADATA].dwOffset);
return 1;
}
if (!ReadFile(hFile.get(), entries.get(), metadataBytes, &bytes, nullptr)
|| metadataBytes != bytes)
{
wprintf(L"ERROR: Failed reading entry metadata\n");
return 1;
}
size_t waveBytes = 0;
size_t waveLen = header.Segments[HEADER::SEGIDX_ENTRYWAVEDATA].dwLength;
if ((bank.dwFlags & BANKDATA::FLAGS_COMPACT) && (waveLen > MAX_COMPACT_DATA_SEGMENT_SIZE * bank.dwAlignment))
{
wprintf(L"ERROR: data segment too large for a valid compact wavebank");
return 1;
}
bool hasxma = false;
for (DWORD j = 0; j = seekLen)
{
wprintf(L"ERROR: Invalid seek table offset entry\n");
}
else
{
seekTable = reinterpret_cast(reinterpret_cast(seekTables.get()) + baseOffset + offset);
if ((((*seekTable + 1) * sizeof(uint32_t)) + baseOffset + offset) > seekLen)
{
wprintf(L"ERROR: Too many seek table entries for size of seek tables segment\n");
}
}
}
}
if (bank.dwFlags & BANKDATA::FLAGS_COMPACT)
{
auto& entry = reinterpret_cast(entries.get())[j];
if (be)
entry.BigEndian();
wprintf(L" Entry %lu\n", j);
miniFmt = &bank.CompactFormat;
dwOffset = entry.dwOffset * bank.dwAlignment;
if (j (entries.get())[j + 1].dwOffset * bank.dwAlignment) - dwOffset - entry.dwLengthDeviation;
}
else
{
dwLength = static_cast(waveLen - dwOffset - entry.dwLengthDeviation);
}
DurationCompact = GetDuration(dwLength, miniFmt, seekTable);
}
else
{
auto& entry = reinterpret_cast(entries.get())[j];
if (be)
entry.BigEndian();
wprintf(L" Entry %lu\n\tFlags %08X\n", j, entry.dwFlags);
if (entry.dwFlags & ENTRY::FLAGS_READAHEAD)
{
wprintf(L"\tFLAGS_READAHEAD\n");
}
if (entry.dwFlags & ENTRY::FLAGS_LOOPCACHE)
{
wprintf(L"\tFLAGS_LOOPCACHE\n");
}
if (entry.dwFlags & ENTRY::FLAGS_REMOVELOOPTAIL)
{
wprintf(L"\tFLAGS_REMOVELOOPTAIL\n");
}
if (entry.dwFlags & ENTRY::FLAGS_IGNORELOOP)
{
wprintf(L"\tFLAGS_IGNORELOOP\n");
}
miniFmt = &entry.Format;
dwOffset = entry.PlayRegion.dwOffset;
dwLength = entry.PlayRegion.dwLength;
Duration = entry.Duration;
DurationCompact = GetDuration(entry.PlayRegion.dwLength, miniFmt, seekTable);
}
if (entryNames)
{
DWORD n = bank.dwEntryNameElementSize * j;
char name[64] = {};
strncpy_s(name, &entryNames[n], 64);
wprintf(L"\t\"%hs\"\n", name);
}
if (bank.dwFlags & BANKDATA::FLAGS_COMPACT)
{
float seconds = float(DurationCompact) / float(miniFmt->nSamplesPerSec);
wprintf(L"\tEstDuration %lu samples (%f seconds)\n", DurationCompact, double(seconds));
}
else
{
float seconds = float(Duration) / float(miniFmt->nSamplesPerSec);
wprintf(L"\tDuration %lu samples (%f seconds), EstDuration %lu\n", Duration, double(seconds), DurationCompact);
}
wprintf(L"\tPlay Region %lu, Length %lu\n", dwOffset, dwLength);
if (!(bank.dwFlags & BANKDATA::FLAGS_COMPACT))
{
auto& entry = reinterpret_cast(entries.get())[j];
if (entry.LoopRegion.dwTotalSamples > 0)
{
wprintf(L"\tLoop Region %u...%u\n", entry.LoopRegion.dwStartSample, entry.LoopRegion.dwTotalSamples);
}
}
const char* fmtstr = nullptr;
switch (miniFmt->wFormatTag)
{
case MINIWAVEFORMAT::TAG_PCM: fmtstr = "PCM"; break;
case MINIWAVEFORMAT::TAG_ADPCM: fmtstr = "MS ADPCM"; break;
case MINIWAVEFORMAT::TAG_WMA: fmtstr = "xWMA"; break;
case MINIWAVEFORMAT::TAG_XMA: fmtstr = "XMA"; break;
}
wprintf(L"\t%hs %u channels, %u-bit, %u Hz\n\tblockAlign %lu, avgBytesPerSec %lu\n",
fmtstr,
miniFmt->nChannels, miniFmt->BitsPerSample(), miniFmt->nSamplesPerSec,
miniFmt->BlockAlign(), miniFmt->AvgBytesPerSec());
if (!dwLength)
{
wprintf(L"ERROR: Entry length is 0\n");
}
if (dwOffset > waveLen
|| (dwOffset + dwLength) > waveLen)
{
wprintf(L"ERROR: Invalid wave data region for entry\n");
}
if ((dwOffset % bank.dwAlignment) != 0)
{
wprintf(L"ERROR: Entry offset doesn't match alignment\n");
}
if (seekTable)
{
wprintf(L"\tSeek table with %u entries", *seekTable);
for (uint32_t k = 0; k wFormatTag)
{
case MINIWAVEFORMAT::TAG_XMA:
hasxma = true;
if ((dwOffset % 2048) != 0)
{
wprintf(L"ERROR: XMA2 data needs to be aligned to a 2K boundary\n");
}
if (!seekTable)
{
wprintf(L"ERROR: Missing seek table entry for XMA2 wave\n");
}
break;
case MINIWAVEFORMAT::TAG_WMA:
if (!seekTable)
{
wprintf(L"ERROR: Missing seek table entry for xWMA wave\n");
}
break;
}
waveBytes += dwLength;
}
if (hasxma)
{
if ((header.Segments[HEADER::SEGIDX_ENTRYWAVEDATA].dwOffset % 2048) != 0)
{
wprintf(L"WARNING: Wave banks containing XMA2 data should have the wave segment offset aligned to a 2K boundary\n");
}
}
wprintf(L" Total wave bytes %zu\n", waveBytes);
if (waveBytes > waveLen)
{
wprintf(L"ERROR: Invalid wave data region\n");
}
return 0;
}