A Sound Editor in HTML5 Using JavaScript

The JavaScript code below implements a sound editor in JavaScript. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript. Or, for an online version, visit http://thiscouldbebetter.neocities.org/soundeditor.html.

The application is a little like Audacity, though it has nowhere near as many features, and a lot more bugs. I originally started programming it as a basis for my own text-to-speech research. You can load a .WAV file and play it back. You can select parts of the sound, give those selections labels, and then play back your labeled selections in some new order. You can zoom in and zoom out. A few rudimentary filters are provided that allow you to silence or amplify selections, or generate a sine wave of a given frequency. You can even export your labeled selections as an subtitle file in .SRT format.

One notable missing feature that I’d eventually like to have is the ability to record from a microphone, but that’s somewhat tricky and browser-dependent at the moment. I’d also like to be able to mix all the tracks in the session into a single .WAV, and of course just improve the experience overall.

If you’re interested, you can test the application by using it to load a pre-prepared session file that will hopefully be made available at http://thiscouldbebetter.neocities.org/JFK-We_Choose.json. Click the “Load Session” button, select the session file in the load dialog, then click the “Play Tagged Selections” button to hear an amateurishly remixed version of John F. Kennedy’s “We Choose to Go to the Moon” speech, in which he says the opposite of that.

It’s good clean fun!

SoundEditor


<html>
<body>
<script type="text/javascript">

// main

function main()
{
	var soundEditor = new SoundEditor
	(
		new Coords(600, 200), // viewSizeInPixels
		null// sessionToEdit
	);

	Globals.Instance.initialize
	(
		soundEditor
	);
}

// extensions

Array.prototype.addLookups = function(propertyName)
{
	for (var i = 0; i < this.length; i++)
	{
		var element = this[i];
		var propertyValue = element[propertyName];
		this[propertyValue] = element;
	}
}

// constants

function Constants()
{
	// static class
}
{
	Constants.BitsPerByte = 8;
	Constants.BitsPerByteTimesTwo = Constants.BitsPerByte * 2;
	Constants.BitsPerByteTimesThree = Constants.BitsPerByte * 3;
	Constants.Newline = "\r\n";

}

// classes

function Base64Encoder()
{
	// do nothing
}
{
	// static methods

	Base64Encoder.encodeBytes = function(bytesToEncode)
	{
		// Encode each three sets of eight bits (octets, or bytes)
		// as four sets of six bits (sextets, or Base 64 digits)

		var returnString = "";

		var bytesPerSet = 3;
		var base64DigitsPerSet = 4;

		var base64DigitsAsString = 
			"ABCDEFGHIJKLMNOPQRSTUVWXYZ" 
			+ "abcdefghijklmnopqrstuvwxyz"
			+ "0123456789"
			+ "+/";

		var numberOfBytesToEncode = bytesToEncode.length;
		var numberOfFullSets = Math.floor(numberOfBytesToEncode / bytesPerSet);
		var numberOfBytesInFullSets = numberOfFullSets * bytesPerSet;
		var numberOfBytesLeftAtEnd = numberOfBytesToEncode - numberOfBytesInFullSets;

		for (var s = 0; s < numberOfFullSets; s++)
		{
			var b = s * bytesPerSet;

			var valueToEncode = 
				(bytesToEncode[b] << Constants.BitsPerByteTimesTwo)
				| (bytesToEncode[b + 1] << Constants.BitsPerByte)
				| (bytesToEncode[b + 2]);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += base64DigitsAsString[((valueToEncode & 0x00003F))];
		}	

		var b = numberOfFullSets * bytesPerSet;

		if (numberOfBytesLeftAtEnd == 1)
		{
			var valueToEncode = (bytesToEncode[b] << 16);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += "==";
		}		
		else if (numberOfBytesLeftAtEnd == 2)
		{
			var valueToEncode = 
				(bytesToEncode[b] << 16)
				| (bytesToEncode[b + 1] << 8);

			returnString += base64DigitsAsString[((valueToEncode & 0xFC0000) >>> 18)];
			returnString += base64DigitsAsString[((valueToEncode & 0x03F000) >>> 12)];
			returnString += base64DigitsAsString[((valueToEncode & 0x000FC0) >>> 6)];
			returnString += "=";
		}

		return returnString;
	}
}

function ByteConverter(numberOfBits)
{	
	this.numberOfBits = numberOfBits;
	this.numberOfBytes = Math.floor(this.numberOfBits / 8);

	this.maxValueSigned = 
		(1 << (numberOfBits - 1)) - 1;	

	this.maxValueUnsigned = 
		(1 << (numberOfBits));
}
{
	ByteConverter.prototype.bytesToFloat = function(bytes)
	{
		var bytesAsInteger = this.bytesToInteger(bytes);

		var returnValue = this.integerToFloat(bytesAsInteger);


		return returnValue;
	}

	ByteConverter.prototype.bytesToInteger = function(bytes)
	{
		var returnValue = 0;

		var numberOfBytes = bytes.length;

		for (var i = 0; i < numberOfBytes; i++)
		{
			returnValue |= bytes[i] << (i * Constants.BitsPerByte);
		}

		if (returnValue > this.maxValueSigned)
		{
			returnValue -= this.maxValueUnsigned;
		}

		return returnValue;
	}

	ByteConverter.prototype.floatToInteger = function(float)
	{
		return float * this.maxValueSigned;
	}

	ByteConverter.prototype.integerToBytes = function(integer)
	{
		var returnValues = [];

		for (var i = 0; i < this.numberOfBytes; i++)
		{
			var byteValue = (integer >> (8 * i)) & 0xFF;
			returnValues.push(byteValue);
		}

		return returnValues;
	}

	ByteConverter.prototype.integerToFloat = function(integer)
	{
		var returnValue = 
			integer / this.maxValueSigned;

		return returnValue;
	}
}

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

	this.numberOfBytesTotal = this.bytes.length;
	this.byteIndexCurrent = 0;
}
{
	ByteStreamLittleEndian.prototype.hasMoreBytes = function()
	{
		return (this.byteIndexCurrent < this.numberOfBytesTotal);
	}

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

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

		return returnValue;
	}

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

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

		return returnValue;
	}

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

		this.byteIndexCurrent++;

		return returnValue;
	}

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

		return returnValue;
	}

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

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.readString = function(numberOfBytesToRead)
	{
		var returnValue = "";

		for (var b = 0; b < numberOfBytesToRead; b++)
		{
			var charAsByte = this.readByte();
			returnValue += String.fromCharCode(charAsByte);
		}

		return returnValue;
	}

	ByteStreamLittleEndian.prototype.writeBytes = function(bytesToWrite)
	{
		for (var b = 0; b < bytesToWrite.length; b++)
		{
			this.bytes.push(bytesToWrite[b]);
		}

		this.byteIndexCurrent = this.bytes.length;
	}

	ByteStreamLittleEndian.prototype.writeByte = function(byteToWrite)
	{
		this.bytes.push(byteToWrite);

		this.byteIndexCurrent++;
	}

	ByteStreamLittleEndian.prototype.writeInt = function(integerToWrite)
	{
		this.bytes.push( (integerToWrite & 0x000000FF) );
		this.bytes.push( (integerToWrite & 0x0000FF00) >>> 8 );
		this.bytes.push( (integerToWrite & 0x00FF0000) >>> 16 );
		this.bytes.push( (integerToWrite & 0xFF000000) >>> 24 );

		this.byteIndexCurrent += 4;
	}

	ByteStreamLittleEndian.prototype.writeShort = function(shortToWrite)
	{
		this.bytes.push( (shortToWrite & 0x00FF) );
		this.bytes.push( (shortToWrite & 0xFF00) >>> 8 );

		this.byteIndexCurrent += 2;
	}

	ByteStreamLittleEndian.prototype.writeString = function(stringToWrite)
	{
		for (var i = 0; i < stringToWrite.length; i++)
		{
			this.writeByte(stringToWrite.charCodeAt(i));
		}
	}
}

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

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

		return this;
	}
}

function FileHelper()
{
	// static class
}
{
	FileHelper.destroyClickedElement = function(event)
	{
		event.target.parentElement.removeChild(event.target);
	}

	FileHelper.loadFileAsText = function(fileToLoad, callback)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(fileLoadedEvent) 
		{
			var textFromFileLoaded = fileLoadedEvent.target.result;
			callback(textFromFileLoaded);
		};
		fileReader.readAsText(fileToLoad, "UTF-8");
	}

	FileHelper.saveBytesToFile = function(bytesToSave, filenameToSaveTo)
	{
		var numberOfBytes = bytesToSave.length;
		var bytesAsArrayBuffer = new ArrayBuffer(numberOfBytes);
		var bytesAsUIntArray = new Uint8Array(bytesAsArrayBuffer);
		for (var i = 0; i < numberOfBytes; i++) 
		{
			bytesAsUIntArray[i] = bytesToSave[i];
		}

		var bytesAsBlob = new Blob
		(
			[ bytesAsArrayBuffer ], 
			{type:'application/type'}
		);

		var downloadLink = document.createElement("a");
		var url = (window.webkitURL != null ? window.webkitURL : window.URL);
		downloadLink.href = url.createObjectURL(bytesAsBlob);
		downloadLink.download = filenameToSaveTo;
		downloadLink.click();

	}

	FileHelper.saveTextAsFile = function(textToWrite, fileNameToSaveAs)
	{
		var textFileAsBlob = new Blob([textToWrite], {type:'text/plain'});

		var downloadLink = document.createElement("a");
		downloadLink.download = fileNameToSaveAs;
		downloadLink.innerHTML = "Download File";
		if (window.webkitURL != null)
		{
			// Chrome allows the link to be clicked
			// without actually adding it to the DOM.
			downloadLink.href = window.webkitURL.createObjectURL(textFileAsBlob);
		}
		else
		{
			// Firefox requires the link to be added to the DOM
			// before it can be clicked.
			downloadLink.href = window.URL.createObjectURL(textFileAsBlob);
			downloadLink.onclick = FileHelper.destroyClickedElement;
			downloadLink.style.display = "none";
			document.body.appendChild(downloadLink);
		}
	
		downloadLink.click();
	}
}

function Filter(name, applyToSampleAtTimeWithParameters)
{
	this.name = name;
	this.applyToSampleAtTimeWithParameters = applyToSampleAtTimeWithParameters;
}
{
	Filter.Instances = new Filter_Instances();

	function Filter_Instances()
	{
		this.Amplify = new Filter
		(
			"Amplify", 
			function(sample, timeInSeconds, parameters) 
			{
				var amplificationFactor = parseFloat(parameters);
				if (isNaN(amplificationFactor) == true)
				{
					return sample;
				}
				return sample * amplificationFactor;
			}
		);

		this.Silence = new Filter
		(
			"Silence", 
			function(sample, timeInSeconds, parameters) 
			{
				return 0;
			}
		);

		this.Sine = new Filter
		(
			"Sine", 
			function(sample, timeInSeconds, parameters) 
			{
				var cyclesPerSecond = parseFloat(parameters);
				if (isNaN(cyclesPerSecond) == true)
				{
					return sample;
				}		
				var timeInCycles = timeInSeconds * cyclesPerSecond;
				var amplitude = .5;
				sample = Math.sin(timeInCycles * Math.PI * 2) * amplitude;
				return sample;
			}
		);


		this._All = 
		[
			this.Amplify,
			this.Silence,
			this.Sine,
		];

		this._All.addLookups("name");
	}
}

function Globals()
{}
{
	Globals.Instance = new Globals();

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

		this.soundEditor.domElementUpdate();
		
		this.inputHelper = new InputHelper();
		this.inputHelper.initialize();
	}
}

function InputHelper()
{
	
}
{
	InputHelper.MouseDragDelayInMilliseconds = 100;

	InputHelper.prototype.initialize = function()
	{
		document.body.onmousedown = this.handleEventMouseDown.bind(this);
		document.body.onmousemove = this.handleEventMouseMove.bind(this);
		document.body.onmouseup = this.handleEventMouseUp.bind(this);
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyEventPrev = event;

		var entity = Globals.Instance.soundEditor; // hack
		if (entity != null && entity.handleEventKeyDown != null)
		{
			entity.handleEventKeyDown(event);
		}
	}

	InputHelper.prototype.handleEventMouseDown = function(event)
	{
		this.mouseButtonPressed = true;
		this.mouseClickTime = new Date();

		var entity = event.srcElement.entity;
		if (entity != null && entity.handleEventMouseDown != null)
		{
			entity.handleEventMouseDown(event);
		}
	}

	InputHelper.prototype.handleEventMouseMove = function(event)
	{
		if (this.mouseButtonPressed == true)
		{
			var now = new Date();
			var millisecondsElapsedSinceClick = now - this.mouseClickTime;
			if (millisecondsElapsedSinceClick >= InputHelper.MouseDragDelayInMilliseconds)
			{
				var entity = event.srcElement.entity;
				if (entity != null && entity.handleEventMouseMove != null)
				{
					entity.handleEventMouseMove(event);
				}
			}
		}
	}

	InputHelper.prototype.handleEventMouseUp = function(event)
	{
		this.mouseButtonPressed = false;

		if (event.srcElement.handleEventMouseUp != null)
		{
			event.srcElement.handleEventMouseUp(event);
		}

		var entity = event.srcElement.entity;
		if (entity != null && entity.handleEventMouseUp != null)
		{
			entity.handleEventMouseUp(event);
		}
	}
}

function NumberHelper()
{
	// static class
}
{
	NumberHelper.trimValueToRange = function(value, range)
	{
		if (value < 0)
		{
			value = 0;
		}
		else if (value >= range)
		{
			value = range;
		}

		return value;
	}
}

function Selection(tag, timesStartAndEndInSeconds)
{
	this.tag = tag;
	this.timesStartAndEndInSeconds = timesStartAndEndInSeconds;
}
{
	// static methods

	Selection.buildFromStringSRT = function(selectionAsStringSRT)
	{
		var newline = Constants.Newline;

		var selectionAsLines = selectionAsStringSRT.split(newline);

		var startAndEndTimeAsString = selectionAsLines[1];
		var startAndEndTimesAsStrings = startAndEndTimeAsString.split
		(
			" --> "
		);

		var startAndEndTimesInSeconds = [];
		for (var i = 0; i < startAndEndTimesAsStrings.length; i++)
		{
			var timeAsString = startAndEndTimesAsStrings[i];
			var timeComponentsHMS = timeAsString.split(":");
		
			// todo - handle minutes and hours

			var timeComponentSeconds = timeComponentsHMS[2];
			var secondsAndMillisecondsAsStrings = timeComponentSeconds.split(","); // French radix point
			var secondsAsString = secondsAndMillisecondsAsStrings[0];
			var millisecondsAsString = secondsAndMillisecondsAsStrings[1];
			var seconds = parseInt(secondsAsString);
			var milliseconds = parseInt(millisecondsAsString);
			
			var timeInSeconds = seconds + (milliseconds / 1000);

			startAndEndTimesInSeconds.push(timeInSeconds);
		}

		var tag = selectionAsLines[2];

		var returnValue = new Selection
		(
			tag,
			startAndEndTimesInSeconds
		);

		return returnValue;
	}

	Selection.buildManyFromStringSRT = function(selectionsAsStringSRT)
	{
		var returnValues = [];

		var newline = Constants.Newline;

		var selectionsAsStringsSRT = selectionsAsStringSRT.split
		(
			newline + newline
		);

		for (var i = 0; i < selectionsAsStringsSRT.length; i++)
		{
			var selectionAsStringSRT = selectionsAsStringsSRT[i];
			if (selectionAsStringSRT.length > 0)
			{
				var selection = Selection.buildFromStringSRT
				(
					selectionAsStringSRT
				);
				returnValues.push(selection);
				returnValues[selection.tag] = selection;
			}
		}

		return returnValues;
	}

	Selection.convertManyToStringSRT = function(selections)
	{
		var returnValue = "";

		var newline = Constants.Newline;

		for (var i = 0; i < selections.length; i++)
		{
			var selection = selections[i];

			var selectionAsString = selection.toStringSRT(i);

			returnValue += selectionAsString + newline + newline;
		}

		return returnValue;
	}

	// instance methods

	Selection.prototype.overlapsWith = function(other)
	{
		return false; // todo
	}

	Selection.prototype.rectify = function()
	{
		var sampleIndexStart = this.timesStartAndEndInSeconds[0];
		var sampleIndexEnd = this.timesStartAndEndInSeconds[1];

		if (sampleIndexStart > sampleIndexEnd)
		{
			this.timesStartAndEndInSeconds[0] = sampleIndexEnd;
			this.timesStartAndEndInSeconds[1] = sampleIndexStart;
		}
	}

	Selection.prototype.durationInSeconds = function()
	{
		var timeStartInSeconds = this.timesStartAndEndInSeconds[0];
		var timeEndInSeconds = this.timesStartAndEndInSeconds[1];
		var returnValue = timeEndInSeconds - timeStartInSeconds;
		return returnValue;
	}

	Selection.prototype.toString = function()
	{
		var timeStartAsString = TimeHelper.secondsToStringSecondsMilliseconds
		(
			this.timesStartAndEndInSeconds[0]
		);

		var timeEndAsString = TimeHelper.secondsToStringSecondsMilliseconds
		(
			this.timesStartAndEndInSeconds[1]
		);

		var returnValue = 
			timeStartAsString + "-" + timeEndAsString
			+ " " + this.tag;

		return returnValue;
	}

	Selection.prototype.toStringSRT = function(index)
	{
		// SubRip subtitle format

		var indexAsString = "" + (index + 1);

		var newline = Constants.Newline;

		var timeStartAsString = TimeHelper.secondsToStringHHMMSSmmm
		(
			this.timesStartAndEndInSeconds[0]
		);

		var timeEndAsString = TimeHelper.secondsToStringHHMMSSmmm
		(
			this.timesStartAndEndInSeconds[1]
		);

		var returnValue = 
			indexAsString + newline
			+ timeStartAsString
			+ " --> " 
			+ timeEndAsString + newline
			+ this.tag;

		return returnValue;
	}
}

function Serializer(knownTypes)
{
	this.knownTypes = knownTypes;

	for (var i = 0; i < this.knownTypes.length; i++)
	{
		var knownType = this.knownTypes[i];
		this.knownTypes[knownType.name] = knownType;
	}
}
{
	Serializer.prototype.deleteClassNameRecursively = function(objectToDeleteClassNameOn)
	{
		var className = objectToDeleteClassNameOn.constructor.name;
		if (this.knownTypes[className] != null)
		{
			delete objectToDeleteClassNameOn.className;

			for (var childPropertyName in objectToDeleteClassNameOn)
			{
				var childProperty = objectToDeleteClassNameOn[childPropertyName];
				this.deleteClassNameRecursively(childProperty);
			}
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToDeleteClassNameOn.length; i++)
			{
				var element = objectToDeleteClassNameOn[i];
				this.deleteClassNameRecursively(element);
			}
		}
	}

	Serializer.prototype.deserialize = function(stringToDeserialize)
	{
		var objectDeserialized = JSON.parse(stringToDeserialize);

		this.setPrototypeRecursively(objectDeserialized);

		this.deleteClassNameRecursively(objectDeserialized);

		return objectDeserialized;
	}

	Serializer.prototype.serialize = function(objectToSerialize)
	{
		this.setClassNameRecursively(objectToSerialize);

		var returnValue = JSON.stringify(objectToSerialize);

		this.deleteClassNameRecursively(objectToSerialize);

		return returnValue;
	}

	Serializer.prototype.setClassNameRecursively = function(objectToSetClassNameOn)
	{
		var className = objectToSetClassNameOn.constructor.name;
		
		if (this.knownTypes[className] != null)
		{
			for (var childPropertyName in objectToSetClassNameOn)
			{
				var childProperty = objectToSetClassNameOn[childPropertyName];
				this.setClassNameRecursively(childProperty);
			}

			objectToSetClassNameOn.className = className;
		}
		else if (className == "Array")
		{
			for (var i = 0; i < objectToSetClassNameOn.length; i++)
			{
				var element = objectToSetClassNameOn[i];
				this.setClassNameRecursively(element);
			}
		}
	}

	Serializer.prototype.setPrototypeRecursively = function(objectToSetPrototypeOn)
	{
		var className = objectToSetPrototypeOn.className;
		var typeOfObjectToSetPrototypeOn = this.knownTypes[className];

		if (typeOfObjectToSetPrototypeOn != null)
		{
			objectToSetPrototypeOn.__proto__ = typeOfObjectToSetPrototypeOn.prototype;
	
			for (var childPropertyName in objectToSetPrototypeOn)
			{
				var childProperty = objectToSetPrototypeOn[childPropertyName];
				if (childProperty.constructor.name == "Array")
				{
					for (var i = 0; i < childProperty.length; i++)
					{
						var element = childProperty[i];
						this.setPrototypeRecursively(element);
					}
				}
				else
				{
					this.setPrototypeRecursively(childProperty);
				}
			}
		}
	}
}

function Session(name, tagsToPlay, tracks, selectionsTagged)
{
	this.name = name;
	this.tagsToPlay = tagsToPlay;
	this.tracks = tracks;
	this.selectionsTagged = selectionsTagged;

	this.trackIndexCurrent = 0;
}
{
	Session.prototype.addLookups = function()
	{
		this.selectionsTagged.addLookups("tag");
	}

	Session.prototype.durationInSeconds = function()
	{
		var trackEndInSecondsMax = 0;

		for (var i = 0; i < this.tracks.length; i++)
		{
			var track = this.tracks[i];
			var trackEndInSeconds = track.durationInSeconds();
			if (trackEndInSeconds > trackEndInSecondsMax)
			{
				trackEndInSecondsMax = trackEndInSeconds;
			}
		}	

		return trackEndInSecondsMax;
	}

	Session.prototype.trackCurrent = function(valueToSet)
	{
		if (valueToSet != null)
		{
			this.trackIndexCurrent = this.tracks.indexOf(valueToSet);
		}

		return this.tracks[this.trackIndexCurrent];
	}

	// dom 

	Session.prototype.domElementRemove = function()
	{
		for (var t = 0; t < this.tracks.length; t++)
		{
			var track = this.tracks[t];
			track.domElementRemove();
		}
	}
}

function Sound(name, offsetWithinTrackInSeconds, sourceWavFile)
{
	this.name = name;
	this.offsetWithinTrackInSeconds = offsetWithinTrackInSeconds;
	this.sourceWavFile = sourceWavFile;
}
{
	// instance methods

	Sound.prototype.durationInSeconds = function()
	{
		return this.sourceWavFile.durationInSeconds();
	}
}

function SoundEditor
(
	viewSizeInPixels, 
	session
)
{
	this.viewSizeInPixels = viewSizeInPixels;
	this.session = session;

	if (this.session == null)
	{
		this.sessionNew();

	}

	this.viewSizeInPixelsHalf = this.viewSizeInPixels.clone().divideScalar(2);

	this.selectionCurrent = null;
	this.cursorOffsetInSeconds = 0;
	this.viewZoomToFit();

}
{
	// constants

	SoundEditor.ColorViewBackground = "White";
	SoundEditor.ColorViewBaseline = "Black";
	SoundEditor.ColorViewBorder = "Gray";
	SoundEditor.ColorViewCursor = "Black";
	SoundEditor.ColorViewSelectionBorder = "Gray";
	SoundEditor.ColorViewSelectionFill = "rgba(0, 0, 0, .05)";
	SoundEditor.ColorViewText = "Black";
	SoundEditor.ColorViewWaveform = "Gray";

	SoundEditor.TextHeightInPixels = 10;

	// instance methods

	SoundEditor.prototype.cursorMove = function
	(
		shouldAdjustmentBeSmall,
		direction
	)
	{
		var distanceToMoveInSeconds = this.distancePerAdjustmentInSeconds
		(
			shouldAdjustmentBeSmall
		);

		var displacementInSeconds = distanceToMoveInSeconds * direction;

		this.cursorOffsetInSeconds = NumberHelper.trimValueToRange
		(
			this.cursorOffsetInSeconds + displacementInSeconds,
			this.session.durationInSeconds()
		);

		this.domElementUpdate();
	}

	SoundEditor.prototype.distancePerAdjustmentInSeconds = function(shouldAdjustmentBeSmall)
	{
		var distanceToMoveInSeconds;

		if (shouldAdjustmentBeSmall == true)
		{
			distanceToMoveInSeconds = .001;
		}
		else
		{
			distanceToMoveInSeconds = this.viewSecondsPerPixel();
		}

		return distanceToMoveInSeconds;
	}

	SoundEditor.prototype.filterSelection = function()
	{
		var track = this.session.trackCurrent();

		if (track == null || this.selectionCurrent == null)
		{
			alert("Nothing to filter!");
			return;
		}

		var filterSelected = this.selectFilterType.selectedOptions[0].entity;
		var parametersForFilter = this.inputFilterParameters.value;
		
		var sound = track.sounds[0];

		var soundSource = sound.sourceWavFile;
		var samplingInfo = soundSource.samplingInfo;
		var samplesPerSecond = samplingInfo.samplesPerSecond;

		var timesStartAndEndInSeconds = this.selectionCurrent.timesStartAndEndInSeconds;

		var timeStartInSeconds = timesStartAndEndInSeconds[0];
		var timeEndInSeconds = timesStartAndEndInSeconds[1];

		var timeStartInSamples = Math.round(samplesPerSecond * timeStartInSeconds);
		var timeEndInSamples = Math.round(samplesPerSecond * timeEndInSeconds);
		var durationInSamples = timeEndInSamples - timeStartInSamples;

		var samplesForChannels = soundSource.samplesForChannels;

		var byteConverter = new ByteConverter(samplingInfo.bitsPerSample);

		for (var c = 0; c < samplesForChannels.length; c++)
		{
			var samplesForChannel = samplesForChannels[c];

			for (var s = 0; s < durationInSamples; s++)
			{
				var sampleIndex = timeStartInSamples + s;
				var sample = samplesForChannel[sampleIndex];

				var timeInSeconds = s / samplesPerSecond;

				var sampleAsFloat = byteConverter.integerToFloat(sample);

				sampleAsFloat = filterSelected.applyToSampleAtTimeWithParameters
				(
					sampleAsFloat,
					timeInSeconds,
					parametersForFilter
				);

				var sampleAsFloatAbsolute = Math.abs(sampleAsFloat);
				if (sampleAsFloatAbsolute > 1)
				{
					sampleAsFloat /= sampleAsFloatAbsolute;	
				}

				sample = byteConverter.floatToInteger(sampleAsFloat);
				samplesForChannel[sampleIndex] = sample;
			}
		}

		this.domElementUpdate();
	}	

	SoundEditor.prototype.play = function()
	{
		if (this.domElementAudio != null)
		{
			return;
		}

		var track = this.session.trackCurrent();

		if (track == null)
		{
			alert("Nothing to play!");
			return;
		}

		var soundToPlay = track.sounds[0];

		if (this.selectionCurrent != null)
		{
			var soundSource = soundToPlay.sourceWavFile;

			var timesStartAndEndInSeconds = this.selectionCurrent.timesStartAndEndInSeconds;

			var numberOfChannels = soundSource.samplesForChannels.length;
			var samplesForChannels = [];
			for (var i = 0; i < numberOfChannels; i++)
			{
				samplesForChannels.push([]);	
			}

			selectionAsWavFile = new WavFile
			(
				soundSource.filePath,
				soundSource.samplingInfo,
				samplesForChannels
			);

			selectionAsWavFile.appendClipFromWavFileBetweenTimesStartAndEnd
			(
				soundSource,
				timesStartAndEndInSeconds[0],
				timesStartAndEndInSeconds[1]
			);

			soundToPlay = new Sound
			(
				"[current selection]",
				0, // offsetWithinTrackInSeconds, 
				selectionAsWavFile
			);
		}


		this.playSound(soundToPlay);
	}

	SoundEditor.prototype.play_PlaybackComplete = function()
	{
		this.domElementAudio = null;
	}

	SoundEditor.prototype.playSound = function(soundToPlay)
	{
		var soundAsWavFile = soundToPlay.sourceWavFile;

		var soundAsBytes = soundAsWavFile.writeToBytes();

		var soundAsStringBase64 = Base64Encoder.encodeBytes(soundAsBytes);

		var soundAsDataURI = 'data:audio/wav;base64,' + soundAsStringBase64;

		var domElementSoundSource = document.createElement("source");
		domElementSoundSource.src = soundAsDataURI;

		var domElementAudio = document.createElement("audio");
		domElementAudio.autoplay = "autoplay";
		domElementAudio.onended = this.play_PlaybackComplete.bind(this);

		domElementAudio.appendChild(domElementSoundSource);

		document.body.appendChild(domElementAudio);
		this.domElementAudio = domElementAudio;
	}

	SoundEditor.prototype.record = function()
	{
		alert("Not yet implemented!");
	}

	SoundEditor.prototype.stop = function()
	{
		if (this.domElementAudio != null)
		{
			this.domElementAudio.parentElement.removeChild
			(
				this.domElementAudio
			);
			this.domElementAudio = null;
		}
	}

	SoundEditor.prototype.selectAll = function()
	{
		this.selectionCurrent = new Selection
		(
			null, // tag
			[0, this.session.durationInSeconds()]
		);
		this.viewZoomToFit();
	}

	SoundEditor.prototype.selectNone = function()
	{
		this.selectionCurrent = null;
		this.domElementUpdate();
	}

	SoundEditor.prototype.selectionRemoveByTag = function()
	{
		var tagToRemove = this.inputTagText.value;

		var session = this.session;
		var selectionsTagged = session.selectionsTagged;

		for (var i = 0; i < selectionsTagged.length; i++)
		{
			var selectionExisting = selectionsTagged[i];
			if (selectionExisting.tag == tagToRemove)
			{
				selectionsTagged.splice
				(
					selectionsTagged.indexOf(selectionExisting),
					1
				);
				delete selectionsTagged[tagToRemove];
				this.domElementUpdate();
				break;
			}
		}		
	}

	SoundEditor.prototype.selectionResize = function
	(
		shouldAdjustmentBeSmall,
		direction
	)
	{
		if (this.selectionCurrent == null)
		{
			this.selectionCurrent = new Selection
			(
				null, // tag
				[
					this.cursorOffsetInSeconds,
					this.cursorOffsetInSeconds,
				]
			);
		}

		var amountToResizeInSeconds = this.distancePerAdjustmentInSeconds
		(
			shouldAdjustmentBeSmall
		);

		var displacementInSeconds = amountToResizeInSeconds * direction;

		var timeIndexStartOrEnd = (direction < 0 ? 0 : 1);
		var timesStartAndEndInSeconds = this.selectionCurrent.timesStartAndEndInSeconds;

		timesStartAndEndInSeconds[timeIndexStartOrEnd] = NumberHelper.trimValueToRange
		(
			timesStartAndEndInSeconds[timeIndexStartOrEnd] + displacementInSeconds,
			this.session.durationInSeconds()
		);

		this.domElementUpdate();
	}

	SoundEditor.prototype.selectionSelectByTag = function()
	{
		var tagToSelect = this.inputTagText.value;

		var session = this.session;
		var selectionsTagged = session.selectionsTagged;

		for (var i = 0; i < selectionsTagged.length; i++)
		{
			var selectionExisting = selectionsTagged[i];
			if (selectionExisting.tag == tagToSelect)
			{
				this.selectionCurrent = selectionExisting;
				this.domElementUpdate();
				break;
			}
		}		
	}

	SoundEditor.prototype.selectionTag = function()
	{
		if (this.selectionCurrent != null)
		{
			var session = this.session;
			var selectionsTagged = session.selectionsTagged;

			var doesSelectionCurrentOverlapWithAnyExisting = false;
			for (var i = 0; i < selectionsTagged.length; i++)
			{
				var selectionExisting = selectionsTagged[i];
				if (this.selectionCurrent.overlapsWith(selectionExisting) == true)
				{
					doesSelectionCurrentOverlapWithAnyExisting = true;
					break;
				}
			}

			if (doesSelectionCurrentOverlapWithAnyExisting == false)
			{
				var tagText = this.inputTagText.value;
				this.selectionCurrent.tag = tagText;			
				selectionsTagged.push(this.selectionCurrent);
				selectionsTagged[this.selectionCurrent.tag] = this.selectionCurrent;
			}

			this.selectionCurrent = null;

			this.domElementUpdate();
		}
	}

	SoundEditor.prototype.selectionsTaggedExport = function()
	{
		var fileContents = Selection.convertManyToStringSRT
		(
			this.session.selectionsTagged
		);

		FileHelper.saveTextAsFile(fileContents, this.session.name + ".srt");
	}

	SoundEditor.prototype.selectionsTaggedImport = function()
	{
		var inputFileToLoad = document.createElement("input");
		inputFileToLoad.type = "file";
		var callback = this.selectionsTaggedImport_LoadComplete.bind(this);
		inputFileToLoad.onchange = function(event)
		{
			var srcElement = event.srcElement;
			var fileToLoad = srcElement.files[0];
			srcElement.parentElement.removeChild(srcElement);

			FileHelper.loadFileAsText(fileToLoad, callback);
		}	

		this.divControlsFile.insertBefore
		(
			inputFileToLoad,
			this.buttonTagsImport.nextSibling
		);
	}

	SoundEditor.prototype.selectionsTaggedImport_LoadComplete = function(textFromFile)
	{
		var selections = Selection.buildManyFromStringSRT
		(
			textFromFile
		);

		this.session.selectionsTagged = selections;

		this.domElementUpdate();
	}

	SoundEditor.prototype.serializer = function()
	{
		return new Serializer
		([
			Selection,
			Session,
			Sound,
			Track,
			WavFile,
			WavFileSamplingInfo,
		]);
	}

	SoundEditor.prototype.sessionLoad = function()
	{
		var inputFileToLoad = document.createElement("input");
		inputFileToLoad.type = "file";
		var callback = this.sessionLoad_LoadComplete.bind(this);
		inputFileToLoad.onchange = function(event)
		{
			var srcElement = event.srcElement;
			var fileToLoad = srcElement.files[0];
			srcElement.parentElement.removeChild(srcElement);

			FileHelper.loadFileAsText(fileToLoad, callback);
		}	

		this.divControlsFile.insertBefore
		(
			inputFileToLoad,
			this.buttonSessionLoad.nextSibling
		);
	}

	SoundEditor.prototype.sessionLoad_LoadComplete = function(textFromFile)
	{
		var serializer = this.serializer();

		this.domElementRemove();

		this.session = serializer.deserialize(textFromFile);

		this.session.addLookups();

		this.viewZoomToFit();

		this.domElementUpdate();
	}

	SoundEditor.prototype.sessionNew = function()
	{
		this.session = new Session
		(
			"[untitled]",
			"", // tagsToPlay
			[], // tracks
			[] // selections
		);

		this.domElementRemove();

		this.domElementUpdate();
	}

	SoundEditor.prototype.sessionSave = function()
	{
		var serializer = this.serializer();

		this.domElementRemove();

		var sessionAsJSON = serializer.serialize(this.session);

		FileHelper.saveTextAsFile(sessionAsJSON, this.session.name + ".json");

		this.domElementUpdate();
	}

	SoundEditor.prototype.tagsExportAsSound = function()
	{
		var track = this.session.trackCurrent();

		if (track == null)
		{
			alert("Nothing to export!");
			return;
		}

		var soundToSelectFrom = track.sounds[0];

		var soundToExport = this.tagsPlay_BuildSound(soundToSelectFrom);
		var wavFileToExport = soundToExport.sourceWavFile;
		var soundToExportAsBytes = wavFileToExport.writeToBytes();

		var filename = this.session.tagsToPlay + ".wav";
		
		FileHelper.saveBytesToFile
		(
			soundToExportAsBytes, 
			filename	
		);
	}

	SoundEditor.prototype.tagsPlay = function()
	{
		var track = this.session.trackCurrent();

		if (track == null)
		{
			alert("Nothing to play!");
			return;
		}

		var soundToSelectFrom = track.sounds[0];

		var soundToPlay = this.tagsPlay_BuildSound(soundToSelectFrom);

		this.playSound(soundToPlay);
	}

	SoundEditor.prototype.tagsPlay_BuildSound = function(soundToSelectFrom)
	{
		var soundAsWavFileSource = soundToSelectFrom.sourceWavFile;
		
		var numberOfChannels = soundAsWavFileSource.samplesForChannels.length;
		var samplesForChannels = [];
		for (var i = 0; i < numberOfChannels; i++)
		{
			samplesForChannels.push([]);	
		}

		var soundAsWavFileTarget = new WavFile
		(
			soundAsWavFileSource.filePath,
			soundAsWavFileSource.samplingInfo,
			samplesForChannels
		);

		var tagsToPlayAsString = this.session.tagsToPlay; // this.inputTagsToPlay.value;
		var tagsToPlayAsStrings = tagsToPlayAsString.split(" ");

		for (var t = 0; t < tagsToPlayAsStrings.length; t++)
		{
			var tagAsString = tagsToPlayAsStrings[t];
			var tag = this.session.selectionsTagged[tagAsString];

			if (tag != null)
			{
				var timesStartAndEndInSeconds = tag.timesStartAndEndInSeconds;
	
				soundAsWavFileTarget.appendClipFromWavFileBetweenTimesStartAndEnd
				(
					soundAsWavFileSource,
					timesStartAndEndInSeconds[0],
					timesStartAndEndInSeconds[1]
				);
			}
		}

		var returnValue = new Sound
		(
			"[tags]",
			0, // offsetFromTrackStartInSeconds
			soundAsWavFileTarget
		);

		return returnValue;
	}

	SoundEditor.prototype.trackAdd = function()
	{
		var inputFileToLoad = document.createElement("input");
		inputFileToLoad.type = "file";
		var callback = this.trackAdd_LoadComplete.bind(this);
		inputFileToLoad.onchange = function(event)
		{
			var srcElement = event.srcElement;
			var fileToLoad = srcElement.files[0];
			var wavFileLoaded = WavFile.readFromFile
			(
				fileToLoad,
				callback
			);

			srcElement.parentElement.removeChild(srcElement);
		}	

		this.divControlsFile.insertBefore
		(
			inputFileToLoad,
			this.buttonTrackAdd.nextSibling
		);	
	}

	SoundEditor.prototype.trackAdd_LoadComplete = function(wavFileLoaded)
	{
		var sound = new Sound
		(
			wavFileLoaded.filePath,
			0, // offsetWithinTrackInSeconds
			wavFileLoaded
		);

		var track = new Track
		(
			wavFileLoaded.filePath, 
			[ sound ]
		);

		this.session.tracks.push(track);

		this.viewZoomToFit();

		this.domElementUpdate();
	}

	SoundEditor.prototype.trackRemove = function()
	{
		alert("Not yet implemented!");
	}

	SoundEditor.prototype.viewSecondsPerPixel = function()
	{
		var returnValue = this.viewWidthInSeconds / this.viewSizeInPixels.x;

		return returnValue;
	}

	SoundEditor.prototype.viewSlide = function(shouldAdjustmentBeSmall, direction)
	{
		var distanceToMoveInSeconds;

		if (shouldAdjustmentBeSmall == true)
		{
			distanceToMoveInSeconds = .001;
		}
		else
		{
			distanceToMoveInSeconds = this.viewSecondsPerPixel();
		}
			
		var displacementInSeconds = 
			distanceToMoveInSeconds * direction;

		this.viewOffsetInSeconds = NumberHelper.trimValueToRange
		(
			this.viewOffsetInSeconds + displacementInSeconds,
			this.session.durationInSeconds() - this.viewWidthInSeconds
		);

		this.domElementUpdate();
	}

	SoundEditor.prototype.viewZoomToFit = function()
	{
		this.viewOffsetInSeconds = 0;
		this.viewWidthInSeconds = this.session.durationInSeconds();
		this.domElementUpdate();
	}

	SoundEditor.prototype.viewZoomToSelection = function()
	{
		if (this.selectionCurrent != null)
		{
			var selectionDurationInSeconds = this.selectionCurrent.durationInSeconds();
			if (selectionDurationInSeconds > 0)
			{
				var timesStartAndEndInSeconds = this.selectionCurrent.timesStartAndEndInSeconds;
				var timeStartInSeconds = timesStartAndEndInSeconds[0];
				var timeEndInSeconds = timesStartAndEndInSeconds[1];

				this.viewOffsetInSeconds = timeStartInSeconds;
				this.viewWidthInSeconds = selectionDurationInSeconds;

				this.selectionCurrent = null;

				this.domElementUpdate();
			}
		}
	}

	// dom

	SoundEditor.prototype.domElementRemove = function()
	{
		if (this.domElement != null)
		{
			this.domElement.parentElement.removeChild(this.domElement);
			delete this.domElement;

			this.session.domElementRemove();
		}
	}

	SoundEditor.prototype.domElementUpdate = function()
	{
		this.domElementUpdate_BuildIfNecessary();
		this.domElementUpdate_Controls();
		this.domElementUpdate_Waveform();
		this.domElementUpdate_Cursor();
		this.domElementUpdate_Selection();

		return this.domElement;	
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary = function()
	{
		if (this.domElement == null)
		{
			var divEditor = document.createElement("div");

			divEditor.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsSessionName()
			);

			var divTracks = this.domElementUpdate_BuildIfNecessary_Tracks();
			divEditor.appendChild(divTracks);
			this.divTracks = divTracks;

			var divControls = document.createElement("div");

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsCursor()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsSelection()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsFilter()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsPlayback()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsComposite()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsZoom()
			);

			divControls.appendChild
			(
				this.domElementUpdate_BuildIfNecessary_ControlsFile()
			);

			divEditor.appendChild(divControls);

			this.divControls = divControls;

			this.domElement = divEditor;

			document.body.appendChild(this.domElement);
		}
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsSessionName = function()
	{
		var divControlsSessionName = document.createElement("div");
		divControlsSessionName.style.border = "1px solid";

		var labelSessionName = document.createElement("label");
		labelSessionName.innerHTML = "Session Name:";
		divControlsSessionName.appendChild(labelSessionName);

		this.inputSessionName = document.createElement("input");
		this.inputSessionName.style.width = 200;
		this.inputSessionName.onchange = this.handleEventInputSessionNameChanged.bind(this);
		divControlsSessionName.appendChild(this.inputSessionName);

		return divControlsSessionName;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_Tracks = function()
	{
		var returnValue = document.createElement("div");

		returnValue.style.border = "1px solid";
		returnValue.style.width = this.viewSizeInPixels.x;

		for (var t = 0; t < this.session.tracks.length; t++)
		{
			var track = this.session.tracks[t];
			var domElementForTrack = track.domElementUpdate(this);
			returnValue.appendChild(domElementForTrack);
		}

		return returnValue;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsComposite = function()
	{
		var divControlsComposite = document.createElement("div");
		divControlsComposite.style.border = "1px solid";

		var labelTagsToPlay = document.createElement("label");
		labelTagsToPlay.innerHTML = "Tags to Play:"
		divControlsComposite.appendChild(labelTagsToPlay);

		this.inputTagsToPlay = document.createElement("input");
		this.inputTagsToPlay.style.width = 256;
		this.inputTagsToPlay.onchange = this.handleEventInputTagsToPlay_Changed.bind(this);
		divControlsComposite.appendChild(this.inputTagsToPlay);

		var buttonPlay = document.createElement("button");
		buttonPlay.innerHTML = "Play Tagged Selections";
		buttonPlay.onclick = this.tagsPlay.bind(this);
		divControlsComposite.appendChild(buttonPlay);

		var buttonExport = document.createElement("button");
		buttonExport.innerHTML = "Export Tagged Selections";
		buttonExport.onclick = this.tagsExportAsSound.bind(this);
		divControlsComposite.appendChild(buttonExport);

		return divControlsComposite;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsCursor = function()
	{
		var divControlsCursor = document.createElement("div");
		divControlsCursor.style.border = "1px solid";

		var labelCursor = document.createElement("label");
		labelCursor.innerHTML = "Cursor:";
		divControlsCursor.appendChild(labelCursor);

		var labelCursorPosInSeconds = document.createElement("label");
		labelCursorPosInSeconds.innerHTML = "Seconds:";
		divControlsCursor.appendChild(labelCursorPosInSeconds);

		this.inputCursorPosInSeconds = document.createElement("input");
		this.inputCursorPosInSeconds.disabled = true; // todo
		this.inputCursorPosInSeconds.style.width = 64;
		this.inputCursorPosInSeconds.type = "number";
		this.inputCursorPosInSeconds.onchange = this.handleEventInputCursorPosInSecondsChanged.bind(this);
		divControlsCursor.appendChild(this.inputCursorPosInSeconds);

		return divControlsCursor;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsFile = function()
	{
		var divControlsFile = document.createElement("div");
		divControlsFile.style.border = "1px solid";

		var buttonTrackAdd = document.createElement("button");
		buttonTrackAdd.innerHTML = "Load Sound as Track";
		buttonTrackAdd.onclick = this.trackAdd.bind(this);
		divControlsFile.appendChild(buttonTrackAdd);
		this.buttonTrackAdd = buttonTrackAdd;

		var buttonSessionNew = document.createElement("button");
		buttonSessionNew.innerHTML = "New Session";
		buttonSessionNew.onclick = this.sessionNew.bind(this);
		divControlsFile.appendChild(buttonSessionNew);

		var buttonSessionLoad = document.createElement("button");
		buttonSessionLoad.innerHTML = "Load Session";
		buttonSessionLoad.onclick = this.sessionLoad.bind(this);
		divControlsFile.appendChild(buttonSessionLoad);
		this.buttonSessionLoad = buttonSessionLoad;

		var buttonSessionSave = document.createElement("button");
		buttonSessionSave.innerHTML = "Save Session";
		buttonSessionSave.onclick = this.sessionSave.bind(this);
		divControlsFile.appendChild(buttonSessionSave);

		var buttonTagsExport = document.createElement("button");
		buttonTagsExport.innerHTML = "Export Tags";
		buttonTagsExport.onclick = this.selectionsTaggedExport.bind(this);
		divControlsFile.appendChild(buttonTagsExport);

		var buttonTagsImport = document.createElement("button");
		buttonTagsImport.innerHTML = "Import Tags";
		buttonTagsImport.onclick = this.selectionsTaggedImport.bind(this);
		divControlsFile.appendChild(buttonTagsImport);
		this.buttonTagsImport = buttonTagsImport;

		this.divControlsFile = divControlsFile;

		return divControlsFile;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsFilter = function()
	{
		var divControlsFilter = document.createElement("div");
		divControlsFilter.style.border = "1px solid";

		var labelFilter = document.createElement("label");
		labelFilter.innerHTML = "Filter:";
		divControlsFilter.appendChild(labelFilter);

		var labelType = document.createElement("label");
		labelType.innerHTML = "Type:";
		divControlsFilter.appendChild(labelType);

		this.selectFilterType = document.createElement("select");
		this.selectFilterType.style.width = 128;
		divControlsFilter.appendChild(this.selectFilterType);

		var filters = Filter.Instances._All;

		for (var i = 0; i < filters.length; i++)
		{
			var filter = filters[i];

			var optionForFilter = document.createElement("option");
			optionForFilter.innerHTML = filter.name;
			optionForFilter.entity = filter;

			this.selectFilterType.appendChild(optionForFilter);
		}

		var labelParameters = document.createElement("label");
		labelParameters.innerHTML = "Parameters:";
		divControlsFilter.appendChild(labelParameters);

		this.inputFilterParameters = document.createElement("input");
		this.inputFilterParameters.style.width = 128;
		divControlsFilter.appendChild(this.inputFilterParameters);

		var buttonFilterSelection = document.createElement("button");
		buttonFilterSelection.innerHTML = "Filter Selection";
		buttonFilterSelection.onclick = this.filterSelection.bind(this);
		divControlsFilter.appendChild(buttonFilterSelection);

		return divControlsFilter;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsPlayback = function()
	{
		var divControlsPlayback = document.createElement("div");
		divControlsPlayback.style.border = "1px solid";

		var buttonPlay = document.createElement("button");
		buttonPlay.innerHTML = "Play";
		buttonPlay.onclick = this.play.bind(this);
		divControlsPlayback.appendChild(buttonPlay);

		var buttonStop = document.createElement("button");
		buttonStop.innerHTML = "Stop";
		buttonStop.onclick = this.stop.bind(this);
		divControlsPlayback.appendChild(buttonStop);

		var buttonRecord = document.createElement("button");
		buttonRecord.innerHTML = "Record";
		buttonRecord.onclick = this.record.bind(this);
		this.buttonRecord = buttonRecord;
		divControlsPlayback.appendChild(buttonRecord);

		this.divControlsPlayback = divControlsPlayback;

		return divControlsPlayback;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsSelection = function()
	{
		var divControlsSelection = document.createElement("div");
		divControlsSelection.style.border = "1px solid";

		var labelSelected = document.createElement("label");
		labelSelected.innerHTML = "Selected:";
		divControlsSelection.appendChild(labelSelected);

		var labelSelectionStartInSeconds = document.createElement("label");
		labelSelectionStartInSeconds.innerHTML = "Seconds:";
		divControlsSelection.appendChild(labelSelectionStartInSeconds);

		this.inputSelectionStartInSeconds = document.createElement("input");
		this.inputSelectionStartInSeconds.disabled = true; // todo
		this.inputSelectionStartInSeconds.style.width = 64;
		this.inputSelectionStartInSeconds.type = "number";
		this.inputSelectionStartInSeconds.onchange = this.handleEventInputSelectionStartInSecondsChanged.bind(this);
		divControlsSelection.appendChild(this.inputSelectionStartInSeconds);

		var labelSelectionEndInSeconds = document.createElement("label");
		labelSelectionEndInSeconds.innerHTML = "to";
		divControlsSelection.appendChild(labelSelectionEndInSeconds);

		this.inputSelectionEndInSeconds = document.createElement("input");
		this.inputSelectionEndInSeconds.disabled = true;
		this.inputSelectionEndInSeconds.style.width = 64;
		this.inputSelectionEndInSeconds.type = "number";
		this.inputSelectionEndInSeconds.onchange = this.handleEventInputSelectionEndInSecondsChanged.bind(this);
		divControlsSelection.appendChild(this.inputSelectionEndInSeconds);

		var buttonSelectAll = document.createElement("button");
		buttonSelectAll.innerHTML = "All";
		buttonSelectAll.onclick = this.selectAll.bind(this);
		divControlsSelection.appendChild(buttonSelectAll);

		var buttonSelectNone = document.createElement("button");
		buttonSelectNone.innerHTML = "None";
		buttonSelectNone.onclick = this.selectNone.bind(this);
		divControlsSelection.appendChild(buttonSelectNone);

		var labelTag = document.createElement("label");
		labelTag.innerHTML = "Tag:";
		divControlsSelection.appendChild(labelTag);

		this.inputTagText = document.createElement("input");
		this.inputTagText.style.width = 128;
		divControlsSelection.appendChild(this.inputTagText);

		var buttonTagAdd = document.createElement("button");
		buttonTagAdd.innerHTML = "Tag Selection";
		buttonTagAdd.onclick = this.selectionTag.bind(this);
		divControlsSelection.appendChild(buttonTagAdd);

		var buttonTagSelect = document.createElement("button");
		buttonTagSelect.innerHTML = "Select by Tag";
		buttonTagSelect.onclick = this.selectionSelectByTag.bind(this);
		divControlsSelection.appendChild(buttonTagSelect);

		var buttonTagRemove = document.createElement("button");
		buttonTagRemove.innerHTML = "Remove Selection by Tag";
		buttonTagRemove.onclick = this.selectionRemoveByTag.bind(this);
		divControlsSelection.appendChild(buttonTagRemove);

		return divControlsSelection;
	}

	SoundEditor.prototype.domElementUpdate_BuildIfNecessary_ControlsZoom = function()
	{
		var divControlsZoom = document.createElement("div");
		divControlsZoom.style.border = "1px solid";

		var buttonZoomToSelection = document.createElement("button");
		buttonZoomToSelection.innerHTML = "Zoom to Selection";
		buttonZoomToSelection.onclick = this.viewZoomToSelection.bind(this);
		divControlsZoom.appendChild(buttonZoomToSelection);

		var buttonZoomToFit = document.createElement("button");
		buttonZoomToFit.innerHTML = "Zoom to Fit";
		buttonZoomToFit.onclick = this.viewZoomToFit.bind(this);
		divControlsZoom.appendChild(buttonZoomToFit);

		return divControlsZoom;
	}

	SoundEditor.prototype.domElementUpdate_Controls = function()
	{
		this.inputSessionName.value = this.session.name;
		this.inputTagsToPlay.value = this.session.tagsToPlay;
	}

	SoundEditor.prototype.domElementUpdate_Cursor = function()
	{
		this.inputCursorPosInSeconds.value = this.cursorOffsetInSeconds;
	}

	SoundEditor.prototype.domElementUpdate_Selection = function()
	{
		if (this.selectionCurrent == null)
		{
			this.inputSelectionStartInSeconds.value = "";
			this.inputSelectionEndInSeconds.value = "";
		}
		else
		{
			var timesStartAndEndInSeconds = this.selectionCurrent.timesStartAndEndInSeconds;
			var timeStartInSeconds = timesStartAndEndInSeconds[0];
			var timeEndInSeconds = timesStartAndEndInSeconds[1];

			this.inputSelectionStartInSeconds.value = timeStartInSeconds;
			this.inputSelectionEndInSeconds.value = timeEndInSeconds;
		}
	}

	SoundEditor.prototype.domElementUpdate_Waveform = function()
	{
		for (var t = 0; t < this.session.tracks.length; t++)
		{
			var track = this.session.tracks[t];
			var domElementForTrack = track.domElementUpdate(this);
			if (domElementForTrack.parentElement == null)
			{
				this.divTracks.appendChild(domElementForTrack);
			}
		}
	}

	// events

	SoundEditor.prototype.handleEventInputCursorPosInSecondsChanged = function(event)
	{
		// todo
	}

	SoundEditor.prototype.handleEventInputSelectionStartInSecondsChanged = function(event)
	{
		// todo
	}

	SoundEditor.prototype.handleEventInputSelectionEndInSecondsChanged = function(event)
	{
		// todo
	}

	SoundEditor.prototype.handleEventInputSessionNameChanged = function(event)
	{
		this.session.name = event.target.value;
	}

	SoundEditor.prototype.handleEventInputTagsToPlay_Changed = function(event)
	{
		this.session.tagsToPlay = event.target.value;
	}

	SoundEditor.prototype.handleEventKeyDown = function(event)
	{
		var keyCode = event.keyCode;

		if (keyCode == 37) // left
		{
			this.handleEventKeyDown_ArrowLeftOrRight(event, -1);
		}
		else if (keyCode == 38) // up
		{
			this.trackIndexCurrent = NumberHelper.trimValueToRange
			(
				this.trackIndexCurrent - 1,
				this.tracks.length
			);
		}
		else if (keyCode == 39) // right
		{
			this.handleEventKeyDown_ArrowLeftOrRight(event, 1);
		}
		else if (keyCode == 40) // down
		{
			this.trackIndexCurrent = NumberHelper.trimValueToRange
			(
				this.trackIndexCurrent + 1,
				this.tracks.length
			);
		}
		else if (keyCode == 65) // a
		{
			// todo - contingent event.preventDefault();

			if (event.ctrlKey == true)
			{
				if (event.shiftKey == true)
				{
					this.selectNone();
				}
				else
				{
					this.selectAll();
				}
			}
		}
	}

	SoundEditor.prototype.handleEventKeyDown_ArrowLeftOrRight = function(event, direction)
	{
		var shouldAdjustmentBeSmall = event.ctrlKey;

		if (event.altKey == true)
		{
			this.viewSlide
			(
				shouldAdjustmentBeSmall,
				direction
			);
		}
		else if (event.shiftKey == true)
		{
			this.selectionResize
			(
				shouldAdjustmentBeSmall,
				direction
			);
		}
		else
		{
			this.cursorMove(shouldAdjustmentBeSmall, direction);
		}
	}
}

function StringHelper()
{
	// static class
}
{
	StringHelper.padLeft = function(stringToBePadded, stringToPadWith, lengthAfterPadding)
	{
		var returnValue = stringToBePadded;

		while (returnValue.length < lengthAfterPadding)
		{
			returnValue = 
				stringToPadWith
				+ returnValue;
		}

		return returnValue;
	}
}

function TimeHelper()
{
	// static class
}
{
	TimeHelper.SecondsPerMinute = 60;
	TimeHelper.MinutesPerHour = 60;
	TimeHelper.MillisecondsPerSecond = 1000;

	TimeHelper.secondsToStringHHMMSSmmm = function(secondsTotal)
	{
		var minutesTotal = Math.floor(secondsTotal / TimeHelper.SecondsPerMinute);
		var minutesPastHour = minutesTotal % TimeHelper.MinutesPerHour;
		var hours = Math.floor(minutesTotal / TimeHelper.MinutesPerHour);
		var secondsPastMinute = Math.floor(secondsTotal % TimeHelper.SecondsPerMinute);
		var millisecondsPastSecond = Math.floor
		(
			(secondsTotal * TimeHelper.MillisecondsPerSecond) 
			% TimeHelper.MillisecondsPerSecond
		);

		var hoursPadded = StringHelper.padLeft("" + hours, "0", 2); 
		var minutesPastHourPadded = StringHelper.padLeft("" + minutesPastHour, "0", 2);
		var secondsPastMinutePadded = StringHelper.padLeft("" + secondsPastMinute, "0", 2);
		var millisecondsPastSecondPadded = StringHelper.padLeft("" + millisecondsPastSecond, "0", 3);

		var returnValue = 
			hoursPadded + ":"
			+ minutesPastHourPadded + ":"
			+ secondsPastMinutePadded + "," // French radix point
			+ millisecondsPastSecondPadded;

		return returnValue;
	}

	TimeHelper.secondsToStringSecondsMilliseconds = function(secondsTotal)
	{
		var secondsWhole = Math.floor(secondsTotal);

		var millisecondsPastSecond = Math.floor
		(
			(secondsTotal * TimeHelper.MillisecondsPerSecond) 
			% TimeHelper.MillisecondsPerSecond
		);

		var millisecondsPastSecondPadded = StringHelper.padLeft
		(
			"" + millisecondsPastSecond, "0", 3
		);

		var returnValue = 
			+ secondsWhole + "." 
			+ millisecondsPastSecondPadded;

		return returnValue;
	}

}

function Track(name, sounds)
{
	this.name = name;
	this.sounds = sounds;
}
{
	Track.prototype.durationInSeconds = function()
	{
		var soundEndInSecondsMax = 0;

		for (var i = 0; i < this.sounds.length; i++)
		{
			var sound = this.sounds[i];
			var soundEndInSeconds = 
				sound.offsetWithinTrackInSeconds
				+ sound.durationInSeconds();

			if (soundEndInSeconds > soundEndInSecondsMax)
			{
				soundEndInSecondsMax = soundEndInSeconds;
			}
		}

		return soundEndInSecondsMax;
	}

	// dom

	Track.prototype.domElementRemove = function()
	{
		delete this.domElement;
		delete this.graphics;
	}

	Track.prototype.domElementUpdate = function(soundEditor)
	{
		var viewSizeInPixels = soundEditor.viewSizeInPixels;

		this.domElementUpdate_BuildIfNecessary(viewSizeInPixels);
		this.domElementUpdate_Background(viewSizeInPixels);

		for (var s = 0; s < this.sounds.length; s++)
		{
			var sound = this.sounds[s];
			this.domElementUpdate_Sound(soundEditor, sound);
		}

		this.domElementUpdate_Selections(soundEditor);

		this.domElementUpdate_Title(viewSizeInPixels);

		return this.domElement;
	}

	Track.prototype.domElementUpdate_Background = function(viewSizeInPixels)
	{	
		this.graphics.fillStyle = SoundEditor.ColorViewBackground;
		this.graphics.strokeStyle = SoundEditor.ColorViewBorder;
		this.graphics.fillRect
		(
			0, 
			0, 
			viewSizeInPixels.x, 
			viewSizeInPixels.y
		);
		this.graphics.strokeRect
		(
			0, 
			0, 
			viewSizeInPixels.x, 
			viewSizeInPixels.y
		);
	}

	Track.prototype.domElementUpdate_BuildIfNecessary = function(viewSizeInPixels)
	{
		if (this.domElement == null)
		{
			var canvasView = document.createElement("canvas");
			canvasView.width = viewSizeInPixels.x;
			canvasView.height = viewSizeInPixels.y;

			this.graphics = canvasView.getContext("2d");

			this.domElement = canvasView;
			this.domElement.entity = this;
		}
	}

	Track.prototype.domElementUpdate_Selections = function(soundEditor)
	{
		var selectionsTagged = soundEditor.session.selectionsTagged;

		for (var i = 0; i < selectionsTagged.length; i++)
		{
			var selectionTagged = selectionsTagged[i];
			this.domElementUpdate_Selection(soundEditor, selectionTagged)
		}

		var selectionCurrent = soundEditor.selectionCurrent;

		if (selectionCurrent != null)
		{
			this.domElementUpdate_Selection(soundEditor, selectionCurrent);
		}

		var cursorPosInPixels = 
			(
				soundEditor.cursorOffsetInSeconds
				- soundEditor.viewOffsetInSeconds
			)
			* soundEditor.viewSizeInPixels.x
			/ soundEditor.viewWidthInSeconds; 

		this.graphics.strokeStyle = SoundEditor.ColorViewCursor;
		this.graphics.strokeRect
		(
			cursorPosInPixels, 0,
			1, soundEditor.viewSizeInPixels.y
		);

		return this.domElement;
	}

	Track.prototype.domElementUpdate_Selection = function(soundEditor, selectionCurrent)
	{
		var viewSizeInPixels = soundEditor.viewSizeInPixels;

		var selectionDurationInSeconds = selectionCurrent.durationInSeconds();

		var timesStartAndEndInSeconds = selectionCurrent.timesStartAndEndInSeconds;
		var timeStartInSecondsRelative = 
			timesStartAndEndInSeconds[0] 
			- soundEditor.viewOffsetInSeconds;
		var timeEndInSecondsRelative = 
			timesStartAndEndInSeconds[1]
			- soundEditor.viewOffsetInSeconds;

		var secondsPerPixel = soundEditor.viewSecondsPerPixel();

		// todo - reversible selections?

		var selectionStartInPixels = timeStartInSecondsRelative / secondsPerPixel;
	
		var selectionSizeInPixels = 
			selectionDurationInSeconds
			/ secondsPerPixel;

		this.graphics.fillStyle = SoundEditor.ColorViewSelectionFill;
		this.graphics.strokeStyle = SoundEditor.ColorViewSelectionBorder;
	
		this.graphics.fillRect
		(
			selectionStartInPixels, 0,
			selectionSizeInPixels, viewSizeInPixels.y
		);
	
		this.graphics.strokeRect
		(
			selectionStartInPixels, 0,
			selectionSizeInPixels, viewSizeInPixels.y
		);
	
		if (selectionCurrent.tag != null)
		{
			this.graphics.fillStyle = SoundEditor.ColorViewText;

			this.graphics.fillText
			(
				selectionCurrent.toString(),
				selectionStartInPixels + 2, 
				SoundEditor.TextHeightInPixels
			);
		}
	}

	Track.prototype.domElementUpdate_Sound = function(soundEditor, sound)
	{
		var soundSource = sound.sourceWavFile;
		var samplingInfo = soundSource.samplingInfo;
		var bitsPerSample = samplingInfo.bitsPerSample;
		var samplesPerSecond = samplingInfo.samplesPerSecond; // hack
		var samples = soundSource.samplesForChannels[0]; // hack

		var soundOffsetWithinTrackInSamples = Math.round
		(
			sound.offsetWithinTrackInSeconds 
			* samplesPerSecond
		);

		var viewSizeInPixels = soundEditor.viewSizeInPixels;
		var viewSizeInPixelsHalf = soundEditor.viewSizeInPixelsHalf;

		var viewOffsetInSamples = Math.round(soundEditor.viewOffsetInSeconds * samplesPerSecond);
		var viewWidthInSamples = Math.round(soundEditor.viewWidthInSeconds * samplesPerSecond);
		var samplePosInPixels = new Coords(0, viewSizeInPixelsHalf.y); 
		var sampleValue = 0;

		this.graphics.beginPath();

		var byteConverter = new ByteConverter(bitsPerSample);

		for (var i = 0; i < viewWidthInSamples; i++)
		{
			var sampleIndex = 
				i 
				+ viewOffsetInSamples
				- soundOffsetWithinTrackInSamples;

			if (sampleIndex >= 0 && sampleIndex <= samples.length)
			{	
				var samplePosInPixelsXNext = 
					i 
					* viewSizeInPixels.x 
					/ viewWidthInSamples;

				if (samplePosInPixelsXNext != samplePosInPixels.x)
				{
					var sampleBytes = samples[sampleIndex];

					sampleValue = byteConverter.integerToFloat
					(
						sampleBytes
					);	

					samplePosInPixels.x = samplePosInPixelsXNext;
	
					samplePosInPixels.y = 
						viewSizeInPixelsHalf.y 
						+ 
						(
							sampleValue
							* viewSizeInPixelsHalf.y
							* .8 // max amplitude
						);
	
					this.graphics.lineTo
					(
						samplePosInPixels.x, 
						samplePosInPixels.y
					);
				}
			}
		}

		this.graphics.stroke();

		this.graphics.strokeStyle = SoundEditor.ColorViewBaseline;
		this.graphics.strokeRect
		(
			0, soundEditor.viewSizeInPixelsHalf.y,
			viewSizeInPixels.x, viewSizeInPixelsHalf.y
		);
	}

	Track.prototype.domElementUpdate_Title = function(viewSizeInPixels)
	{
		this.graphics.fillStyle = SoundEditor.ColorViewText;

		this.graphics.fillText
		(
			this.name,
			2, viewSizeInPixels.y - SoundEditor.TextHeightInPixels * .2
		);
	}

	// event

	Track.prototype.handleEventMouseDown = function(event)
	{
		var soundEditor = Globals.Instance.soundEditor;
		var session = soundEditor.session;
		session.trackCurrent(this);
		soundEditor.selectionCurrent = null;
		var clickOffsetInSeconds = this.mousePointerOffsetInSecondsForEvent(event);
		soundEditor.cursorOffsetInSeconds = clickOffsetInSeconds;

		for (var i = 0; i < session.selectionsTagged.length; i++)
		{
			var selectionTagged = session.selectionsTagged[i];
			var timesStartAndEndInSeconds = selectionTagged.timesStartAndEndInSeconds;
			var timeStartInSeconds = timesStartAndEndInSeconds[0];
			var timeEndInSeconds = timesStartAndEndInSeconds[1];

			var isClickWithinSelection = 
			(
				clickOffsetInSeconds >= timeStartInSeconds 
				&& clickOffsetInSeconds <= timeEndInSeconds
			);

			if (isClickWithinSelection == true)
			{
				// For now, existing selections cannot be selected.
				//soundEditor.selectionCurrent = selectionTagged;
			}
		}

		soundEditor.domElementUpdate();
	}

	Track.prototype.handleEventMouseMove = function(event)
	{
		var soundEditor = Globals.Instance.soundEditor;

		if (soundEditor.selectionCurrent == null)
		{
			soundEditor.selectionCurrent = new Selection
			(
				null, // tag
				[
					soundEditor.cursorOffsetInSeconds,
					soundEditor.cursorOffsetInSeconds
				]
			);
		}

		var mousePointerOffsetInSeconds = this.mousePointerOffsetInSecondsForEvent(event);

		soundEditor.cursorOffsetInSeconds = mousePointerOffsetInSeconds;
		soundEditor.selectionCurrent.timesStartAndEndInSeconds[1] = soundEditor.cursorOffsetInSeconds;
		soundEditor.domElementUpdate();
	}

	Track.prototype.handleEventMouseUp = function(event)
	{
		var soundEditor = Globals.Instance.soundEditor;

		if (soundEditor.selectionCurrent != null)
		{
			soundEditor.selectionCurrent.rectify();
		}
	}

	Track.prototype.mousePointerOffsetInSecondsForEvent = function(event)
	{
		var mousePointerPosInPixels = 
			event.x
			- event.srcElement.getBoundingClientRect().left;

		var soundEditor = Globals.Instance.soundEditor;

		var mousePointerPosInSeconds = 
			mousePointerPosInPixels 
			* soundEditor.viewWidthInSeconds 
			/ soundEditor.viewSizeInPixels.x;

		var mousePointerOffsetInSeconds = 
			mousePointerPosInSeconds
			+ soundEditor.viewOffsetInSeconds

		return mousePointerOffsetInSeconds;
	}
}

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

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

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

		this.samplesForChannels = [];
		for (var c = 0; c < numberOfChannels; c++)
		{
			this.samplesForChannels[c] = [];
		}
	}
}
{
	// constants

	WavFile.NumberOfBytesInRiffWaveAndFormatChunks = 36;

	// static methods

	WavFile.readFromFile = function(fileToReadFrom, callback)
	{		
		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.readFromFile_ReadChunks(reader);
			}

			callback(returnValue);
		}

		fileReader.readAsBinaryString(fileToReadFrom);
	}

	// instance methods

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

		var numberOfBytesInFile = reader.readInt();

		var waveStringAsBytes = reader.readBytes(4);

		while (reader.hasMoreBytes() == true)
		{
			var chunkTypeAsString = reader.readString(4);

			if (chunkTypeAsString == "data")
			{
				this.readFromFile_ReadChunks_Data(reader);
			}
			else if (chunkTypeAsString == "fmt ")
			{
				this.readFromFile_ReadChunks_Format(reader);
			}
			else
			{
				this.readFromFile_ReadChunks_Unrecognized(reader);
			}
		}
	}

	WavFile.prototype.readFromFile_ReadChunks_Data = function(reader)
	{
		var subchunk2SizeInBytes = reader.readInt();

		var samplesForChannelsMixedAsBytes = reader.readBytes(subchunk2SizeInBytes);

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

		this.samplesForChannels = samplesForChannels;	
	}

	WavFile.prototype.readFromFile_ReadChunks_Format = function(reader)
	{
		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 numberOfBytesInChunkSoFar = 16;
		var numberOfExtraBytesInChunk = 
			chunkSizeInBytes 
			- numberOfBytesInChunkSoFar;

		var extraBytes = reader.readBytes(numberOfExtraBytesInChunk)

		if (bitsPerSample == 0)
		{
			bitsPerSample = bytesPerSampleMaybe * Constants.BitsPerByte;
		}

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

		this.samplingInfo = samplingInfo;
	}

	WavFile.prototype.readFromFile_ReadChunks_Unrecognized = function(reader)
	{
		var chunkDataSizeInBytes = reader.readInt();
		var chunkData = reader.readBytes(chunkDataSizeInBytes);
	}

	WavFile.prototype.appendClipFromWavFileBetweenTimesStartAndEnd = function
	(
		wavFileToClipFrom,
		timeStartInSeconds, 
		timeEndInSeconds
	)
	{
		var samplesPerSecond = wavFileToClipFrom.samplingInfo.samplesPerSecond;

		var timeStartInSamples = Math.floor
		(
			samplesPerSecond * timeStartInSeconds
		);

		var timeEndInSamples = Math.ceil
		(
			samplesPerSecond * timeEndInSeconds
		);

		var samplesForChannelsInClip = [];

		for (var c = 0; c < wavFileToClipFrom.samplesForChannels.length; c++)
		{
			var samplesForChannelSource = wavFileToClipFrom.samplesForChannels[c];
			var samplesForChannelTarget = this.samplesForChannels[c];

			for (var s = timeStartInSamples; s <= timeEndInSamples; s++)
			{
				var sample = samplesForChannelSource[s];
				samplesForChannelTarget.push(sample);	
			}
		}

		return this;
	}

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

		return returnValue;		
	}

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

	WavFile.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 = [];

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

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

			samplesForChannelsNew[c] = samplesForChannelNew;
		}

		this.samplesForChannels = samplesForChannelsNew;
	}

	WavFile.prototype.writeToBytes = function()
	{
		var writer = new ByteStreamLittleEndian([]);

		this.writeToBytes_WriteChunks(writer);

		return writer.bytes;
	}

	WavFile.prototype.writeToBytes_WriteChunks = function(writer)
	{
		writer.writeString("RIFF");

		// hack
		var numberOfBytesOfOverhead = 
			"RIFF".length
			+ "WAVE".length
			+ "fmt ".length
			+ 20 // additional bytes In format header
			+ "data".length;

			//+ 4; // additional bytes in data header?

		var numberOfBytesInFile = 
			this.samplingInfo.numberOfChannels
			* this.samplesForChannels[0].length
			* this.samplingInfo.bitsPerSample
			/ Constants.BitsPerByte
			+ numberOfBytesOfOverhead;

		writer.writeInt(numberOfBytesInFile);

		writer.writeString("WAVE");

		this.writeToBytes_WriteChunks_Format(writer);
		this.writeToBytes_WriteChunks_Data(writer);
	}

	WavFile.prototype.writeToBytes_WriteChunks_Data = function(writer)
	{
		writer.writeString("data");

		var samplesForChannelsMixedAsBytes = WavFileSample.convertManyToBytes
		(
			this.samplesForChannels,
			this.samplingInfo
		);

		writer.writeInt(samplesForChannelsMixedAsBytes.length);

		writer.writeBytes(samplesForChannelsMixedAsBytes);
	}

	WavFile.prototype.writeToBytes_WriteChunks_Format = function(writer)
	{
		writer.writeString("fmt ");

		writer.writeInt(this.samplingInfo.chunkSizeInBytes);
		writer.writeShort(this.samplingInfo.formatCode);

		writer.writeShort(this.samplingInfo.numberOfChannels);
		writer.writeInt(this.samplingInfo.samplesPerSecond);

		writer.writeInt(this.samplingInfo.bytesPerSecond);
		writer.writeShort(this.samplingInfo.bytesPerSampleMaybe);
		writer.writeShort(this.samplingInfo.bitsPerSample);

		if (this.samplingInfo.extraBytes != null)
		{
			writer.writeBytes(this.samplingInfo.extraBytes);
		}
	}
}

function WavFileSample()
{
	// do nothing
}
{
	// static methods

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

		var numberOfChannels = samplingInfo.numberOfChannels;

		var returnSamples = [];

		var bytesPerSample = samplingInfo.bitsPerSample / Constants.BitsPerByte;

		var samplesPerChannel =
			numberOfBytes
			/ bytesPerSample
			/ numberOfChannels;

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

		var b = 0;

		var byteConverter = new ByteConverter(samplingInfo.bitsPerSample);
		var sampleValueAsBytes = [];

		for (var s = 0; s < samplesPerChannel; s++)
		{				
			for (var c = 0; c < numberOfChannels; c++)
			{
				sampleValueAsBytes.length = 0;

				for (var i = 0; i < bytesPerSample; i++)
				{
					sampleValueAsBytes.push(bytesToConvert[b]);
					b++;
				}

				var sampleValueAsInteger = byteConverter.bytesToInteger
				(
					sampleValueAsBytes
				);

				returnSamples[c][s] = sampleValueAsInteger;
			}
		}

		return returnSamples;
	}

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

		var numberOfChannels = samplingInfo.numberOfChannels;

		var samplesPerChannel = samplesToConvert[0].length;

		var bitsPerSample = samplingInfo.bitsPerSample;

		var bytesPerSample = bitsPerSample / Constants.BitsPerByte;

		var numberOfBytes =
			numberOfChannels
			* samplesPerChannel
			* bytesPerSample;

		returnBytes = [];

		var b = 0;

		var byteConverter = new ByteConverter(bitsPerSample);

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

				var sampleAsBytes = byteConverter.integerToBytes
				(
					sampleAsInteger
				);

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

		return returnBytes;
	}	
}

function WavFileSamplingInfo
(
	name,	   
	chunkSizeInBytes,
	formatCode,
	numberOfChannels,		
	samplesPerSecond,
	bitsPerSample,
	extraBytes
)
{
	this.name = name;
	this.chunkSizeInBytes = chunkSizeInBytes;
	this.formatCode = formatCode;
	this.numberOfChannels = numberOfChannels;
	this.samplesPerSecond = samplesPerSecond;
	this.bitsPerSample = bitsPerSample;
	this.extraBytes = extraBytes;
}
{
	WavFileSamplingInfo.buildDefault = function()
	{
		return new WavFileSamplingInfo
		(
			"Default",
			16, // chunkSizeInBytes
			1, // formatCode
			1, // numberOfChannels
			44100,	 // samplesPerSecond
			16, // bitsPerSample
			null // extraBytes
		);
	}

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

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

		return returnValue;
	}		
}


// run

main();

</script>
</body>
</html>

Advertisements
This entry was posted in Uncategorized and tagged , , , . Bookmark the permalink.

2 Responses to A Sound Editor in HTML5 Using JavaScript

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