//-------------------------------------------------------------------------------------- // File: wavdump.cpp // // WAV file content examination utility // // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. //-------------------------------------------------------------------------------------- #include #include #include #include #include #pragma warning(push) #pragma warning(disable : 4619 4616 5246) #include #pragma warning(pop) #include #include #include #pragma comment(lib,"winmm.lib") #if !defined(WAVE_FORMAT_XMA2) #define WAVE_FORMAT_XMA2 0x166 #pragma pack(push,1) typedef struct XMA2WAVEFORMATEX { WAVEFORMATEX wfx; // Meaning of the WAVEFORMATEX fields here: // wFormatTag; // Audio format type; always WAVE_FORMAT_XMA2 // nChannels; // Channel count of the decoded audio // nSamplesPerSec; // Sample rate of the decoded audio // nAvgBytesPerSec; // Used internally by the XMA encoder // nBlockAlign; // Decoded sample size; channels * wBitsPerSample / 8 // wBitsPerSample; // Bits per decoded mono sample; always 16 for XMA // cbSize; // Size in bytes of the rest of this structure (34) WORD NumStreams; // Number of audio streams (1 or 2 channels each) DWORD ChannelMask; // Spatial positions of the channels in this file, // stored as SPEAKER_xxx values (see audiodefs.h) DWORD SamplesEncoded; // Total number of PCM samples per channel the file decodes to DWORD BytesPerBlock; // XMA block size (but the last one may be shorter) DWORD PlayBegin; // First valid sample in the decoded audio DWORD PlayLength; // Length of the valid part of the decoded audio DWORD LoopBegin; // Beginning of the loop region in decoded sample terms DWORD LoopLength; // Length of the loop region in decoded sample terms BYTE LoopCount; // Number of loop repetitions; 255 = infinite BYTE EncoderVersion; // Version of XMA encoder that generated the file WORD BlockCount; // XMA blocks in file (and entries in its seek table) } XMA2WAVEFORMATEX; #pragma pack(pop) #endif static_assert(sizeof(XMA2WAVEFORMATEX) == 52, "Mismatch of XMA2 type" ); namespace { class ScopedMMHandle { public: explicit ScopedMMHandle(HMMIO handle) noexcept : _handle(handle) {} ~ScopedMMHandle() { if (_handle != nullptr) { mmioClose(_handle, 0); _handle = nullptr; } } bool IsValid() const noexcept { return (_handle != nullptr); } HMMIO Get() const noexcept { return _handle; } private: HMMIO _handle; }; const char* GetFormatTagName(WORD wFormatTag) { switch (wFormatTag) { case WAVE_FORMAT_PCM: return "PCM"; case WAVE_FORMAT_ADPCM: return "MS ADPCM"; case WAVE_FORMAT_EXTENSIBLE: return "EXTENSIBLE"; case WAVE_FORMAT_IEEE_FLOAT: return "IEEE float"; case WAVE_FORMAT_MPEGLAYER3: return "ISO/MPEG Layer3"; case WAVE_FORMAT_DOLBY_AC3_SPDIF: return "Dolby Audio Codec 3 over S/PDIF"; case WAVE_FORMAT_WMAUDIO2: return "Windows Media Audio"; case WAVE_FORMAT_WMAUDIO3: return "Windows Media Audio Pro"; case WAVE_FORMAT_WMASPDIF: return "Windows Media Audio over S/PDIF"; case 0x165: /*WAVE_FORMAT_XMA*/ return "Xbox 360 XMA"; case WAVE_FORMAT_XMA2: return "Xbox 360/Xbox One XMA2"; default: return "*UNKNOWN*"; } } const char* ChannelDesc(DWORD dwChannelMask) { switch (dwChannelMask) { case SPEAKER_MONO: return "Mono"; case SPEAKER_STEREO: return "Stereo"; case SPEAKER_2POINT1: return "2.1"; case SPEAKER_SURROUND: return "Surround"; case SPEAKER_QUAD: return "Quad"; case SPEAKER_4POINT1: return "4.1"; case SPEAKER_5POINT1: return "5.1"; case SPEAKER_7POINT1: return "7.1"; case SPEAKER_5POINT1_SURROUND: return "Surround5.1"; case SPEAKER_7POINT1_SURROUND: return "Surround7.1"; default: return "Custom"; } } } int wmain(int argc, const wchar_t** argv) { if (argc < 2 || argc > 2) { printf("Usage: wavdump \n"); return 0; } ScopedMMHandle h(mmioOpenW(const_cast(argv[1]), nullptr, MMIO_ALLOCBUF | MMIO_READ)); if (!h.IsValid()) { printf("Failed opening %ls\n", argv[1]); return 1; } // Validate file is a WAVE MMCKINFO riff = {}; if (mmioDescend(h.Get(), &riff, nullptr, 0) != MMSYSERR_NOERROR) { printf("Failed validating file %ls\n", argv[1]); return 1; } if ((riff.ckid != FOURCC_RIFF) || ((riff.fccType != mmioFOURCC('W', 'A', 'V', 'E')) && (riff.fccType != mmioFOURCC('X', 'W', 'M', 'A')))) { printf("Not a wave file: %ls\n", argv[1]); return 1; } bool xwma = riff.fccType == mmioFOURCC('X', 'W', 'M', 'A'); std::unique_ptr chunk; // find 'fmt ' chunk MMCKINFO c = {}; c.ckid = mmioFOURCC('f', 'm', 't', ' '); if (mmioDescend(h.Get(), &c, &riff, MMIO_FINDCHUNK) != MMSYSERR_NOERROR) { printf("Failed to find 'fmt ' chunk in wave file %ls\n", argv[1]); return 1; } if (c.cksize < sizeof(WAVEFORMAT)) { printf("Header size is only %lu bytes for wave file %ls\n", c.cksize, argv[1]); return 1; } chunk.reset(new (std::nothrow) BYTE[c.cksize]); if (!chunk) { printf("Out of memory loading %lu bytes", c.cksize); return 1; } if (static_cast(mmioRead(h.Get(), reinterpret_cast(chunk.get()), static_cast(c.cksize))) != c.cksize) { printf("Failed reading %lu header bytes from wave file %ls\n", c.cksize, argv[1]); return 1; } printf("WAVE file %ls\n", argv[1]); printf("riff '%s', chunk 'fmt ', %lu bytes\n", (xwma) ? "XWMA" : "WAVE", c.cksize); auto header = reinterpret_cast(chunk.get()); printf("format tag %04u (%s)\n", header->wFormatTag, GetFormatTagName(header->wFormatTag)); printf("number of channels %u\n", header->nChannels); if (header->nChannels < 1 || header->nChannels > 64) printf("ERROR: Expected between 1..64 channels\n"); printf("samples per second %lu\n", header->nSamplesPerSec); if (header->nSamplesPerSec < 1000 || header->nSamplesPerSec > 200000) printf("ERROR: Expected between 1..200kHZ\n"); printf("avg bytes per second %lu\n", header->nAvgBytesPerSec); printf("sample block size %u bytes\n", header->nBlockAlign); switch (header->wFormatTag) { case WAVE_FORMAT_PCM: case WAVE_FORMAT_IEEE_FLOAT: if (c.cksize < sizeof(PCMWAVEFORMAT)) { printf("Header is too small to be valid in wave file %ls\n", argv[1]); return 1; } else if (c.cksize < sizeof(WAVEFORMATEX)) { auto pcm = reinterpret_cast(chunk.get()); printf("bits per sample %u\n", pcm->wBitsPerSample); } else { auto wfx = reinterpret_cast(chunk.get()); printf("bits per sample %u\n", wfx->wBitsPerSample); printf("extra bytes %u\n", wfx->cbSize); } break; case WAVE_FORMAT_ADPCM: if (c.cksize < sizeof(ADPCMWAVEFORMAT)) { printf("Header is too small to be valid in wave file %ls\n", argv[1]); return 1; } else { auto adpcm = reinterpret_cast(chunk.get()); printf("bits per sample %u\n", adpcm->wfx.wBitsPerSample); printf("extra bytes %u\n", adpcm->wfx.cbSize); printf("samples per block %u\n", adpcm->wSamplesPerBlock); printf("number of coefficients %u\n", adpcm->wNumCoef); if (adpcm->wNumCoef != 7) printf("ERROR: MS ADPCM expected to have 7 coefficients\n"); } break; case WAVE_FORMAT_XMA2: if (c.cksize < sizeof(XMA2WAVEFORMATEX)) { printf("Header is too small to be valid in wave file %ls\n", argv[1]); return 1; } else { auto xma = reinterpret_cast(chunk.get()); printf("number of streams %u\n", xma->NumStreams); printf("xma channel mask %lu (%s)\n", xma->ChannelMask, ChannelDesc(xma->ChannelMask)); printf("samples encoded %lu\n", xma->SamplesEncoded); printf("bytes per block %lu\n", xma->BytesPerBlock); printf("play [%lu, %lu]\n", xma->PlayBegin, xma->PlayLength); printf("loop [%lu, %lu] %u\n", xma->LoopBegin, xma->LoopLength, xma->LoopCount); printf("encoder version %u\n", xma->EncoderVersion); printf("block count %u\n", xma->BlockCount); } break; case WAVE_FORMAT_EXTENSIBLE: if (c.cksize < sizeof(WAVEFORMATEXTENSIBLE)) { printf("Header is too small to be valid in wave file %ls\n", argv[1]); return 1; } else { auto ext = reinterpret_cast(chunk.get()); printf("bits per sample %u\n", ext->Format.wBitsPerSample); printf("extra bytes %u\n", ext->Format.cbSize); printf("valid bits-per-sample / samples per block / reserved %u\n", ext->Samples.wReserved); printf("channel mask %08lX (%s)\n", ext->dwChannelMask, ChannelDesc(ext->dwChannelMask)); printf("Subformat GUID %08lX-%04X-%04X-%02X%02X%02X%02X%02X%02X%02X%02X", ext->SubFormat.Data1, ext->SubFormat.Data2, ext->SubFormat.Data3, ext->SubFormat.Data4[0], ext->SubFormat.Data4[1], ext->SubFormat.Data4[2], ext->SubFormat.Data4[3], ext->SubFormat.Data4[4], ext->SubFormat.Data4[5], ext->SubFormat.Data4[6], ext->SubFormat.Data4[7]); static const GUID s_wfexBase = { 0x00000000, 0x0000, 0x0010, { 0x80, 0x00, 0x00, 0xAA, 0x00, 0x38, 0x9B, 0x71} }; if (memcmp(reinterpret_cast(&ext->SubFormat) + sizeof(DWORD), reinterpret_cast(&s_wfexBase) + sizeof(DWORD), sizeof(GUID) - sizeof(DWORD)) == 0) { printf(" (%s)\n", GetFormatTagName(static_cast(ext->SubFormat.Data1))); } else printf("\n"); } break; default: if (c.cksize < sizeof(WAVEFORMATEX)) { printf("Header is is too small for a WAVEFORMATEX in wave file %ls\n", argv[1]); return 1; } else { auto wfx = reinterpret_cast(chunk.get()); printf("bits per sample %u\n", wfx->wBitsPerSample); printf("extra bytes %u\n", wfx->cbSize); } break; } if (mmioAscend(h.Get(), &c, 0) != MMSYSERR_NOERROR) { printf("Failed to exit 'fmt ' chunk in wave file %ls\n", argv[1]); return 1; } // find optional 'wsmp' chunk if (mmioSeek(h.Get(), static_cast(riff.dwDataOffset + 4u), SEEK_SET) == -1) { printf("Failed to reset seek\n"); return 1; } MMCKINFO cwsmp = {}; cwsmp.ckid = mmioFOURCC('w', 's', 'm', 'p'); if (mmioDescend(h.Get(), &cwsmp, &riff, MMIO_FINDCHUNK) == MMSYSERR_NOERROR) { //static const uint32_t LOOP_TYPE_FORWARD = 0x00000000; //static const uint32_t LOOP_TYPE_RELEASE = 0x00000001; //static const uint32_t OPTIONS_NOTRUNCATION = 0x00000001; //static const uint32_t OPTIONS_NOCOMPRESSION = 0x00000002; struct DLSLoop { uint32_t size; uint32_t loopType; uint32_t loopStart; uint32_t loopLength; }; struct DLSSample { uint32_t size; uint16_t unityNote; int16_t fineTune; int32_t gain; uint32_t options; uint32_t loopCount; }; if (cwsmp.cksize < sizeof(DLSSample)) { printf("wsmp chunk size is only %lu bytes\n", cwsmp.cksize); return 1; } chunk.reset(new (std::nothrow) BYTE[cwsmp.cksize]); if (!chunk) { printf("Out of memory loading %lu bytes", cwsmp.cksize); return 1; } if (static_cast(mmioRead(h.Get(), reinterpret_cast(chunk.get()), static_cast(cwsmp.cksize))) != cwsmp.cksize) { printf("Failed reading %lu bytes of wsmp chunk from wave file %ls\n", cwsmp.cksize, argv[1]); return 1; } auto dlsSample = reinterpret_cast(chunk.get()); if (cwsmp.cksize >= (dlsSample->size + dlsSample->loopCount * sizeof(DLSLoop))) { printf("Found DLS Sample WSMP chunk\n"); printf("\tFound %u loop points\n", dlsSample->loopCount); auto loops = reinterpret_cast(chunk.get() + dlsSample->size); for (uint32_t j = 0; j < dlsSample->loopCount; ++j) { printf("\tType %u, start %u, length %u\n", loops[j].loopType, loops[j].loopStart, loops[j].loopLength); } } else { printf("ERROR: Found DLS Sample WSMP chunk, but it's too small to be valid\n"); } if (mmioAscend(h.Get(), &cwsmp, 0) != MMSYSERR_NOERROR) { printf("Failed to exit 'wsmp' chunk in wave file %ls\n", argv[1]); return 1; } } // find optional 'smpl' chunk if (mmioSeek(h.Get(), static_cast(riff.dwDataOffset + 4u), SEEK_SET) == -1) { printf("Failed to reset seek\n"); return 1; } MMCKINFO csmpl = {}; csmpl.ckid = mmioFOURCC('s', 'm', 'p', 'l'); if (mmioDescend(h.Get(), &csmpl, &riff, MMIO_FINDCHUNK) == MMSYSERR_NOERROR) { //static const uint32_t LOOP_TYPE_FORWARD = 0x00000000; //static const uint32_t LOOP_TYPE_ALTERNATING = 0x00000001; //static const uint32_t LOOP_TYPE_BACKWARD = 0x00000002; struct MIDILoop { uint32_t cuePointId; uint32_t type; uint32_t start; uint32_t end; uint32_t fraction; uint32_t playCount; }; struct MIDISample { uint32_t manufacturerId; uint32_t productId; uint32_t samplePeriod; uint32_t unityNode; uint32_t pitchFraction; uint32_t SMPTEFormat; uint32_t SMPTEOffset; uint32_t loopCount; uint32_t samplerData; }; if (csmpl.cksize < sizeof(MIDISample)) { printf("smpl chunk size is only %lu bytes\n", csmpl.cksize); return 1; } chunk.reset(new (std::nothrow) BYTE[csmpl.cksize]); if (!chunk) { printf("Out of memory loading %lu bytes", csmpl.cksize); return 1; } if (static_cast(mmioRead(h.Get(), reinterpret_cast(chunk.get()), static_cast(csmpl.cksize))) != csmpl.cksize) { printf("Failed reading %lu bytes of smpl chunk from wave file %ls\n", csmpl.cksize, argv[1]); return 1; } auto midiSample = reinterpret_cast(chunk.get()); if (csmpl.cksize >= (sizeof(MIDISample) + midiSample->loopCount * sizeof(MIDILoop))) { printf("Found MIDI Sample SMPL chunk\n"); printf("\tFound %u loop points\n", midiSample->loopCount); auto loops = reinterpret_cast(chunk.get() + sizeof(MIDISample)); for (uint32_t j = 0; j < midiSample->loopCount; ++j) { printf("\tType %u, start %u, end %u\n", loops[j].type, loops[j].start, loops[j].end); } } else { printf("ERROR: Found MIDI Sample SMPL chunk, but it's too small to be valid\n"); } if (mmioAscend(h.Get(), &csmpl, 0) != MMSYSERR_NOERROR) { printf("Failed to exit 'smpl' chunk in wave file %ls\n", argv[1]); return 1; } } // find optional 'dpds' chunk if (mmioSeek(h.Get(), static_cast(riff.dwDataOffset + 4u), SEEK_SET) == -1) { printf("Failed to reset seek\n"); return 1; } MMCKINFO cdpds = {}; cdpds.ckid = mmioFOURCC('d', 'p', 'd', 's'); if (mmioDescend(h.Get(), &cdpds, &riff, MMIO_FINDCHUNK) == MMSYSERR_NOERROR) { printf("chunk 'dpds', %lu bytes", cdpds.cksize); if (cdpds.cksize > 0) { chunk.reset(new (std::nothrow) BYTE[cdpds.cksize]); if (!chunk) { printf("\nOut of memory loading %lu bytes", cdpds.cksize); return 1; } if (static_cast(mmioRead(h.Get(), reinterpret_cast(chunk.get()), static_cast(cdpds.cksize))) != cdpds.cksize) { printf("\nFailed reading %lu bytes of dpds chunk from wave file %ls\n", cdpds.cksize, argv[1]); return 1; } auto table = reinterpret_cast(chunk.get()); for (size_t k = 0; k < (cdpds.cksize / 4); ++k) { if ((k % 6) == 0) printf("\n\t"); printf("%u ", table[k]); } } printf("\n"); if (mmioAscend(h.Get(), &cdpds, 0) != MMSYSERR_NOERROR) { printf("Failed to exit 'dpds' chunk in wave file %ls\n", argv[1]); return 1; } } // find optional 'seek' chunk if (mmioSeek(h.Get(), static_cast(riff.dwDataOffset + 4u), SEEK_SET) == -1) { printf("Failed to reset seek\n"); return 1; } MMCKINFO cseek = {}; cseek.ckid = mmioFOURCC('s', 'e', 'e', 'k'); if (mmioDescend(h.Get(), &cseek, &riff, MMIO_FINDCHUNK) == MMSYSERR_NOERROR) { printf("chunk 'seek', %lu bytes", cseek.cksize); if (cseek.cksize > 0) { chunk.reset(new (std::nothrow) BYTE[cseek.cksize]); if (!chunk) { printf("\nOut of memory loading %lu bytes", cseek.cksize); return 1; } if (static_cast(mmioRead(h.Get(), reinterpret_cast(chunk.get()), static_cast(cseek.cksize))) != cseek.cksize) { printf("\nFailed reading %lu bytes of seek chunk from wave file %ls\n", cseek.cksize, argv[1]); return 1; } auto table = reinterpret_cast(chunk.get()); for (size_t k = 0; k < (cseek.cksize / 4); ++k) { if ((k % 6) == 0) printf("\n\t"); printf("%lu ", _byteswap_ulong(table[k])); // seek chunk entries are BigEndian } } printf("\n"); if (mmioAscend(h.Get(), &cseek, 0) != MMSYSERR_NOERROR) { printf("Failed to exit 'seek' chunk in wave file %ls\n", argv[1]); return 1; } } return 0; }