////////////////////////////////////////////////////////////////////////////
//                           **** WAVPACK ****                            //
//                  Hybrid Lossless Wavefile Compressor                   //
//                Copyright (c) 1998 - 2019 David Bryant.                 //
//                          All Rights Reserved.                          //
//      Distributed under the BSD Software License (see license.txt)      //
////////////////////////////////////////////////////////////////////////////

// wvparser.c

// This is a completely self-contained utility to parse complete Wavpack files
// into their component blocks and display some informative details about them
// (including all the "metadata" sub-blocks contained in each block). This can
// be very handy for visualizing how WavPack files are structured or debugging.
// Some sanity checking is performed, but no thorough verification is done
// (and, of course, no decoding takes place). APEv2 tags are ignored as is any
// other garbage between blocks.

// Acts as a filter: WavPack file to stdin, output to stdout.

#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <fcntl.h>
#ifdef _WIN32
#include <io.h>
#endif

#if defined(_MSC_VER) && _MSC_VER < 1600
typedef unsigned __int64 uint64_t;
typedef unsigned __int32 uint32_t;
typedef unsigned __int16 uint16_t;
typedef unsigned __int8 uint8_t;
typedef __int64 int64_t;
typedef __int32 int32_t;
typedef __int16 int16_t;
typedef __int8  int8_t;
#else
#include <stdint.h>
#endif

typedef int32_t (*read_stream)(void *, int32_t);

////////////////////////////// WavPack Header /////////////////////////////////

// Note that this is the ONLY structure that is written to (or read from)
// WavPack 4.0 files, and is the preamble to every block in both the .wv
// and .wvc files.

typedef struct {
    char ckID [4];
    uint32_t ckSize;
    int16_t version;
    unsigned char track_no, index_no;
    uint32_t total_samples, block_index, block_samples, flags, crc;
} WavpackHeader;

#define WavpackHeaderFormat "4LS2LLLLL"

// or-values for "flags"

#define BYTES_STORED	3	// 1-4 bytes/sample
#define MONO_FLAG	4	// not stereo
#define HYBRID_FLAG	8	// hybrid mode
#define JOINT_STEREO	0x10	// joint stereo
#define CROSS_DECORR	0x20	// no-delay cross decorrelation
#define HYBRID_SHAPE	0x40	// noise shape (hybrid mode only)
#define FLOAT_DATA	0x80	// ieee 32-bit floating point data

#define INT32_DATA	0x100	// special extended int handling
#define HYBRID_BITRATE	0x200	// bitrate noise (hybrid mode only)
#define HYBRID_BALANCE	0x400	// balance noise (hybrid stereo mode only)

#define INITIAL_BLOCK	0x800	// initial block of multichannel segment
#define FINAL_BLOCK	0x1000	// final block of multichannel segment

#define SHIFT_LSB	13
#define SHIFT_MASK	(0x1fL << SHIFT_LSB)

#define MAG_LSB		18
#define MAG_MASK	(0x1fL << MAG_LSB)

#define SRATE_LSB	23
#define SRATE_MASK	(0xfL << SRATE_LSB)

#define FALSE_STEREO    0x40000000      // block is stereo, but data is mono
#define NEW_SHAPING     0x20000000      // use IIR filter for negative shaping

#define MONO_DATA (MONO_FLAG | FALSE_STEREO)

// Introduced in WavPack 5.0:
#define HAS_CHECKSUM    0x10000000      // block contains a trailing checksum
#define DSD_FLAG        0x80000000      // block is encoded DSD (1-bit PCM)

#define IGNORED_FLAGS   0x08000000      // reserved, but ignore if encountered
#define UNKNOWN_FLAGS   0x00000000      // we no longer have any of these spares

#define MIN_STREAM_VERS     0x402       // lowest stream version we'll decode
#define MAX_STREAM_VERS     0x410       // highest stream version we'll decode or encode

static const uint32_t sample_rates [] = { 6000, 8000, 9600, 11025, 12000, 16000, 22050,
    24000, 32000, 44100, 48000, 64000, 88200, 96000, 192000 };

//////////////////////////// WavPack Metadata /////////////////////////////////

// This is an internal representation of metadata.

typedef struct {
    int32_t byte_length;
    void *data;
    unsigned char id;
} WavpackMetadata;

#define ID_UNIQUE               0x3f
#define ID_OPTIONAL_DATA        0x20
#define ID_ODD_SIZE             0x40
#define ID_LARGE                0x80

static const char *metadata_names [] = {
    "DUMMY", "ENCODER_INFO", "DECORR_TERMS", "DECORR_WEIGHTS", "DECORR_SAMPLES", "ENTROPY_VARS", "HYBRID_PROFILE", "SHAPING_WEIGHTS",
    "FLOAT_INFO", "INT32_INFO", "WV_BITSTREAM", "WVC_BITSTREAM", "WVX_BITSTREAM", "CHANNEL_INFO", "DSD_BLOCK", "UNASSIGNED",
    "UNASSIGNED", "RIFF_HEADER", "RIFF_TRAILER", "ALT_HEADER", "ALT_TRAILER", "CONFIG_BLOCK", "MD5_CHECKSUM", "SAMPLE_RATE",
    "ALT_EXTENSION", "ALT_MD5_CHECKSUM", "NEW_CONFIG", "CHANNEL_IDENTITIES", "UNASSIGNED", "UNASSIGNED", "UNASSIGNED", "BLOCK_CHECKSUM"
};

static int32_t read_bytes (void *buff, int32_t bcount);
static uint32_t read_next_header (read_stream infile, WavpackHeader *wphdr);
static void little_endian_to_native (void *data, char *format);
static void native_to_little_endian (void *data, char *format);
static void parse_wavpack_block (unsigned char *block_data);
static int verify_wavpack_block (unsigned char *buffer);

static const char *sign_on = "\n"
" WVPARSER  WavPack Audio File Parser Test Filter  Version 1.10\n"
" Copyright (c) 1998 - 2020 David Bryant.  All Rights Reserved.\n\n";

int main (int argc, char **argv)
{
    uint32_t bcount, total_bytes, sample_rate, first_sample, last_sample = -1L;
    int channel_count, block_count;
    char flags_list [256];
    FILE *outfile = NULL;
    WavpackHeader wphdr;

    if (argc > 1) {
        outfile = fopen (argv [1], "wb");

        if (!outfile) {
            fprintf (stderr, "can't open file '%s'!\n", argv [1]);
            return 1;
        }
    }

#ifdef _WIN32
    setmode (_fileno (stdin), O_BINARY);
#endif
    fprintf (stderr, "%s", sign_on);

    while (1) {

	// read next WavPack header

	bcount = read_next_header (read_bytes, &wphdr);

	if (bcount == (uint32_t) -1) {
	    printf ("\nend of file\n\n");
	    break;
	}

	if (bcount)
	    printf ("\nunknown data skipped, %u bytes\n", bcount);

        if (((wphdr.flags & SRATE_MASK) >> SRATE_LSB) == 15) {
            if (sample_rate != 44100)
                printf ("\nwarning: unknown sample rate...using 44100 default\n");
            sample_rate = 44100;
        }
        else {
            sample_rate = sample_rates [(wphdr.flags & SRATE_MASK) >> SRATE_LSB];

            if (wphdr.flags & DSD_FLAG)
                sample_rate *= 4;       // most common multiplier (DSD64), but could be wrong
        }

        // basic summary of the block

        if ((wphdr.flags & INITIAL_BLOCK) || !wphdr.block_samples)
            printf ("\n");

	if (wphdr.block_samples) {
	    printf ("%s audio block, version 0x%03x, %u samples in %u bytes, time = %.2f-%.2f\n",
                (wphdr.flags & MONO_FLAG) ? "mono" : "stereo", wphdr.version, wphdr.block_samples, wphdr.ckSize + 8,
                (double) wphdr.block_index / sample_rate, (double) (wphdr.block_index + wphdr.block_samples - 1) / sample_rate);

            // now show information from the "flags" field of the header

            printf ("samples are %d bits in %d bytes, shifted %d bits, sample rate = %u Hz\n",
                (int)((wphdr.flags & MAG_MASK) >> MAG_LSB) + 1,
                (wphdr.flags & BYTES_STORED) + 1,
                (int)(wphdr.flags & SHIFT_MASK) >> SHIFT_LSB,
                sample_rate);

            flags_list [0] = 0;

            if (wphdr.flags) {
                if (wphdr.flags & INITIAL_BLOCK) strcat (flags_list, "INITIAL ");
                if (wphdr.flags & MONO_FLAG) strcat (flags_list, "MONO ");
                if (wphdr.flags & DSD_FLAG) strcat (flags_list, "DSD ");
                if (wphdr.flags & HYBRID_FLAG) strcat (flags_list, "HYBRID ");
                if (wphdr.flags & JOINT_STEREO) strcat (flags_list, "JOINT-STEREO ");
                if (wphdr.flags & CROSS_DECORR) strcat (flags_list, "CROSS-DECORR ");
                if (wphdr.flags & HYBRID_SHAPE) strcat (flags_list, "NOISE-SHAPING ");
                if (wphdr.flags & FLOAT_DATA) strcat (flags_list, "FLOAT ");
                if (wphdr.flags & INT32_DATA) strcat (flags_list, "INT32 ");
                if (wphdr.flags & HYBRID_BITRATE) strcat (flags_list, "HYBRID-BITRATE ");
                if (wphdr.flags & FALSE_STEREO) strcat (flags_list, "FALSE-STEREO ");
                if (wphdr.flags & NEW_SHAPING) strcat (flags_list, "NEW-SHAPING ");
                if (wphdr.flags & HAS_CHECKSUM) strcat (flags_list, "CHECKSUM ");
                if (wphdr.flags & (IGNORED_FLAGS | UNKNOWN_FLAGS)) strcat (flags_list, "UNKNOWN-FLAGS ");
                if (wphdr.flags & FINAL_BLOCK) strcat (flags_list, "FINAL");
            }
            else
                strcat (flags_list, "none");

            printf ("flags: %s\n", flags_list);
        }
        else
            printf ("non-audio block of %u bytes, version 0x%03x\n", wphdr.ckSize + 8, wphdr.version);

	// read and parse the actual block data (which is entirely composed of "meta" blocks)

	if (wphdr.ckSize > sizeof (WavpackHeader) - 8) {
	    uint32_t block_size = wphdr.ckSize + 8;
	    char *block_buff = malloc (block_size);

            memcpy (block_buff, &wphdr, sizeof (WavpackHeader));
	    read_bytes (block_buff + sizeof (WavpackHeader), block_size - sizeof (WavpackHeader));

            if (outfile)    // add condition here if desired to select only certain blocks be written
                fwrite (block_buff, block_size, 1, outfile);

            parse_wavpack_block (block_buff);
	    free (block_buff);
	}

	// if there's audio samples in there do some other sanity checks (especially for multichannel)

	if (wphdr.block_samples) {
	    if ((wphdr.flags & INITIAL_BLOCK) && wphdr.block_index != last_sample + 1)
		printf ("error: discontinuity detected!\n");

	    if (!(wphdr.flags & INITIAL_BLOCK))
		if (first_sample != wphdr.block_index ||
		    last_sample != wphdr.block_index + wphdr.block_samples - 1)
			printf ("error: multichannel block mismatch detected!\n");

	    last_sample = (first_sample = wphdr.block_index) + wphdr.block_samples - 1;

	    if (wphdr.flags & INITIAL_BLOCK) {
		channel_count = (wphdr.flags & MONO_FLAG) ? 1 : 2;
		total_bytes = wphdr.ckSize + 8;
		block_count = 1;
	    }
	    else {
		channel_count += (wphdr.flags & MONO_FLAG) ? 1 : 2;
		total_bytes += wphdr.ckSize + 8;
		block_count++;

		if (wphdr.flags & FINAL_BLOCK)
		    printf ("multichannel: %d channels in %d blocks, %u bytes total\n",
			channel_count, block_count, total_bytes);
	    }
	}
    }

    if (outfile)
        fclose (outfile);

    return 0;
}

// read the next metadata block, or return 0 if there aren't any more (or an error occurs)

static int read_metadata_buff (WavpackMetadata *wpmd, unsigned char *blockbuff, unsigned char **buffptr)
{
    WavpackHeader *wphdr = (WavpackHeader *) blockbuff;
    unsigned char *buffend = blockbuff + wphdr->ckSize + 8;

    if (buffend - *buffptr < 2)
        return 0;

    wpmd->id = *(*buffptr)++;
    wpmd->byte_length = *(*buffptr)++ << 1;

    if (wpmd->id & ID_LARGE) {
        wpmd->id &= ~ID_LARGE;

        if (buffend - *buffptr < 2)
            return 0;

        wpmd->byte_length += *(*buffptr)++ << 9;
        wpmd->byte_length += *(*buffptr)++ << 17;
    }

    if (wpmd->id & ID_ODD_SIZE) {
        if (!wpmd->byte_length)         // odd size and zero length makes no sense
            return 0;
        wpmd->id &= ~ID_ODD_SIZE;
        wpmd->byte_length--;
    }

    if (wpmd->byte_length) {
        if (buffend - *buffptr < wpmd->byte_length + (wpmd->byte_length & 1)) {
            wpmd->data = NULL;
            return 0;
        }

        wpmd->data = *buffptr;
        (*buffptr) += wpmd->byte_length + (wpmd->byte_length & 1);
    }
    else
        wpmd->data = NULL;

    return 1;
}

// given a pointer to a WavPack block, parse all the "meta" blocks and display something about them

static void parse_wavpack_block (unsigned char *block_data)
{
    unsigned char *blockptr = block_data + sizeof (WavpackHeader);
    WavpackHeader *wphdr = (WavpackHeader *) block_data;
    int metadata_count = 0;
    WavpackMetadata wpmd;

    while (read_metadata_buff (&wpmd, block_data, &blockptr)) {
        metadata_count++;
        if (wpmd.id & 0x10)
            printf ("  metadata: ID = 0x%02x (UNASSIGNED), size = %d bytes\n", wpmd.id, wpmd.byte_length);
        else if (wpmd.id & 0x20)
            printf ("  metadata: ID = 0x%02x (%s), size = %d bytes\n", wpmd.id, metadata_names [wpmd.id - 0x10], wpmd.byte_length);
        else
            printf ("  metadata: ID = 0x%02x (%s), size = %d bytes\n", wpmd.id, metadata_names [wpmd.id], wpmd.byte_length);
    }

    if (blockptr != block_data + wphdr->ckSize + 8)
        printf ("error: garbage at end of WavPack block\n");

    if (!verify_wavpack_block (block_data))
        printf ("error: checksum failure on WavPack block\n");
}

// Quickly verify the referenced block. It is assumed that the WavPack header has been converted
// to native endian format. If a block checksum is performed, that is done in little-endian
// (file) format. It is also assumed that the caller has made sure that the block length
// indicated in the header is correct (we won't overflow the buffer). If a checksum is present,
// then it is checked, otherwise we just check that all the metadata blocks are formatted
// correctly (without looking at their contents). Returns FALSE for bad block.

#define ID_BLOCK_CHECKSUM       (ID_OPTIONAL_DATA | 0xf)

static int verify_wavpack_block (unsigned char *buffer)
{
    WavpackHeader *wphdr = (WavpackHeader *) buffer;
    uint32_t checksum_passed = 0, bcount, meta_bc;
    unsigned char *dp, meta_id, c1, c2;

    if (strncmp (wphdr->ckID, "wvpk", 4) || wphdr->ckSize + 8 < sizeof (WavpackHeader))
        return 0;

    bcount = wphdr->ckSize - sizeof (WavpackHeader) + 8;
    dp = (unsigned char *)(wphdr + 1);

    while (bcount >= 2) {
        meta_id = *dp++;
        c1 = *dp++;

        meta_bc = c1 << 1;
        bcount -= 2;

        if (meta_id & ID_LARGE) {
            if (bcount < 2)
                return 0;

            c1 = *dp++;
            c2 = *dp++;
            meta_bc += ((uint32_t) c1 << 9) + ((uint32_t) c2 << 17);
            bcount -= 2;
        }

        if (bcount < meta_bc)
            return 0;

        if ((meta_id & ID_UNIQUE) == ID_BLOCK_CHECKSUM) {
            unsigned char *csptr = buffer;

            int wcount = (int)(dp - 2 - buffer) >> 1;
            uint32_t csum = (uint32_t) -1;

            if ((meta_id & ID_ODD_SIZE) || meta_bc < 2 || meta_bc > 4)
                return 0;

            native_to_little_endian ((WavpackHeader *) buffer, WavpackHeaderFormat);

            while (wcount--) {
                csum = (csum * 3) + csptr [0] + (csptr [1] << 8);
                csptr += 2;
            }

            little_endian_to_native ((WavpackHeader *) buffer, WavpackHeaderFormat);

            if (meta_bc == 4) {
                if (*dp++ != (csum & 0xff) || *dp++ != ((csum >> 8) & 0xff) || *dp++ != ((csum >> 16) & 0xff) || *dp++ != ((csum >> 24) & 0xff))
                    return 0;
            }
            else {
                csum ^= csum >> 16;

                if (*dp++ != (csum & 0xff) || *dp++ != ((csum >> 8) & 0xff))
                    return 0;
            }

            checksum_passed++;
        }

        bcount -= meta_bc;
        dp += meta_bc;
    }

    return (bcount == 0) && (!(wphdr->flags & HAS_CHECKSUM) || checksum_passed);
}

static int32_t read_bytes (void *buff, int32_t bcount)
{
    return fread (buff, 1, bcount, stdin);
}

// Read from current file position until a valid 32-byte WavPack 4.0 header is
// found and read into the specified pointer. The number of bytes skipped is
// returned. If no WavPack header is found within 1 meg, then a -1 is returned
// to indicate the error. No additional bytes are read past the header and it
// is returned in the processor's native endian mode. Seeking is not required.

static uint32_t read_next_header (read_stream infile, WavpackHeader *wphdr)
{
    unsigned char buffer [sizeof (*wphdr)], *sp = buffer + sizeof (*wphdr), *ep = sp;
    uint32_t bytes_skipped = 0;
    int bleft;

    while (1) {
	if (sp < ep) {
	    bleft = ep - sp;
	    memmove (buffer, sp, bleft);
	}
	else
	    bleft = 0;

	if (infile (buffer + bleft, sizeof (*wphdr) - bleft) != (int32_t) sizeof (*wphdr) - bleft)
	    return -1;

	sp = buffer;

	if (*sp++ == 'w' && *sp == 'v' && *++sp == 'p' && *++sp == 'k' &&
            !(*++sp & 1) && sp [2] < 16 && !sp [3] && (sp [2] || sp [1] || *sp >= 24) && sp [5] == 4 &&
            sp [4] >= (MIN_STREAM_VERS & 0xff) && sp [4] <= (MAX_STREAM_VERS & 0xff) && sp [18] < 3 && !sp [19]) {
		memcpy (wphdr, buffer, sizeof (*wphdr));
		little_endian_to_native (wphdr, WavpackHeaderFormat);
		return bytes_skipped;
	    }

        // printf ("read_next_header() did not see valid block right away: %c %c %c %c\n", buffer [0], buffer [1], buffer [2], buffer [3]);

	while (sp < ep && *sp != 'w')
	    sp++;

	if ((bytes_skipped += sp - buffer) > 1024 * 1024)
	    return -1;
    }
}

// using the specified format string, convert the referenced structure from little-endian to native

static void little_endian_to_native (void *data, char *format)
{
    unsigned char *cp = (unsigned char *) data;
    int32_t temp;

    while (*format) {
	switch (*format) {
	    case 'L':
		temp = cp [0] + ((int32_t) cp [1] << 8) + ((int32_t) cp [2] << 16) + ((int32_t) cp [3] << 24);
		* (int32_t *) cp = temp;
		cp += 4;
		break;

	    case 'S':
		temp = cp [0] + (cp [1] << 8);
		* (int16_t *) cp = (int16_t) temp;
		cp += 2;
		break;

	    default:
		if (isdigit (*format))
		    cp += *format - '0';

		break;
	}

	format++;
    }
}

static void native_to_little_endian (void *data, char *format)
{
    unsigned char *cp = (unsigned char *) data;
    int32_t temp;

    while (*format) {
        switch (*format) {
            case 'L':
                temp = * (int32_t *) cp;
                *cp++ = (unsigned char) temp;
                *cp++ = (unsigned char) (temp >> 8);
                *cp++ = (unsigned char) (temp >> 16);
                *cp++ = (unsigned char) (temp >> 24);
                break;

            case 'S':
                temp = * (int16_t *) cp;
                *cp++ = (unsigned char) temp;
                *cp++ = (unsigned char) (temp >> 8);
                break;

            default:
                if (isdigit (*format))
                    cp += *format - '0';

                break;
        }

        format++;
    }
}
