
/**
 * wavdump: a utility to parse and dump WAV audio files.
 * <br>© 2019 Ian Cameron Smith.  All rights reserved.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 */


package org.hermit.wavdump;


import java.io.DataInputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;


/**
 * WAV file dumper.
 * 
 * The purpose of this class is to dump the content of a WAV file
 * for illustrative purposes only; in other words, to illustrate how
 * a WAV file is structured.
 * 
 * THIS IS NOT PRODUCTION CODE.  It has not been tested on most WAV
 * file formats, such as 8-bit audio, mono, multi-channel, etc.  It
 * is purely for educational purposes, and is probably flawed even
 * for that.  Use at your own risk.
 * 
 * References:
 *     http://soundfile.sapp.org/doc/WaveFormat/
 *     http://www-mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html
 */
public class Dumper
{
	
    // ******************************************************************** //
    // Public Types.
    // ******************************************************************** //

	/**
	 * Enumeration of the data display modes supported for various types
	 * of lengthy data.
	 */
	public enum DataMode {
		/** Display all the data. */
		FULL,
		/** Display a summary of the first and last items. */
		SHORT,
		/** Don't display data. */
		NONE;
	}
	

	/**
	 * User-initiated exception.
	 * 
	 * <p>This exception class represents an error based on what the
	 * user has asked us to do; such as loading a non-existent file.
	 * This error should be reported as a simple information message.
	 */
	public class UserException
		extends Exception
	{
		/**
		 * Create an exception with a String message.
		 * 
		 * @param   s           Error message.
		 */
		public UserException(String s) { super(s); }

		/**
		 * Create an exception with a String message.
		 * 
		 * @param   s           Error message.
		 * @param   e           Exception which indicates the specific problem.
		 */
		public UserException(String s, Throwable e) { super(s, e); }

		// UUID.
		private static final long serialVersionUID = 1L;
	}


	/**
	 * Application-initiated exception.
	 * 
	 * <p>This exception class represents an error caused by a problem
	 * within the application; ie. something that shouldn't happen.
	 * This error should be reported as an internal error, with appropriate
	 * debugging.
	 */
	public class AppException
		extends RuntimeException
	{
		/**
		 * Create an exception with a String message.
		 * 
		 * @param   s           Error message.
		 */
		public AppException(String s) { super(s); }

		/**
		 * Create an exception with a String message.
		 * 
		 * @param   s           Error message.
		 * @param   e           Exception which indicates the specific problem.
		 */
		public AppException(String s, Throwable e) { super(s, e); }

		// UUID.
		private static final long serialVersionUID = 1L;
	}


	// ******************************************************************** //
    // Constructor.
    // ******************************************************************** //

	/**
	 * Create a dumper to parse the given WAV file.
	 * 
	 * @param	file		File to parse; must be a WAV file.
	 * @param	smode		Audio sample display mode.
	 * @param	xmode		XML display mode.
	 */
	public Dumper(File file, DataMode smode, DataMode xmode) {
		wavFile = file;
		sampleMode = smode;
		xmlMode = xmode;
	}
	
	
    // ******************************************************************** //
    // WAV File Parsing Interface.
    // ******************************************************************** //

	/**
	 * Dump the file.
	 * 
	 * @throws UserException	An error occurred in the given file.
	 */
    public void dump()
    	throws UserException
    {
    	try (FileInputStream fis = new FileInputStream(wavFile);
    		 DataInputStream dis = new DataInputStream(fis)) {
    		dump(dis);
    	} catch (IOException e) {
    		throw new UserException("Can't open " + wavFile.getPath(), e);
    	}
    }
    
	
    // ******************************************************************** //
    // WAV File Parsing.
    // ******************************************************************** //

	/**
	 * Dump the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @throws UserException	An error occurred in the given stream.
	 */
    private void dump(DataInputStream dis)
    	throws UserException
    {
    	// Dump chunks from the file.  Generally there should be 1
    	// chunk, and it should be of type RIFF; but we will dump
    	// whatever we find.
    	for (int index = 0; ; ++index)
    		if (dumpChunk(dis, 1, index) == 0)
        		break;
    }
    

	/**
	 * Dump a chunk (or sub-chunk, etc) from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	index			Index of this chunk in its parent.
	 * @return					The total size of the chunk we found,
	 * 							including its headers; zero if we hit EOF.
	 * @throws UserException	An error occurred in the given stream.
	 */
    private long dumpChunk(DataInputStream dis, int in, int index)
    	throws UserException
    {
    	// Read the sub-chunk ID.
    	String id = read4Char(dis);
    	if (id == null) {
        	System.out.printf("<EOF>\n");
    		return 0L;
    	}

    	// Get the sub-chunk size.  This is the size of the chunk after
    	// this number.
    	long size = readUInt32(dis);

    	// Dump the content.
    	dumpContent(dis, in, id, size);
    	
    	// If the content is odd-sized, skip the padding byte.
    	if (size % 2 != 0) {
    		skipAByte(dis);
    		
    		// Account for the padding size in our parent list.
    		++size;
    	}
    	
    	// Return the total size taken up by this sub-chunk,
    	// including the format and size headers.
    	return size + 8;
    }
    

	/**
	 * Dump the content of a sub-chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	id				The sub-chunk ID.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpContent(DataInputStream dis, int in, String id, long size)
    	throws UserException
    {
    	switch (id) {
    	// The top-level chunk should be RIFF, which is a list of sub-
    	// chunks; a LIST chunk is basically the same thing.
    	case "RIFF":
    	case "LIST":
    		dumpChunkList(dis, in, id, size);
    		break;
    		
    	case "fmt ":				// Note: F M T <space>
        	System.out.printf("%s%4s: %,d bytes:\n", indent(in), id, size);
    		dumpWavFmt(dis, in + 1, size);
    		break;
    	case "data":
    		dumpWavData(dis, in, size);
    		break;
    	case "bext":
        	System.out.printf("%s%4s: %,d bytes:\n", indent(in), id, size);
    		dumpBextData(dis, in + 1, size);
    		break;
    		
		// These are from the INFO chunk.
    	case "IARL":
    	case "IART":
    	case "ICMS":
    	case "ICMT":
    	case "ICOP":
    	case "ICRD":
    	case "IENG":
    	case "IGNR":
    	case "IKEY":
    	case "IMED":
    	case "INAM":
    	case "IPRD":
    	case "ISBJ":
    	case "ISFT":
    	case "ISRC":
    	case "ISRF":
    	case "ITCH":
    		dumpTextData(dis, in, id, size);
    		break;
    		
    	// "iXML" is BWF extended meta-data, in XML format,
    	// so it's basically a chunk of text.
    	case "iXML":
    		dumpBigTextData(dis, in, id, size);
    		break;

    	// "_PMX" is an Adobe special -- it's basically XMP meta-data,
    	// which is XML, so it's basically a chunk of text.
    	case "_PMX":
    		dumpBigTextData(dis, in, id, size);
    		break;
    		
    	default:
        	System.out.printf("%s%4s: %,d bytes (unknown)\n", indent(in), id, size);
        	
        	// Just skip it.
        	skipBytes(dis, size);
        	break;
    	}
    }
    

	/**
	 * Dump the content of a compound chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	parent			The name of the parent chunk: usually
	 * 							RIFF or LIST.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpChunkList(DataInputStream dis, int in, String parent, long size)
    	throws UserException
    {
    	// We must have the sub-format.
    	if (size < 4)
    		throw new UserException("Invalid chunk list size " + size +
							        " in " + wavFile.getPath());
    	
    	// Get the format.  Deduct it from the size.
    	String fmt = read4Char(dis);
    	size -= 4;
    	
    	System.out.printf("%s%4s / %4s: %,d bytes:\n", indent(in), parent, fmt, size);

    	// Dump sub-chunks up to the given size.  If we hit EOF during
    	// this, that's an error.
    	for (int index = 0; size > 0; ++index) {
    		long sub = dumpChunk(dis, in + 1, index);
    		if (sub == 0L)
        		throw new UserException("Unexpected EOF in " + parent +
        								" in " + wavFile.getPath());
    		size -= sub;
    	}
    }
    

	/**
	 * Dump the content of a WAV "fmt " sub-chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpWavFmt(DataInputStream dis, int in, long size)
    	throws UserException
    {
    	// Get the audio format.  1 is PCM; 3 FLOAT; others unknown.
    	wavFormat = readUInt16(dis);
    	
    	// Get the number of channels.
    	wavChannels = readUInt16(dis);
    	
    	// Get the sample rate.
    	wavSampleRate = readUInt32(dis);
    	
    	// Get the byte rate.
    	long brate = readUInt32(dis);
    	
    	// Get the bytes per sample across all channels.
    	int bytes = readUInt16(dis);
    	
    	// Get the bits per sample per channel.
    	wavSampleSize = readUInt16(dis);
    	
    	// Sanity check it.
    	if (wavSampleSize % 8 != 0)
    		throw new UserException("WAV fmt: bits non byte-aligned: " + wavSampleSize);
    	if (wavSampleSize < 8 || wavSampleSize > 32)
    		throw new UserException("WAV fmt: absurd bits: " + wavSampleSize);
    	if (bytes != wavChannels * (wavSampleSize / 8))
    		throw new UserException("WAV fmt: bytes mismatch: " + bytes);
    	if (brate != wavSampleRate * wavChannels * (wavSampleSize / 8))
    		throw new UserException("WAV fmt: byte rate mismatch: " + brate);

    	// Print it all out.
    	String fmtDesc;
    	switch (wavFormat) {
    	case 1:
    		fmtDesc = "PCM";
    		break;
    	case 3:
    		fmtDesc = "FLOAT";
    		break;
		default:
	    	fmtDesc = "UNK:" + wavFormat;
    		break;
    	}
    	System.out.printf("%sWAV %s: %d chan %d bits %d/sec\n", indent(in),
    					  fmtDesc, wavChannels, wavSampleSize, wavSampleRate);

    	// If we haven't read it all (can be true for non-PCM data),
    	// skip the remainder.
    	size -= 16;
    	if (size > 0)
    		skipBytes(dis, size);
    }


	/**
	 * Dump the content of a WAV "data" sub-chunk from the given stream.
	 * Because this is a huge mess of numbers, we'll just print the
	 * start and end chunks.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpWavData(DataInputStream dis, int in, long size)
    	throws UserException
    {
    	// We need to have the format info.
    	if (wavFormat == 0)
    		throw new UserException("WAV data: missing format in WAV file");
    	
    	// If the format is not PCM or FLOAT, just say so.
    	if (wavFormat != 1 && wavFormat != 3) {
    		System.out.printf("%sWAV data format %d unknown\n", indent(in), wavFormat);
        	skipBytes(dis, size);
    		return;
    	}
    	if (wavFormat == 3 && wavSampleSize != 32) {
    		System.out.printf("%sWAV float data size %d not supported\n",
    									indent(in), wavSampleSize);
        	skipBytes(dis, size);
    		return;
    	}
  	
    	// Display some lines of audio data, if required.
		final int bytesPerLine = wavChannels * (wavSampleSize / 8);
    	switch (sampleMode) {
		case NONE:
			// Don't display anything but the size.
        	System.out.printf("%s%4s: %,d bytes.\n", indent(in), "data", size);
			break;
		case SHORT:
        	System.out.printf("%s%4s: %,d bytes:\n", indent(in), "data", size);
        	
        	// Display some lines from the start.
    		final int dispSize = bytesPerLine * AUDIO_DATA_LINES;
    		for (int i = 0; i < AUDIO_DATA_LINES && size >= bytesPerLine; ++i) {
    			dumpWavLine(dis, in + 1);
    			size -= bytesPerLine;
    		}

        	// Display some lines from the end, if there's enough.
    		if (size > dispSize) {
    			// Skip the stuff in the middle.
    			final long middle = size - dispSize;
    			final long lines = middle / bytesPerLine;
    			System.out.printf("%s... (%,d lines)\n", indent(in + 1), lines);
    			skipBytes(dis, middle);
    			size -= middle;
    			
    			// Dump the last lines.
        		for (int i = 0; i < AUDIO_DATA_LINES && size >= bytesPerLine; ++i) {
        			dumpWavLine(dis, in + 1);
        			size -= bytesPerLine;
        		}
    		} else if (size > 0) {
    			long lines = size / bytesPerLine;
    			System.out.printf("%s... (%,d lines)\n", indent(in + 1), lines);
    		}
			break;
		case FULL:
        	System.out.printf("%s%4s: %,d bytes:\n", indent(in), "data", size);
        	
        	// Display all the lines of samples.
    		while (size >= bytesPerLine) {
    			dumpWavLine(dis, in + 1);
    			size -= bytesPerLine;
    		}
			break;
    	}

    	// Skip any remaining data.
    	skipBytes(dis, size);
    }


	/**
	 * Dump the a line of data (i.e. one sample across all channels)
	 * from a WAV "data" sub-chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpWavLine(DataInputStream dis, int in)
    	throws UserException
    {
    	System.out.printf("%s", indent(in));
    	
    	switch (wavFormat) {
    	case 1:
    		dumpWavLineInt(dis);
    		break;
    	case 3:
    		dumpWavLineFloat(dis);
    		break;
		default:
			throw new AppException("dumpWavLine got format " + wavFormat);
    	}
    	
    	System.out.printf("\n");
    }


	/**
	 * Dump the a line of integer data (i.e. one sample across all channels)
	 * from a WAV "data" sub-chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpWavLineInt(DataInputStream dis)
    	throws UserException
    {
    	for (int c = 0; c < wavChannels; ++c) {
    		long samp = wavSampleSize == 8 ?
    						readUIntN(dis, wavSampleSize) :
    						readSIntN(dis, wavSampleSize);
        	System.out.printf(" %,12d", samp);
    	}
    }


	/**
	 * Dump the a line of float data (i.e. one sample across all channels)
	 * from a WAV "data" sub-chunk from the given stream.  The sample
	 * size must be 32.
	 * 
	 * @param	dis				The input stream to dump.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpWavLineFloat(DataInputStream dis)
    	throws UserException
    {
    	assert wavSampleSize == 32;
    	
    	for (int c = 0; c < wavChannels; ++c) {
    		int samp = (int) readUIntN(dis, 32);
    		int mantissa = (samp & 0x007fffff) | 0x00800000;
    		double fmantissa = (double) mantissa / (double) 0x007fffff;
    		int exponent = ((samp & 0x7f800000) >>> 23) - 127;
    		int sign =     ((samp & 0x80000000) >>> 31) * -2 + 1;
        	System.out.printf(" %c%,8fx2^%04d",
        					  sign < 0 ? '-' : '+',
        					  fmantissa, exponent);
    	}
    }


	/**
	 * Dump the content of a WAV "bext" (BWF meta-data) sub-chunk
	 * from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpBextData(DataInputStream dis, int in, long size)
    	throws UserException
    {
    	String description = readNtString(dis, 256);
    	size -= 256;
    	String originator = readNtString(dis, 32);
    	size -= 32;
    	String origRef = readNtString(dis, 32);
    	size -= 32;
    	String origDate = readNtString(dis, 10);
    	size -= 10;
    	String origTime = readNtString(dis, 8);
    	size -= 8;

    	@SuppressWarnings("unused")
		long timeRefLow = readUInt32(dis);
    	@SuppressWarnings("unused")
    	long timeRefHigh = readUInt32(dis);
    	size -= 8;
   	
    	int version = readUInt16(dis);
    	size -= 2;

    	@SuppressWarnings("unused")
    	byte[] umid = readNBytes(dis, 64);
    	size -= 64;
    	
    	@SuppressWarnings("unused")
    	int loudVal = readSInt16(dis);
    	@SuppressWarnings("unused")
    	int loudRange = readSInt16(dis);
    	@SuppressWarnings("unused")
    	int maxTruePeak = readSInt16(dis);
    	@SuppressWarnings("unused")
    	int maxMomLoudness = readSInt16(dis);
    	@SuppressWarnings("unused")
    	int maxShortLoudness = readSInt16(dis);
    	size -= 10;

    	// Skip the reserved space.
    	skipBytes(dis, 180);
    	size -= 180;
    	
    	if (size < 0)
    		throw new UserException("WAV bext: size too small");

    	// Read the remaining characters as the coding history.
    	String coding = readNtString(dis, (int) size);

    	// Print it all out.
    	System.out.printf("%sBEXT version %d:\n", indent(in), version);
    	if (description != null)
        	System.out.printf("%sdescription: %s\n", indent(in + 1), description);
    	if (originator != null)
        	System.out.printf("%soriginator:  %s\n", indent(in + 1), originator);
    	if (origRef != null)
        	System.out.printf("%sorig. ref :  %s\n", indent(in + 1), origRef);
    	if (origDate != null)
        	System.out.printf("%sdate:        %s\n", indent(in + 1), origDate);
    	if (origTime != null)
        	System.out.printf("%stime:        %s\n", indent(in + 1), origTime);
    	if (coding != null)
        	System.out.printf("%scoding hist: %s\n", indent(in + 1), coding);
    }


	/**
	 * Dump the content of a textual sub-chunk from the given stream.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	id				The name of this sub-chunk.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpTextData(DataInputStream dis, int in, String id, long size)
    	throws UserException
    {
    	String text = readNtString(dis, (int) size);

    	// Print it all out.
    	System.out.printf("%s%4s: %s\n", indent(in), id, text);
    }


	/**
	 * Dump the content of a large (multi-line) textual sub-chunk from
	 * the given stream.  We trim blank lines -- unfortunately Adobe _PMX
	 * chunks include lots of blank lines.
	 * 
	 * @param	dis				The input stream to dump.
	 * @param	in				The indent level.
	 * @param	id				The name of this sub-chunk.
	 * @param	size			The size of the sub-chunk content.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including an EOF in the content.
	 */
    private void dumpBigTextData(DataInputStream dis, int in, String id, long size)
    	throws UserException
    {
    	// Read the text, and break it into lines.
    	String text = readNtString(dis, (int) size);
    	String lines[] = text.split("\\r?\\n");
    	
    	// Trim trailing white space from each line.
		for (int i = 0 ; i < lines.length; ++i)
    		lines[i] = lines[i].replaceAll("\\s+$","");

    	switch (xmlMode) {
		case NONE:
			// Display nothing but the line count.
	    	System.out.printf("%s%4s: %d lines.\n", indent(in), id, lines.length);
			break;
		case SHORT:
			// Display the head and tail.
	    	System.out.printf("%s%4s: %d lines:\n", indent(in), id, lines.length);

	    	// Display the first TEXT_DATA_LINES non-blank lines.
	    	int i = 0;
	    	for (int n = 0; i < lines.length && n < TEXT_DATA_LINES; ++i) {
	    		String l = lines[i];
	    		// Print non-blank lines.  We don't actually trim the
	    		// white space, to preserve indentation.
	    		if (l.length() > 0) {
	    	    	System.out.printf("%s%s\n", indent(in + 1), l);
	    	    	++n;
	    		}
	    	}
	    	
	    	// If there are lines left, display the last TEXT_DATA_LINES lines.
	    	if (i < lines.length) {
	    		// Count TEXT_DATA_LINES non-blank lines back from the end.
	    		int end = lines.length;
	    		int count = 0;
	    		while (end > i && count < TEXT_DATA_LINES) {
	    			--end;
	    			if (lines[end].length() > 0)
	    				++count;
	    		}
	    		
	    		// Push the index up to the tail-end section.
	    		if (i < end) {
	    			System.out.printf("%s... (%,d lines)\n", indent(in + 1), end - i);
	    			i = end;
	    		}
	    		
	    		// Print the tail-end lines.
	    		for ( ; i < lines.length; ++i) {
		    		String l = lines[i];
		    		if (l.length() > 0)
		    	    	System.out.printf("%s%s\n", indent(in + 1), l);
	    		}
	    	}
			break;
		case FULL:
	    	// Print it all out.
	    	System.out.printf("%s%4s: %d lines:\n", indent(in), id, lines.length);
	    	for (String l : lines) {
	    		// Print non-blank lines.  We don't actually trim the
	    		// white space, to preserve indentation.
	    		if (l.length() > 0)
	    	    	System.out.printf("%s%s\n", indent(in + 1), l);
	    	}
			break;
    	}
    }


    // ******************************************************************** //
    // File Utilities.
    // ******************************************************************** //
	
	/**
	 * Read a 4-char identifier from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @return					A String containing the 4-char identifier;
	 * 							null on EOF.
	 * @throws UserException	An error occurred in the given stream.
	 */
    private String read4Char(DataInputStream dis)
    	throws UserException
    {
    	StringBuffer id = new StringBuffer(4);
    	
    	// Read the first byte.  An EOF is valid here.
    	try {
    		int b1 = dis.readUnsignedByte();
    		id.append((char) b1);
    	} catch (EOFException eof) {
    		return null;
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
		}
    	
    	// Read the remaining 3 bytes.
    	try {
    		int b2 = dis.readUnsignedByte();
    		id.append((char) b2);
    		int b3 = dis.readUnsignedByte();
    		id.append((char) b3);
    		int b4 = dis.readUnsignedByte();
    		id.append((char) b4);
    	} catch (EOFException e) {
    		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
		}
    	
    	return id.toString();
    }
    
	
	/**
	 * Read a null-terminated string from a fixed-size field
	 * in the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @param	field			The field size; this is the maximum
	 * 							size of the string, but if shorter it
	 * 							will be null-terminated.
	 * @return					The String read from the field; null
	 * 							if it's empty.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private String readNtString(DataInputStream dis, int field)
    	throws UserException
    {
    	StringBuffer sval = new StringBuffer(field);

    	try {
    		for ( ; field > 0; --field) {
    			int b = dis.readUnsignedByte();
    			if (b == 0) {
    				--field;
    				break;
    			}
    			sval.append((char) b);
    		}
    		
        	if (field > 0)
        		skipBytes(dis, field);
    	} catch (EOFException e) {
    		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
    	}
    	
    	return sval.length() == 0 ? null : sval.toString();
    }
    

	/**
	 * Read a 16-bit unsigned integer from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @return					The integer value.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private int readUInt16(DataInputStream dis)
    	throws UserException
    {
    	int val = 0;
    	
    	for (int i = 0; i < 2; ++i) {
    		try {
    			int b = dis.readUnsignedByte();
    			val |= b << 8 * i;
        	} catch (EOFException e) {
        		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    		} catch (IOException e) {
    			throw new UserException("Error in " + wavFile.getPath(), e);
    		}
    	}
    	
    	return val;
    }
    

	/**
	 * Read a 16-bit signed integer from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @return					The integer value.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private int readSInt16(DataInputStream dis)
    	throws UserException
    {
    	int val = 0;
    	
    	try {
    		int b1 = dis.readUnsignedByte();
    		val |= b1;
			int b2 = dis.readByte();
			val |= b2 << 8;
    	} catch (EOFException e) {
    		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
    	}
    	
    	return val;
    }
    
	
	/**
	 * Read a 32-bit unsigned integer from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @return					The integer value.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private long readUInt32(DataInputStream dis)
    	throws UserException
    {
    	long val = 0;
    	
    	for (int i = 0; i < 4; ++i) {
    		try {
    			int b = dis.readUnsignedByte();
    			val |= b << 8 * i;
        	} catch (EOFException e) {
        		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    		} catch (IOException e) {
    			throw new UserException("Error in " + wavFile.getPath(), e);
    		}
    	}
    	
    	return val;
    }
    
	
	/**
	 * Read an N-bit unsigned integer from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @param	n				The number of bits in the integer.  Must
	 * 							be a multiple of 8, and 8 - 32.
	 * @return					The integer value.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private long readUIntN(DataInputStream dis, int n)
    	throws UserException
    {
    	long val = 0;
    	
    	for (int i = 0; i < n / 8; ++i) {
    		try {
    			int b = dis.readUnsignedByte();
    			val |= b << 8 * i;
        	} catch (EOFException e) {
        		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    		} catch (IOException e) {
    			throw new UserException("Error in " + wavFile.getPath(), e);
    		}
    	}
    	
    	return val;
    }
    
	
	/**
	 * Read an N-bit signed integer from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @param	n				The number of bits in the integer.  Must
	 * 							be a multiple of 8, and 8 - 32.
	 * @return					The integer value.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private long readSIntN(DataInputStream dis, int n)
    	throws UserException
    {
    	long val = 0;
    	
    	for (int i = 0; i < (n / 8) - 1; ++i) {
    		try {
    			int b = dis.readUnsignedByte();
    			val |= b << 8 * i;
        	} catch (EOFException e) {
        		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
    		} catch (IOException e) {
    			throw new UserException("Error in " + wavFile.getPath(), e);
    		}
    	}
    	
		try {
			int b = dis.readByte();
			val |= b << n - 8;
    	} catch (EOFException e) {
    		throw new UserException("Unexpected EOF in " + wavFile.getPath(), e);
		} catch (IOException e) {
			throw new UserException("Error in " + wavFile.getPath(), e);
		}
    	
    	return val;
    }
    

	/**
	 * Read a number of bytes from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @param	size			The number of bytes to read.
	 * @return					A new buffer containing the bytes read.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private byte[] readNBytes(DataInputStream dis, int size)
    	throws UserException
    {
    	try {
    		byte[] buf = new byte[size];
    		int r = dis.read(buf);
    		if (r != size)
    			throw new UserException("Unexpected EOF in " + wavFile.getPath());
        	
        	return buf;
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
    	}
    }
    

	/**
	 * Skip a number of bytes from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @param	size			The number of bytes to skip.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private void skipBytes(DataInputStream dis, long size)
    	throws UserException
    {
    	try {
    		long rem = size;
        	while (rem > 0) {
        		int r = dis.read(skipBuf, 0, (int) Math.min(rem, skipBuf.length));
        		if (r < 0)
            		throw new UserException("Unexpected EOF skipping " +
            								(size - rem) + "/" + size +
            								" bytes in " + wavFile.getPath());
        		rem -= r;
        	}
    	} catch (IOException e) {
    		throw new UserException("Error in " + wavFile.getPath(), e);
		}
    }
    
	
	/**
	 * Skip a single byte from the given stream.
	 * 
	 * @param	dis				The input stream to read from.
	 * @throws UserException	An error occurred in the given stream,
	 * 							including EOF.
	 */
    private void skipAByte(DataInputStream dis)
    	throws UserException
    {
		try {
			dis.readByte();
    	} catch (EOFException e) {
    		throw new UserException("Unexpected EOF skipping byte in " + wavFile.getPath(), e);
		} catch (IOException e) {
			throw new UserException("Error in " + wavFile.getPath(), e);
		}
    }
    

    /**
     * Create a padding string to indent the output by a given amount.
	 * 
	 * @param	n				Levels of indent.
	 * @return					A String containing the required padding.
     */
    private String indent(int n) {
    	StringBuffer sb = new StringBuffer(n * 4);
    	for (int i = 0; i < n; ++i)
    		sb.append("    ");
    	return sb.toString();
    }
    

    // ******************************************************************** //
    // Private Constants.
    // ******************************************************************** //
    
    // The size of the buffer we allocate for skipping junk.
    private static final int SKIP_BUF_SIZE = 1024 * 64;
    
    // The number of lines of audio data to show.
    private static final int AUDIO_DATA_LINES = 8;
    
    // The number of lines of big text fields to show.
    private static final int TEXT_DATA_LINES = 8;
    
    
    // ******************************************************************** //
    // Private Data.
    // ******************************************************************** //

    // The WAV input file we're reading.
    private final File wavFile;

    // Audio sample display mode.
    private final DataMode sampleMode;

    // XML display mode.
    private final DataMode xmlMode;
    
    // Junk buffer for skipping data.
    private final byte[] skipBuf = new byte[SKIP_BUF_SIZE];

    // The WAV data format, read from the file.
    private int wavFormat = 0;

    // The number of channels in the WAV data, read from the file.
    private int wavChannels = 0;

    // The sample rate of the WAV data, read from the file.
    private long wavSampleRate = 0;

    // The sample size in bits of the WAV data, read from the file.
    // Ths is checked to be a multiple of 8.
    private int wavSampleSize = 0;
   
}

