Rendering a WAV File Using HTML5 and JavaScript

The code included below uses the FileReader.readAsBinaryString() function of JavaScript to allow the user to load a .WAV file from the local machine and display its waveform as an image. For an online version, visit http://thiscouldbebetter.neocities.org/wavvisualizer.html.

UPDATE 2013/04/30 – I have modified the code to fix the clustering of samples around the top and bottom line, based on the insight provided by Charles McGeorge in his recent comment on this post.  The waveform now looks a great deal more like it looks when you open it in, say, Audacity.  I suppose the next step would be to split the waveform into left and right stereo channels.

WavVisualizer

<html>
<body>

<script type='text/javascript'>

function WavFileVisualizer()
{
	// do nothing
}
{
	var prototype = WavFileVisualizer.prototype;

	prototype.initialize = function()
	{
		Globals.Instance.initialize(this);

		var inputFileChooser = document.createElement("input");
		inputFileChooser.type = "file";
		inputFileChooser.addEventListener("change", this.loadFileAndVisualize);
		document.body.appendChild(inputFileChooser);
	}

	prototype.loadFileAndVisualize = function(event)
	{
		var fileSpecified = event.target.files[0];			   
		WavFile.readFromFile(fileSpecified);
	}

	prototype.loadFileAndVisualize_LoadComplete = function(wavFileLoaded)
	{
		var viewport = new Viewport
		(
			"Viewport0",
			new Coords(400, 200)
		);

		var canvas = document.createElement("canvas");
		canvas.width = viewport.size.x;
		canvas.height = viewport.size.y;
		document.body.appendChild(canvas);

		var graphics = canvas.getContext("2d");
		graphics.fillStyle="#000080";
		graphics.fillRect(0,0,viewport.size.x, viewport.size.y);		   
		graphics.fillStyle="#00ff00";

		var samples = wavFileLoaded.samplesForChannels[0];
		var numberOfSamples = samples.length;
		var pixelsPerSample = viewport.size.x / wavFileLoaded.durationInSamples();

		var drawPos = new Coords(0, 0);

		for (var s = 0; s < numberOfSamples; s++)
		{
			drawPos.x = s * pixelsPerSample;

			drawPos.y = 
				viewport.sizeHalf.y 
				+ (samples[s].convertToDouble() * viewport.sizeHalf.y);

			graphics.fillRect(drawPos.x, drawPos.y, 1, 1);
		}
	}
}

function ByteStreamLittleEndian(bytes)
{
	this.bytes = bytes;  

	this.numberOfBytesTotal = this.bytes.length;
	this.byteIndexCurrent = 0;
}
{
	var prototype = ByteStreamLittleEndian.prototype;

	prototype.peekBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.bytes[this.byteIndexCurrent + b];
		}

		return returnValue;
	}

	prototype.readBytes = function(numberOfBytesToRead)
	{
		var returnValue = [];

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			returnValue[b] = this.readByte();
		}

		return returnValue;
	}

	prototype.readByte = function()
	{
		var returnValue = this.bytes.charCodeAt(this.byteIndexCurrent);

		this.byteIndexCurrent++;

		return returnValue;
	}

	prototype.readInt = function()
	{
		var returnValue =
		(
			(this.readByte() & 0xFF)
			| ((this.readByte() & 0xFF) << 8 )
			| ((this.readByte() & 0xFF) << 16)
			| ((this.readByte() & 0xFF) << 24)
		);

		return returnValue;
	}

	prototype.readShort = function()
	{
		var returnValue =
		(
			(this.readByte() & 0xFF)
			| ((this.readByte() & 0xFF) << 8 )
		);

		return returnValue;
	}
}

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	var prototype = Coords.prototype;

	prototype.clone = function()
	{
		return new Coords(this.x, this.y);
	}

	prototype.divideScalar = function(scalar)
	{
		this.x /= scalar;
		this.y /= scalar;

		return this;
	}
}

function Globals()
{
	// do nothing
}
{
	Globals.Instance = new Globals();

	var prototype = Globals.prototype;

	prototype.initialize = function(visualizer)
	{
		this.visualizer = visualizer;   
	}
}

function Viewport(name, size)
{
	this.name = name;
	this.size = size;
	this.sizeHalf = this.size.clone().divideScalar(2);
}

function WavFile
(
	filePath,
	samplingInfo,
	samplesForChannels
)
{
	this.filePath = filePath;
	this.samplingInfo = samplingInfo;
	this.samplesForChannels = samplesForChannels;

	 // hack
	if (this.samplingInfo == null)
	{
		this.samplingInfo = SamplingInfo.buildDefault();
	}

	if (this.samplesForChannels == null)
	{
		var numberOfChannels = this.samplingInfo.numberOfChannels; 

		this.samplesForChannels = [];
		for (var c = 0; c < numberOfChannels; c++)
		{
			this.samplesForChannels[c] = [];
		}
	}
}
{
	WavFile.BitsPerByte = 8;
	WavFile.NumberOfBytesInRiffWaveAndFormatChunks = 36;

	var prototype = WavFile.prototype;

	// static methods

	WavFile.readFromFile = function(fileToReadFrom)
	{		
		var returnValue = new WavFile(fileToReadFrom.name, null, null);

		var fileReader = new FileReader();
		fileReader.onloadend = function(fileLoadedEvent)
		{
			if (fileLoadedEvent.target.readyState == FileReader.DONE)
			{
				var bytesFromFile = fileLoadedEvent.target.result;
				var reader = new ByteStreamLittleEndian(bytesFromFile);

				returnValue.readFromFilePath_ReadChunks(reader);
			}

			Globals.Instance.visualizer.loadFileAndVisualize_LoadComplete(returnValue);
		}

		fileReader.readAsBinaryString(fileToReadFrom);
	}

	prototype.readFromFilePath_ReadChunks = function(reader)
	{
		var riffStringAsBytes = reader.readBytes(4);		  

		var numberOfBytesInFile = reader.readInt();

		var waveStringAsBytes = reader.readBytes(4);

		this.readFromFile_ReadChunks_Format(reader);
		this.readFromFile_ReadChunks_Data(reader);
	}

	prototype.readFromFile_ReadChunks_Format = function(reader)
	{
		var fmt_StringAsBytes = reader.readBytes(4);
		var chunkSizeInBytes = reader.readInt();
		var formatCode = reader.readShort();

		var numberOfChannels = reader.readShort();
		var samplesPerSecond = reader.readInt();

		var bytesPerSecond = reader.readInt();
		var bytesPerSampleMaybe = reader.readShort();
		var bitsPerSample = reader.readShort();

		var samplingInfo = new SamplingInfo
		(
			"[from file]",
			chunkSizeInBytes,
			formatCode,
			numberOfChannels,
			samplesPerSecond,
			bitsPerSample	
		);

		this.samplingInfo = samplingInfo;
	}

	prototype.readFromFile_ReadChunks_Data = function(reader)
	{
		var dataStringAsBytes = reader.readBytes(4);
		var subchunk2SizeInBytes = reader.readInt();

		var samplesForChannelsMixedAsBytes = reader.readBytes(subchunk2SizeInBytes);

		var samplesForChannels = Sample.buildManyFromBytes
		(
			this.samplingInfo,
			samplesForChannelsMixedAsBytes
		);

		this.samplesForChannels = samplesForChannels;	
	}

	// instance methods

	prototype.durationInSamples = function()
	{
		var returnValue = 0;
		if (this.samplesForChannels != null)
		{
			if (this.samplesForChannels.length > 0)
			{
				returnValue = this.samplesForChannels[0].length;
			}
		}

		return returnValue;		
	}

	prototype.durationInSeconds = function()
	{
		return this.durationInSamples() / this.samplingInfo.samplesPerSecond;
	}

	prototype.extendOrTrimSamples = function(numberOfSamplesToExtendOrTrimTo)
	{
		var numberOfChannels = this.samplingInfo.numberOfChannels;
		var samplesForChannelsNew = [];

		for (var c = 0; c < numberOfChannels; c++)
		{
			var samplesForChannelOld = this.samplesForChannels[c];
			var samplesForChannelNew = new Sample[numberOfSamplesToExtendOrTrimTo];

			for (var s = 0; s < samplesForChannelOld.length && s < numberOfSamplesToExtendOrTrimTo; s++)
			{
				samplesForChannelNew[s] = samplesForChannelOld[s];				
			}

			var samplePrototype = this.samplingInfo.samplePrototype();

			for (var s = samplesForChannelOld.length; s < numberOfSamplesToExtendOrTrimTo; s++)
			{
				samplesForChannelNew[s] = samplePrototype.build();
			}

			samplesForChannelsNew[c] = samplesForChannelNew;
		}

		this.samplesForChannels = samplesForChannelsNew;
	}

	// inner classes

	function Sample()
	{
		// do nothing
	}
	{
		var prototype = Sample.prototype;

		prototype.build = function(){}
		prototype.setFromBytes = function(valueAsBytes){}
		prototype.setFromDouble = function(valueAsDouble){}
		prototype.convertToBytes = function(){}
		prototype.convertToDouble = function(){}

	   	Sample.buildManyFromBytes = function
		(
			samplingInfo,
			bytesToConvert
		)
		{
			var numberOfBytes = bytesToConvert.length;

			var numberOfChannels = samplingInfo.numberOfChannels;

			var returnSamples = [];

			var bytesPerSample = samplingInfo.bitsPerSample / WavFile.BitsPerByte;

			var samplesPerChannel =
				numberOfBytes
				/ bytesPerSample
				/ numberOfChannels;

			for (var c = 0; c < numberOfChannels; c++)
			{
				returnSamples[c] = [];
			}

			var b = 0;

			var halfMaxValueForEachSample = Math.pow
			(
				2, WavFile.BitsPerByte * bytesPerSample - 1
			);

			var samplePrototype = samplingInfo.samplePrototype();

			var sampleValueAsBytes = [];

			for (var s = 0; s < samplesPerChannel; s++)
			{				
				for (var c = 0; c < numberOfChannels; c++)
				{
					for (var i = 0; i < bytesPerSample; i++)
					{
						sampleValueAsBytes[i] = bytesToConvert[b];
						b++;
					}

					returnSamples[c][s] = samplePrototype.build().setFromBytes
					(
						sampleValueAsBytes
					);
				}
			}

			return returnSamples;
		}

		prototype.convertManyToBytes = function
		(
			samplesToConvert,
			samplingInfo
		)
		{
			var returnBytes = null;

			var numberOfChannels = samplingInfo.numberOfChannels;

			var samplesPerChannel = samplesToConvert[0].length;

			var bitsPerSample = samplingInfo.bitsPerSample;

			var bytesPerSample = bitsPerSample / WavFile.BitsPerByte;

			var numberOfBytes =
				numberOfChannels
				* samplesPerChannel
				* bytesPerSample;

			returnBytes = [];

			var halfMaxValueForEachSample = Math.pow
			(
				2, WavFile.BitsPerByte * bytesPerSample - 1
			);

			var b = 0;

			for (var s = 0; s < samplesPerChannel; s++)
			{
				for (var c = 0; c < numberOfChannels; c++)
				{
					var sample = samplesToConvert[c][s];	

					var sampleAsBytes = sample.convertToBytes();

					for (var i = 0; i < bytesPerSample; i++)
					{
						returnBytes[b] = sampleAsBytes[i];
						b++;
					}
				}						
			}

			return returnBytes;
		}	
	}

	function Sample16(value)
	{
		this.value = value;
	}
	{
		Sample16.MaxValue = Math.pow(2, 15) - 1;
		Sample16.DoubleMaxValue = Math.pow(2, 16);

		var prototype = Sample16.prototype;

		// Sample members
		prototype.build = function()
		{
			return new Sample16(0);
		}

		prototype.setFromBytes = function(valueAsBytes)
		{
			this.value =
			(
				(valueAsBytes[0] & 0xFF)
				| ((valueAsBytes[1] & 0xFF) << 8 )
			);

			if (this.value > Sample16.MaxValue) 
			{
				this.value -= Sample16.DoubleMaxValue;
			}

			return this;
		}

		prototype.setFromDouble = function(valueAsDouble)
		{
			this.value =
			(
				valueAsDouble * Sample16.MaxValue
			);

			return this;
		}

		prototype.convertToBytes = function()
		{
			return new Array()
			{
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF)
			};
		}		

		prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample16.MaxValue;
		}
	}

	function Sample24(value)
	{
		this.value = value;
	}
	{
		Sample24.MaxValue = Math.pow(2, 23) - 1;
		Sample24.DoubleMaxValue = Math.pow(2, 24);

		// Sample members

		var prototype = Sample24.prototype;

		prototype.build = function()
		{
			return new Sample24(0);
		}

		prototype.setFromBytes = function(valueAsBytes)
		{
			this.value =
			(
				((valueAsBytes[0] & 0xFF))
				| ((valueAsBytes[1] & 0xFF) << 8 )
				| ((valueAsBytes[2] & 0xFF) << 16)
			);

			if (this.value > Sample24.MaxValue) 
			{
				this.value -= Sample24.DoubleMaxValue;
			}

			return this;
		}

		prototype.setFromDouble = function(valueAsDouble)
		{
			this.value = 
			(
				valueAsDouble
				* Sample24.MaxValue
			);

			return this;
		}

		prototype.convertToBytes = function()
		{
			return new Array()
			{
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF),
				((this.value >>> 16) & 0xFF)
			};
		}		

		prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample24.MaxValue;
		}
	}

	function Sample32(value)
	{
		this.value = value;
	}
	{
		Sample32.MaxValue = Math.pow(2, 32);
		Sample32.MaxValueHalf = Math.pow(2, 31);

		// Sample members

		prototype.build = function()
		{
			return new Sample32(0);
		}

		prototype.setFromBytes = function(valueAsBytes)
		{
			this.value = 
			(
				((valueAsBytes[0] & 0xFF))
				| ((valueAsBytes[1] & 0xFF) << 8 )
				| ((valueAsBytes[2] & 0xFF) << 16)
				| ((valueAsBytes[3] & 0xFF) << 24)
			);

			if (this.value > Sample32.MaxValue) 
			{
				this.value -= Sample32.DoubleMaxValue;
			}

			return this;
		}

		prototype.setFromDouble = function(valueAsDouble)
		{
			this.value = 
			(
				valueAsDouble
				* Sample32.MaxValue
			);

			return this;
		}

		prototype.convertToBytes = function()
		{
			return new Array()
			{
				((this.value) & 0xFF),
				((this.value >>> 8 ) & 0xFF),
				((this.value >>> 16) & 0xFF),
				((this.value >>> 24) & 0xFF)
			};
		}	

		prototype.convertToDouble = function()
		{
			return 1.0 * this.value / Sample32.MaxValue;
		}	
	}

	function SamplingInfo
	(
		 name,	   
		 chunkSizeInBytes,
		 formatCode,
		 numberOfChannels,		
		 samplesPerSecond,
		 bitsPerSample
	)
	{
		this.name = name;
		this.chunkSizeInBytes = chunkSizeInBytes;
		this.formatCode = formatCode;
		this.numberOfChannels = numberOfChannels;
		this.samplesPerSecond = samplesPerSecond;
		this.bitsPerSample = bitsPerSample;
	}
	{
		var prototype = SamplingInfo.prototype;

		SamplingInfo.buildDefault = function()
		{
			return new SamplingInfo
			(
				"Default",
				16, // chunkSizeInBytes
				1, // formatCode
				1, // numberOfChannels
				44100,	 // samplesPerSecond
				16 // bitsPerSample
			);
		}

		prototype.bytesPerSecond = function()
		{	
			return this.samplesPerSecond
				* this.numberOfChannels
				* this.bitsPerSample / WavFile.BitsPerByte;
		}

		prototype.samplePrototype = function()
		{
			var returnValue = null;

			if (this.bitsPerSample == 16)
			{
				returnValue = new Sample16(0);
			}
			else if (this.bitsPerSample == 24)
			{
				returnValue = new Sample24(0);
			}
			else if (this.bitsPerSample == 32)
			{
				returnValue = new Sample32(0);
			}

			return returnValue;
		}

		prototype.toString = function()
		{
			var returnValue =
				"<SamplingInfo "
				+ "chunkSizeInBytes='" + this.chunkSizeInBytes + "' "
				+ "formatCode='" + this.formatCode + "' "
				+ "numberOfChannels='" + this.numberOfChannels + "' "
				+ "samplesPerSecond='" + this.samplesPerSecond + "' "
				+ "bitsPerSample='" + this.bitsPerSample + "' "
				+ "/>";

			return returnValue;
		}		
	}
}

new WavFileVisualizer().initialize();

</script>

</body>
</html>
Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

7 Responses to Rendering a WAV File Using HTML5 and JavaScript

  1. Charles McGeorge says:

    The samples are clustering that way because Javascript bitwise operators treat their arguments as 32-bit unsigned integers, but the WAV data you use are 16-, 24-, or 32-bit two’s complement integers. The fix is easy: in Sample16.prototype.setFromBytes, for example, just add the line
    if (this.value > 32767) this.value -= 65536;
    which performs the correction in decimal.

    Excellent work though!

  2. Hi there. This code will be very useful for a project that I am working on, but for some reason I can’t get any image to appear. What do I have to do once I choose the .wav file. Currently, it shows that it has been selected but nothing happens.

    Best,

    Conor

    • Hm. All you should have to do is pick the file in your browser’s Open File dialog and click open. I tried it just now on Chrome and Firefox, with no special options set, and it worked on both with no problems.

  3. Matz Radloff says:

    First of all: nice work!
    Unfortunately I ran into the same problem Conor had. The problem for me was that it wouldn’t load 32bit samples. After some debugging I found out that in the Sample32-object one line was missing: var prototype = Sample32.prototype; It should be added after the line 584.

  4. Page is getting crashed while i am selecting the file

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s