/*
	Chiptune.js
	(c) Sean Kelly 2013-2018
	www.psychsoftware.org
	Current codebase is VERY preliminary.  If you really want to use it, please ask.

	Version History
	
	2018.9.1 
	- revised to accommodate new Audio API constraints & behavior of 
	leaving generators in a suspended state until resumed on user interaction

	x.x.x - pre formal versioning
	2013.12.20 - original version
*/

"use strict";
//var Chiptune_ctx=null; 
if (typeof AudioContext !== "undefined") { 
	//Chiptune_ctx = new AudioContext(); 
	//alert("full AudioContext!");
} 
else if (typeof webkitAudioContext !== "undefined") { 
	//Chiptune_ctx = new webkitAudioContext();
	//alert("webkit AudioContext."); 
}
else{
	alert("AudioContext support not found; chiptune cannot play!")
}

var DEFINE_CHIPTUNE_PITCHES = {
	/*// OCTAVE 1
	An1 : 55, // *pow(2,0); A440/2/2/2
	
	As1 : 55*Math.pow(2,1/12),
	Bf1 : 55*Math.pow(2,1/12),
	
	Bn1 : 55*Math.pow(2,2/12),
	
	Cn2 : 55*Math.pow(2,3/12),

	Cs2 : 55*Math.pow(2,4/12),
	Df2 : 55*Math.pow(2,4/12),
	
	Dn2 : 55*Math.pow(2,5/12),
	
	Ds2 : 55*Math.pow(2,6/12),
	Ef2 : 55*Math.pow(2,6/12),
	
	En2 : 55*Math.pow(2,7/12),
	
	Fn2 : 55*Math.pow(2,8/12),
	
	Fs2 : 55*Math.pow(2,9/12),
	Gf2 : 55*Math.pow(2,9/12),
	
	Gn2 : 55*Math.pow(2,10/12),
	
	Gs2 : 55*Math.pow(2,11/12),
	Af2 : 55*Math.pow(2,4/12),	

	An2 : 110, // *pow(2,0); A440/2/2
	
	As2 : 110*Math.pow(2,1/12),
	Bf2 : 110*Math.pow(2,1/12),
	
	Bn2 : 110*Math.pow(2,2/12),
	
	Cn3 : 110*Math.pow(2,3/12),

	Cs3 : 110*Math.pow(2,4/12),
	Df3 : 110*Math.pow(2,4/12),
	
	Dn3 : 110*Math.pow(2,5/12),
	
	Ds3 : 110*Math.pow(2,6/12),
	Ef3 : 110*Math.pow(2,6/12),
	
	En3 : 110*Math.pow(2,7/12),
	
	Fn3 : 110*Math.pow(2,8/12),
	
	Fs3 : 110*Math.pow(2,9/12),
	Gf3 : 110*Math.pow(2,9/12),
	
	Gn3 : 110*Math.pow(2,10/12),
	
	Gs3 : 110*Math.pow(2,11/12),
	Af3 : 110*Math.pow(2,4/12),
	
	// TODO: more later*/
}

for (var octave=0; octave<8; octave++){
	// 55 = A440/2/2/2
	DEFINE_CHIPTUNE_PITCHES["An"+octave] = 55*Math.pow(2,octave-1);

	DEFINE_CHIPTUNE_PITCHES["As"+octave] = 55*Math.pow(2,(octave-1)+1/12);
	DEFINE_CHIPTUNE_PITCHES["Bf"+octave] = 55*Math.pow(2,(octave-1)+1/12);

	DEFINE_CHIPTUNE_PITCHES["Bn"+octave] = 55*Math.pow(2,(octave-1)+2/12);

	DEFINE_CHIPTUNE_PITCHES["Cn"+(octave+1)] = 55*Math.pow(2,(octave-1)+3/12);

	DEFINE_CHIPTUNE_PITCHES["Cs"+(octave+1)] = 55*Math.pow(2,(octave-1)+4/12);
	DEFINE_CHIPTUNE_PITCHES["Df"+(octave+1)] = 55*Math.pow(2,(octave-1)+4/12);

	DEFINE_CHIPTUNE_PITCHES["Dn"+(octave+1)] = 55*Math.pow(2,(octave-1)+5/12);

	DEFINE_CHIPTUNE_PITCHES["Ds"+(octave+1)] = 55*Math.pow(2,(octave-1)+6/12);
	DEFINE_CHIPTUNE_PITCHES["Ef"+(octave+1)] = 55*Math.pow(2,(octave-1)+6/12);

	DEFINE_CHIPTUNE_PITCHES["En"+(octave+1)] = 55*Math.pow(2,(octave-1)+7/12);

	DEFINE_CHIPTUNE_PITCHES["Fn"+(octave+1)] = 55*Math.pow(2,(octave-1)+8/12);

	DEFINE_CHIPTUNE_PITCHES["Fs"+(octave+1)] = 55*Math.pow(2,(octave-1)+9/12);
	DEFINE_CHIPTUNE_PITCHES["Gf"+(octave+1)] = 55*Math.pow(2,(octave-1)+9/12);

	DEFINE_CHIPTUNE_PITCHES["Gn"+(octave+1)] = 55*Math.pow(2,(octave-1)+10/12);

	DEFINE_CHIPTUNE_PITCHES["Gs"+(octave+1)] = 55*Math.pow(2,(octave-1)+11/12);
	DEFINE_CHIPTUNE_PITCHES["Af"+(octave+1)] = 55*Math.pow(2,(octave-1)+11/12);
}

var Soundchannel = function(id,def,context){
	this.def = def;
	this.context = context;

	this.sourceType = "oscillator";
	this.source = null;
	this.env = null;
	this.vol = null;
	this._split = null;
	this.left = null;
	this.right = null;
	this.out = null;
	this.id = id;
	// bitfield set by any playing chiptunes.
	// chiptunes |= their priority onto it when playing,
	// &= their ~priority when done, and only play if
	// no higher priority bit is set.
	this.priority = 0; 

	this.rebuild();
	this.clear();
};
	
	// (re)constructs the node hierarchy driving this channel
	Soundchannel.prototype.rebuild = function(){
		this.teardown();

		// TODO: custom types
		if (typeof(this.def)==="string"){
			if (this.def === "noise"){
				var noiseSampleRate=22050;
				
				var buf = this.context.createBuffer(1,noiseSampleRate,noiseSampleRate);
				var data = buf.getChannelData(0);
				var dataArr = new Float32Array(data);

				for (var i=0; i<dataArr.length; i++){
					dataArr[i]=2*Math.random()-1;
				}
				data.set(dataArr); // shouldn't be needed since dataArr is a "view" of data, but somehow, it is


				this.source = this.context.createBufferSource();
				this.sourceType="buffer";
				this.source.buffer=buf;
				this.source.loop=true;
			}
			else{
				this.source = this.context.createOscillator();
				this.source.type=this.def;
			}
		}
		else{
			var real = new Float32Array(this.def.real.length);
			real.set(this.def.real);

			var imag = new Float32Array(this.def.imag.length);
			imag.set(this.def.imag);

			//source.type="custom";
			var wt = this.context.createWaveTable(real,imag);
			this.source = this.context.createOscillator();
			this.source.setWaveTable(wt);
		}
		
		this.env = this.context.createGain();
		this.vol = this.context.createGain();
		this._split = this.context.createChannelSplitter(2); // old specs worked withou an explicit argument
		this.left = this.context.createGain();
		this.right = this.context.createGain();
		this.out = this.context.createChannelMerger(2); // old specs worked withou an explicit argument

		this.source.connect(this.env);
		this.env.connect(this.vol);
		this.vol.connect(this._split);
		this._split.connect(this.left,0);
		this._split.connect(this.right,0);//source is mono?
		this.left.connect(this.out,0,0); // the eff... if both are not connected...
		this.right.connect(this.out,0,1); // then the connected one is upgraded to stereo
		this.out.connect(this.context.destination);

		this.source.start();
	};
	// cancels all values on this channel and immediately schedules defaults
	Soundchannel.prototype.clear = function(){
		if (this.sourceType == "buffer"){
			this.source.playbackRate.cancelScheduledValues(this.context.currentTime);
			this.source.playbackRate.setValueAtTime(0,this.context.currentTime);
		}
		else if (this.sourceType == "oscillator"){
			this.source.frequency.cancelScheduledValues(this.context.currentTime);
			this.source.detune.cancelScheduledValues(this.context.currentTime);

			this.source.frequency.setValueAtTime(0,this.context.currentTime);
			this.source.detune.setValueAtTime(0,this.context.currentTime);
		}

		this.env.gain.cancelScheduledValues(this.context.currentTime);
		this.vol.gain.cancelScheduledValues(this.context.currentTime);
		this.left.gain.cancelScheduledValues(this.context.currentTime);
		this.right.gain.cancelScheduledValues(this.context.currentTime);

		this.env.gain.setValueAtTime(0,this.context.currentTime);
		this.vol.gain.setValueAtTime(0,this.context.currentTime);
		this.left.gain.setValueAtTime(0.5,this.context.currentTime);
		this.right.gain.setValueAtTime(0.5,this.context.currentTime);
	};
	// stops/detatches anything needed before disposal
	Soundchannel.prototype.teardown = function(){
		if (this.source){
			this.source.stop();
			this.source.disconnect();
		}

		if (this.env) this.env.disconnect();
		if (this.vol) this.vol.disconnect();
		if (this._split) this._split.disconnect();
		if (this.left) this.left.disconnect();
		if (this.right) this.right.disconnect();
		if (this.out) this.out.disconnect();
	};

var Soundchip = function(name,context){
	this.context=context;
	this.name = name;

	this.context_is_dynamic = false;
	if (!this.context){
		this.context_is_dynamic = true;
		this.resetContext();
	}
	else{
		this.buildChannels(); // didn't hit it in resetContext, so do it now
	}
	
	// final lockout check; don't register the chip name (and make it match lookups) if it isn't functional
	if (!this.context) this.name=null;

	this._heap = {};
	
	return this;
};
	Soundchip.prototype.buildChannels = function(){
		this.context.createGain = this.context.createGain || this.context.createGainNode; // legacy check

		if (this.channels){
			for (var chan in this.channels){
				this.channels[chan].teardown();
				this.channels[chan].context = null; // help the GC a bit
			}
		}
		this.channels = {};

		if (!this.context) return; 
		if (!DATA_SOUND_CHIPS[this.name] || !DATA_SOUND_CHIPS[this.name].oscillators) return null;
		
		var id=0;
		for (var osc in DATA_SOUND_CHIPS[this.name].oscillators){
			this.channels[osc]=new Soundchannel(id,DATA_SOUND_CHIPS[this.name].oscillators[osc],this.context);
			++id;
		}
	};

	Soundchip.prototype.shutdown = function(){
		for (this._heap.chan in this.channels){
			this.channels[this._heap.chan].source.stop();
			this.channels[this._heap.chan].out.disconnect();
			delete this.channels[this._heap.chan];
		}
		this.name=null;

		if (this.context_is_dynamic){
			this.context.close();
			this.context = null;
		}
	};

	Soundchip.prototype.resetContext = function(){
		if (!this.context_is_dynamic) return;
		if (this.context) this.context.close();
		this.context = null;
		if (typeof AudioContext !== "undefined") { 
			this.context = new AudioContext(); 
			//alert("full AudioContext!");
		} 
		else if (typeof webkitAudioContext !== "undefined") { 
			this.context = new webkitAudioContext();
			//alert("webkit AudioContext."); 
		}
		if (this.context){
			this.buildChannels();
		}
	};

	// Web Audio autoplay policy ca October 2018
	Soundchip.prototype.resume = function(){
		if (this.context) this.context.resume();
	}




var Chiptune = function(name,soundchip){
	this.valid=true;
	if (!soundchip) this.valid=false; 
	else if (!DATA_CHIPTUNES[name]) this.valid=false;
	else if (!DATA_CHIPTUNES[name].chip || DATA_CHIPTUNES[name].chip!=soundchip.name) this.valid=false;

	this.name=name;
	this.chip=soundchip;
	this.next_note_times={}; // timing seconds
	this.beat_lengths={}; // timing seconds
	this.command_indices={}; // int index
	this.loop_counters={}; // int index
	this.envelopes={}; /* {
		"start": 1.0, // float level 0-1
		"end": 0.0, // float level 0-1
		"dur": 0.25 // float seconds, or 0 for duration of note
	}*/
	this.sweeps={}; /* {
		"start": 1.0, // float multiple of frequency
		"end": 1.0, // float multiple of frequency
		"dur": 0 // float seconds, or 0 for duration of note
	}*/

	this.volumes={}; // float level 0-1; 0.1 is sane-loud
	this.pans={}; /* {
		"position": 0.25, // float 0-1; 0.0=left, 1.0=right
		"travel": 0.5, // float; offset from position for auto-panning; optional (no auto-pan if omitted)
		"dur": 1.0, // float, seconds; optional (no auto-pan if omitted)
		"alternate": true // bool; false = always pan from position to position+travel, true = pan from position to position+travel and back; optional (false if omitted)
	}*/
	this.pan_starts={}; // timing seconds; reference time for start of auto-pan
	this.detunes={}; // cents

	this.is_playing=false;
	this.loop=false;
	// has any channel looped since we started (or this flag was cleared externally)?
	this.has_looped=false;
	
	// how far in advance (in seconds) to place the first/last event when playing/stopping
	// larger values mean more delay, smaller values risk the context time being past
	// by the time all events are added
	this.delay=0.05; 
	// Firefox, and now even Chrome(?), seem to have some difficulty when the
	// RampToValueAtTime calls collide with other value setting times, which they do
	// whenever we need to prorate a ramp.  As a test-hack-fix, end ramps minutely
	// earlier than the note itself ends and/or the next note starts
	this.epsilon=0.00125;
	// time in advance (in seconds) to look for and parse new events
	// larger values mean more events in the lists, and more cleanup when transitioning
	// smaller values risk dropping notes if Update is slow
	this.horizon = 1.0;


	var tuneData = this.valid ? DATA_CHIPTUNES[name] : {};
	this.beat = tuneData.bpm ? 60.0/tuneData.bpm : 0.5; // length of 1 beat in seconds

	this._heap = {};

	return this;
};

	Chiptune.prototype.play = function(loop){
		if (!this.valid) return;

		if (this.is_playing) this.stop();
		//this.chip.resetContext(); // really once and for all prevent scheduled value leakage, and likely mitigate extended-run cut-out

		this.loop=loop;
		this.has_looped=false;
		for (this._heap.chan in this.chip.channels){
			this.next_note_times[this._heap.chan] = this.chip.context.currentTime+this.delay;
			this.beat_lengths[this._heap.chan] = this.beat;
			this.envelopes[this._heap.chan] = {start: 1.0, end:1.0, dur:0};
			this.sweeps[this._heap.chan] = {start: 1.0, end:1.0, dur:0};

			this.volumes[this._heap.chan] = 1.0;
			this.pans[this._heap.chan] = {position: 0.5};
			this.pan_starts[this._heap.chan] = this.next_note_times[this._heap.chan]; // reference mark for dynamic panning computation
			this.detunes[this._heap.chan] = 0;

			this.command_indices[this._heap.chan] = 0;
			this.loop_counters[this._heap.chan] = 0;

			//if (this.chip.channels[this._heap.chan].sourceType == "oscillator"){
			//	this.chip.channels[this._heap.chan].source.detune.setValueAtTime(0,this.next_note_times[this._heap.chan]);
			//}

			// if this tune has data on the given channel,
			// clear that channel's existing values
			if (DATA_CHIPTUNES[this.name].songdata[this._heap.chan] && DATA_CHIPTUNES[this.name].songdata[this._heap.chan].length!==0){
				this.chip.channels[this._heap.chan].clear();
			}
			// otherwise (e.g. for sfx which only use a channel or two), let whatever was playing persist
		}
		this.is_playing=true;
	};
	Chiptune.prototype.stop = function(nondestructive){
		if (!this.valid) return;
		
		for (this._heap.chan in this.chip.channels){
			if (!nondestructive) this.chip.channels[this._heap.chan].clear();

			this.chip.channels[this._heap.chan].priority &= ~(DATA_CHIPTUNES[this.name].priority);
		}

		this.is_playing=false;
	};

	// parses the next command in the tune data
	// returns true if parse successful; false if no data or invalid command
	Chiptune.prototype._parseNextCommand = function(channel){
		if (!this.valid) return;
		
		if (!DATA_CHIPTUNES[this.name].songdata[channel] || DATA_CHIPTUNES[this.name].songdata[channel].length==0) return false;
		if (this.command_indices[channel]>=DATA_CHIPTUNES[this.name].songdata[channel].length){
			if (this.loop) {
				this.command_indices[channel]=0;
				this.has_looped = true;
			}
			else{
				// explicitly cut off channel when done;
				// workaround for value collision artefacts when notes were allowed to end themselves
				this.chip.channels[channel].env.gain.setValueAtTime(
					0,
					this.next_note_times[channel]);
				//console.log(channel+" set  "+0+" at "+this.next_note_times[channel]);

				this.chip.channels[channel].priority &= ~(DATA_CHIPTUNES[this.name].priority);
				return false;
			}
		}

		this._heap.res = false;
		switch(DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].command){
			case "env":
				this.envelopes[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data;
				this._heap.res = true;
			break;
			case "sweep":
				this.sweeps[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data;
				this._heap.res = true;
			break;
			case "vol":
				//this.chip.channels[channel].vol.gain.setValueAtTime(
				//	DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.level,
				//	this.next_note_times[channel]);
				this.volumes[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.level;
				this._heap.res = true;
			break;
			case "pan":
				//this.chip.channels[channel].left.gain.setValueAtTime(
				//	1.0-DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.position,
				//	this.next_note_times[channel]);
				//this.chip.channels[channel].right.gain.setValueAtTime(
				//	DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.position,
				//	this.next_note_times[channel]);
				//this.pans[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.position;
				this.pans[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data;
				this.pan_starts[channel] = this.next_note_times[channel];
				this._heap.res = true;
			break;
			case "tempo":
				this.beat_lengths[channel] = 60.0/DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.bpm;
				this._heap.res = true;
			break;
			case "detune":
				//this.chip.channels[channel].source.detune.setValueAtTime(
				//	DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.cents,
				//	this.next_note_times[channel]);
				this.detunes[channel] = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.cents;
				this._heap.res = true;
			break;
			case "jump":
				if (DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.reset){
					this.loop_counters[channel]=0;
				}
				else if (this.loop_counters[channel]<DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.times){
					this.command_indices[channel] += Math.round(DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.delta);
					this.command_indices[channel]--; // offset the ++ later
					this.loop_counters[channel]++;
				}

				this._heap.res = true;
			break;

			case "notes":
				for (var i=0; i<DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data.length; i++){
					this._heap.noteCode = DATA_CHIPTUNES[this.name].songdata[channel][this.command_indices[channel]].data[i];
					this._heap.pitchCode = this._heap.noteCode.substr(0,3);
					this._heap.lengthCode = this._heap.noteCode.substr(4);

					this._heap.noteLen = this.beat_lengths[channel] * parseFloat(this._heap.lengthCode);
					this._heap.notePitch = DEFINE_CHIPTUNE_PITCHES[this._heap.pitchCode];
					if (typeof(this._heap.notePitch)==="undefined") this._heap.notePitch=0;

					// if there are no higher priority bits set, we can play
					this._heap.hasPriority = (this.chip.channels[channel].priority < (DATA_CHIPTUNES[this.name].priority<<1));

					if (this._heap.notePitch!=0 && this._heap.hasPriority){
						// with notes and fx sharing one chip, we need to cancel ongoing
						// sweeps/envs when we play our next note
						
						this._heap.stopTime = this.next_note_times[channel]+this.delay;
						if (this.chip.channels[channel].sourceType == "buffer"){
							this.chip.channels[channel].source.playbackRate.cancelScheduledValues(this._heap.stopTime);
						}
						else if (this.chip.channels[channel].sourceType == "oscillator"){
							this.chip.channels[channel].source.frequency.cancelScheduledValues(this._heap.stopTime);
						}
						this.chip.channels[channel].env.gain.cancelScheduledValues(this._heap.stopTime);
						//this.chip.channels[channel].vol.gain.cancelScheduledValues(this._heap.stopTime);
						//this.chip.channels[channel].vol.gain.setValueAtTime(0,this._heap.stopTime);
						//this.chip.channels[channel].left.gain.cancelScheduledValues(this._heap.stopTime);
						//this.chip.channels[channel].right.gain.cancelScheduledValues(this._heap.stopTime);


						
						this.chip.channels[channel].vol.gain.setValueAtTime(
							this.volumes[channel],
							this.next_note_times[channel]);
					
						// convoluted best-magic-on-short-notice
						// to automate panning on note start.
						// more tangled still would be to set up
						// independent timing cycle logic to constantly
						// prefill the queue with linearRampToValueAtTime calls.

						// total ongoing time of this auto-pan
						this._heap.pantime = this.next_note_times[channel] - this.pan_starts[channel];
						// number of auto-pan cycles so far, plus trailing fraction (if any)
						this._heap.panprogress = this.pans[channel].dur ? this._heap.pantime/this.pans[channel].dur : 0;
						// pan position at the far end of a cycle
						this._heap.panend = this.pans[channel].position + (this.pans[channel].travel ? this.pans[channel].travel : 0);
						this._heap.panend = Math.min(Math.max(0,this._heap.panend),1);
						// number of complete panning cycles so far
						this._heap.pancycles = Math.floor(this._heap.panprogress);
						this._heap.panprogress -= this._heap.pancycles; // now just the fractional part
						this._heap.pancycles = this._heap.pancycles&1; // all we care about now is even or odd.  & is probably faster than %
						// final right pan position
						this._heap.finalpan = (this.pans[channel].alternate && this._heap.pancycles) ?
							(this._heap.panprogress*this.pans[channel].position +
											(1.0-this._heap.panprogress)*this._heap.panend) :
							((1.0-this._heap.panprogress)*this.pans[channel].position +
											this._heap.panprogress*this._heap.panend);

						this.chip.channels[channel].left.gain.setValueAtTime(
							1.0-this._heap.finalpan,
							this.next_note_times[channel]);
						this.chip.channels[channel].right.gain.setValueAtTime(
							this._heap.finalpan,
							this.next_note_times[channel]);
					
						if (this.chip.channels[channel].source.detune){
							this.chip.channels[channel].source.detune.setValueAtTime(
								this.detunes[channel],
								this.next_note_times[channel]);
						}
						

						if (this.chip.channels[channel].sourceType == "buffer"){
							// start pitch
							this.chip.channels[channel].source.playbackRate.setValueAtTime(
								this.sweeps[channel].start * this._heap.notePitch/880.0,
								this.next_note_times[channel]);
							
							// ramp pitch if swept
							if (this.sweeps[channel].dur>0){
								if (this.sweeps[channel].dur>=this._heap.noteLen-this.epsilon){
									this._heap.durProrate = this.beat_lengths[channel]/this.sweeps[channel].dur;
									this._heap.endVal = this.sweeps[channel].start + (this.sweeps[channel].end-this.sweeps[channel].start)*this._heap.durProrate;
									this.chip.channels[channel].source.playbackRate.linearRampToValueAtTime(
										this._heap.endVal * this._heap.notePitch/880.0,
										this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
								}
								else{
									this.chip.channels[channel].source.playbackRate.linearRampToValueAtTime(
										this.sweeps[channel].end * this._heap.notePitch/880.0,
										this.next_note_times[channel]+this.sweeps[channel].dur);
								}
							}
							else if (this.sweeps[channel].dur==0 && this.sweeps[channel].end!=this.sweeps[channel].start){
								this.chip.channels[channel].source.playbackRate.linearRampToValueAtTime(
									this.sweeps[channel].end * this._heap.notePitch/880.0,
									this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
							}

							// end pitch (should play nice with ramp, since ramp defines from T0<=t<T1)
							// probably not even needed, but determinism is nice
							this.chip.channels[channel].source.playbackRate.setValueAtTime(
								this.sweeps[channel].end * this._heap.notePitch/880.0,
								this.next_note_times[channel]+this._heap.noteLen);
						}
						else if (this.chip.channels[channel].sourceType == "oscillator"){
							// start pitch
							this.chip.channels[channel].source.frequency.setValueAtTime(
								this.sweeps[channel].start * this._heap.notePitch,
								this.next_note_times[channel]);
							
							// ramp pitch if swept
							if (this.sweeps[channel].dur>0){
								if (this.sweeps[channel].dur>=this._heap.noteLen-this.epsilon){
									this._heap.durProrate = this.beat_lengths[channel]/this.sweeps[channel].dur;
									this._heap.endVal = this.sweeps[channel].start + (this.sweeps[channel].end-this.sweeps[channel].start)*this._heap.durProrate;
									this.chip.channels[channel].source.frequency.linearRampToValueAtTime(
										this._heap.endVal * this._heap.notePitch,
										this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
								}
								else{
									this.chip.channels[channel].source.frequency.linearRampToValueAtTime(
										this.sweeps[channel].end * this._heap.notePitch,
										this.next_note_times[channel]+this.sweeps[channel].dur);
								}
							}
							else if (this.sweeps[channel].dur==0 && this.sweeps[channel].end!=this.sweeps[channel].start){
								this.chip.channels[channel].source.frequency.linearRampToValueAtTime(
									this.sweeps[channel].end * this._heap.notePitch,
									this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
							}

							// end pitch (should play nice with ramp, since ramp defines from T0<=t<T1)
							// probably not even needed, but determinism is nice
							this.chip.channels[channel].source.frequency.setValueAtTime(
								this.sweeps[channel].end * this._heap.notePitch,
								this.next_note_times[channel]+this._heap.noteLen);
						}

						// flush any future values so they don't interfere with the ones we want.
						// attempted fix for ramps colliding with start/end even after using epsilon,
						// but even chrome appears to over-zealously cull nearby sets, and ones which
						// haven't even been set yet (e.g. the start of the tone below), resulting
						// most commonly in total silence on most/all channels
						//this.chip.channels[channel].env.gain.cancelScheduledValues(this.envelopes[channel].start);
						//console.log(channel+" clrV at "+this.next_note_times[channel]);

						// start tone
						this.chip.channels[channel].env.gain.setValueAtTime(
							this.envelopes[channel].start,
							this.next_note_times[channel]);
						//console.log(channel+" set  "+this.envelopes[channel].start+" at "+this.next_note_times[channel]);
						
						// ramp tone if enveloped
						if (this.envelopes[channel].dur>0){
							if (this.envelopes[channel].dur>=this._heap.noteLen-this.epsilon){
								this._heap.durProrate = this.beat_lengths[channel]/this.envelopes[channel].dur;
								this._heap.endVal = this.envelopes[channel].start + (this.envelopes[channel].end-this.envelopes[channel].start)*this._heap.durProrate;
								this.chip.channels[channel].env.gain.linearRampToValueAtTime(
									this._heap.endVal,
									this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
								//console.log(channel+" Ramp "+this._heap.endVal+" at "+(this.next_note_times[channel]+this._heap.noteLen-this.epsilon));
							}
							else{
								this.chip.channels[channel].env.gain.linearRampToValueAtTime(
									this.envelopes[channel].end,
									this.next_note_times[channel]+this.envelopes[channel].dur);
								//console.log(channel+" rAmp "+this.envelopes[channel].end+" at "+(this.next_note_times[channel]+this.envelopes[channel].dur));
							}
						}
						else if (this.envelopes[channel].dur==0 && this.envelopes[channel].end!=this.envelopes[channel].start){
							this.chip.channels[channel].env.gain.linearRampToValueAtTime(
								this.envelopes[channel].end,
								this.next_note_times[channel]+this._heap.noteLen-this.epsilon);
							//console.log(channel+" raMp "+this.envelopes[channel].end+" at "+(this.next_note_times[channel]+this._heap.noteLen-this.epsilon));
						}

						// end tone (should play nice with ramp, since ramp defines from T0<=t<T1)
						// but even chrome is now getting confused and ignoring ~earlier~ rampTo values
						// when each note ends itself AND a subsequent note starts on the same time.
						// new fix is trust the next note to set itself up, and add an explicit 0 set
						// for any invalid notes (below), and when out of commands (top)
						//this.chip.channels[channel].env.gain.setValueAtTime(
						//	0,
						//	this.next_note_times[channel]+this._heap.noteLen);
						//console.log(channel+" set  "+0+" at "+(this.next_note_times[channel]+this._heap.noteLen));

						this.chip.channels[channel].priority |= DATA_CHIPTUNES[this.name].priority;
					}
					else{
						// invalid or intentionally OoB pitch (e.g. Rst)
						this.chip.channels[channel].env.gain.setValueAtTime(
							0,
							this.next_note_times[channel]);
						//console.log(channel+" set  "+0+" at "+this.next_note_times[channel]);

						this.chip.channels[channel].priority &= ~(DATA_CHIPTUNES[this.name].priority);
					}

					this.next_note_times[channel]+=this._heap.noteLen;
				}
				this._heap.res = true;
			break;
		}

		this.command_indices[channel]++;
		return this._heap.res;
	};

	Chiptune.prototype.Update = function(){
		if (!this.valid) return;

		// Web Audio autoplay policy ca October 2018
		if (this.chip && this.chip.context && this.chip.context.state==="suspended") return;
		
		if (!this.is_playing) return;
		this._heap.anythingLeft = false;
		for (this._heap.chan in this.chip.channels){
			this._heap.chanDataLeft = true;
			while(this.next_note_times[this._heap.chan] < this.chip.context.currentTime+this.horizon && this._heap.chanDataLeft){
				this._heap.chanDataLeft=this._parseNextCommand(this._heap.chan);
			}
			
			this._heap.anythingLeft = this._heap.anythingLeft || this._heap.chanDataLeft;
		}

		this.is_playing = this._heap.anythingLeft;
	};


function playSound(e) {
	//if (!Chiptune_ctx) return; 
	/*var source = Chiptune_ctx.createOscillator();
	source.type="square"; 
	source.frequency.value=440; // A440?
	var gain = Chiptune_ctx.createGainNode();
	gain.gain.value=0.1;
	gain.gain.setValueAtTime(0.1,0);
	gain.gain.linearRampToValueAtTime(0,0.5);
	source.connect(gain);
	gain.connect(Chiptune_ctx.destination);
	//source.connect(Chiptune_ctx.destination);
	
	source.loop=true;
	source.loopStart=0;
	source.loopEnd=2.0;
	
	source.start(0);
	source.stop(10.0);*/

	var chip = new Soundchip("oneofeach");//,Chiptune_ctx);
	var bgm = new Chiptune("TakeOnMe",chip);
	if (bgm){
		bgm.play(true);
	}
	setInterval(function(){bgm.Update();},250);
}
//window.addEventListener("load",playSound);
