Source: View.js

/*
This file is part of SeaSound.

SeaSound is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.

SeaSound is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.

You should have received a copy of the GNU General Public License along with SeaSound. If not, see <https://www.gnu.org/licenses/>.
*/

// TODO: Add seekbars to widgets
// TODO: renderCSD should emit a statements rather than B statements for seek time, need to fix this.
//			for some reason a statements don't work in my version of csound.
// TODO: Tracklane section requires clicking to redraw
// TODO: All coords in widgets need to be modified to be screen agnostic. I.e., we need a real global/local coord split.
// TODO: Make all widget interfaces modal like graph and tracklane, then add note highlighting+copy/paste/delete functionality.

/**
* The view class stores the state of the view of the app and handles all interactions with the DOM and other widgets.
* @class
* @public
*/
class View
{
	/** 
	* The TrackLane object of the program. 
	* This widget corresponds to the canvas in the playlist editor of the program.
	*/
	trackLaneObject = new TrackLaneCanvas("trackLaneCanvas",10,20);

	/**
	*  An array to store the list of parameters for the in progress instrument
	*/
	paramList = new Array();

	/**
	* The instrument map contains all the instruments keyed by name.
	*/
	instrumentMap = new Map();

	/**
	* The track map contains all tracks keyed by name
	*/
	trackMap = new Map();

	/**
	* The default number of cells for a pianoroll widget.
	*/
	pianoRollVCellDefault = 176;

	/** 
	* The default number of vertical divisions for slider type widgets
	*/
	sliderVCellDefault = 50;

	//snapAmount = 1;

	/**
	* Array containing [name, data] pairs corresponding to all loaded audio files
	*/
	audioFiles = new Array();

	/**
	* Opens the corresponding tab.
	* @param {string} tabName - The name of the tab to be opened.
	* @param {string} btnID - The button clicked to open the tab.
	*/
	OpenTab(tabName, btnID) 
	{
		var i;
		var x = document.getElementsByClassName("tab");

		// Hide all the tabs
		for (i = 0; i < x.length; i++) x[i].style.display = "none";

		// Color all the tab buttons 
		var x = document.getElementsByClassName("tab-button");
		for (i = 0; i < x.length; i++) x[i].style.background = "black";

		// Display the selected tab and color in its corresponding button
		document.getElementById(tabName).style.display = "block";
		document.getElementById(btnID).style.background = "green";

		// Reset the parameters on the track editor tab for consistency
		this.ResetParameter();
	}

	/**
	* Updates display when drop down is changed.
	* This is mainly used for switching tracks/instruments in the track and instrument
	* editor tabs.
	* @param {string} divId - The div containing the canvases to select from.
	* @param {string} value - The canvas to be selected.
	*/
	SelectDropDown(divId,value)
	{
		// Get the children canvases
		let children = document.getElementById(divId).children;
		// Hide whichever canvases are not currently selected and display the one that is
		for (let i = 0; i < children.length; i++)
			if (children[i].id == value) children[i].style.display = "inline";
			else children[i].style.display = "none";
		// Changes track editor tab parameter back to 0 regardless of which tab we call this from
		this.ResetParameter();
	}

	/**
	* Handles selection in playlist editor tab.
	*/
	PatternSelect()
	{
		let sel = document.getElementById('pattern-select'); // get the select tag
		let instrument = sel.options[sel.selectedIndex].text; // the text of the selected option
		// get the number of notes
		let notes = this.trackMap.get(instrument)[0].getNotes();
		// get the number of cells per beat
		let beatsPerCell = this.trackMap.get(instrument)[0].getBeatsPerCell();
		// Get the number of beats per block
		let beatsPerBlock = document.getElementById('playlist-bpb').value;
		if (beatsPerBlock == "") beatsPerBlock = document.getElementById('playlist-bpb').placeholder;
		beatsPerBlock = Number(beatsPerBlock);
		// Convert to the number of blocks rounded to the nearest integer
		// TODO: Should this be ceiling, round or floor?
		// TODO: I think this unit conversion is wrong
		let blocks = Math.ceil(beatsPerCell*notes/(beatsPerBlock));
		this.trackLaneObject.setBlockSize(blocks);
		this.trackLaneObject.setBlockName(instrument);
	}

	/**
	* Hides all canvases attached to the specified div.
	* @param {string} divId - The div to hide attached canvases of.
	*/
	HideAllCanvases(divId)
	{
		// Get the children canvases
		let children = document.getElementById(divId).children;
		// Hide all canvases
		for (let i = 0; i < children.length; i++) children[i].style.display = "none";
		// Changes track editor tab parameter back to 0 regardless of which tab we call this from
		this.ResetParameter();
	}

	/**
	* Add canvas to either the instrument editor or track editor.
	* @param {string} canvasDiv - The canvas div to modify (differs based on whether this is an instrument or track).
	* @param {string} prefix - Prefix used to specify whether the new canvas is an instrument or track.
	* @param {string} name - The name of the instrument/track to be added.
	*/
	AddCanvas(canvasDiv,prefix,name)
	{
		// if the canvasDiv or name is the empty string end early
		if (canvasDiv == "" || name == "") return;

		// Only add new options that don't already exist
		let optionCount = document.getElementById(canvasDiv+"-select").options.length;
		for (let i = 0; i < optionCount; i++)
			if (prefix+"-"+name == document.getElementById(canvasDiv+"-select").options[i].value)
				return;

		// load the display based on which type of canvas we are dealing with
		switch (canvasDiv)
		{
			case "instrument-canvases":
				this.InstrumentCanvasHelper(canvasDiv,name);
				break;
			case "track-canvases": 
				this.TrackCanvasHelper(canvasDiv,name);
				break;
			default:
				break;
		}
	}

	/**
	* Hack to catch enter button presses on form inputs for adding canvases.
	*/
	EnterHandler(e,div,prefix,name)
	{
		if (e && e.keyCode == 13) this.AddCanvas(div,prefix,name);
	}

	/**
	* Deletes value from select tag.
	* @param {string} select - Id of select tag to delete from.
	* @param {string} value - The value to be deleted.
	*/
	DeleteSelectOptionHelper(select,value)
	{
		let selectTag = document.getElementById(select);
		for (var i=0; i<selectTag.length; i++)
			if (selectTag.options[i].value == value)
				selectTag.remove(i);
	}

	/**
	* Delete the currently selected track.
	*/
	DeleteTrackSelection()
	{
		let sel = document.getElementById('track-canvases-select'); // get the select tag
		let value = sel.value; // get the value selected
		let name = sel.options[sel.selectedIndex].text; // the text of the selected option
		this.DeleteSelectOptionHelper('track-canvases-select',value); // delete the options from the select tag
		this.DeleteSelectOptionHelper('pattern-select',value); // same
		document.getElementById('track-canvases-select').value = ""; // reset select value
		document.getElementById('pattern-select').value = ""; // same
		this.HideAllCanvases('track-canvases'); // hide canvases
		this.trackMap.delete(name); // delete selection from track map
	}

	/**
	* Delete the currently selected instrument.
	*/
	DeleteInstrumentSelection()
	{
		let sel = document.getElementById('instrument-canvases-select'); // get the select tag
		let value = sel.value; // the value selected
		let name = sel.options[sel.selectedIndex].text; // the text of the selected option
		this.DeleteSelectOptionHelper('instrument-canvases-select',value); // delete options from select tag
		document.getElementById('instrument-canvases-select').value = ""; // reset select value
		this.HideAllCanvases('instrument-canvases'); // hide canvases
		this.instrumentMap.delete(name); // delete selection from instrument map

		// Remove the corresponding entry in the track modal dropdown
		let instList = document.getElementById("instruments-datalist");
		for (var i=0; i<instList.options.length; i++)
			if (instList.options[i].value == name)
				instList.children[i].remove();
	}

	/**
	* Increase the blocksize for the playlist editor.
	*/
	IncrementPlaylistBlockSize()
	{
		this.trackLaneObject.incrementBlockSize();
	}

	/**
	* decrease the blocksize for the playlist editor.
	*/
	DecrementPlaylistBlockSize()
	{
		this.trackLaneObject.decrementBlockSize();
	}

	/**
	* Reset the playlist object.
	*/
	ResetPlaylist()
	{
		let vCells = document.getElementById("playlist-vertical-cells").value;
		let hCells = document.getElementById("playlist-horizontal-cells").value;
		if (vCells == "") vCells = 20;
		else vCells = Number(vCells);
		if (hCells == "") hCells = 40;
		else hCells = Number(hCells);
		this.trackLaneObject.reset(hCells,vCells);
	}

	/**
	* Display next parameter for the currently selected instrument.
	*/
	NextParameter()
	{
		let name = document.getElementById("track-canvases-select").value;
		name = name.replace(/instrument-/g,"");
		let children = document.getElementById("instrument-"+name).children;
		for (let i = 0; i < children.length; i++)
		{
			if (children[i].style.display == "inline")
			{
				children[i].style.display = "none";
				children[(i+1)%children.length].style.display = "inline";
				document.getElementById("param-num").innerText = "Current Parameter: "+(i+1)%children.length;
				break;
			}
		}
	}

	/**
	* Display previous parameter for the currently selected instrument.
	*/
	PrevParameter()
	{
		let name = document.getElementById("track-canvases-select").value;
		name = name.split("-")[1];
		let children = document.getElementById("instrument-"+name).children;
		for (let i = 0; i < children.length; i++)
		{
			if (children[i].style.display == "inline")
			{
				children[i].style.display = "none";
				children[(i-1+children.length)%children.length].style.display = "inline";
				document.getElementById("param-num").innerText = "Current Parameter: "+(i+1)%children.length;
				break;
			}
		}
	}

	/**
	* Reset display of currently selected instrument to show only the 0th parameters widget and hide all others.
	*/
	ResetParameter()
	{
		let name = document.getElementById("track-canvases-select").value;
		name = name.split("-")[1];
		let ele = document.getElementById("instrument-"+name);
		if (ele == null) return;
		let children = ele.children;
		for (let i = 0; i < children.length; i++)
		{
			children[i].style.display = "none";
		}
		children[0].style.display = "inline";
		document.getElementById("param-num").innerText = "Current Parameter: "+0;
	}

	/**
	* This is a hack for reopening the dialog on adding new parameters.
	* The methods AddParameter() and RemoveParameter() toggle dialogShouldReopen.
	* We can then check dialogShouldReopen on closing our dialog to determine whether we should reshow the dialog.
	*/
	dialogShouldReopen = false;

	/**
	* Check if the track dialog modal needs reopened. If so reshow it.
	*/
	CheckDialogReopen()
	{
		if (!this.dialogShouldReopen) return;
		document.getElementById("track-dialog").showModal();
		this.dialogShouldReopen = false;
	}

	/**
	* Add a a parameter to the new instrument modal dialog.
	*/
	AddParameter()
	{
		//get the selected value
		let selectedValue = document.getElementById("parameter-type-select").value;

		//get the tag to add parameters to
		let pListTag = document.getElementById("param-list");

		//create the tags
		let newRow = document.createElement("tr");
		let content = document.createElement("td");

		// The first "starred" parameter widget is in charge of triggering notes
		// the remaining widgets control parameters for the trigger widget
		if (pListTag.childElementCount == 0) content.innerText = selectedValue + " *";
		else content.innerText = selectedValue;

		//build the new element
		newRow.appendChild(content);
		pListTag.appendChild(newRow);

		//add to our list of parameters
		this.paramList.push(selectedValue);

		// The dialog needs to reopen when we add parameters
		this.dialogShouldReopen = true;
	}

	/**
	* Remove a a parameter to the new instrument modal dialog.
	*/
	RemoveParameter()
	{
		if (this.paramList.length == 0) return;
		this.paramList.splice(0,1); // remove the first item in the list
		let params = document.getElementById("param-list"); // get the params tag
		let elements = params.getElementsByTagName("tr"); // get the elements of params tag
		params.removeChild(elements[0]); // remove the first child of the params tag
		if (elements[0] != null) elements[0].innerText += " *"; // readd the star to first element
		this.dialogShouldReopen = true; // dialog should reopen when we remove params
	}

	/**
	* Helper to build all instrument canvases.
	* @param {string} canvasDiv - The div containing instrument canvases.
	* @param {string} name - The name of the instrument to be added.
	*/
	InstrumentCanvasHelper(canvasDiv,name)
	{
		// Figure out what type of instrument input we are doing
		let type = document.getElementById("new-instrument-input-type").value;

		// add the associated select entry
		let selectEle = document.getElementById(canvasDiv+"-select");
		let newOption = document.createElement("option");
		newOption.innerText = name;
		selectEle.append(newOption);

		// add to the instrument data list in the track modal
		let dataList = document.getElementById("instruments-datalist");
		let listOption = document.createElement("option");
		listOption.value = name;
		dataList.append(listOption);

		if (type=="graph")
		{
			// add the associated canvas tag
			let ele = document.getElementById(canvasDiv);
			let newCanvas = document.createElement("canvas");
			newCanvas.setAttribute("tabindex","1");
			ele.appendChild(newCanvas);

			newCanvas.setAttribute("id","instrument-"+name);
			newOption.setAttribute("value","instrument-"+name);
			newCanvas.setAttribute("class","trackLaneCanvas");
			let instrumentCanvasObject = new GraphDiagramCanvas("instrument-"+name,name,20);

			// Add the instrument canvas to our map of all instruments
			this.instrumentMap.set(this.CleanName(name),instrumentCanvasObject);
		}
		else
		{
			// add the associated text area tag
			let ele = document.getElementById(canvasDiv);
			let newTextArea = document.createElement("textarea");
			newTextArea.setAttribute("tabindex","1");
			newTextArea.setAttribute("spellcheck","false");
			ele.appendChild(newTextArea);

			newTextArea.setAttribute("id","instrument-"+name);
			newTextArea.setAttribute("cols","160");
			newTextArea.setAttribute("rows","20");
			newOption.setAttribute("value","instrument-"+name);
			newTextArea.setAttribute("class","trackLaneCanvas");
			let instrumentCanvasObject = new TextAreaInstrumentCanvas("instrument-"+name,name);

			// Add the instrument canvas to our map of all instruments
			this.instrumentMap.set(this.CleanName(name),instrumentCanvasObject);
		}

		// Clear out old instrument name
		document.getElementById("instrument-name").value = "";
	}

	/**
	* Helper to build all track canvases.
	* @param {string} canvasDiv - The div containing track canvases.
	* @param {string} name - The name of the track to be added.
	*/
	TrackCanvasHelper(canvasDiv,name)
	{
		// Exit early if no parameters are specified on confirm button press
		if (this.paramList.length == 0) return;

		// Read in input arguments 
		let hCells = document.getElementById("track-horizontal-cells").value;
		if (hCells == "") hCells = Number(document.getElementById("track-horizontal-cells").placeholder);
		else hCells = Number(hCells);
		let beatsPerCell = document.getElementById("track-beats-per-cell").value;
		if (beatsPerCell == "") beatsPerCell = Number(document.getElementById("track-beats-per-cell").placeholder);
		else beatsPerCell = Number(beatsPerCell);

		// add a div to contain all our parameter canvases
		let ele = document.getElementById(canvasDiv);
		let instDiv  = document.createElement("div");
		instDiv.setAttribute("id","instrument-"+name);
		ele.appendChild(instDiv);

		// add the associated select entry
		let selectEle = document.getElementById(canvasDiv+"-select");
		let newOption = document.createElement("option");
		newOption.value = "instrument-"+name;
		newOption.innerText = name;
		selectEle.append(newOption);

		// Display the currently selected parameter
		document.getElementById("param-num").innerText = "Current Parameter: 0";

		// Collect the canvas objects in a list
		let tempCanv = Array();
		// create one canvas per parameter
		for (let i = 0; i < this.paramList.length; i++)
		{
			// create the canvas
			let newCanvas = document.createElement("canvas");
			newCanvas.setAttribute("tabindex","1");
			newCanvas.setAttribute("id","track-p"+i+"-"+name);
			//newCanvas.setAttribute("class","pianoRollCanvas");
			if (i==0) newCanvas.style.display = "inline";
			else newCanvas.style.display = "none";
			instDiv.appendChild(newCanvas);
			let canvObj = null;
			//if (this.paramList[i]=="Pianoroll")canvObj=new PianoRollCanvas("track-p"+i+"-"+name,vCells,hCells);
			if (this.paramList[i] == "Pianoroll")
				canvObj=new PianoRollCanvas("track-p"+i+"-"+name,name,this.pianoRollVCellDefault,hCells,beatsPerCell);
			else if (this.paramList[i] == "Lollipop")
				canvObj=new SliderCanvas("track-p"+i+"-"+name,name,this.sliderVCellDefault,hCells,beatsPerCell,"lollipop");
			else if (this.paramList[i] == "Bars")
				canvObj=new SliderCanvas("track-p"+i+"-"+name,name,this.sliderVCellDefault,hCells,beatsPerCell,"solid");
			else if (this.paramList[i] == "Event")
				canvObj=new CodedEventCanvas("track-p"+i+"-"+name,name,hCells,beatsPerCell);
			else 
				canvObj=new PianoRollCanvas("track-p"+i+"-"+name,name,this.pianoRollVCellDefault,hCells,beatsPerCell);
			tempCanv.push(canvObj);
		}

		// Add the new track to our map of all tracks
		this.trackMap.set(this.CleanName(name),tempCanv);

		// Register the instruments with each other
		let instname = document.getElementById("instrument-for-track").value
			if (instname == "") instname = "EMPTY-INSTRUMENT";
		for (let i = 0; i < tempCanv.length; i++)
			tempCanv[i].registerInstrument(tempCanv,instname);

		// Set up the canvas trigger modes
		tempCanv[0].setTriggerMode(true);
		for (let i = 1; i < tempCanv.length; i++) tempCanv[i].setTriggerMode(false);

		// Playlist editor needs an associated pattern entry too
		let newPat = document.getElementById("pattern-select");
		let newOpt = document.createElement("option");
		newOpt.innerText = name;
		newOpt.setAttribute("value","instrument-"+name);
		newPat.append(newOpt);

		// Reset the parameter list array here and the parameter list tag in our modal dialog
		this.paramList = new Array();
		let paramListTag = document.getElementById("param-list");
		while (paramListTag.firstChild) paramListTag.removeChild(paramListTag.lastChild);

		// Reset the input boxes
		document.getElementById("track-name").value = "";
		document.getElementById("track-horizontal-cells").value = "";
		//document.getElementById("track-vertical-cells").value = "";
	}

	/**
	* Delete whitespace from the input name string
	* @param {string} name - The string containing the name we want to clean.
	*/
	CleanName(name)
	{
		return name.replace(/\s+/g, '');
	}

	/**
	* Configures the currently selected node based on the node dialog in the instrument editor.
	*/
	configureNode()
	{
		let name = document.getElementById("node-name").value;
		let inputs = document.getElementById("node-inputs").value;
		let outputs = document.getElementById("node-outputs").value;
		let type = document.getElementById("node-output-type").value;
		outputs = this.CleanName(outputs); // clear whitespace
		outputs = outputs.toLowerCase(); // convert to lower case
		outputs = outputs.split(','); // split on commas
		let allowedCases = new Set(["a","k","i","ga","gk","gi","p","S","pvs","w"]); // Collection of allowed values
		outputs = outputs.filter((s) => { return allowedCases.has(s); }); // Filter array using collection
		let sel = document.getElementById('instrument-canvases-select'); // get the select tag
		let instrument = sel.options[sel.selectedIndex].text; // the text of the selected option
		instrument = this.CleanName(instrument);
		if (instrument == "") return;
		this.instrumentMap.get(instrument).configureNode(name,inputs,outputs,type);
	}

	/**
	* Render the currently selected instrument to text and display in a pop up modal.
	*/
	renderInstrument()
	{
		// Get the instrument text name
		let sel = document.getElementById('instrument-canvases-select'); // get the select tag
		let instrument = sel.options[sel.selectedIndex].text; // the text of the selected option
		instrument = this.CleanName(instrument);
		if (instrument == "") return;
		// Get a string with the instrument code
		let outString = this.instrumentMap.get(instrument).renderToText();
		// Print the instrument code to modal in browser
		document.getElementById("instr-code-dialog").showModal();
		document.getElementById("instrument-code-dialog-output").textContent = outString;
	}
	/**
	* Render the currently selected track.
	* @param {number} offset - A time to offset generated times of track by.
	* @param {boolean} displayModal - If true displays a modal containing generated code.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderTrack(offset,displayModal)
	{
		// Get the track text name
		// TODO: This line with track seems unneeded
		let track = document.getElementById('track-canvases-select').textContent; // get the select tag
		let sel = document.getElementById('track-canvases-select'); // get the select tag
		track = sel.options[sel.selectedIndex].text; // the text of the selected option
		track = this.CleanName(track);
		// Get the beats per minute of the project
		let bpmText = document.getElementById('playlist-bpm').value; // get the select tag
		if (bpmText == "") bpmText = document.getElementById('playlist-bpm').placeholder;
		// Get the track
		let params = this.trackMap.get(track)
		// Get the note output for the triggering parameter, this includes the start and duration times
		let paramList = params[0].getNoteOutput(Number(bpmText));
		// Prefix each paramList element with the name of the selected instrument
		for (let i = 0; i < paramList.length; i++) paramList[i].unshift(params[0].getName());
		// Add offset times to start times
		for (let i = 0; i < paramList.length; i++) paramList[i][1] += offset;
		// Get the remaining parameters
		for (let i = 1; i < params.length; i++)
		{
			let out = params[i].getNoteOutput(Number(bpmText));
			for (let j = 0; j < out.length; j++) paramList[j].push(out[j][2]);
		}
		// For convenience sort the notes by their start times, csound does this anyway, so this is for easy reading
		params.sort(function(a,b){ return a[1] > b[1]; });
		// Convert the track to a string
		let outStr = "";
		for (let i = 0; i < paramList.length; i++) // for every note 
		{
			outStr += "i \""+paramList[i][0]+"\""; // instrument name, named instruments are surrounded in quotes
			for (let j = 1; j < paramList[i].length; j++) // for every parameter of the note
			{
				outStr += " "+String(paramList[i][j]); // add the parameter to the current line
			}
			outStr += "\n";
		}

		// Print the instrument code to modal in browser
		if (displayModal)
		{
			document.getElementById("track-code-dialog").showModal();
			document.getElementById("track-code-dialog-output").textContent = outStr;
		}
		return outStr;
	}
	/**
	* Render the entire score.
	* @param {boolean} displayModal - If true displays a modal containing generated code.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderScore(displayModal)
	{
		// Get the beats per minute of the project
		let bpmText = document.getElementById('playlist-bpm').value; // get the select tag
		if (bpmText == "") bpmText = document.getElementById('playlist-bpm').placeholder;
		// Get the beats per minute of the project
		let bpbText = document.getElementById('playlist-bpb').value; // get the select tag
		if (bpbText == "") bpbText = document.getElementById('playlist-bpb').placeholder;

		// get the events we intend to output
		let outEvents = this.trackLaneObject.getOffsetsAndNames(Number(bpmText),Number(bpbText));
		// For convenience sort all of the events
		outEvents.sort(function(a,b){ return a[1] > b[1]; });
		// We will store the score in this string
		let score = "";
		// Get all of the track blocks
		for (let i = 0; i < outEvents.length; i++)
		{
			score += "// track="+outEvents[i][0] + ", offset="+outEvents[i][1]+"\n";
			score += this.renderTrackByName(Number(bpmText),outEvents[i][0],outEvents[i][1]);
			score += "\n"; // add a trailing newline
		}
		// Print the score code to modal in browser
		if (displayModal)
		{
			document.getElementById("score-code-dialog").showModal();
			document.getElementById("score-code-dialog-output").textContent = score;
		}
		return score;
	}	

	/**
	* Render a track by name.
	* @param {number} bpm - Bpm to use for rendering track.
	* @param {string} - The name of the track to render.
	* @param {number} offset - Time to offset note events by.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderTrackByName(bpm,name,offset)
	{
		// Get the track
		let params = this.trackMap.get(name);
		// Geet the note output for the triggering parameter, this includes the start and duration times
		let paramList = params[0].getNoteOutput(bpm);
		// Prefix each paramList element with the name of the selected instrument
		for (let i = 0; i < paramList.length; i++) paramList[i].unshift(params[0].getName());
		// Add offset times to start times
		for (let i = 0; i < paramList.length; i++) paramList[i][1] += offset;
		// Get the remaining parameters
		for (let i = 1; i < params.length; i++)
		{
			let out = params[i].getNoteOutput(bpm);
			for (let j = 0; j < out.length; j++) paramList[j].push(out[j][2]);
		}
		// For convenience sort the notes by their start times, csound does this anyway, so this is for easy reading
		params.sort(function(a,b){ return a[1] > b[1]; });
		// Convert the track to a string
		let outStr = "";
		for (let i = 0; i < paramList.length; i++) // for every note 
		{
			outStr += "i \""+paramList[i][0]+"\""; // instrument name, named instruments are surrounded in quotes
			for (let j = 1; j < paramList[i].length; j++) // for every parameter of the note
			{
				outStr += " "+String(paramList[i][j]); // add the parameter to the current line
			}
			outStr += "\n";
		}
		return outStr;
	}
	/**
	* Render the orchestra.
	* @param {boolean} displayModal - true/false to display a modal on the page containing the orchestra code.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderOrchestra(displayModal)
	{
		// Get the instrument text name
		// TODO: Is this line needed?
		let instrument = document.getElementById('instrument-canvases-select').textContent; // get the select tag
		instrument = this.CleanName(instrument);
		// Get a string with the instrument code
		//let outString = this.instrumentMap.get(instrument).renderToText();
		let outString = "";
		for (const [key,value] of this.instrumentMap)
		{
			outString += "// instrument="+value.getName()+"\n";
			outString += value.renderToText();
			outString += "\n";
		}
		// Print the instrument code to modal in browser
		if (displayModal)
		{
			document.getElementById("instr-code-dialog").showModal();
			document.getElementById("instrument-code-dialog-output").textContent = outString;
		}
		return outString;
	}
	/**
	* Render and play back the entire project so far.
	*/
	playTrack()
	{
		// get the score and the orchestra strings
		let csd = this.renderCSD(false);
		playCode(csd);
	}
	/**
	* Render and play back the currently selected pattern.
	*/
	playPattern()
	{
		let csd = this.renderPatternCSD();
		playCode(csd);
	}
	/**
	* Render a CSD corresponding the whole project so far.
	* @param {boolean} displayModal - true/false to display a modal on the page containing the rendered code.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderCSD(displayModal)
	{
		// Get the beats per min and beats per block of the track
		let bpm = document.getElementById('playlist-bpm').value;
		if (bpm == "") bpm = document.getElementById('playlist-bpm').placeholder;
		let bpb = document.getElementById('playlist-bpb').value;
		if (bpb == "") bpb = document.getElementById('playlist-bpb').placeholder;
		bpm = Number(bpm);
		bpb = Number(bpb);
		let seekPos = this.trackLaneObject.seekToSeconds(bpm,bpb)

		let outStr = "<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n";
		//outStr += "sr = 44100\nksmps = 32\nnchnls = 2\n0dbfs  = 1\n\n";
		// get the orchestra string
		outStr += this.getOrchestraHeader()+"\n";
		outStr += "//orchestra:\n";
		outStr += this.renderOrchestra(false);
		outStr += this.getOrchestraFooter()+"\n";
		// get the score string
		outStr += "</CsInstruments>\n<CsScore>\n";
		outStr += this.getScoreHeader() +"\n";
		//outStr += "//advance time:\n";
		//outStr += "a 0 0 "+seekPos+"\n"; // skip to seekPos seconds into track
		outStr += "B -"+seekPos+"\n"; // a statements do not work for me for some reason
		outStr += "//score:\n";
		outStr += this.renderScore(false);
		outStr += this.getScoreFooter()+"\n";
		outStr += "e\n</CsScore>\n</CsoundSynthesizer>\n";
		// Print the score code to modal in browser
		if (displayModal)
		{
			document.getElementById("score-code-dialog").showModal();
			document.getElementById("score-code-dialog-output").textContent = outStr;
		}

		return outStr;
	}
	/**
	* Render a CSD corresponding the currently selected pattern.
	* @param {boolean} displayModal - true/false to display a modal on the page containing the rendered code.
	* @return {string} Returns string containing the text of the generated code.
	*/
	renderPatternCSD()
	{
		let outStr = "<CsoundSynthesizer>\n<CsOptions>\n-odac\n</CsOptions>\n<CsInstruments>\n";
		// get the orchestra string
		outStr += this.getOrchestraHeader() + "\n";
		outStr += "//orchestra:\n";
		outStr += this.renderOrchestra(false);
		outStr += this.getOrchestraFooter() + "\n";
		// get the score string
		outStr += "</CsInstruments>\n<CsScore>\n";
		outStr += this.getScoreHeader() +"\n";
		outStr += "//score:\n";
		outStr += this.renderTrack(false);
		outStr += this.getScoreFooter() +"\n";
		outStr += "e\n</CsScore>\n</CsoundSynthesizer>\n";
		return outStr;
	}
	/**
	* Stops playback if csound backend is playing audio.
	*/
	stopPlayBack()
	{
		stopCsound();
	}
	/**
	* Returns a string containing code intended to be emitted at the start of the orchestra code.
	* @return {string} Returns string containing the desired code.
	*/
	getOrchestraHeader()
	{
		return "//orchestra header:\n"+document.getElementById("orchestra-header").value;
	}
	/**
	* Returns a string containing code intended to be emitted at the end of the orchestra code.
	* @return {string} Returns string containing the desired code.
	*/
	getOrchestraFooter()
	{
		return "//orchestra footer:\n"+document.getElementById("orchestra-footer").value;
	}
	/**
	* Returns a string containing code intended to be emitted at the start of the score code.
	* @return {string} Returns string containing the desired code.
	*/
	getScoreHeader()
	{
		return "//score header:\n"+document.getElementById("score-header").value;
	}
	/**
	* Returns a string containing code intended to be emitted at the end of the score code.
	* @return {string} Returns string containing the desired code.
	*/
	getScoreFooter()
	{
		return "//score footer:\n"+document.getElementById("score-footer").value;
	}
	/**
	* Generate and download a .synth file containing the currently selected instrument.
	*/
	saveInstrument()
	{
		// Get the instrument text name
		let sel = document.getElementById('instrument-canvases-select'); // get the select tag
		let instrument = sel.options[sel.selectedIndex].text; // the text of the selected option
		instrument = this.CleanName(instrument);
		if (instrument == "") return;

		// the filename and contents of the file to be downloaded
		let filename = instrument+".synth";
		let text = this.instrumentMap.get(instrument).toText();

		// create and click an invisible link to the file we intend to download, then remove the link.
		// Comes from SO
		let element = document.createElement('a');
		element.setAttribute('href','data:text/plain;charset=utf-8,'+encodeURIComponent(text))
		element.setAttribute('download',filename);
		element.style.display = 'none'
		document.body.appendChild(element);
		element.click();
		document.body.removeChild(element);
	}
	/**
	* Prompt user for .synth file and create/load the instrument from the selected file.
	*/
	loadInstrument()
	{
		// This is mostly from SO

		// Open a file picker
		let input = document.createElement('input');
		input.type = 'file';

		input.onchange = e => { 
			// getting a hold of the file reference
			let file = e.target.files[0]; 

			// setting up the reader
			let reader = new FileReader();
			reader.readAsText(file,'UTF-8');

			// When the reader is done reading we can load/setup the instrument
			reader.onload = readerEvent => {
				let text = readerEvent.target.result; // this is the content!
				let instrument = this.buildInstrument(text);
			}

		}

		input.click();
	}
	/**
	* Given instrument state in a string create/load the instrument into the project.
	* See saveInstrument() code to better understand the instrument text format.
	* @param {string} text - An instrument in text form.
	*/
	buildInstrument(text)
	{
		// break file down into an array of arrays of lines
		let file = text.split("#".repeat(64)+"\n");
		file.shift(); // remove the empty starting line
		for (let i = 0; i < file.length; i++)
		{
			file[i] = file[i].split("\n");
			file[i].pop(); // remove empty ending line
		}

		// Build the page elements for the canvas
		let canvasDiv = "instrument-canvases";
		let name = "";
		if (file[0][0] == "GraphDiagramCanvas")
			name = JSON.parse(file[0][1]).instrumentName; // This is slow and probably unnecessary
		else if (file[0][0] == "TextAreaInstrumentCanvas")
		{
			name = file[0][1].slice(1,-1);
		}

		// add the associated select entry
		let selectEle = document.getElementById(canvasDiv+"-select");
		let newOption = document.createElement("option");
		newOption.innerText = name;
		selectEle.append(newOption);

		// add to the instrument data list in the track modal
		let dataList = document.getElementById("instruments-datalist");
		let listOption = document.createElement("option");
		listOption.value = name;
		dataList.append(listOption);

		newOption.setAttribute("value","instrument-"+name);

		// Figure out which type of canvas we are loading
		let instrumentCanvasObject = null;
		if (file[0][0] == "GraphDiagramCanvas")
		{
			// add the associated canvas tag
			let ele = document.getElementById(canvasDiv);
			let newCanvas = document.createElement("canvas");
			newCanvas.setAttribute("tabindex","1");
			ele.appendChild(newCanvas);

			newCanvas.setAttribute("id","instrument-"+name);
			newCanvas.setAttribute("class","trackLaneCanvas");

			instrumentCanvasObject = new GraphDiagramCanvas("instrument-"+name,name,20);
			newCanvas.style.display = "none";
		}
		else if (file[0][0] == "TextAreaInstrumentCanvas")
		{
			// add the associated textarea tag
			let ele = document.getElementById(canvasDiv);
			let newTextArea = document.createElement("textarea");
			newTextArea.setAttribute("tabindex","1");
			newTextArea.setAttribute("spellcheck","false");
			ele.appendChild(newTextArea);

			newTextArea.setAttribute("id","instrument-"+name);
			newTextArea.setAttribute("cols","160");
			newTextArea.setAttribute("rows","20");
			newTextArea.setAttribute("class","trackLaneCanvas");
	
			instrumentCanvasObject = new TextAreaInstrumentCanvas("instrument-"+name,name);
			newTextArea.style.display = "none";
		}
		else console.log("ERROR: file type read error");

		// Add the instrument canvas to our map of all instruments
		this.instrumentMap.set(this.CleanName(name),instrumentCanvasObject);
		// reconfigure the widget using our file data
		instrumentCanvasObject.reconfigure(file);	
	
	}
	/**
	* Generate and download a .track file containing the currently selected track.
	*/
	saveTrack()
	{
		// Get the track text name
		let sel = document.getElementById('track-canvases-select'); // get the select tag
		let track = sel.options[sel.selectedIndex].text; // the text of the selected option
		track = this.CleanName(track);
		if (track== "") return;

		// the filename and contents of the file to be downloaded
		let filename = track+".track";
		track = this.trackMap.get(track);

		// Convert the track to text, but ignore the instruments value to avoid circularity
		let text = JSON.stringify(track, (key,value) => {
			if(key == "instrument")
			{
				return new Array();
			}
			else return value;
		});

		// create and click an invisible link to the file we intend to download, then remove the link.
		// Comes from SO
		let element = document.createElement('a');
		element.setAttribute('href','data:text/plain;charset=utf-8,'+encodeURIComponent(text))
		element.setAttribute('download',filename);
		element.style.display = 'none'
		document.body.appendChild(element);
		element.click();
		document.body.removeChild(element);
	}
	/**
	* Prompt user for .track file and create/load the track from the selected file.
	*/
	loadTrack()
	{
		// This is mostly from SO

		// Open a file picker
		let input = document.createElement('input');
		input.type = 'file';

		input.onchange = e => { 
			// getting a hold of the file reference
			let file = e.target.files[0]; 

			// setting up the reader
			let reader = new FileReader();
			reader.readAsText(file,'UTF-8');

			// When the reader is done reading we can load/setup the instrument
			reader.onload = readerEvent => {
				let text = readerEvent.target.result; // this is the content!
				let instrument = this.buildTrack(text,file.name);
			}
		}

		input.click();
	}

	// TODO: The naming here for the constructors "track-p blah blah" should probably be changed
	/**
	* Given track state in a string create/load the track into the project.
	* See saveTrack() code to better understand the instrument text format.
	* @param {string} text - A track in text form.
	* @param {string} filename - Name of the file the track is being loaded from.
	*/
	buildTrack(text,filename)
	{
		filename = filename.split(".track")[0];
		// Parse the text from our file
		let trackName = JSON.parse(text)[0].trackName;
		let temp = JSON.parse(text);

		// Build the corresponding html tags for this instrument
		// add a div to contain all our parameter canvases
		let ele = document.getElementById("track-canvases");
		let instDiv  = document.createElement("div");
		instDiv.setAttribute("id","instrument-"+filename);
		ele.appendChild(instDiv);

		// add the associated select entry
		let selectEle = document.getElementById("track-canvases-select");
		let newOption = document.createElement("option");
		newOption.value = "instrument-"+filename;
		newOption.innerText = filename;
		selectEle.append(newOption);

		// Display the currently selected parameter
		document.getElementById("param-num").innerText = "Current Parameter: 0";

		// Playlist editor needs an associated pattern entry too
		let newPat = document.getElementById("pattern-select");
		let newOpt = document.createElement("option");
		newOpt.innerText = filename;
		newOpt.setAttribute("value","instrument-"+filename);
		newPat.append(newOpt);

		// Build the actual instrument from the file i.e. an array of parameter widgets
		let instr = new Array();
		for (let i = 0; i < temp.length; i++)
		{
		// create the canvas
			let newCanvas = document.createElement("canvas");
			newCanvas.setAttribute("tabindex","1");
			newCanvas.setAttribute("id","track-p"+i+"-"+filename);
			if (i==0) newCanvas.style.display = "inline";
			else newCanvas.style.display = "none";
			instDiv.appendChild(newCanvas);
		
			let workingWidget = null;
			if (temp[i].widgetType == "PianoRollCanvas") 
				workingWidget = new PianoRollCanvas("track-p"+i+"-"+filename,trackName,0,0,0);
			else if (temp[i].widgetType == "SliderCanvas")
				workingWidget = new SliderCanvas("track-p"+i+"-"+filename,trackName,0,0,0,"lollipop");
			else if (temp[i].widgetType == "CodedEventCanvas")
				workingWidget = new CodedEventCanvas("track-p"+i+"-"+filename,trackName,0,0);
			else console.log("ERROR: invalid parameter type on track load.");
			workingWidget.reconfigure(temp[i]);
			workingWidget.setInstrument(instr);
			instr.push(workingWidget);
		}
		instDiv.style.display = "none";
		
		this.trackMap.set(this.CleanName(filename),instr);
	}	
	/**
	* Create a zip file containing the full state of the current project and download it
	* to the user computer.
	*/
	saveProject()
	{
		let projName = prompt("Input the project name.");
		var zip = new JSZip();

		// Add the instruments to the zip file
		for (const val of this.instrumentMap.values())
			zip.file(projName+"/instruments/"+val.getName()+".synth", val.toText());

		// Add the tracks to the zipfile
		for (const val of this.trackMap.values())
		{
			// Convert the track to text, but ignore the instruments value to avoid circularity
			let text = JSON.stringify(val, (key,value) => {
				if(key == "instrument")
				{
					return new Array();
				}
				else return value;
			});

			zip.file(projName+"/tracks/"+val[0].getTrack()+".track", text);
		}
		
		// Add the track lane object to the zipfile
		zip.file(projName+"/tracklane.score", JSON.stringify(this.trackLaneObject));

		// Add the score and orchestra headers and footers to the file
		zip.file(projName+"/orchestra_section.header", document.getElementById("orchestra-header").value);
		zip.file(projName+"/orchestra_section.footer", document.getElementById("orchestra-footer").value);
		zip.file(projName+"/score_section.header", document.getElementById("score-header").value);
		zip.file(projName+"/score_section.footer", document.getElementById("score-footer").value);

		// add the bpm and bpb values to the file too
		if (document.getElementById("playlist-bpm").value == "") zip.file(projName+"/beats_per_min", "140");
		else zip.file(projName+"/beats_per_min", String(document.getElementById("playlist-bpm").value));

		if (document.getElementById("playlist-bpb").value == "") zip.file(projName+"/beats_per_block","4");
		else zip.file(projName+"/beats_per_block", document.getElementById("playlist-bpb").value);

		// Add the loaded audio files to the zip file
		for (let i = 0; i < this.audioFiles.length; i++)
			zip.file(projName+"/"+this.audioFiles[i][0],this.audioFiles[i][1]);

		zip.generateAsync({type:"blob"}).then(function (blob) { // 1) generate the zip file
			saveAs(blob, projName+".zip");                          // 2) trigger the download
		});
	}
	/**
	* Prompt user for a zip file containing a project and load the project from the file.
	* NOTE: This code requires the csound engine already be initialized to run due to
	* csound needing to be initialized in order for us to load samples into memory correctly.
	*/
	loadProject()
	{
		if (csound == null) 
		{
			alert("Csound subsystem must be initialized to load project files.");
			return;
		}

		// This is mostly from SO

		// Open a file picker
		let input = document.createElement('input');
		input.type = 'file';

		var that = this; // so calls below have access to this
		input.onchange = e => { 
			var new_zip = new JSZip();
			new_zip.loadAsync(e.target.files[0])
			.then(function(zip) {
				// Iterate across all files in the zip
				zip.forEach((path, file) => {
					if (/\.synth$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							that.buildInstrument(content);
						});
					}
					else if (/\.track$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							let filename = file.name.split(".track")[0];
							filename = filename.split("/");
							filename = filename[filename.length-1];
							that.buildTrack(content,filename);
						});
					}
					else if (/\.score$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							let state = JSON.parse(content);
							that.trackLaneObject.reconfigure(state);
						});
					}
					else if (/score_section.header$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("score-header").value = content;
						});

					}
					else if (/score_section.footer$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("score-footer").value = content;
						});
					}
					else if (/orchestra_section.header$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("orchestra-header").value = content;
						});
					}
					else if (/orchestra_section.footer$/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("orchestra-footer").value = content;
						});
					}
					else if (/beats_per_min/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("playlist-bpm").value = Number(content);
						});
					}
					else if (/beats_per_block/.test(path)) 
					{
						file.async("string")
						.then( (content) => {
							document.getElementById("playlist-bpb").value = Number(content);
						});
					}
					else // load anything else as a raw audio file to the browser filesystem
					{
						file.async("uint8array")
						.then( (content) => {
							// Load the file into csound
							let filename = file.name;
							filename = filename.split("/")[1];
							// any filename with no dot as assumed to not be an audio file and skipped
							if (!filename.includes(".")) return;
							csound.fs.writeFile(filename, content);
							// Add tag to list of loaded samples
							let pListTag = document.getElementById("sample-list");
							let newRow = document.createElement("tr");
							let val = document.createElement("td");
							val .innerText = file.name;
							newRow.appendChild(val);
							pListTag.appendChild(newRow);
							// Append file contents to our list of audio files
							that.audioFiles.push([filename,content]);
						});
					}
				});
			});
		}
		input.click(); // this click triggers project loading callback above
	}
	/**
	* Prompt user for an audio file and load the audio file into memory/csound.
	* NOTE: This code requires the csound engine already be initialized to run due to
	* csound needing to be initialized in order for us to load samples into it.
	*/
	loadAudioFile()
	{
		// This is mostly from SO
		if (csound == null) 
		{
			alert("Csound subsystem must be initialized to load audio files.");
			return;
		}

		// Open a file picker
		let input = document.createElement('input');
		input.type = 'file';

		input.onchange = e => { 
			// getting a hold of the file reference
			let file = e.target.files[0]; 

			// setting up the reader
			let reader = new FileReader();
			//reader.readAsText(file,'UTF-8');
			reader.readAsArrayBuffer(file);

			// When the reader is done reading we can load/setup the instrument
			reader.onload = readerEvent => {
				let dat = readerEvent.target.result;
				csound.fs.writeFile(file.name, new Uint8Array(dat));

				//get the tag to add parameters to
				let pListTag = document.getElementById("sample-list");

				//create the tags
				let newRow = document.createElement("tr");
				let content = document.createElement("td");

				// the content of the new tag is the filename
				content.innerText = file.name;

				//build the new element
				newRow.appendChild(content);
				pListTag.appendChild(newRow);

				// Append file contents to our list of audio files
				this.audioFiles.push([file.name,new Uint8Array(dat)]);
			}
		}
		input.click();
	}
	/**
	* Returns the array of audio files that View keeps track of.
	*/
	getAudioFiles()
	{
		return this.audioFiles;
	}
}

let viewObj = new View();