A Music Notation Renderer In JavaScript

The JavaScript below prompts the user for a musical composition in JavaScript format, and then renders that composition in musical notation. To see it in action, copy the code into an .html file and open that file in a web browser that runs JavaScript. This program is by no means complete, and is published here mostly as a basis for further work.

MusicNotationRenderer.png


<html>
<body>

<div id="divSession">

	<h1>Musical Notation Renderer</h1>

	<label>Song as JSON:</label>
	<button onclick="buttonSongDemo_Clicked();">Demo</button>
	<br />
	<textarea id="textareaSongAsJSON" cols="80" rows="20"></textarea>
	<br />

	<button onclick="buttonSongRender_Clicked();">Render</button>
	<br />

	<label>View:</label><br />
	<div id="divView"></div><br />

</div>

<script type="text/javascript">

// ui events

function buttonSongDemo_Clicked()
{
	var song = Song.demo();
	var songAsJSON = song.toStringJSON();
	var textareaSongAsJSON = document.getElementById("textareaSongAsJSON");
	textareaSongAsJSON.value = songAsJSON;
}

function buttonSongRender_Clicked()
{
	var d = document;
	var textareaSongAsJSON = d.getElementById("textareaSongAsJSON");
	var songAsJSON = textareaSongAsJSON.value;
	var song = Song.fromStringJSON(songAsJSON);
	var songAsImgElement = song.toImgElement();
	var divView = d.getElementById("divView");
	divView.innerHTML = "";
	divView.appendChild(songAsImgElement);
}

// classes

function Coords(x, y)
{
	this.x = x;
	this.y = y;
}
{
	Coords.Instances = new Coords_Instances();

	function Coords_Instances()
	{
		this.Zeroes = new Coords(0, 0);
	}

	Coords.prototype.add = function(other)
	{
		this.x += other.x;
		this.y += other.y;
		return this;
	}

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

	Coords.prototype.overwriteWith = function(other)
	{
		this.x = other.x;
		this.y = other.y;
		return this;
	}

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

	Coords.prototype.multiply = function(other)
	{
		this.x *= other.x;
		this.y *= other.y;
		return this;
	}

	Coords.prototype.multiplyScalar = function(scalar)
	{
		this.x *= scalar;
		this.y *= scalar;
		return this;
	}
}

function Cursor(pos, symbol)
{
	this.pos = pos;
	this.symbol = symbol;
}

function Display(size)
{
	this.size = size;
	this.colorFore = "Black";
	this.colorBack = "White";
}
{
	Display.prototype.toCanvas = function()
	{
		this.canvas = document.createElement("canvas");
		this.canvas.width = this.size.x;
		this.canvas.height = this.size.y;

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

		return this.canvas;
	}

	// drawing

	Display.prototype.drawCircle = function(center, radius, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.beginPath();
			this.graphics.arc(center.x, center.y, radius, 0, Math.PI * 2);
			this.graphics.fill();
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.beginPath();
			this.graphics.arc(center.x, center.y, radius, 0, Math.PI * 2);
			this.graphics.stroke();
		}
	}

	Display.prototype.drawEllipse = function(center, size, rotationInTurns, colorFill, colorBorder)
	{
		var rotationInRadians = Math.PI * 2 * rotationInTurns;
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.beginPath();
			this.graphics.ellipse(center.x, center.y, size.x, size.y, rotationInRadians, 0, Math.PI * 2);
			this.graphics.fill();
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.beginPath();
			this.graphics.ellipse(center.x, center.y, size.x, size.y, rotationInRadians, 0, Math.PI * 2);
			this.graphics.stroke();
		}
	}

	Display.prototype.drawLine = function(fromPos, toPos, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		this.graphics.moveTo(fromPos.x, fromPos.y);
		this.graphics.lineTo(toPos.x, toPos.y);
		this.graphics.stroke();
	}

	Display.prototype.drawPath = function(vertices, color)
	{
		this.graphics.strokeStyle = color;
		this.graphics.beginPath();
		var vertex = vertices[0];
		this.graphics.moveTo(vertex.x, vertex.y);
		for (var i = 1; i < vertices.length; i++)
		{
			vertex = vertices[i];
			this.graphics.lineTo(vertex.x, vertex.y);
		}
		this.graphics.stroke();
	}

	Display.prototype.drawRectangle = function(pos, size, colorFill, colorBorder)
	{
		if (colorFill != null)
		{
			this.graphics.fillStyle = colorFill;
			this.graphics.fillRect(pos.x, pos.y, size.x, size.y);
		}

		if (colorBorder != null)
		{
			this.graphics.strokeStyle = colorBorder;
			this.graphics.strokeRect(pos.x, pos.y, size.x, size.y);
		}
	}

	Display.prototype.drawText = function(text, font, pos, color)
	{
		this.graphics.fillStyle = color;
		this.graphics.font = font.systemFont;
		var textWidth = this.graphics.measureText(text).width;
		var textWidthHalf = textWidth / 2;
		this.graphics.fillText(text, pos.x - textWidthHalf, pos.y + font.heightInPixelsHalf);
	}
}

function Font(familyName, heightInPixels)
{
	this.familyName = familyName;
	this.heightInPixels = heightInPixels;

	this.heightInPixelsHalf = this.heightInPixels / 2;
	this.systemFont = this.heightInPixels + "px " + this.familyName;
}
{
	Font.InstanceDefault = new Font("sans-serif", 10);
}

function Image(systemImageSrc)
{
	this.systemImage = document.createElement("img");

	this.systemImage.src = this.systemImageSrc;
}

function InputHelper()
{
	this.keyPressed = null;
}
{
	InputHelper.prototype.initialize = function()
	{
		document.body.onkeydown = this.handleEventKeyDown.bind(this);
		document.body.onkeyup = this.handleEventKeyUp.bind(this);
	}

	// events

	InputHelper.prototype.handleEventKeyDown = function(event)
	{
		this.keyPressed = event.key;
	}

	InputHelper.prototype.handleEventKeyUp = function(event)
	{
		this.keyPressed = null;
	}
}

function Page(index, size, staves)
{
	this.index = index;
	this.size = size;
	this.staves = staves;

	this.staffIndexSelected = 0;
}
{
	Page.prototype.margin = function()
	{
		return 16;
	}

	Page.prototype.staffSelected = function()
	{
		return this.stave[this.staffIndexSelected];
	}

	Page.prototype.spaceBetweenStaves = function()
	{
		return 64;
	}

	// json

	Page.prototype.fromStringJSON_ObjectPrototypesSet = function()
	{
		for (var i = 0; i < this.staves.length; i++)
		{
			var staff = this.staves[i];
			staff.__proto__ = Staff.prototype;
			staff.fromStringJSON_ObjectPrototypesSet();
		}
	}

	// drawing

	Page.prototype.draw = function(display, session, song)
	{
		display.drawRectangle
		(
			Coords.Instances.Zeroes,
			this.size,
			null, // colorFill
			display.colorFore
		)

		for (var i = 0; i < this.staves.length; i++)
		{
			var staff = this.staves[i];
			staff.draw(display, session, song, this);
		}
	}
}

function Session()
{
	// do nothing
}
{
	Session.Instance = new Session();
}
{
	Session.prototype.draw = function(display)
	{
		this.song.draw(display, this);
	}
}

function Song(name, pages)
{
	this.name = name;
	this.pages = pages;

	this.pageIndexSelected = 0;
}
{
	Song.demo = function()
	{
		var pageSize = new Coords(425, 550);

		var returnValue = new Song
		(
			"Demo",
			[
				new Page
				(
					0, // index
					pageSize,
					[
						new Staff
						(
							0,
							[
								new Symbol("ClefG", new Coords(1, 0)),
								new Symbol("TimeSignatureFourFour", new Coords(3, 0)),
								new Symbol("AccidentalSharp", new Coords(4, 0)),
								new Symbol("MeasureLineDouble", new Coords(5, 0)),

								new Symbol("NoteWhole", new Coords(6, 6)),
								new Symbol("MeasureLine", new Coords(7, 0)),

								new Symbol("NoteHalf", new Coords(8, 2)),
								new Symbol("NoteQuarter", new Coords(9, 3)),
								new Symbol("NoteQuarter", new Coords(10, 3)),
								new Symbol("MeasureLine", new Coords(11, 0)),

								new Symbol("NoteHalf", new Coords(12, 4)),
								new Symbol("NoteQuarter", new Coords(14, 3)),
								new Symbol("Dot", new Coords(14.7, 3)),
								new Symbol("NoteEighth", new Coords(16, 4)),
								new Symbol("NoteQuarter", new Coords(17, 3)),
								new Symbol("Dot", new Coords(17.7, 3)),
								new Symbol("NoteEighth", new Coords(19, 5)),

								new Symbol("MeasureLine", new Coords(20, 0)),

								new Symbol("NoteWhole", new Coords(22, 6)),

								new Symbol("MeasureLineDouble", new Coords(24.3, 0)),
							]
						)
					]
				),
			]
		);

		return returnValue;
	}

	Song.prototype.pageSelected = function()
	{
		return this.pages[this.pageIndexSelected];
	}

	Song.prototype.spaceBetweenPages = function()
	{
		return 16;
	}

	// json

	Song.fromStringJSON = function(songAsJSON)
	{
		var returnValue = JSON.parse(songAsJSON);
		returnValue.__proto__ = Song.prototype;
		returnValue.fromStringJSON_ObjectPrototypesSet();
		return returnValue;
	}

	Song.prototype.fromStringJSON_ObjectPrototypesSet = function()
	{
		for (var i = 0; i < this.pages.length; i++)
		{
			var page = this.pages[i];
			page.__proto__ = Page.prototype;
			page.fromStringJSON_ObjectPrototypesSet();
		}
	}

	Song.prototype.toStringJSON = function()
	{
		var returnValue = JSON.stringify(this, null, 4);
		return returnValue;
	}

	// drawing

	Song.prototype.draw = function(display, session)
	{
		for (var i = 0; i < this.pages.length; i++)
		{
			var page = this.pages[i];
			page.draw(display, session, this);
		}
	}

	Song.prototype.toCanvas = function()
	{
		var numberOfPages = this.pages.length;
		var pageSize = this.pages[0].size;
		var spaceBetweenPages = this.spaceBetweenPages();
		var displaySize = new Coords
		(
			pageSize.x,
			(pageSize.y * numberOfPages)
				+ (spaceBetweenPages * numberOfPages - 1)
		);
		var display = new Display(displaySize);
		var displayAsCanvas = display.toCanvas();
		this.draw(display, Session.Instance);
		return displayAsCanvas;
	}

	Song.prototype.toImgElement = function()
	{
		var thisAsCanvas = this.toCanvas();
		var thisAsDataURL = thisAsCanvas.toDataURL();
		var returnValue = document.createElement("img");
		returnValue.src = thisAsDataURL;
		return returnValue;
	}
}

function Staff(index, symbols)
{
	this.index = index;
	this.symbols = symbols;
}
{
	Staff.prototype.pos = function(session, song, page)
	{
		if (this._pos == null)
		{
			var pageMargin = page.margin();
			var spaceBetweenStaves = page.spaceBetweenStaves();

			this._pos = new Coords
			(
				pageMargin,
				pageMargin + this.index * spaceBetweenStaves
			);
		}

		return this._pos;
	}

	Staff.prototype.numberOfLines = function()
	{
		return 5;
	}

	Staff.spaceBetweenLines = function()
	{
		return 8;
	}

	Staff.prototype.spaceBetweenLines = function()
	{
		return Staff.spaceBetweenLines();
	}

	// json

	Staff.prototype.fromStringJSON_ObjectPrototypesSet = function()
	{
		for (var i = 0; i < this.symbols.length; i++)
		{
			var symbol = this.symbols[i];
			symbol.__proto__ = Symbol.prototype;
			symbol.fromStringJSON_ObjectPrototypesSet();
		}
	}

	// drawing

	Staff.prototype.draw = function(display, session, song, page)
	{
		var numberOfLines = this.numberOfLines();
		var spaceBetweenLines = this.spaceBetweenLines();

		var fromPos = this.pos(session, song, page).clone();
		var toPos = fromPos.clone();
		toPos.x += page.size.x - page.margin() * 2;
		for (var i = 0; i < numberOfLines; i++)
		{
			display.drawLine(fromPos, toPos, display.colorFore);
			fromPos.y += spaceBetweenLines;
			toPos.y += spaceBetweenLines;
		}

		for (var i = 0; i < this.symbols.length; i++)
		{
			var symbol = this.symbols[i];
			symbol.draw(display, session, song, page, this);
		}
	}
}

function Symbol(defnName, posInNotes)
{
	this.defnName = defnName;
	this.posInNotes = posInNotes;

	this._drawPos = new Coords();
}
{
	Symbol.prototype.defn = function()
	{
		return SymbolDefn.Instances[this.defnName];
	}

	// json

	Symbol.prototype.fromStringJSON_ObjectPrototypesSet = function()
	{
		this.posInNotes.__proto__ = Coords.prototype;
		this._drawPos.__proto__ = Coords.prototype;
	}

	// drawing

	Symbol._noteSize = new Coords();
	Symbol.noteSize = function(staff)
	{
		return Symbol._noteSize.overwriteWithXY(2, .5).multiplyScalar
		(
			staff.spaceBetweenLines()
		);
	}

	Symbol.prototype.draw = function(display, session, song, page, staff)
	{
		var defn = this.defn();
		var visual = defn.visual;
		var noteSize = Symbol.noteSize(staff);
		var drawPos = this._drawPos.overwriteWith
		(
			this.posInNotes
		).multiply
		(
			noteSize
		).add
		(
			staff.pos(session, song, page)
		);
		visual.draw(display, drawPos);
	}

}

function SymbolDefn(name, code, visual)
{
	this.name = name;
	this.code = code;
	this.visual = visual;
}
{
	SymbolDefn.Instances = new SymbolDefn_Instances();

	function SymbolDefn_Instances()
	{
		var staffLineSpacing = Staff.spaceBetweenLines();
		var noteSpacing = new Coords(1.5, 1).multiplyScalar(staffLineSpacing / 2);
		var noteSize = noteSpacing.clone().multiplyScalar(.8);
		var fontClef = new Font("sans-serif", staffLineSpacing * 4);
		var fontTimeSignature = new Font("sans-serif", staffLineSpacing * 2);

		this.AccidentalFlat = new SymbolDefn
		(
			"AccidentalFlat",
			"b",
			new VisualOffset
			(
				new Coords(0, - staffLineSpacing / 4),
				new VisualText("b", fontTimeSignature)
			)
		);

		this.AccidentalSharp = new SymbolDefn
		(
			"AccidentalSharp",
			"#",
			new VisualOffset
			(
				new Coords(0, - staffLineSpacing / 4),
				new VisualText("#", fontTimeSignature)
			)
		);

		this.ClefF = new SymbolDefn
		(
			"ClefF",
			"F",
			new VisualGroup
			([
				new VisualOffset
				(
					new Coords(0, 1).multiplyScalar(fontClef.heightInPixels / 3),
					new VisualText("F", fontClef)
				),

				new VisualOffset
				(
					new Coords(1, -.3).multiplyScalar(fontClef.heightInPixels / 2),
					new VisualText(".", fontClef)
				),
				new VisualOffset
				(
					new Coords(1, -.8).multiplyScalar(fontClef.heightInPixels / 2),
					new VisualText(".", fontClef)
				),
			])
		);

		this.ClefG = new SymbolDefn
		(
			"ClefG",
			"G",
			new VisualGroup
			([
				new VisualOffset
				(
					new Coords(0, 1).multiplyScalar(fontClef.heightInPixels / 3),
					new VisualText("G", fontClef)
				),

				new VisualOffset
				(
					new Coords(1, .3).multiplyScalar(fontClef.heightInPixels / 2),
					new VisualText(".", fontClef)
				),
				new VisualOffset
				(
					new Coords(1, .8).multiplyScalar(fontClef.heightInPixels / 2),
					new VisualText(".", fontClef)
				),
			])
		);

		this.Dot = new SymbolDefn
		(
			"Dot",
			".",
			new VisualCircle(staffLineSpacing * .125, true)
		);

		this.MeasureLine = new SymbolDefn
		(
			"MeasureLine",
			"m",
			new VisualLine(new Coords(0, 0), new Coords(0, 4).multiplyScalar(staffLineSpacing))
		);

		this.MeasureLineDouble = new SymbolDefn
		(
			"MeasureLineDouble",
			"mm",
			new VisualGroup
			([
				new VisualLine
				(
					new Coords(0, 0),
					new Coords(0, 4).multiplyScalar(staffLineSpacing)
				),
				new VisualLine
				(
					new Coords(.5, 0).multiplyScalar(staffLineSpacing),
					new Coords(.5, 4).multiplyScalar(staffLineSpacing)
				),
			])
		);

		this.NoteEighth = new SymbolDefn
		(
			"NoteEighth",
			"n8",
			new VisualGroup
			([
				new VisualEllipse(noteSize, -.0625, true),
				new VisualPath
				([
					new Coords(.85, 0).multiply(noteSpacing),
					new Coords(.85, -6).multiply(noteSpacing),
					new Coords(1.3, -6).multiply(noteSpacing),
					new Coords(1.6, -5).multiply(noteSpacing),
					new Coords(1.6, -3).multiply(noteSpacing),
				]),
			])
		);

		this.NoteHalf = new SymbolDefn
		(
			"NoteHalf",
			"n2",
			new VisualGroup
			([
				new VisualEllipse(noteSize, -.0625, false),
				new VisualLine
				(
					new Coords(.85, 0).multiply(noteSpacing),
					new Coords(.85, -6).multiply(noteSpacing)
				)
			])
		);

		this.NoteQuarter = new SymbolDefn
		(
			"NoteQuarter",
			"n4",
			new VisualGroup
			([
				new VisualEllipse(noteSize, -.0625, true),
				new VisualLine
				(
					new Coords(.85, 0).multiply(noteSpacing),
					new Coords(.85, -6).multiply(noteSpacing)
				)
			])
		);

		this.NoteSixteenth = new SymbolDefn
		(
			"NoteSixteenth",
			"n16",
			new VisualGroup
			([
				new VisualEllipse(noteSize, -.0625, true),
				new VisualPath
				([
					new Coords(.85, 0).multiply(noteSpacing),
					new Coords(.85, -6).multiply(noteSpacing),
					new Coords(1.3, -6).multiply(noteSpacing),
					new Coords(1.6, -5).multiply(noteSpacing),
					new Coords(1.6, -3).multiply(noteSpacing),
				]),
				new VisualPath
				([
					new Coords(.85, 0).multiply(noteSpacing),
					new Coords(.85, -5).multiply(noteSpacing),
					new Coords(1.1, -5).multiply(noteSpacing),
					new Coords(1.4, -4).multiply(noteSpacing),
					new Coords(1.4, -2).multiply(noteSpacing),
				]),
			])
		);

		this.NoteWhole = new SymbolDefn
		(
			"NoteWhole",
			"n1",
			new VisualGroup
			([
				new VisualEllipse(noteSize, -.0625, false)
			])
		);

		this.TimeSignatureTwoFour = new SymbolDefn
		(
			"TimeSignatureTwoFour",
			"2_4",
			new VisualOffset
			(
				new Coords(0, .8).multiplyScalar(staffLineSpacing),
				new VisualGroup
				([
					new VisualText("2", fontTimeSignature),
					new VisualOffset
					(
						new Coords(0, 2).multiplyScalar(staffLineSpacing),
						new VisualText("4", fontTimeSignature)
					),
				])
			)
		);

		this.TimeSignatureFourFour = new SymbolDefn
		(
			"TimeSignatureFourFour",
			"4_4",
			new VisualOffset
			(
				new Coords(0, .8).multiplyScalar(staffLineSpacing),
				new VisualGroup
				([
					new VisualText("4", fontTimeSignature),
					new VisualOffset
					(
						new Coords(0, 2).multiplyScalar(staffLineSpacing),
						new VisualText("4", fontTimeSignature)
					),
				])
			)
		);
	}
}

function VisualCircle(radius, isFilled)
{
	this.radius = radius;
	this.isFilled = isFilled;
}
{
	VisualCircle.prototype.draw = function(display, drawPos)
	{
		display.drawCircle
		(
			drawPos,
			this.radius,
			(this.isFilled ? display.colorFore : null), // colorFill
			display.colorFore // colorBorder
		);
	}
}

function VisualEllipse(size, rotationInTurns, isFilled)
{
	this.size = size;
	this.rotationInTurns = rotationInTurns;
	this.isFilled = isFilled;
}
{
	VisualEllipse.prototype.draw = function(display, drawPos)
	{
		display.drawEllipse
		(
			drawPos,
			this.size,
			this.rotationInTurns,
			(this.isFilled ? display.colorFore : null), // colorFill
			display.colorFore // colorBorder
		);
	}
}

function VisualGroup(children)
{
	this.children = children;
}
{
	VisualGroup.prototype.draw = function(display, drawPos)
	{
		for (var i = 0; i < this.children.length; i++)
		{
			var child = this.children[i];
			child.draw(display, drawPos);
		}
	}
}

function VisualImage(image)
{
	this.image = image;
}
{
	VisualLine.prototype.draw = function(display, drawPos)
	{
		display.drawImage(this.image.systemImage);
	}
}

function VisualLine(fromPos, toPos)
{
	this.fromPos = fromPos;
	this.toPos = toPos;
}
{
	VisualLine.prototype.draw = function(display, drawPos)
	{
		display.drawLine
		(
			this.fromPos.clone().add(drawPos),
			this.toPos.clone().add(drawPos),
			display.colorFore
		);
	}
}

function VisualOffset(offset, child)
{
	this.offset = offset;
	this.child = child;

	this._drawPosSaved = new Coords();
}
{
	VisualOffset.prototype.draw = function(display, drawPos)
	{
		this._drawPosSaved.overwriteWith(drawPos);
		drawPos.add(this.offset);
		this.child.draw(display, drawPos);
		drawPos.overwriteWith(this._drawPosSaved);
	}
}

function VisualPath(vertices)
{
	this.vertices = vertices;

	this._verticesTransformed = [];
}
{
	VisualPath.prototype.draw = function(display, drawPos)
	{
		for (var i = 0; i < this.vertices.length; i++)
		{
			var vertex = this.vertices[i];
			var vertexTransformed = vertex.clone().add(drawPos);
			this._verticesTransformed[i] = vertexTransformed;
		}

		display.drawPath
		(
			this._verticesTransformed,
			display.colorFore
		);
	}
}

function VisualText(text, font)
{
	this.text = text;
	this.font = font;
}
{
	VisualText.prototype.draw = function(display, drawPos)
	{
		display.drawText(this.text, this.font, drawPos, display.colorFore);
	}
}

</script>

</body>
</html>

Advertisements
Posted in Uncategorized | Tagged , , , , , , | Leave a comment

A MIDI Viewer in JavaScript

The JavaScript program below prompts for a MIDI file, loads that file, parses it into text, and displays that text. It also allows the file to be re-saved, though, since the file can’t be edited, re-saving is solely a way to verify whether the parsing was correct.

To see the code in action, copy it into an .html file and open that file in a web browser that runs JavaScript. It is also planned that this code will soon be published as a source code repository at the URL “http://github.com/thiscouldbebetter/MIDIReader.git“.

MIDIReader.png





<div id="divUI">
	MIDI File to Load:<br />
	
	<br />

	File Contents as JSON:<br />
	
	<br />

	Save
</div>



// ui events

function buttonSave_Clicked()
{
	var midiFile = Session.Instance.midiFile;
	var midiFileAsBytes = midiFile.toBytes();

	var bytesToSave = midiFileAsBytes;
	var numberOfBytes = bytesToSave.length;
	var bytesAsArrayBuffer = new ArrayBuffer(numberOfBytes);
	var bytesAsUIntArray = new Uint8Array(bytesAsArrayBuffer);
	for (var i = 0; i &lt; numberOfBytes; i++)
	{
		bytesAsUIntArray[i] = bytesToSave[i];
	}

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

	var downloadLink = document.createElement(&quot;a&quot;);
	downloadLink.href = URL.createObjectURL(bytesAsBlob);
	downloadLink.download = &quot;Song.mid&quot;;
	downloadLink.click();
}

function inputMIDIFile_Changed(event)
{
	var inputMIDIFile = event.target;
	var file = inputMIDIFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event2)
		{
			var fileAsBinaryString = event2.target.result;
			var fileAsBytes = [];
			for (var i = 0; i &lt; fileAsBinaryString.length; i++)
			{
				var byte = fileAsBinaryString.charCodeAt(i);
				fileAsBytes.push(byte);
			}
			var midiFile = MIDIFile.fromBytes(fileAsBytes);
			Session.Instance.midiFile = midiFile;
			var midiFileAsJSON = midiFile.toStringJSON();
			var textareaMIDIFileAsJSON = document.getElementById
			(
				&quot;textareaMIDIFileAsJSON&quot;
			);
			textareaMIDIFileAsJSON.value = midiFileAsJSON;
		}
		fileReader.readAsBinaryString(file);
	}
}

// extensions

function ArrayExtensions()
{
	// Extension class.
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i &lt; this.length; i++)
		{
			var element = this[i];
			if (element != null)
			{
				var key = element[keyName];
				this[key] = element;
			}
		}
		return this;
	}
}

// classes

function ByteStream(bytes)
{
	this.bytes = bytes;
	this.byteOffset = 0;
}
{
	ByteStream.prototype.hasMoreBytes = function()
	{
		return (this.byteOffset &lt; this.bytes.length);
	}

	ByteStream.prototype.readByte = function()
	{
		var returnValue = this.bytes[this.byteOffset];
		this.byteOffset++;
		return returnValue;
	}

	ByteStream.prototype.readBytes = function(numberOfBytes)
	{
		var bytes = [];

		for (var i = 0; i &lt; numberOfBytes; i++)
		{
			var byte = this.readByte();
			bytes.push(byte);
		}

		return bytes;
	}

	ByteStream.prototype.readIntegerBE = function(numberOfBytes)
	{
		var returnValue = 0;

		// &quot;BE&quot; == &quot;Big-Endian&quot;.
		for (var i = 0; i &lt; numberOfBytes; i++)
		{
			var byte = this.readByte();
			returnValue = (returnValue &lt;&lt; 8) | byte;
		}

		return returnValue;
	}

	ByteStream.prototype.readString = function(numberOfBytes)
	{
		var returnValue = &quot;&quot;;
		for (var i = 0; i &lt; numberOfBytes; i++)
		{
			var byte = this.readByte();
			var char = String.fromCharCode(byte);
			returnValue += char;
		}
		return returnValue;
	}

	ByteStream.prototype.readVariableLengthQuantity = function()
	{
		returnValue = 0;
		while (true)
		{
			returnValue = returnValue &lt;&gt; 7) &amp; 1;
			if (bit7 == 0)
			{
				break;
			}
		}

		return returnValue;
	}

	ByteStream.prototype.seek = function(byteOffset)
	{
		this.byteOffset = byteOffset;
		return this;
	}

	ByteStream.prototype.writeByte = function(byte)
	{
		this.bytes.push(byte);
		this.byteOffset++;
		return this;
	}

	ByteStream.prototype.writeBytes = function(bytes)
	{
		for (var i = 0; i &lt; bytes.length; i++)
		{
			var byte = bytes[i];
			this.writeByte(byte);
		}
		return this;
	}

	ByteStream.prototype.writeIntegerBE = function(value, numberOfBytes)
	{
		// &quot;BE&quot; == &quot;Big-Endian&quot;.
		for (var i = 0; i &gt; (8 * iReversed) ) &amp; 255;
			this.writeByte(byte);
		}

		return this;
	}

	ByteStream.prototype.writeString = function(value)
	{
		for (var i = 0; i  0)
			{
				byte |= 128;
			}
			bytes.splice(0, 0, byte);
			value = value &gt;&gt; 7;
		} while (value &gt; 0)

		this.writeBytes(bytes);

		return this;
	}
}

function MIDIFile(chunks)
{
	// Based on file specifications found at the URLs
	// https://www.csie.ntu.edu.tw/~r92092/ref/midi/
	// and
	// http://www.music.mcgill.ca/~ich/classes/mumt306/StandardMIDIfileformat.html.

	this.chunks = chunks;
}
{
	// bytes

	MIDIFile.fromBytes = function(bytes)
	{
		var byteStream = new ByteStream(bytes);

		var chunks = [];

		while (byteStream.hasMoreBytes() == true)
		{
			var chunkTypeCode = byteStream.readString(4);
			var chunkDataLengthInBytes = byteStream.readIntegerBE(4);

			var chunk;

			if (chunkTypeCode == MIDIFileChunk_Header.ChunkTypeCode)
			{
				chunk = MIDIFileChunk_Header.fromBytes(byteStream, chunkDataLengthInBytes);
			}
			else if (chunkTypeCode == MIDIFileChunk_Track.ChunkTypeCode)
			{
				chunk = MIDIFileChunk_Track.fromBytes(byteStream, chunkDataLengthInBytes);
			}
			else
			{
				chunk = MIDIFileChunk_Other.fromBytes
				(
					byteStream, chunkTypeCode, chunkDataLengthInBytes
				);
			}

			chunks.push(chunk);
		}

		var returnValue = new MIDIFile(chunks);

		return returnValue;
	}

	MIDIFile.prototype.toBytes = function()
	{
		var byteStream = new ByteStream([]);

		for (var i = 0; i &gt; 15) &amp; 1;
		var division;
		if (divisionTypeCode == 0)
		{
			var ticksPerQuarterNote = divisionCode;
			division = new MIDIFileChunk_Header_Division_Ticks
			(
				ticksPerQuarterNote
			);
		}
		else
		{
			var framesPerSecondCode = (divisionCode &gt;&gt; 8) &amp; 127;
			var ticksPerFrame = divisionCode &amp; 255;

			var framesPerSecond = framesPerSecondCode; // todo
			// -24 = 24 frames per second
			// -25 = 25 frames per second
			// -29 = 30 frames per second, drop frame
			// -30 = 30 frames per second, non-drop frame

			division = new MIDIFileChunk_Header_Division_Frames
			(
				framesPerSecond, ticksPerFrame
			);
		}

		chunk = new MIDIFileChunk_Header
		(
			formatCode, numberOfTracks, division
		);

		return chunk;
	}

	MIDIFileChunk_Header.prototype.toBytes = function(byteStream)
	{
		byteStream.writeString(MIDIFileChunk_Header.ChunkTypeCode);
		var numberOfDataBytes = 6;
		byteStream.writeIntegerBE(numberOfDataBytes, 4);
		byteStream.writeIntegerBE(this.formatCode, 2);
		byteStream.writeIntegerBE(this.numberOfTracks, 2);
		this.division.toBytes(byteStream);
	}
}

function MIDIFileChunk_Header_Division_Frames(framesPerSecond, ticksPerFrame)
{
	this.framesPerSecond = framesPerSecond;
	this.ticksPerFrame = ticksPerFrame;
}
{
	MIDIFileChunk_Header_Division_Frames.prototype.toBytes = function(byteStream)
	{
		throw "todo";
	}
}

function MIDIFileChunk_Header_Division_Ticks(ticksPerQuarterNote)
{
	this.ticksPerQuarterNote = ticksPerQuarterNote;
}
{
	MIDIFileChunk_Header_Division_Ticks.prototype.toBytes = function(byteStream)
	{
		byteStream.writeIntegerBE(this.ticksPerQuarterNote, 2);
	}
}

function MIDIFileChunk_Other(chunkTypeCode, dataBytes)
{
	this._chunkTypeName = "Other";
	this.chunkTypeCode = chunkTypeCode;
	this.dataBytes = dataBytes;
}
{
	MIDIFileChunk_Other.fromBytes = function(byteStream, chunkTypeCode, chunkDataLengthInBytes)
	{
		var chunkDataBytes = byteStream.readBytes(chunkDataLengthInBytes);
		chunk = new MIDIFileChunk_Other(chunkTypeCode, chunkDataBytes);
		return chunk;
	}

	MIDIFileChunk_Other.prototype.toBytes = function(byteStream)
	{
		byteStream.writeVariableLengthQuantity(this.dataBytes.length);
		byteStream.writeBytes(this.dataBytes);
	}
}

function MIDIFileChunk_Track(dataLengthInBytes, events)
{
	this._chunkTypeName = "Track";
	this.dataLengthInBytes = dataLengthInBytes; // hack
	this.events = events;
}
{
	MIDIFileChunk_Track.ChunkTypeCode = "MTrk";

	// bytes

	MIDIFileChunk_Track.fromBytes = function(byteStream, chunkDataLengthInBytes)
	{
		var chunkDataBytes = byteStream.readBytes(chunkDataLengthInBytes);
		var byteStreamEvents = new ByteStream(chunkDataBytes);
		var events = [];
		var statusByte = null;

		while (byteStreamEvents.hasMoreBytes() == true)
		{
			var delta = byteStreamEvents.readVariableLengthQuantity();					
			
			var statusByteNext = byteStreamEvents.readByte();
			if (statusByteNext &gt;= 128)
			{
				statusByte = statusByteNext;
			}
			else
			{
				// We're in "running status" mode, 
				// so it's not really a status byte.
				// Use the same statusByte as last time,
				// and back up the byteStream so no data is lost.
				byteStreamEvents.byteOffset--;
			}
			var eventTypeCode = (statusByte &gt;&gt; 4) &amp; 15;
			
			var eventDefn
			if (eventTypeCode &lt; 15)
			{
				eventDefn = MIDIFileChunk_Track.fromBytes_EventChannel
				(
					byteStreamEvents, statusByte, eventTypeCode
				);
			}
			else if (eventTypeCode == 15)
			{
				eventDefn = MIDIFileChunk_Track.fromBytes_EventNonChannel
				(
					byteStreamEvents, statusByte
				);
			}

			var event = new MIDIFileEvent(delta, eventDefn);
			
			events.push(event);
		}

		var chunk = new MIDIFileChunk_Track(chunkDataLengthInBytes, events);

		return chunk;
	}	

	MIDIFileChunk_Track.fromBytes_EventNonChannel = function
	(
		byteStreamEvents, statusByte
	)
	{
		var eventDefn = null;

		var eventSubTypeCode = statusByte &amp; 15;

		if (eventSubTypeCode == 0) 
		{
			eventDefn = MIDIFileEventDefn_SystemExclusive.fromBytes(byteStreamEvents);
		}
		else if (eventSubTypeCode == 2)
		{
			// song position pointer
		}
		else if (eventSubTypeCode == 3)
		{
			// song select
		}
		else if (eventSubTypeCode == 6)
		{
			// tune request
		}
		else if (eventSubTypeCode == 7)
		{
			// &quot;f7 sysex event&quot;
			// &quot;escape&quot;
			var length = byteStreamEvents.readVariableLengthQuantity();
			var data = byteStreamEvents.readBytes(length);
		}
		else if (eventSubTypeCode == 8)
		{
			// timing clock
		}
		else if (eventSubTypeCode == 10)
		{
			// &quot;start&quot;
		}
		else if (eventSubTypeCode == 11)
		{
			// &quot;continue&quot;
		}
		else if (eventSubTypeCode == 12)
		{
			// &quot;stop&quot;
		}
		else if (eventSubTypeCode == 14)
		{
			// active sensing
		}
		else if (eventSubTypeCode == 15)
		{
			// &quot;reset&quot;
			// meta
			eventDefn = MIDIFileChunk_Track.fromBytes_EventMeta
			(
				byteStreamEvents
			);
		}

		if (eventDefn == null)
		{
			throw &quot;todo&quot;;
		}

		return eventDefn;
	}

	MIDIFileChunk_Track.fromBytes_EventMeta = function(byteStreamEvents)
	{
		var eventDefn = null;

		var metaTypeCode = byteStreamEvents.readByte();
		if (metaTypeCode == 0)
		{
			// sequence number
			var two = byteStreamEvents.readByte();
		}
		else if (metaTypeCode &lt; 8)
		{
			eventDefn = MIDIFileEventDefn_Meta_Text.fromBytes
			(
				byteStreamEvents, metaTypeCode
			);
		}
		else if (metaTypeCode == 0x20) // 32
		{
			// MIDI channel prefix
			var one = byteStreamEvents.readByte();
		}
		else if (metaTypeCode == MIDIFileEventDefn_Meta_EndOfTrack.MetaTypeCode) // 47
		{
			eventDefn = MIDIFileEventDefn_Meta_EndOfTrack.fromBytes(byteStreamEvents);
		}
		else if (metaTypeCode == MIDIFileEventDefn_Meta_Tempo.MetaTypeCode) // 81
		{
			eventDefn = MIDIFileEventDefn_Meta_Tempo.fromBytes(byteStreamEvents);
		}
		else if (metaTypeCode == 0x54) // 84
		{
			// SMTPE offset
			var five = byteStreamEvents.readByte();
			var hours = byteStreamEvents.readByte();
			var minutes = byteStreamEvents.readByte();
			var seconds = byteStreamEvents.readByte();
			var frames = byteStreamEvents.readByte();
			var hundredthsOfFrame = byteStreamEvents.readByte();
			eventDefn = new MIDIFileEventDefn_Meta_SMTPEOffset
			(
				hours, minutes, seconds, frames, hundredthsOfFrame
			);
		}
		else if (metaTypeCode == MIDIFileEventDefn_Meta_TimeSignature.MetaTypeCode) // 88
		{
			// time signature
			var four = byteStreamEvents.readByte();
			var numerator = byteStreamEvents.readByte();
			var denominatorAsPowerOf2 = byteStreamEvents.readByte();
			// denominator values
			// &quot;2&quot; = quarter note,
			// &quot;3&quot; = eighth note, etc.
			var denominator = Math.pow(2, denominatorAsPowerOf2);
			var midiClocksPerMetronomeClick = byteStreamEvents.readByte();
			var numberOf32ndNotesPer24MIDIClocks = byteStreamEvents.readByte();
			// 24 (hex or dec?) MIDI clocks = &quot;1 quarter note&quot;.
			eventDefn = new MIDIFileEventDefn_Meta_TimeSignature
			(
				numerator, denominator, 
				midiClocksPerMetronomeClick, 
				numberOf32ndNotesPer24MIDIClocks
			);
		}
		else if (metaTypeCode == 0x59) // 89
		{
			// key signature
			var two = byteStreamEvents.readByte();
		}
		else if (metaTypeCode == 0x7F)
		{
			// sequencer-specific meta-event
		}
		else
		{
			throw &quot;Unrecognized metaTypeCode!&quot;;
		}

		if (eventDefn == null)
		{
			throw &quot;todo&quot;;
		}

		return eventDefn;
	}

	MIDIFileChunk_Track.fromBytes_EventChannel = function
	(
		byteStreamEvents, statusByte, eventTypeCode
	)
	{
		var channel = statusByte &amp; 15;

		var eventDefn = null;

		if (eventTypeCode == MIDIFileEventDefn_NoteOff.EventTypeCode) // 8
		{
			eventDefn = MIDIFileEventDefn_NoteOff.fromBytes(byteStreamEvents, channel);
		}
		else if (eventTypeCode == MIDIFileEventDefn_NoteOn.EventTypeCode) // 9
		{
			eventDefn = MIDIFileEventDefn_NoteOn.fromBytes(byteStreamEvents, channel);
		}
		else if (eventTypeCode == 10)
		{
			// polyphonic key pressure
			var keyCode = byteStreamEvents.readByte();
			var pressure = byteStreamEvents.readByte();
			eventDefn = new MIDIFileEventDefn_Pressure(channel, keyCode, pressure);
		}
		else if (eventTypeCode == MIDIFileEventDefn_Controller.EventTypeCode) // 11
		{
			eventDefn = MIDIFileEventDefn_Controller.fromBytes(byteStreamEvents, channel);
		}
		else if (eventTypeCode == MIDIFileEventDefn_ProgramChange.EventTypeCode) // 12
		{
			eventDefn = MIDIFileEventDefn_ProgramChange.fromBytes(byteStreamEvents, channel);
		}
		else if (eventTypeCode == MIDIFileEventDefn_ChannelKeyPressure.EventTypeCode) // 13
		{
			// channel key pressure
			var channelPressure = byteStreamEvents.readByte();
			eventDefn = new MIDIFileEventDefn_ChannelKeyPressure
			(
				channel, keyCode, channelPressure
			);
		}
		else if (eventTypeCode == 14)
		{
			// pitch bend
			var lsb = byteStreamEvents.readByte();
			var msb = byteStreamEvents.readByte();
			var value = (msb &lt;&lt; 8) | lsb;
			eventDefn = new MIDIFileEventDefn_PitchBend(channel, value);
		}
		else
		{
			throw &quot;todo&quot;;
		}

		return eventDefn;
	}

	MIDIFileChunk_Track.prototype.toBytes = function(byteStream)
	{
		byteStream.writeString(MIDIFileChunk_Track.ChunkTypeCode);
		var byteStreamEvents = new ByteStream([]);
		var eventStatusCodeRunning = null;
		for (var i = 0; i &lt; this.events.length; i++)
		{
			var event = this.events[i];
			var eventStatusCode = event.statusCode();
			if (eventStatusCode == eventStatusCodeRunning)
			{
				eventStatusCode = null;
			}
			else if (eventStatusCode &lt; 128)
			{
				eventStatusCodeRunning = eventStatusCode;
			}
			else
			{
				eventStatusCodeRunning = null;
			}
			event.toBytes(byteStreamEvents, eventStatusCode);
		}

		var eventsAsBytes = byteStreamEvents.bytes;
		byteStream.writeIntegerBE(this.dataLengthInBytes, 4);
		byteStream.writeBytes(eventsAsBytes);
	}
}

function MIDIFileEvent(delta, defn)
{
	this.delta = delta;
	this._typeName = 
		defn.constructor.name.split(&quot;_&quot;).slice(1).join(&quot;_&quot;);
	this.defn = defn;
}
{
	MIDIFileEvent.prototype.statusCode = function()
	{
		return this.defn.statusCode();
	}
	
	MIDIFileEvent.prototype.toBytes = function(byteStream, statusCode)
	{
		byteStream.writeVariableLengthQuantity(this.delta);
		if (statusCode != null)
		{
			byteStream.writeByte(statusCode);
		}
		this.defn.toBytes(byteStream);
	}
}

function MIDIFileEventDefn_ChannelKeyPressure()
{
	// todo
}
{
	MIDIFileEventDefn_ChannelKeyPressure.EventTypeCode = 13;
}


function MIDIFileEventDefn_Controller
(
	channel, controllerNumber, controllerValue
)
{
	this.channel = channel;
	this.controllerNumber = controllerNumber;
	this.controllerValue = controllerValue;
}
{
	MIDIFileEventDefn_Controller.EventTypeCode = 11;

	// bytes

	MIDIFileEventDefn_Controller.fromBytes = function(byteStream, channel)
	{
		var controllerNumber = byteStream.readByte();
		var controllerValue = byteStream.readByte();
		var eventDefn = new MIDIFileEventDefn_Controller
		(
			channel, controllerNumber, controllerValue
		);
		return eventDefn;
	}

	MIDIFileEventDefn_Controller.prototype.statusCode = function()
	{
		return (MIDIFileEventDefn_Controller.EventTypeCode &lt;&lt; 4) | this.channel;
	}

	MIDIFileEventDefn_Controller.prototype.toBytes = function(byteStream)
	{
		byteStream.writeByte(this.controllerNumber);
		byteStream.writeByte(this.controllerValue);
	}
}

function MIDIFileEventDefn_Controller_Type(code, name)
{
	this.code = code;
	this.name = name;
}
{
	MIDIFileEventDefn_Controller_Type.Instances = 
		new MIDIFileEventDefn_Controller_Type_Instances();

	function MIDIFileEventDefn_Controller_Type_Instances()
	{
		var ControllerType = MIDIFileEventDefn_Controller_Type;
		this.BankSelect = new ControllerType(0, &quot;BankSelect&quot;);
		this.ModulationWheel = new ControllerType(1, &quot;ModulationWheel&quot;);
		this.BreathControl = new ControllerType(2, &quot;BreathControl&quot;);
		this.FootController = new ControllerType(3, &quot;FootController&quot;);
		this.PortamentoTime = new ControllerType(5, &quot;PortamentoTime&quot;);
		// 6 - data entry
		this.ChannelVolume = new ControllerType(7, &quot;ChannelVolume&quot;);
		// 8 - balance
		this.Pan = new ControllerType(10, &quot;Pan&quot;);
		// 11 - expression controller
		// 12-13 - effect control 1
		// 16-19 - general purpose controller 1-4
		// repeats from 32-63?
		// 64 - damper pedal on/off
		// 65 - portamento on/off
		// 66 - sostenuto on/off
		// 67 - soft pedal on/off
		// 68 - legato footswitch
		// 69 - hold 2
		// 70 - sound variation (sound controller 1)
		// 71 - timbre
		// 72 - release time
		// 73 - attack time
		// 74 - brightness
		// 75-79 - sound controllers 6-10
		// 80-83 - general purpose controllers 5-8
		this.EffectDepth1 = new ControllerType(91, &quot;EffectDepth1&quot;);
		this.EffectDepth2 = new ControllerType(92, &quot;EffectDepth2&quot;);
		this.EffectDepth3 = new ControllerType(93, &quot;EffectDepth3&quot;);
		this.EffectDepth4 = new ControllerType(94, &quot;EffectDepth4&quot;);
		this.EffectDepth5 = new ControllerType(95, &quot;EffectDepth5&quot;);
		// 96 - data entry +1
		// 97 - data entry -1
		// 98 - non-registered parm number lsb
		// 99 - non-registered parm number msb
		// 100 - registered parm number lsb
		// 101 - registered parm number msb
		// 120 - all sound off
		this.ResetAllControllers = new ControllerType(121, &quot;ResetAllControllers&quot;);
		// 122 - local control on/off
		// 123 - all notes off
		// 124 - omni mode off (+ all notes off)
		// 125 - omni mode on (+ all notes off)
		// 126 - poly mode on/off (+ all notes off)
		// 127 - poly mode on (incl moni=off + all notes off)

		this._All = 
		[
			this.BankSelect,
			this.ModulationWheel,
			this.BreathControl,
			this.FootController,
			null, // 4
			this.PortamentoTime,
			null, // 6 - data entry
			this.ChannelVolume,
			null, // 8 - balance
			null, // 9
			this.Pan,
		];

		this._All.addLookups(&quot;name&quot;);
	}
}

function MIDIFileEventDefn_Meta()
{
	// Static class.
}
{
	MIDIFileEventDefn_Meta.StatusCode = 0xFF;
}

function MIDIFileEventDefn_Meta_EndOfTrack()
{
	// Do nothing.
}
{
	MIDIFileEventDefn_Meta_EndOfTrack.MetaTypeCode = 47;

	// bytes

	MIDIFileEventDefn_Meta_EndOfTrack.fromBytes = function(byteStream)
	{
		var zero = byteStream.readByte();
		var eventDefn = new MIDIFileEventDefn_Meta_EndOfTrack();
		return eventDefn;
	}

	MIDIFileEventDefn_Meta_EndOfTrack.prototype.statusCode = function()
	{
		return MIDIFileEventDefn_Meta.StatusCode;
	}

	MIDIFileEventDefn_Meta_EndOfTrack.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(MIDIFileEventDefn_Meta_EndOfTrack.MetaTypeCode);
		byteStream.writeByte(0);
	}
}

function MIDIFileEventDefn_Meta_Tempo(microsecondsPerQuarterNote)
{
	this.microsecondsPerQuarterNote = microsecondsPerQuarterNote;
}
{
	MIDIFileEventDefn_Meta_Tempo.MetaTypeCode = 81;

	MIDIFileEventDefn_Meta_Tempo.fromBytes = function(byteStream)
	{
		var three = byteStream.readByte();
		var microsecondsPerQuarterNote = byteStream.readIntegerBE(3);
		var eventDefn = new MIDIFileEventDefn_Meta_Tempo(microsecondsPerQuarterNote);
		return eventDefn;
	}

	MIDIFileEventDefn_Meta_Tempo.prototype.statusCode = function()
	{
		return MIDIFileEventDefn_Meta.StatusCode;
	}
	
	MIDIFileEventDefn_Meta_Tempo.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(MIDIFileEventDefn_Meta_Tempo.MetaTypeCode);
		byteStream.writeByte(3);
		byteStream.writeIntegerBE(this.microsecondsPerQuarterNote, 3);
	}
}

function MIDIFileEventDefn_Meta_Text(metaTypeCode, text)
{
	this.metaTypeCode = metaTypeCode;
	this.text = text;
}
{
	MIDIFileEventDefn_Meta_Text.fromBytes = function(byteStream, metaTypeCode)
	{
		var textLength = byteStream.readVariableLengthQuantity();
		var text = byteStream.readString(textLength);
		var eventDefn = new MIDIFileEventDefn_Meta_Text(metaTypeCode, text);

		if (metaTypeCode == 1)
		{
			// text
		}
		else if (metaTypeCode == 2)
		{
			// copyright
		}
		else if (metaTypeCode == 3)
		{
			// track/sequence name
		}
		else if (metaTypeCode == 4)
		{
			// instrument name
		}
		else if (metaTypeCode == 5)
		{
			// lyric
		}
		else if (metaTypeCode == 6)
		{
			// marker
		}
		else if (metaTypeCode == 7)
		{
			// cue point
		}

		return eventDefn;
	}

	MIDIFileEventDefn_Meta_Text.prototype.statusCode = function()
	{
		return MIDIFileEventDefn_Meta.StatusCode;
	}
	
	MIDIFileEventDefn_Meta_Text.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(this.metaTypeCode);
		byteStream.writeVariableLengthQuantity(this.text.length);
		byteStream.writeString(this.text);
	}
}

function MIDIFileEventDefn_Meta_TimeSignature
(
	numerator, 
	denominator, 
	midiClocksPerMetronomeClick, 
	numberOf32ndNotesPer24MIDIClocks
)
{
	this.numerator = numerator;
	this.denominator = denominator;
	this.midiClocksPerMetronomeClick = midiClocksPerMetronomeClick;
	this.numberOf32ndNotesPer24MIDIClocks = numberOf32ndNotesPer24MIDIClocks;
}
{
	MIDIFileEventDefn_Meta_TimeSignature.MetaTypeCode = 88;

	MIDIFileEventDefn_Meta_TimeSignature.prototype.statusCode = function()
	{
		return MIDIFileEventDefn_Meta.StatusCode;
	}

	MIDIFileEventDefn_Meta_TimeSignature.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(MIDIFileEventDefn_Meta_TimeSignature.MetaTypeCode);
		byteStream.writeByte(4);
		byteStream.writeByte(this.numerator);
		var denominatorAsPowerOf2 = Math.round
		(
			Math.log(this.denominator) / Math.log(2)
		);
		byteStream.writeByte(denominatorAsPowerOf2);
		byteStream.writeByte(this.midiClocksPerMetronomeClick);
		byteStream.writeByte(this.numberOf32ndNotesPer24MIDIClocks);
	}
}

function MIDIFileEventDefn_NoteOff(channel, keyCode, velocity)
{
	this.channel = channel;
	this.keyCode = keyCode;
	this.velocity = velocity;
}
{
	MIDIFileEventDefn_NoteOff.EventTypeCode = 8;

	MIDIFileEventDefn_NoteOff.fromBytes = function(byteStream, channel)
	{
		var keyCode = byteStream.readByte();
		var velocity = byteStream.readByte();
		var eventDefn = new MIDIFileEventDefn_NoteOff(channel, keyCode, velocity);
		return eventDefn;
	}

	MIDIFileEventDefn_NoteOff.prototype.statusCode = function()
	{
		return (MIDIFileEventDefn_NoteOff.EventTypeCode &lt;&lt; 4) | this.channel;
	}

	MIDIFileEventDefn_NoteOff.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(this.keyCode);
		byteStream.writeByte(this.velocity);
	}
}

function MIDIFileEventDefn_NoteOn(channel, keyCode, velocity)
{
	this.channel = channel;
	this.keyCode = keyCode;
	this.velocity = velocity;
}
{
	MIDIFileEventDefn_NoteOn.EventTypeCode = 9;

	MIDIFileEventDefn_NoteOn.fromBytes = function(byteStream, channel)
	{
		var keyCode = byteStream.readByte();
		var velocity = byteStream.readByte();
		var eventDefn = new MIDIFileEventDefn_NoteOn(channel, keyCode, velocity);
		return eventDefn;
	}

	MIDIFileEventDefn_NoteOn.prototype.statusCode = function()
	{
		return (MIDIFileEventDefn_NoteOn.EventTypeCode &lt;&lt; 4) | this.channel;
	}

	MIDIFileEventDefn_NoteOn.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(this.keyCode);
		byteStream.writeByte(this.velocity);
	}
}

function MIDIFileEventDefn_ProgramChange(channel, programNumber)
{
	this.channel = channel;
	this.programNumber = programNumber;
}
{
	MIDIFileEventDefn_ProgramChange.EventTypeCode = 12;

	MIDIFileEventDefn_ProgramChange.fromBytes = function(byteStream, channel)
	{
		var programNumber = byteStream.readByte();
		var eventDefn = new MIDIFileEventDefn_ProgramChange(channel, programNumber);	
		return eventDefn;
	}

	MIDIFileEventDefn_ProgramChange.prototype.statusCode = function()
	{
		return (MIDIFileEventDefn_ProgramChange.EventTypeCode &lt;&lt; 4) | this.channel;
	}

	MIDIFileEventDefn_ProgramChange.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeByte(this.programNumber);
	}
}

function MIDIFileEventDefn_SystemExclusive(data)
{
	this.data = data;
}
{
	MIDIFileEventDefn_SystemExclusive.StatusCode = 0xF0; // ?

	MIDIFileEventDefn_SystemExclusive.fromBytes = function(byteStream)
	{
		var length = byteStream.readVariableLengthQuantity();
		var data = byteStream.readBytes(length);
		var eventDefn = new MIDIFileEventDefn_SystemExclusive(data);
		return eventDefn;
	}

	MIDIFileEventDefn_SystemExclusive.prototype.statusCode = function()
	{
		return MIDIFileEventDefn_SystemExclusive.StatusCode;
	}

	MIDIFileEventDefn_SystemExclusive.prototype.toBytes = function
	(
		byteStream
	)
	{
		byteStream.writeVariableLengthQuantity(this.data.length);
		byteStream.writeBytes(this.data);
	}
}

function Session()
{
	// Do nothing.
}
{
	Session.Instance = new Session();
}

// tests

function ByteStreamTests()
{}
{
	ByteStreamTests.prototype.readAndWriteVariableLengthQuantity = function()
	{
		var valueBefore = 324;
		var byteStream = new ByteStream([]);
		byteStream.writeVariableLengthQuantity(valueBefore);
		byteStream.seek(0);
		var valueAfter = byteStream.readVariableLengthQuantity();
		if (valueBefore != valueAfter)
		{
			var error = valueBefore + &quot; != &quot; + valueAfter;
			throw error;
		}
		return true;
	}
}






Posted in Uncategorized | Tagged , , , , , , | Leave a comment

A Line-by-Line Text Differencer in JavaScript

The JavaScript code below compares two text files line-by-line to find the differences between them, and then displays those differences with appropriate highlighting. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

This program is similar to a program in a previous post, except that it compares the two text strings line-by-line rather than character-by-character. It is to be hoped that this version will be more practical to use with longer files, but I haven’t really done much testing along those lines.

This version also supports drag-and-drop. And I also created the algorithm in this one without using external references, which may or may not make it easier for me to understand it the next time I read it and to enhance it when I need to.

LineDifferencer.png





Before:<br />

a
b
c
t
u
v
w
x
y
z
d
e
f

<br />

After:<br />

g
h
i
j
t
u
v
www
x
y
z
k
l
m
a

</br>

Compare

Comparison:<br />
<div id="divComparison" style="border:1px solid;"></div>



// ui events

function buttonCompare_Clicked()
{
	var d = document;

	var textareaBefore = d.getElementById("textareaBefore");
	var textareaAfter = d.getElementById("textareaAfter");

	var textBefore = textareaBefore.value;
	var textAfter = textareaAfter.value;

	var comparer = new Comparer();

	var comparison = comparer.compare(textBefore, textAfter);
	var comparisonAsString = comparison.toString();
	var divComparison = d.getElementById("divComparison");
	divComparison.innerHTML = comparisonAsString;
}

function textareaAfter_DraggedOver(event)
{
	event.preventDefault();
}

function textareaAfter_DroppedOnto(event)
{
	event.preventDefault();

	var file = event.dataTransfer.files[0];
	if (file != null)
	{
		var fileType = file.type;
		var isFileText = fileType.startsWith("text/");
		if (isFileText == true)
		{
			var fileReader = new FileReader();
			fileReader.onload = function(event2)
			{
				var fileAsText = event2.target.result;
				var textareaAfter = event.target;
				textareaAfter.value = fileAsText;
			}
			fileReader.readAsText(file);
		}
	}
}

function textareaBefore_DraggedOver(event)
{
	event.preventDefault();
}

function textareaBefore_DroppedOnto(event)
{
	event.preventDefault();

	var file = event.dataTransfer.files[0];
	if (file != null)
	{
		var fileType = file.type;
		var isFileText = fileType.startsWith("text/");
		if (isFileText == true)
		{
			var fileReader = new FileReader();
			fileReader.onload = function(event2)
			{
				var fileAsText = event2.target.result;
				var textareaBefore = event.target;
				textareaBefore.value = fileAsText;
			}
			fileReader.readAsText(file);
		}
	}
}

// extensions

function StringExtensions()
{
	// Extension class.
}
{
	String.prototype.replace = function(substringToBeReplaced, substringToReplaceWith)
	{
		return this.split(substringToBeReplaced).join(substringToReplaceWith);
	}
}

// classes

function Comparer()
{
	// Do nothing.
}
{
	Comparer.prototype.compare = function(before, after)
	{
		var newline = "\n";

		var beforeAsLines = before.split(newline);
		var afterAsLines = after.split(newline);

		function LineGroup(beforeOffset, beforeLines, afterOffset, afterLines)
		{
			this.beforeOffset = beforeOffset;
			this.beforeLines = beforeLines;
			this.afterOffset = afterOffset;
			this.afterLines = afterLines;
		}

		var lineGroupsBeforeAndAfter =
		[
			new LineGroup(0, beforeAsLines, 0, afterAsLines)
		];

		var pairsMatching = [];

		while (true)
		{
			var doAnyGroupsWithMatchesRemain = false;

			for (var g = 0; g &lt; lineGroupsBeforeAndAfter.length; g++)
			{
				var lineGroupBeforeAndAfter = lineGroupsBeforeAndAfter[g];
				var linesBefore = lineGroupBeforeAndAfter.beforeLines;
				var linesAfter = lineGroupBeforeAndAfter.afterLines;

				var linesInLongestMatchSoFar = 0;
				var passagePairForLongestMatchSoFar = null;

				for (var b = 0; b &lt; linesBefore.length; b++)
				{
					var lineBefore = linesBefore[b];

					for (var a = 0; a &lt; linesAfter.length; a++)
					{
						var lineAfter = linesAfter[a];

						var lineBefore2 = lineBefore;
						var bb = b;
						var aa = a;

						while
						(
							bb &lt; linesBefore.length
							&amp;&amp; aa  linesInLongestMatchSoFar)
						{
							linesInLongestMatchSoFar = numberOfLinesMatching;
							passagePairForLongestMatchSoFar = new ComparisonPassagePair
							(
								true, // areBeforeAndAfterSame
								b, // beforeOffset
								numberOfLinesMatching, // beforeLength
								a, // afterOffset
								numberOfLinesMatching // afterLength
							);
						}

					} // end for a

				} // end for b

				if (linesInLongestMatchSoFar &gt; 0)
				{
					doAnyGroupsWithMatchesRemain = true;

					var pairMatching = passagePairForLongestMatchSoFar;

					var linesBeforePrecedingMatch = linesBefore.slice
					(
						0,
						pairMatching.beforeOffset
					);
					var linesBeforeFollowingMatch = linesBefore.slice
					(
						pairMatching.beforeEnd()
					);

					var linesAfterPrecedingMatch = linesAfter.slice
					(
						0,
						pairMatching.afterOffset
					);
					var linesAfterFollowingMatch = linesAfter.slice
					(
						pairMatching.afterEnd()
					);

					lineGroupsBeforeAndAfter.splice(g, 1);

					if (linesBeforeFollowingMatch.length &gt; 0 || linesAfterFollowingMatch.length &gt; 0)
					{
						var lineGroupFollowingMatch = new LineGroup
						(
							lineGroupBeforeAndAfter.beforeOffset + pairMatching.beforeEnd(),
							linesBeforeFollowingMatch,
							lineGroupBeforeAndAfter.afterOffset + pairMatching.afterEnd(),
							linesAfterFollowingMatch
						);
						lineGroupsBeforeAndAfter.splice
						(
							g, 0, lineGroupFollowingMatch
						);
					}

					if (linesBeforePrecedingMatch.length &gt; 0 || linesAfterPrecedingMatch.length &gt; 0)
					{
						var lineGroupPrecedingMatch = new LineGroup
						(
							lineGroupBeforeAndAfter.beforeOffset,
							linesBeforePrecedingMatch,
							lineGroupBeforeAndAfter.afterOffset,
							linesAfterPrecedingMatch
						);
						lineGroupsBeforeAndAfter.splice
						(
							g, 0, lineGroupPrecedingMatch
						);
					}

					pairMatching.beforeOffset +=
						lineGroupBeforeAndAfter.beforeOffset;
					pairMatching.afterOffset +=
						lineGroupBeforeAndAfter.afterOffset;
					pairsMatching.push(pairMatching);

					break; // g
				}

			} // end for each group

			if (doAnyGroupsWithMatchesRemain == false)
			{
				break;
			}

		} // end while

		var pairsToSort = pairsMatching;
		var pairsSorted = [];

		for (var i = 0; i &lt; pairsToSort.length; i++)
		{
			var pairToSort = pairsToSort[i];

			var j;
			for (j = i; j &lt; pairsSorted.length; j++)
			{
				var pairSorted = pairsSorted[j];
				if (pairToSort.beforeIndex &lt;= pairSorted.beforeIndex)
				{
					break;
				}
			}

			pairsSorted.splice(j, 0, pairToSort);
		}

		var pairsMatchingToInterleave = pairsSorted;
		var pairsInterleaved = [];
		for (var i = 0; i  0 || pairMatchingFirst.afterOffset &gt; 0)
		{
			var pairDifferingFirst = new ComparisonPassagePair
			(
				false,
				0,
				pairMatchingFirst.beforeOffset,
				0,
				pairMatchingFirst.afterOffset
			);

			pairsInterleaved.splice(0, 0, pairDifferingFirst);
		}

		var pairMatchingFinal =
			pairsMatchingToInterleave[pairsMatchingToInterleave.length - 1];
		pairsInterleaved.push(pairMatchingFinal);

		var pairMatchingFinalBeforeEnd = pairMatchingFinal.beforeEnd();
		var pairMatchingFinalAfterEnd = pairMatchingFinal.afterEnd();

		if
		(
			pairMatchingFinalBeforeEnd &lt; beforeAsLines.length
			|| pairMatchingFinalAfterEnd &lt; afterAsLines.length
		)
		{
			var pairDifferingFinal = new ComparisonPassagePair
			(
				pairMatchingFinalBeforeEnd,
				beforeAsLines.length - pairMatchingFinalBeforeEnd,
				pairMatchingFinalAfterEnd,
				afterAsLines.length - pairMatchingFinalAfterEnd
			);

			pairsInterleaved.push(pairDifferingFinal);
		}

		var pairsAll = pairsInterleaved;

		var returnValue = new Comparison
		(
			beforeAsLines,
			afterAsLines,
			pairsAll
		);

		return returnValue;

	} // end function
}

function Comparison(beforeAsLines, afterAsLines, passagePairs)
{
	this.beforeAsLines = beforeAsLines;
	this.afterAsLines = afterAsLines;
	this.passagePairs = passagePairs;
}
{
	Comparison.prototype.toString = function()
	{
		var returnValue = &quot;&quot;;

		var newline = &quot;\n&quot;;

		for (var i = 0; i &lt; this.passagePairs.length; i++)
		{
			var pair = this.passagePairs[i];
			var pairAsString = pair.toString(this);
			returnValue += pairAsString + newline;
		}

		return returnValue;
	}
}

function ComparisonPassagePair(areBeforeAndAfterSame, beforeOffset, beforeLength, afterOffset, afterLength)
{
	this.areBeforeAndAfterSame = areBeforeAndAfterSame;
	this.beforeOffset = beforeOffset;
	this.beforeLength = beforeLength;
	this.afterOffset = afterOffset;
	this.afterLength = afterLength;
}
{
	ComparisonPassagePair.prototype.after = function(afterAsLines)
	{
		var returnValue = &quot;&quot;;
		var newline = &quot;\n&quot;;

		for (var i = 0; i &lt; this.afterLength; i++)
		{
			var line = afterAsLines[this.afterOffset + i];
			returnValue += line + newline;
		}

		return returnValue;
	}

	ComparisonPassagePair.prototype.afterEnd = function()
	{
		return this.afterOffset + this.afterLength;
	}

	ComparisonPassagePair.prototype.before = function(beforeAsLines)
	{
		var returnValue = &quot;&quot;;
		var newline = &quot;\n&quot;;

		for (var i = 0; i &lt; this.beforeLength; i++)
		{
			var line = beforeAsLines[this.beforeOffset + i];
			returnValue += line + newline;
		}
		return returnValue;
	}

	ComparisonPassagePair.prototype.beforeEnd = function()
	{
		return this.beforeOffset + this.beforeLength;
	}

    ComparisonPassagePair.prototype.toString = function(comparison)
    {
        var returnValue = &quot;&quot;;

        if (this.areBeforeAndAfterSame == true)
        {
            returnValue = this.before(comparison.beforeAsLines);
            returnValue = this.escapeStringForHTML(returnValue);
        }
        else
        {
            returnValue += &quot;<mark>";
            returnValue += this.escapeStringForHTML
			(
				this.before(comparison.beforeAsLines)
			);
            returnValue += "</mark><mark>";
            returnValue += this.escapeStringForHTML
			(
				this.after(comparison.afterAsLines)
			);
            returnValue += "</mark>";

        }

        return returnValue;
    }

    ComparisonPassagePair.prototype.escapeStringForHTML = function(stringToEscape)
    {
        var returnValue = stringToEscape.replace
        (
            "&amp;", "&amp;"
        ).replace
        (
            "&lt;&quot;, &quot;", "&gt;"
        ).replace
        (
            "\n", "<br />"
        );

        return returnValue;
    }
}






Posted in Uncategorized | Tagged , , , , | Leave a comment

A Color Palette Generator in JavaScript

The JavaScript code below generates a color palette of a specified size from a specified image, then re-draws the image using that palette. (At least, I think that’s what it does. It worked on the first try, which always makes me suspicious as to whether something’s actually working at all.) To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

PaletteGenerator.png





<div id="divUI">

	Image Before:
	
	<div id="divImageBefore"></div>
	<br />

	Number of Colors in Palette:
	
	<br />

	Calculate Palette
	<br />

	Image After:
	<div id="divImageAfter"></div>
	<br />


</div>



// ui events

function buttonPaletteCalculate_Clicked()
{
	var divImageBefore = document.getElementById("divImageBefore");
	var imageBeforeAsCanvas = divImageBefore.children[0];
	if (imageBeforeAsCanvas == null)
	{
		alert("No file specified!");
	}
	else
	{
		var colorsBefore = [];
		var graphicsBefore = imageBeforeAsCanvas.getContext("2d");

		for (var y = 0; y &lt; imageBeforeAsCanvas.height; y++)
		{
			for (var x = 0; x &lt; imageBeforeAsCanvas.width; x++)
			{
				var pixelRGBA = graphicsBefore.getImageData(x, y, 1, 1).data;
				var pixelRGB = [ pixelRGBA[0], pixelRGBA[1], pixelRGBA[2] ];
				var pixelColor = new Color(pixelRGB);
				colorsBefore.push(pixelColor);
			}
		}

		var colorsAfter = Color.colorsMergeToCount(colorsBefore, 16);
		var colorDifference = new Color([]);

		var imageAfterAsCanvas = document.createElement(&quot;canvas&quot;);
		imageAfterAsCanvas.width = imageBeforeAsCanvas.width;
		imageAfterAsCanvas.height = imageBeforeAsCanvas.height;
		var graphicsAfter = imageAfterAsCanvas.getContext(&quot;2d&quot;);

		for (var y = 0; y &lt; imageBeforeAsCanvas.height; y++)
		{
			for (var x = 0; x &lt; imageBeforeAsCanvas.width; x++)
			{
				var pixelRGBA = graphicsBefore.getImageData(x, y, 1, 1).data;
				var pixelRGB = [ pixelRGBA[0], pixelRGBA[1], pixelRGBA[2] ];
				var pixelColorBefore = new Color(pixelRGB);

				var differenceMinSoFar = null;
				var iWithDifferenceMinSoFar = null;

				for (var i = 0; i &lt; colorsAfter.length; i++)
				{
					var pixelColorAfter = colorsAfter[i];
					var difference = colorDifference.overwriteWith
					(
						pixelColorBefore
					).subtract
					(
						pixelColorAfter
					).magnitude();

					if (differenceMinSoFar == null || difference &lt; differenceMinSoFar)
					{
						differenceMinSoFar = difference;
						iWithDifferenceMinSoFar = i;

						if (difference == 0)
						{
							break;
						}
					}
				}

				var colorAfter = colorsAfter[iWithDifferenceMinSoFar];
				var colorAfterAsSystemColor = colorAfter.toSystemColor();
				graphicsAfter.fillStyle = colorAfterAsSystemColor;
				graphicsAfter.fillRect(x, y, 1, 1);
			}
		}

		var divImageAfter = document.getElementById(&quot;divImageAfter&quot;);
		divImageAfter.innerHTML = &quot;&quot;;
		divImageAfter.appendChild(imageAfterAsCanvas);
	}
}

function inputImageFile_Changed(inputImageFile)
{
	var file = inputImageFile.files[0];
	if (file != null)
	{
		var fileReader = new FileReader();
		fileReader.onload = function(event)
		{
			var imageAsDataURL = event.target.result;
			var imageAsImgElement = document.createElement(&quot;img&quot;);
			imageAsImgElement.onload = function(event2)
			{
				var imageAsCanvas = document.createElement(&quot;canvas&quot;);
				imageAsCanvas.width = imageAsImgElement.width;
				imageAsCanvas.height = imageAsImgElement.height;

				var imageAsGraphics = imageAsCanvas.getContext(&quot;2d&quot;);
				imageAsGraphics.drawImage(imageAsImgElement, 0, 0);

				var divImageBefore = document.getElementById(&quot;divImageBefore&quot;);
				divImageBefore.innerHTML = &quot;&quot;;
				divImageBefore.appendChild(imageAsCanvas);
			}
			imageAsImgElement.src = imageAsDataURL;
		}
		fileReader.readAsDataURL(file);
	}
}

// classes

function Color(componentsRGB)
{
	this.componentsRGB = componentsRGB;
}
{
	Color.prototype.add = function(other)
	{
		for (var i = 0; i &lt; other.componentsRGB.length; i++)
		{
			this.componentsRGB[i] += other.componentsRGB[i];
		}
		return this;
	}

	Color.prototype.divideScalar = function(scalar)
	{
		for (var i = 0; i &lt; this.componentsRGB.length; i++)
		{
			this.componentsRGB[i] /= scalar;
		}
		return this;
	}

	Color.prototype.magnitude = function()
	{
		var returnValue = 0;
		for (var i = 0; i &lt; this.componentsRGB.length; i++)
		{
			returnValue += Math.abs(this.componentsRGB[i]);
		}
		return returnValue;
	}

	Color.prototype.overwriteWith = function(other)
	{
		for (var i = 0; i &lt; other.componentsRGB.length; i++)
		{
			this.componentsRGB[i] = other.componentsRGB[i];
		}
		return this;
	}

	Color.prototype.round = function()
	{
		for (var i = 0; i &lt; this.componentsRGB.length; i++)
		{
			this.componentsRGB[i] = Math.round(this.componentsRGB[i]);
		}
		return this;
	}

	Color.prototype.subtract = function(other)
	{
		for (var i = 0; i  numberOfColorsInPalette)
		{
			var differenceMinSoFar = null;
			var iWithDifferenceMinSoFar = null;
			var jWithDifferenceMinSoFar = null;

			for (var i = 0; i &lt; colorsToMerge.length; i++)
			{
				var colorThis = colorsToMerge[i];

				for (var j = i + 1; j &lt; colorsToMerge.length; j++)
				{
					var colorOther = colorsToMerge[j];

					var difference = colorDifference.overwriteWith
					(
						colorOther
					).subtract
					(
						colorThis
					).magnitude();

					if (differenceMinSoFar == null || difference &lt; differenceMinSoFar)
					{
						differenceMinSoFar = difference;
						iWithDifferenceMinSoFar = i;
						jWithDifferenceMinSoFar = j;

						if (differenceMinSoFar == 0)
						{
							i = colorsToMerge.length;
							break;
						}
					}
				}
			}

			var colorI = colorsToMerge[iWithDifferenceMinSoFar];
			var colorJ = colorsToMerge[jWithDifferenceMinSoFar];
			var colorAverage = colorI.add(colorJ).divideScalar(2).round();
			colorsToMerge.splice(jWithDifferenceMinSoFar, 1);
		}

		return colorsToMerge;
	}
}





Posted in Uncategorized | Tagged , , | Leave a comment

A Music Tracker in JavaScript

Below is a screenshot of a music tracker implemented in JavaScript. To see the code, visit the URL “https://github.com/thiscouldbebetter/MusicTracker“. Or, for an online version, visit the URL “https://thiscouldbebetter.neocities.org/MusicTracker/_MusicTracker.html“.

MusicTracker.png

Posted in Uncategorized | Tagged , , , , , , | Leave a comment

A Touch-Typing Trainer in JavaScript

The JavaScript code below implements a simple typing trainer 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 the URL “https://thiscouldbebetter.neocities.org/touchtypingtrainer.html“.

TouchTypingTrainer.png


<html>
<body onload="Session.Instance().uiUpdate();" onkeydown="body_KeyDown(event);">

<p><b>Typing Trainer</b></p>
<div id="divUI">
	<div>
		<label>Lesson:</label></br>
		<select id="selectLesson" onchange="selectLesson_Changed(this);"></select>
	</div><br />
	<div>
		<label>Sequence to Type:</label><br/>
		<input id="inputPresentation" readonly="true" size="32"></input><br />
		<br />
		<label>Type the Sequence Above Here:</label><br />
		<input id="inputResponse" size="32"></input><br />
	</div>
</div>

<script type="text/javascript">

// UI event handlers.

function body_KeyDown(event)
{
	var keyPressed = event.key;
	if (keyPressed == "Enter")
	{
		var inputResponse = document.getElementById("inputResponse");
		var sequenceResponse = inputResponse.value;
		Session.Instance().sequencesCompare(sequenceResponse);			
		inputResponse.value = "";
	}
}

function selectLesson_Changed(selectLesson)
{
	var lessonSelectedAsOption = selectLesson.selectedOptions[0];
	var lessonName = lessonSelectedAsOption.value;
	Session.Instance().lessonSelectByName(lessonName);
}

// extensions

function ArrayExtensions()
{
	// Extension class.
}
{
	Array.prototype.addLookups = function(keyName)
	{
		for (var i = 0; i < this.length; i++)
		{
			var element = this[i];
			var key = element[keyName];
			this[key] = element;
		}
		return this;
	}
}

// classes

function Lesson(name, sequencesToPresent)
{
	this.name = name;
	this.sequencesToPresent = sequencesToPresent;
	this.start();
}
{	
	// methods

	Lesson.prototype.presentationCurrent = function()
	{
		var returnValue;
		
		var sequenceCurrent = this.sequenceCurrent();
		if (sequenceCurrent == null)
		{
			returnValue = 
				"Lesson completed in " 
				+ this.timeToCompleteInSeconds()
				+ " seconds."
		}
		else
		{
			returnValue = sequenceCurrent;
		}
		return returnValue;
	}
	
	Lesson.prototype.sequenceCurrent = function()
	{
		return (this.sequenceIndexCurrent == null ? null : this.sequencesToPresent[this.sequenceIndexCurrent]);
	}
		
	Lesson.prototype.sequenceCurrentAdvance = function()
	{
		this.sequenceIndexCurrent++;
		if (this.sequenceIndexCurrent >= this.sequencesToPresent.length)
		{
			this.timeCompleted = new Date();
		}
		this.uiUpdate();
	}
	
	Lesson.prototype.sequencesCompare = function(sequenceResponse)
	{
		var sequencePresented = this.sequenceCurrent();
		if (sequencePresented == null || sequenceResponse == sequencePresented)
		{
			this.sequenceCurrentAdvance();
		}
	}
	
	Lesson.prototype.start = function()
	{
		this.sequenceIndexCurrent = 0;
		this.timeStarted = new Date();
		this.timeCompleted = null;
	}
		
	Lesson.prototype.timeToCompleteInSeconds = function()
	{
		return (this.timeCompleted - this.timeStarted) / 1000;
	}
	
	// ui

	Lesson.prototype.uiUpdate = function()
	{
		var selectLesson = document.getElementById("selectLesson");
		selectLesson.value = this.name;
	
		var inputPresentation = document.getElementById("inputPresentation");
		var presentationCurrent = this.presentationCurrent();
		inputPresentation.value = presentationCurrent;
		
		var inputResponse = document.getElementById("inputResponse");
		inputResponse.focus();
	}
}

function LessonBuilder(name, build)
{
	this.name = name;
	this.build = build;
}
{
	LessonBuilder.Instances = function()
	{
		if (LessonBuilder._Instances == null)
		{
			LessonBuilder._Instances = new LessonBuilder_Instances();
		}
		return LessonBuilder._Instances;
	}
	
	function LessonBuilder_Instances()
	{
		var numberOfSequences = 4;
	
		this.HomeRowAtRestLeft4 = new LessonBuilder_Generic
		(
			"Home Row at Rest - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		); 
		
		this.HomeRowAtRestRight4 = new LessonBuilder_Generic
		(
			"Home Row at Rest - Right - 4 Chars",
			"jkl;", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		); 

		this.HomeRowAtRest4 = new LessonBuilder_Generic
		(
			"Home Row at Rest - 4 Chars",
			"asdfjkl;", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneAboveLeft4 = new LessonBuilder_Generic
		(
			"Home Row and One Above - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			"qwer", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);		
		
		this.HomeRowAndOneAboveRight4 = new LessonBuilder_Generic
		(
			"Home Row and One Above - Right - 4 Chars",
			"jkl;", // charsToChooseFrom
			"uiop", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);		
		
		this.HomeRowAndOneAbove4 = new LessonBuilder_Generic
		(
			"Home Row and One Above - 4 Chars",
			"asdfjkl;", // charsToChooseFrom
			"qweruiop", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);		
		
		this.HomeRowAndAbove4 = new LessonBuilder_Generic
		(
			"Home Row and Above - 4 Chars",
			"asdfjkl;qweruiop", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);		
						
		this.HomeRowAndOneReachLeft4 = new LessonBuilder_Generic
		(
			"Home Row with Reach - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			"g", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachRight4 = new LessonBuilder_Generic
		(
			"Home Row with One Reach - Right - 4 Chars",
			"jkl;",
			"h",
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReach4 = new LessonBuilder_Generic
		(
			"Home Row with One Reach - 4 Chars",
			"asdfjkl;",
			"gh",
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndReaches4 = new LessonBuilder_Generic
		(
			"Home Row with Reaches - 4 Chars",
			"asdfghjkl;",
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneBelowLeft4 = new LessonBuilder_Generic
		(
			"Home Row and One Below - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			"zxcv", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneBelowRight4 = new LessonBuilder_Generic
		(
			"Home Row and One Below - Right - 4 Chars",
			"jkl;", // charsToChooseFrom
			"m,./", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneBelow4 = new LessonBuilder_Generic
		(
			"Home Row and One Below - 4 Chars",
			"asdfjkl;", // charsToChooseFrom
			"zxcvm,./", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndBelow4 = new LessonBuilder_Generic
		(
			"Home Row and Below - 4 Chars",
			"asdfjkl;zxcvm,./", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachUpLeft4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Up - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			"t", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachUpRight4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Up - Right - 4 Chars",
			"jkl;", // charsToChooseFrom
			"y", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachUp4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Up - 4 Chars",
			"asdfjkl;", // charsToChooseFrom
			"ty", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndReachesUp4 = new LessonBuilder_Generic
		(
			"Home Row and Reaches Up - 4 Chars",
			"asdfjkl;ty", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachDownLeft4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Down - Left - 4 Chars",
			"asdf", // charsToChooseFrom
			"v", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachDownRight4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Down - Right - 4 Chars",
			"jkl;", // charsToChooseFrom
			"b", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndOneReachDown4 = new LessonBuilder_Generic
		(
			"Home Row and One Reach Down - 4 Chars",
			"asdfjkl;", // charsToChooseFrom
			"vb", // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.HomeRowAndReachesDown4 = new LessonBuilder_Generic
		(
			"Home Row and Reaches Down - 4 Chars",
			"asdfjkl;vb", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.Left4 = new LessonBuilder_Generic
		(
			"Left - 4 Chars",
			"qwertasdfgzxcvb", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.Right4 = new LessonBuilder_Generic
		(
			"Right - 4 Chars",
			"yuiophjkl;nm,./", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this.All4 = new LessonBuilder_Generic
		(
			"All - 4 Chars",
			"qwertyuiopasdfghjkl;zxcvbnm,.", // charsToChooseFrom
			null, // charsRequired
			numberOfSequences,
			4 // charsPerSequence
		);
		
		this._All = 
		[
			this.HomeRowAtRestLeft4,
			this.HomeRowAtRestRight4,
			this.HomeRowAtRest4,
			
			this.HomeRowAndOneAboveLeft4,
			this.HomeRowAndOneAboveRight4,
			this.HomeRowAndOneAbove4,
			this.HomeRowAndAbove4,
			
			this.HomeRowAndOneReachLeft4,
			this.HomeRowAndOneReachRight4,
			this.HomeRowAndOneReach4,
			this.HomeRowAndReaches4,
			
			this.HomeRowAndOneBelowLeft4,
			this.HomeRowAndOneBelowRight4,
			this.HomeRowAndOneBelow4,
			this.HomeRowAndBelow4,
			
			this.HomeRowAndOneReachUpLeft4,
			this.HomeRowAndOneReachUpRight4,
			this.HomeRowAndOneReachUp4,
			this.HomeRowAndReachesUp4,

			this.HomeRowAndOneReachDownLeft4,
			this.HomeRowAndOneReachDownRight4,
			this.HomeRowAndOneReachDown4,
			this.HomeRowAndReachesDown4,
			
			this.Left4,
			this.Right4,
			this.All4,
		];
	}
	
	// methods
	
	LessonBuilder.lessonsBuildFromMany = function(builders)
	{
		var returnValues = [];
		for (var i = 0; i < builders.length; i++)
		{
			var builder = builders[i];
			var lesson = builder.build(builder);
			returnValues.push(lesson);
		}
		return returnValues;
	}
}

function LessonBuilder_Generic
(
	name, charsToChooseFrom, charsRequired, numberOfSequences, charsPerSequence
)
{
	this.name = name;
	this.charsToChooseFrom = charsToChooseFrom;
	this.charsRequired = charsRequired;
	this.numberOfSequences = numberOfSequences;
	this.charsPerSequence = charsPerSequence;
}
{
	LessonBuilder_Generic.prototype.build = function(builder)
	{
		var sequences = [];
				
		for (var i = 0; i < this.numberOfSequences; i++)
		{
			var sequence = "";
			
			for (var j = 0; j < this.charsPerSequence; j++)
			{
				var charIndexRandom = Math.floor
				(
					Math.random() * this.charsToChooseFrom.length
				);
				
				var charRandom = 
					this.charsToChooseFrom[charIndexRandom];
				
				sequence += charRandom;
			}
			
			if (this.charsRequired != null)
			{
				var charIndexRandom = Math.floor
				(
					Math.random() * this.charsRequired.length
				);
				
				var charRequired = this.charsRequired[charIndexRandom];
					
				var indexToReplaceAt = Math.floor
				(
					Math.random() * this.charsPerSequence
				);
				
				sequence = 
					sequence.substr(0, indexToReplaceAt)
					+ charRequired
					+ sequence.substr(indexToReplaceAt + 1);
			}
			
			sequences.push(sequence);
		}
		
		var returnValue = new Lesson(builder.name, sequences);
		
		return returnValue;
	}
}

function Session(lessons)
{
	this.lessons = lessons.addLookups("name");
	this.start();
}
{
	Session.Instance = function()
	{
		if (this._Instance == null)
		{
			this._Instance = new Session
			(
				LessonBuilder.lessonsBuildFromMany
				(
					LessonBuilder.Instances()._All
				)
			);
		}
		
		return this._Instance;
	}
	
	// methods
	
	Session.prototype.lessonCurrent = function()
	{
		return (this.lessonIndexCurrent == null ? null : this.lessons[this.lessonIndexCurrent] );
	}
	
	Session.prototype.lessonCurrentAdvance = function()
	{
		this.lessonIndexCurrent++;
		if (this.lessonIndexCurrent >= this.lessons.length)
		{
			this.lessonIndexCurrent = null;
			this.timeCompleted = new Date();
		}
	}	
	
	Session.prototype.lessonSelectByName = function(lessonName)
	{
		var lessonSelected = this.lessons[lessonName];
		var lessonIndex = this.lessons.indexOf(lessonSelected);
		this.lessonIndexCurrent = lessonIndex;
		lessonSelected.start();
		this.uiUpdate();
	}
	
	Session.prototype.presentationCurrent = function()
	{
		var returnValue;
		
		var lessonCurrent = this.lessonCurrent();
		if (lessonCurrent == null)
		{
			returnValue = "All lessons complete!";
		}
		else
		{
			returnValue = lessonCurrent.presentationCurrent();
		}
		
		return returnValue;
	}
	
	Session.prototype.sequenceCurrent = function()
	{
		return this.lessonCurrent().sequenceCurrent();
	}
		
	Session.prototype.sequenceCurrentAdvance = function()
	{
		this.lessonCurrent().sequenceCurrentAdvance();
	}
	
	Session.prototype.sequencesCompare = function(sequenceResponse)
	{
		var lessonCurrent = this.lessonCurrent();
		if (lessonCurrent == null)
		{
			this.lessonIndexCurrent = 0;
		}
		else
		{
			var sequenceCurrent = lessonCurrent.sequenceCurrent();
			if (sequenceCurrent == null)
			{
				this.lessonCurrentAdvance();
			}
			else
			{
				lessonCurrent.sequencesCompare(sequenceResponse);
			}
			this.uiUpdate();
		}
	}
	
	Session.prototype.start = function()
	{
		this.lessonIndexCurrent = 0;
		this.timeStarted = new Date();
		this.timeCompleted = null;
	}	
		
	Session.prototype.timeToComplete = function()
	{
		return this.timeCompleted - this.timeStarted;
	}
	
	Session.prototype.uiUpdate = function()
	{
		var selectLesson = document.getElementById("selectLesson");
		var selectOptions = selectLesson.options;
		if (selectOptions.length == 0)
		{
			for (var i = 0; i < this.lessons.length; i++)
			{
				var lesson = this.lessons[i];
				var lessonAsSelectOption = document.createElement("option");
				lessonAsSelectOption.value = lesson.name;
				lessonAsSelectOption.innerHTML = lesson.name;
				selectOptions.add(lessonAsSelectOption);
			}
		}
		this.lessonCurrent().uiUpdate();
	}
}
</script>

</body>
</html>

Posted in Uncategorized | Tagged , , , , | Leave a comment

Drag and Drop with External Files in HTML5 and JavaScript

The JavaScript code below, when run, allows the user to drag and drop image files onto a box, upon which the image and its file name will be displayed. To see it in action, copy it into an .html file and open that file in a web browser that runs JavaScript.

DragAndDropImageViewer.png


<html>
<body>

<div 
	id="divDropTarget" 
	style="border:1px solid" 
	ondragover="divDropTarget_DraggedOver(event);" 
	ondrop="divDropTarget_DroppedOnto(event);" 
>
	<label>Drag an image file into this box to display it.</label>
</div>

<script type="text/javascript">

// UI event handlers.

function divDropTarget_DraggedOver(event)
{
	event.preventDefault();	
}

function divDropTarget_DroppedOnto(eventDropped)
{
	eventDropped.preventDefault();

	var divDropTarget = document.getElementById("divDropTarget");

	var filesDropped = eventDropped.dataTransfer.files;
	for (var i = 0; i < filesDropped.length; i++) 
	{
		var file = filesDropped[i];
		var fileType = file.type;
		var isFileAnImage = fileType.startsWith("image/");
		if (isFileAnImage == true)
		{
			var fileReader = new FileReader();
			fileReader.onload = function(eventFileLoaded) 
			{
				var divFile = document.createElement("div");
				divFile.style.border = "1px solid";

				var fileAsDataURL = eventFileLoaded.target.result;
				var imgContent = document.createElement("img");
				imgContent.src = fileAsDataURL;
				divFile.appendChild(imgContent);

				var spanFileName = document.createElement("span");
				spanFileName.innerHTML = file.name;
				divFile.appendChild(spanFileName);

				divDropTarget.appendChild(divFile);
			}
			fileReader.readAsDataURL(file);
		}
	}
}

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


Posted in Uncategorized | Tagged , , , | Leave a comment