// keytar.htm
// For Moogfest 2018
// Circuit Bending Challenge
// Peter Churchyard
// @codewizard58
//
"use strict"
//
// This is where you can hand edit various options that control the web page.

var hideall = true;			//  or false. If true, hide all when the web page loads except the Unhide button.

// When dragging the mouse around the page to do pitch bends or play notes, any text on the page
// can get highlighted and the browser then tries to drag the text. The interferes with the music.
// by only showing the button, dragging the mouse around with a button pressed does not have any impact.

// Midi control change numbers.
// filter
var CUTOFF = 17;		// filter cutoff. CC(17)
var RESONANCE = 84;

// lfo
var LFORATE = 73;		// LFO frequency control
var LFODEPTH = 72;		// How much of the LFO output is used to modify the osc or filter.
var LFOGLIDE = 75;		
var LFOWAVE = 77;
var MODDELAY = 76;
// osc
var GLIDE = 71;			// Note glide rate control.
var WAVE = 78;          // wave shape
var DRIVE = 79;

//
var ATTACK = 0;
var DECAY = 0;
var FREQ = 74;
var FILTERMOD = 70;

var filters = [ null, null, null, null, null, null ];
var showmidiUI = null;
var curknob=0;
var isguitar = true;
var needresize=true;


function showselected( a, b)
{
	if( a == b){
		return "selected='selected' ";
	}
	return "";
}

function status(msg)
{	var sdiv = document.getElementById("status");

	if( sdiv != null){
		sdiv.innerHTML = msg;
	}
}

function status2(msg)
{	var sdiv2 = document.getElementById("status2");

	if( sdiv2 != null){
		sdiv2.innerHTML = msg;
	}
}

///////////////////////////////////////////////////////////////////////////////////
////
//
var curfilt = null;

function appendfilter(filt)
{
	if( curfilt == null){
		filters[0] = filt;
		curfilt = filters[0];
		return;
	}
	curfilt.next = filt;
	curfilt = curfilt.next;
}

function initkeytar()
{	var synth = basicsynth();
	var conf = document.getElementById("config");
	

	appendfilter( new repfilter());	// absorb keyboard auto repeats
	appendfilter( synth);

	window.onkeydown = keydown;
	window.onkeyup = keyup;
	window.onkeypress = keypress;

	window.onmousedown = mousedown;
	window.onmousemove = mousemove;
	window.onmouseup = mouseup;

	document.body.addEventListener("touchstart", touchstart, false);
	document.body.addEventListener("touchend", touchend, false);
	document.body.addEventListener("touchmove", touchmove, false);

}



//////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////// UI       /////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

var strokes = [ null, null, null, null, null, null ];
var strings = [ null, null, null, null, null, null ];

function stroke(id, idx)
{	this.id = id;
	this.idx = idx;
	this.lastx = 0;
	this.lasty = 0;
	this.firstx = 0;
	this.firsty = 0;
	this.control = null;
	this.active = false;

	this.start = function(x, y)
	{
		this.lastx = x;
		this.lasty = y;
		this.firstx = x;
		this.firsty = y;

		this.dostart(x, y);
	}

	// override this.dostart
	this.dostart = function(x, y)
	{	var k = findctrl(x, y);

		if( k != null){
//			status(k.id);
			this.control = k;
			this.control.start(x, y, this);
		}
		this.active = true;
	}

	this.move = function(x, y)
	{   var k;
	
	    if( this.control == null && this.active){
            k = findctrl(x, y);
		    if( k != null){
			    this.control = k;
			    this.control.start(x, y, this);
		    }
	    }
		this.domove(x, y);
		this.lastx = x;
		this.lasty = y;
	}

	// override this.domove
	this.domove = function(x, y)
	{
		if( this.control != null){
//status("Stroke "+this.idx+" "+this.id+"  move: "+(x-this.firstx)+" "+(y-this.firsty) );
			this.control.move(x, y, this);
		}
	}

	this.end = function(x, y)
	{
		this.doend(x, y);
		this.id = null;
		strokes[this.idx] = null;
		this.active = false;
	}

	// override this.doend
	this.doend = function(x, y)
	{
		if( this.control != null){
//status("Stroke "+this.idx+" "+this.id+"  end: "+x+" "+y);
			this.control.end(x, y, this);
			this.control = null;
		}
	}

	// adjust values based on x-lastx, y-lasty
	// used when switching back from mouse button pressed 
	// to no mouse button.
	this.reset = function (x, y)
	{	var dx = x-this.lastx;
		var dy = y-this.lasty;

		this.firstx += dx;
		this.firsty += dy;

//		status("Reset "+this.id+" "+dx+" "+dy);

		this.lastx = x;
		this.lasty = y;
	}

}

// look for stroke and add it if not exist
function findstroke( id)
{	var first = -1;
	var i;

	for(i = 0; i < strokes.length; i++){
		if( strokes[i] == null ){
			if( first == -1){
				first = i;
			}
		}else if( strokes[i].id == id){
			return i;
		}
	}
	// first free entry
	if( first >= 0){
		strokes[first] = new stroke(id, first);
		return first;
	}
	return i;
}

var isfullscreen = false;


function touchstart(evt)
{	var s;
	var i;
	var changed = evt.changedTouches;
	var idx;

//	status2("CLS: "+changed.length+":"+changed[0].identifier);
	if( isfullscreen){
		evt.preventDefault();
//		evt.stopPropagation();
	}
	for(i=0 ; i < changed.length; i++){
		idx = findstroke( changed[i].identifier);

		if( idx != strokes.length){
			s = strokes[idx];
//			status("IDX S="+idx+":"+s.idx);
			s.start( Math.floor(changed[i].screenX), Math.floor(changed[i].screenY) );
		}else {
			status("TS no stroke");
		}
	}

//	if( !isfullscreen){
//		isfullscreen = true;
		toggleFullScreen();
//	}
}

function touchmove(evt)
{	var i;
	var changed = evt.changedTouches;
	var s;
	var idx;

//	status2("CLM: "+changed.length+":"+changed[0].identifier);
	for(i=0; i < changed.length; i++){
		idx = findstroke( changed[i].identifier);
		if( idx != strokes.length){
			s = strokes[idx];
//	status("IDX M="+idx+":"+s.idx+" "+s.id);
			s.move( Math.floor(changed[i].screenX), Math.floor(changed[i].screenY) );
		}else {
			status("TM no stroke");
		}
	}
	evt.preventDefault();
	evt.stopPropagation();
}

function touchend(evt)
{	var i;
	var changed = evt.changedTouches;
	var s;
	var idx;

	for(i=0; i < changed.length; i++){
		idx = findstroke( changed[i].identifier);
//	status2("CLE2: "+changed.length+":["+changed[i].identifier+"] "+idx);
		if( idx != strokes.length){
			s = strokes[idx];
//	status("IDX E="+idx+":"+s.idx+" "+s.id);
			s.end( Math.floor(changed[i].screenX), Math.floor(changed[i].screenY) );
			strokes[idx] = null;
		}else {
			status("TE no stroke");
		}
	}
	evt.preventDefault();
//	evt.stopPropagation();
}

var curbutton = "mouse";

function nodefault(evt)
{   var x = evt.screenX;
    var y = evt.screenY;
     
    if( x < 120 || x > 300){
        if( y < 90 || y > 150){
        	evt.preventDefault();
	        evt.stopPropagation();
        }
    }
}
function mousedown(evt)
{	var s;
	var idx;
	
	curbutton = "mouse"+evt.button;
	idx = findstroke(curbutton);

	if( idx != strokes.length){
		s = strokes[idx];
		s.start( evt.screenX, evt.screenY);
		if( !isfullscreen){
			isfullscreen = true;
			toggleFullScreen();
		}
	}
	nodefault(evt);
}

function mousemove(evt)
{	var s;
    var idx;
	
//	status2("X="+evt.screenX+" Y="+evt.screenY);
	idx = findstroke(curbutton);

	if( idx != strokes.length){
		s = strokes[idx];
		nodefault(evt);

		s.move( evt.screenX, evt.screenY);
	}
	
}

function mouseup(evt)
{	var s;
    var idx;
	
	idx = findstroke(curbutton);

	if( idx != strokes.length){
		s = strokes[idx];
		nodefault(evt);

		s.end( evt.screenX, evt.screenY);
	}
	
	curbutton = "mouse";

	idx = findstroke(curbutton);

	if( idx != strokes.length){
		s = strokes[idx];
		s.reset( evt.screenX, evt.screenY);
	}

}

//////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////// ACTIONS /////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////

function CC14(cmd, rset, rvalue)
{   this.value = rvalue;
    this.cmd = cmd; // 0xe0 for pitchbend
    this.sval=0;
    this.scale=1;
    this.mscale=1;
    this.last = rvalue;
    this.active = false;
    this.reset = rset;
    this.rvalue = rvalue;

    this.start = function(x, w, mw)
    {   var tmp = 0x3fff - this.value;
//    status("CC14 "+this.cmd+" x="+x+" w="+w);
        this.sval = x;
        if( tmp != 0){
            this.scale = w / tmp;   // ticks per movement in positive direction
        }else {
            this.scale = 0;
        }
        if( this.value != 0){
            this.mscale = mw / this.value;          //
        }else {
            this.mscale = 0;
        }
//    status2("CC14 "+this.cmd+": x="+x+" w="+w+" scale="+this.scale);
        processmidi([this.cmd, this.value&0x7f, Math.floor(this.value/0x80) ], 0);
        this.active = true;
    }

    this.move = function(x)
    {   var cx = x - this.sval;   // how far moved
        var vx=0;
        
        if( x < this.sval){
            vx = Math.floor(cx / this.mscale);   // fraction of range
        }else {
            vx = Math.floor(cx / this.scale);   // fraction of range
        }
        if( this.active){
            
            vx = this.value+vx;
            if( vx < 0){
                vx = 0;
            }else if( vx > 0x3fff){
                vx = 0x3fff;
            }
            
//        status2("CC14 "+this.cmd+": vx="+vx+" val="+this.value);
        
            if( this.last != vx){
                this.last = vx;
                processmidi([this.cmd, this.last&0x7f, Math.floor(this.last/0x80) ], 0);
                
                // recalculate the scaling?
            }
        }
    }

    this.end = function(x, w)
    {
        this.value = this.last;
        if( this.reset){
            // reset to init value
            this.value = this.rvalue;
        }
        processmidi([this.cmd, this.value&0x7f, Math.floor(this.value/0x80) ], 0);
        this.active = false;
    }

// set initial value
    processmidi([this.cmd, this.value&0x7f, Math.floor(this.value/0x80) ], 0);

}

function CC7(cmd, cc, rset, rvalue)
{   this.value = rvalue;
    this.cc = cc;
    this.cmd = cmd;
    this.sval=0;
    this.scale=1;
    this.mscale=1;
    this.last = rvalue;
    this.active = false;
    this.reset = rset;
    this.rvalue = rvalue;

// initial x, positive width, negative width
    this.start = function(x, w, mw)
    {   var tmp = 0x7f - this.value;
    
        this.sval = x;
        if( tmp != 0){
            this.scale = w / (0x7f - this.value);   // ticks per movement in positive direction
        }else {
            this.scale = 0;
        }
        if( this.value != 0){
            this.mscale = mw / this.value;          //
        }else {
            this.mscale = 0;
        }
//    status("CC7 "+this.cmd+":"+this.cc+" x="+x+" w="+w+" scale="+this.scale);
        this.active = true;
    }

    this.move = function(x)
    {   var cx = x - this.sval;   // how far moved
        var vx;
        
        if( x < this.sval){
            vx = Math.floor(cx / this.mscale);   // fraction of range
        }else {
            vx = Math.floor(cx / this.scale);   // fraction of range
        }
        if( this.active){
            
            vx = this.value+vx;
            if( vx < 0){
                vx = 0;
            }else if( vx > 0x7f){
                vx = 0x7f;
            }
            
//        status("CC7 "+this.cmd+":"+this.cc+" vx="+vx+" val="+this.value);
        
            if( this.last != vx){
                this.last = vx;
                processmidi([this.cmd, this.cc, this.last ], 0);
                
                // recalculate the scaling?
            }
        }
    }

    this.end = function(x)
    {   
        this.value = this.last;
        if( this.reset){
            // reset to init value
            this.value = this.rvalue;
        }
        processmidi([this.cmd, this.cc, this.value ], 0);
        this.active = false;
    }

// set initial value
    processmidi([this.cmd, this.cc, this.value ], 0);

}


//////////////////////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////// CONTROLS /////////////////////////////////////
//////////////////////////////////////////////////////////////////////////////////////////////////
function xyvisual()
{   this.state = 0;
    this.pi180 = Math.PI/180;
    this.angle = 0;
    this.red = 255;
    this.green = 0;
    this.blue = 0;
    this.active = false;
    this.color = "black";
    this.x = 0;
    this.y = 0;

// in screen coordinates..
    this.draw = function(ctx, l, t, r, b, x, y, active)
    {   var w = r-l;
        var h = b - t;
        var cx = l + Math.floor(w/2);
        var cy = t + Math.floor(h/2);
        var perx;
        var pery;
        
        this.active = active;
        this.x = (x-l)*100/ w;
        this.y = y-t;
        
        if( active){
            perx = Math.floor((this.x * 2.55) );
            pery = Math.floor((this.y * 2.55) );
      		ctx.fillStyle = "rgb("+pery+","+perx+","+perx+")";
    		ctx.fillRect(l, t, w, h);
        }else {
      		ctx.fillStyle = this.color;
    		ctx.fillRect(l, t, w, h);
        }

    }

    this.animate = function()
    {
        if( this.active){
        }else {
            if( this.state > 0){
                this.red += 8;
    //            this.green += 64;
    //          this.blue += 64;
                if( this.red > 255){ this.red = 255; this.state = 0; }
                if( this.green > 255){ this.green = 255; }
                if( this.blue > 255){ this.blue = 255; }
                
            }else {
                this.red -= 8;
    //            this.green -= 64;
    //            this.blue -= 64;
                if( this.red < 128){ this.red = 0; this.state = 1; }
                if( this.green < 0){ this.green = 0; }
                if( this.blue < 0){ this.blue = 0; }
            }
            this.color = "rgb("+this.red+","+this.green+","+this.blue+")";
        }
        needredraw = true;
    }
}

var debugcnt=0;
function xycontrol(l, t, r, b, ctrlx, ctrly)
{   // layout values
    this.left = l;
    this.top = t;
    this.right = r;
    this.bottom = b;
    // actual coords
    this.l = 0;
    this.r = 0;
    this.t = 0;
    this.b = 0;
    
    //
    this.id = "xycontrol";
    this.sx=50;
    this.sy=50;
    // Midi function
    this.ctrlx = ctrlx;
    this.ctrly = ctrly;
    this.angle=0.0;
    this.pi180 = Math.PI/180;
    
    this.color = "red";
    this.visual = null;
    this.active = false;
    
    
    this.hit = function(x, y)
    {   
        if( x < this.l || x > this.r){
            return false;
        }
        if( y < this.t || y > this.b ){
            return false;
        }
        return true;
    }
    
    this.start = function(x, y, stroke)
    {   var cw = this.r - this.l;
        var ch = this.b - this.t;
        var xr = x - this.l;
        var yr = y - this.t;
        
        if( this.ctrlx != null){
            this.ctrlx.start(xr, cw - xr, xr);
        }
        if( this.ctrly != null){
            this.ctrly.start(yr, ch - yr, yr);
        }
        this.active = true;
    }
   
    this.end = function(x, y, stroke)
    {
        if( this.ctrlx != null){
            this.ctrlx.end(x - this.l);
        }
        if( this.ctrly != null){
            this.ctrly.end(y - this.t);
        }
        this.active = false;
//        status2("end");
    }

    this.move = function(x, y, stroke)
    {
        if( this.ctrlx != null){
            this.ctrlx.move(x - this.l);
        }
        if( this.ctrly != null){
            this.ctrly.move(y - this.t);
        }
        this.sx = x;
        this.sy = y;
    }
    
    this.resize = function()
    {   this.l = Math.floor( (screenwidth * this.left) / 100);
        this.r = Math.floor( (screenwidth * this.right) / 100);
        this.t = Math.floor( (screenheight * this.top) / 100);
        this.b = Math.floor( (screenheight * this.bottom) / 100);
    }

    this.process = function( data, port )
    {
    }
    
    this.key = function( r, code)
    {
        return false;
    }
    
    // xycontrol
    this.draw = function()
    {   var cnvs = document.getElementById("display");
    	var ctx;
    	var w = Math.floor((this.r-this.l)/2);
    	var h = Math.floor((this.b-this.t)/2);
    	
//    	alert("XY="+this.l+" "+this.t+" "+this.r+" "+this.b);
    	
    	if( cnvs == null){
    	    return;
    	}
    	ctx = cnvs.getContext('2d');

		ctx.save();
		ctx.scale(1, 1);

		
		if( this.visual != null){
		    this.visual.draw(ctx, this.l, this.t, this.r, this.b, this.sx, this.sy, this.active );
		}else {
    		ctx.fillStyle = this.color;
	    	ctx.fillRect(this.l, this.t, this.r-this.l, this.b-this.t);
		}
		
		ctx.restore();

    }
    
}

// grid control
// x by y 
function gridcontrol(l, t, r, b, x, y)
{   // layout values
    this.left = l;  // percents
    this.top = t;
    this.right = r;
    this.bottom = b;
    // actual coords
    this.l = 0;
    this.r = 0;
    this.t = 0;
    this.b = 0;
    //
    this.id = "grid";
    this.sx=0;
    this.sy=0;
    this.nx = x;
    this.ny = y;
    
    this.current = -1;  // current cell
    
    this.data = new Array( x * y);
    var i;
    
    for(i=0; i < x*y; i++ ){
        this.data[i] = null;
    }
    
    this.hit = function(x, y)
    {   
        if( x < this.l || x > this.r){
            return false;
        }
        if( y < this.t || y > this.b ){
            return false;
        }
        return true;
    }
    
    // return cell number or a -ve if out of range.
    this.cell = function(x, y)
    {   var cw = this.r - this.l;
        var ch = this.b - this.t;
        var w, nx;
        var h, ny;
        
        w = cw/this.nx;
        nx = Math.floor( x / w);
        if( nx < 0 || nx >= this.nx){
            return -2;
        }

        h = ch/this.ny;
        ny = Math.floor(y / h);
        if( ny < 0 || ny >= this.ny){
            return -3;
        }
        
        status2("Cell="+x+" "+y+" : "+nx+" "+ny);
        return nx + this.nx*ny;
    }
    
    this.start = function(x, y, stroke)
    {   var xr = x - this.l;
        var yr = y - this.t;
        var c = this.cell(xr, yr);

        if( c < 0){
            this.current = -1;
            return;
        }
        this.current = c;
        if( this.data[ this.current ] != null){
            this.data[ this.current].start();
        }
    }
   
    this.end = function(x, y, stroke)
    {   var xr = x - this.l;
        var yr = y - this.t;
        var c = this.cell(xr, yr);

        if( this.current != c ){
            if(this.current != -1 && this.data[ this.current ] != null){
                this.data[ this.current].end();
            }
        }
        if( c >= 0){
            this.current = c;
            if( this.data[ this.current ] != null){
                this.data[ this.current].end();
            }
        }
        this.current = -1;
    }

    this.move = function(x, y, stroke)
    {   var xr = x - this.l;
        var yr = y - this.t;
        var c = this.cell(xr, yr);
        
        if( this.current < 0 && c >= 0){
            this.current = c;
            if( this.data[ this.current ] != null){
                this.data[ this.current].start();
            }
            return;
        }
        
        if( this.current != c){
            if(this.current != -1 && this.data[ this.current ] != null){
                this.data[ this.current].end();
            }
            if( c < 0){
                this.current = -1;
            }else {
                this.current = c;
                if( this.data[ this.current ] != null){
                    this.data[ this.current].start();
                }
            }
        }
    }
    
    this.resize = function()
    {   this.l = Math.floor( (screenwidth * this.left) / 100);
        this.r = Math.floor( (screenwidth * this.right) / 100);
        this.t = Math.floor( (screenheight * this.top) / 100);
        this.b = Math.floor( (screenheight * this.bottom) / 100);
    }
    
    this.process = function( data, port )
    {
    }
    
    this.key = function( r, code)
    {
        return false;
    }
    
    this.init = function(x, y, obj)
    {
        this.data[x+this.nx*y] = obj;
    }

// grid control
    this.draw = function()
    {   var cnvs = document.getElementById("display");
    	var ctx;
    	var x, y;
    	var w = Math.floor( (this.r-this.l)/this.nx);
    	var h = Math.floor( (this.b-this.t)/this.ny);
    	var cell = 0;
    	
       	if( cnvs == null){
    	    return;
    	}
//    	alert("GRID="+this.l+" "+this.t+" "+this.r+" "+this.b);

    	ctx = cnvs.getContext('2d');

		ctx.save();
		ctx.scale(1, 1);

		ctx.fillStyle = "blue";
		ctx.fillRect(this.l, this.t, this.r-this.l, this.b-this.t);
		ctx.strokeStyle = "black;";
		
		for(x=0; x < this.nx; x++){
		    for(y=0; y < this.ny; y++){
        		ctx.strokeRect(this.l + x*w, this.t+y*h, w-1, h-1);
        		if( this.data[cell ] != null){
            		this.data[cell].draw(ctx, this.l + x*w, this.t+y*h, this.l + x*w+w, this.t+y*h+h);
                }
        		cell++;
		    }
		}
		ctx.restore();

    }
}

function keycontrol(l, t, r, b)
{   // layout values
    this.left = l;  // percents
    this.top = t;
    this.right = r;
    this.bottom = b;
    // actual coords
    this.l = 0;
    this.r = 0;
    this.t = 0;
    this.b = 0;
    //
    this.id = "keyboard";
    this.sx=0;
    this.sy=0;

    this.hit = function(x, y)
    {   
        if( x < this.l || x > this.r){
            return false;
        }
        if( y < this.t || y > this.b ){
            return false;
        }
        return true;
    }
    
    this.start = function(x, y, stroke)
    {
    }

    this.move = function(x, y, stroke)
    {
    }

    this.end = function(x, y, stroke)
    {
    }
    
    
    this.resize = function()
    {   this.l = Math.floor( (screenwidth * this.left) / 100);
        this.r = Math.floor( (screenwidth * this.right) / 100);
        this.t = Math.floor( (screenheight * this.top) / 100);
        this.b = Math.floor( (screenheight * this.bottom) / 100);
    }

    this.process = function(data, port)
    {
        processmidi(data, port);
    }

    this.key = function( r, code)
    {
        if( code == 8 && r == 1){
            // guitar mode
            isguitar = true;
            controls = null;
            needredraw = true;
            return false;
        }
        if( code == 187 && r == 1){
            changewave();
            return false;
        }
        if( code == 220 && r == 1){
            changedrive();
            return false;
        }
        return true;
    }
    
    this.draw = function()
    {
    }
}

// create some "knobs"
var pitchbend = new CC14(0xe0, true, 0x2000);
var cutoff =  new CC7(0xb0, CUTOFF, false, 0x40);
var resonance =  new CC7(0xb0, RESONANCE, false, 0x40);
var lfodepth =  new CC7(0xb0, LFODEPTH, true, 0x00);
var lforate =  new CC7(0xb0, LFORATE, false, 0x40);
var filtermod = new CC7(0xb0, FILTERMOD, true, 0x00);

var keytarcontrols = [
new xycontrol(0,10,50,99,filtermod, pitchbend ),
new xycontrol(52,0,99,90, lfodepth ,lforate),
new keycontrol(0,0,99,99),
null
];

keytarcontrols[1].color = "green";

// open string..
var gnotes = [ 48, 53, 58, 63, 67, 72 ];
var major = [ 0, 2, 2, 1, 0, 0 ];
var minor = [ 0, 2, 2, 0, 0, 0 ];
var fingering = [ 0, 0, 0, 0, 0, 0 ];
var root=0;


// control
function guitarstring(idx)
{   this.idx = idx;
    this.last = -1;
    this.active = false;

    this.start = function()
    {
        if( synths[this.idx] != null){
            synths[this.idx].attack = 0.05;
            synths[this.idx].vcfattack = 0.2;
            this.last = gnotes[this.idx]+fingering[this.idx]+root;
            synths[this.idx].doinput([0x90, this.last, 64], 0);
            this.active = true;
        }
    }
    
    this.end = function()
    {
        if( synths[this.idx] != null){
            synths[this.idx].doinput([0x80, this.last, 64], 0);
        }
        this.active = false;
    }

    this.changed = function()
    {
        if( this.active){
            this.end();     // stop current
            this.start();   // start new
        }
        return this.active;
    }
    
    this.draw = function(ctx, l, t, r, b)
    {
       if( this.active){
        	ctx.fillStyle = "white";
	    	ctx.fillRect(l+5, t+5, r-l-10, b-t-10);
	    }
    }
}

// CODE, ROOT, CHORD
var fingermap = [
16, 0, 1, 
13, 0, 2,
222, 0, 2,
//
191, 1, 1,
186, 1, 2,

//
190, 2, 1,
76, 2, 2,
188, 3, 1,
75, 3, 2,
77, 4, 1,
74, 4, 2,
78, 5, 1, 
72, 5, 2,
66, 6, 1,
71, 6, 2,
86, 7, 1, 
70, 7, 2,
67, 8, 1,
68, 8, 2,
88, 9, 1, 
83, 9, 2,
90, 10, 1, 
65, 10, 2,
20, 11, 2,
0
]; 


function openstrings()
{   var i;

    root = 0;
    for(i=0; i < 6; i++){
        fingering[i] = 0;
    }
    guitarctrl.changed();
}

function setfingering(chord)
{   var i;

    if( chord == 1){
        for(i=0 ; i < 6; i++){
            fingering[i] = major[i];
        }
    }else if( chord == 2){
        for(i=0 ; i < 6; i++){
            fingering[i] = minor[i];
        }
    }
    guitarctrl.changed();
}

function fingerboard(l, t, r, b)
{   // layout values
    this.left = l;  // percents
    this.top = t;
    this.right = r;
    this.bottom = b;
    // actual coords
    this.l = 0;
    this.r = 0;
    this.t = 0;
    this.b = 0;
    //
    this.id = "fingerboard";
    this.sx=0;
    this.sy=0;
    this.notelist = new objlist();

    this.hit = function(x, y)
    {   
        if( x < this.l || x > this.r){
            return false;
        }
        if( y < this.t || y > this.b ){
            return false;
        }
        return true;
    }
    
    this.start = function(x, y, stroke)
    {
    }

    this.move = function(x, y, stroke)
    {
    }

    this.end = function(x, y, stroke)
    {
    }
    
    
    this.resize = function()
    {   this.l = Math.floor( (screenwidth * this.left) / 100);
        this.r = Math.floor( (screenwidth * this.right) / 100);
        this.t = Math.floor( (screenheight * this.top) / 100);
        this.b = Math.floor( (screenheight * this.bottom) / 100);
    }

// midi note on off...
    this.process = function(data, port)
    {
    }

    // more complicated than I would like.
    // the active keycode is the head of the notelist.
    // new key press becomes the new head
    // release of head will make the next in list the new active one if there is one.
    // if after release, list is empty, then openstrings.    
    this.key = function( r, code)
    {   var i;
        var idx;
        var chord=0;
        var n = this.notelist.head; // .obj is the code
        var nc = null;
        
        if( code == 8 && r == 1){
            // keytar mode on release
            isguitar = false;
            controls = null;
//            alert("set keytar");
            return false;
        }
        if( code == 187 && r == 1){
            changewave();
            return false;
        }
        if( code == 220 && r == 1){
            changedrive();
            return false;
        }
        status2("Keycode="+code);
        
        if( r == 1){
            // remove code from list
            if( n == null){
                return false;   // do not care...
            }
            nc = n.obj;
            if( nc == code){
                // first one.
                this.notelist.head = n.next;
                n = this.notelist.head;
                if( n != null){
                    nc = n.obj;
                }else {
                    openstrings();
                    return false;
                }
            }else {
                while(n.next != null){
                    nc = n.next.obj;
                    if( nc == code){
                        // unlink next
                        n.next = n.next.next;
                        return false;
                    }
                
                    n = n.next;
                }
                return false;   // not found so ignore
            }
            code = nc;          // new first code, so need to process.
        }else {
            // r ==0 key press.
            while(n != null){
                nc = n.obj;
                if( nc == code){
                    return false;   // already pressed
                }
                n = n.next;
            }
        }

        // find what to do.
        for(idx = 0; fingermap[idx] != 0; idx += 3){
            if( fingermap[idx] == code){
                root = fingermap[idx+1];
                chord = fingermap[idx+2];
                break;
            }
        }
        if( fingermap[idx] != 0){
            // add this one
            addlist(this.notelist, code);
            setfingering(chord);
        }else {
            status2("Keycode="+code);
        }
        return false;
    }
    
    this.draw = function()
    {
    }
}


// control generator
function guitar(l,t,r,b)
{   this.control = new gridcontrol(l, t, r, b, 1, 8);
    this.offset = 1;
    var i;
    
    for(i=0; i < 6; i++){
        this.control.data[i+this.offset] = new guitarstring(i);
    }

    this.changed = function()
    {   var i;
        
        for(i=0; i < 6; i++){
            this.control.data[i+this.offset].changed();
        }
    }

}

// (left, top, right, bottom)
var guitarctrl = new guitar(0, 5, 50, 95);
var guitarxyctrl = new xycontrol(52,0,99,50, filtermod, pitchbend );

guitarxyctrl.visual = new xyvisual();

// addlist(animatelist, guitarxyctrl.visual);

var guitarcontrols = [
guitarctrl.control,
guitarxyctrl,
new xycontrol(52,52,99,99, lfodepth ,lforate),
new fingerboard(0,0,50,99),
null
];

guitarcontrols[2].color = "green";

var controls = null;

// use either guitar controls or keytar controls.
function setcontrols()
{   var i;

    if( isguitar){
        controls = guitarcontrols;
    }else {
        controls = keytarcontrols;
    }
    needredraw = true;
    needresize = true;
}

function resizeall()
{   var idx;

//	status("W="+screenwidth+" H="+screenheight);
    if( controls != null){
        for(idx=0; controls[idx] != null; idx++){
            controls[idx].resize();
        }
    }
    needredraw=true;
}


function findctrl(x, y)
{   var idx;

    if( controls != null){
        for(idx = 0; controls[idx] != null; idx++){
            if( controls[idx].hit(x, y)){
                return controls[idx];
            }
        }
    }
    return null;
}

// keyCode, Shift, Ctrl, Alt, Meta

var keymap = [ 
	0,		// a	
	55,		// b
	52,		// c
	51,		// d
	64,		// e
	0,		// f
	54,		// g
	56,		// h
	72,		// i
	58,		// j
	0,		// k
	61,		// l
	59,		// m
	57,		// n
	74,		// o
	76,		// p
	60,		// q
	65,		// r
	49,		// s
	67,		// t
	71,		// u
	53,		// v
	62,		// w
	50,		// x
	69,		// y
	48,		// z
	];

var keymapn = [
	75,		// 0
	0,		// 1
	61,		// 2
	63,		// 3
	0,		// 4
	66,		// 5
	68,		// 6
	70,		// 7
	0,		// 8
	73		// 9
];

var keymap2 = [
	63,		// ;
	0,		// =
	60,		// ,
	0,		// -
	62,		// .
	64,		// /
];

function keytonote(event)
{	var note = 0;
	var key = event.keyCode;

	if( key >= 48 && key <= 57){	// 1-9
		note = keymapn[ key-48];
	}else if( key >= 65 && key <= 90){
		note = keymap[ key-65];
	}else if( key >= 186 && key <= 191){
		note = keymap2[ key-186];
	}

	return note;
}
 
function keydown(event)
{	var note;
    var idx;
    var ctrls;

	if( event.keyCode == 116 ||  event.keyCode == 122||  event.keyCode == 123){
		return true;
	}
	event.preventDefault();
	event.stopPropagation();

//		status2("keydown: "+note+" "+event.shiftKey+" "+event.ctrlKey);
    // local copy since .key() can set controls to null for a redraw.
    ctrls = controls;
    if( ctrls != null){
        for(idx=0; ctrls[idx] != null; idx++){
            if( ctrls[idx].key( 0, event.keyCode)){
                note = keytonote( event);
                if( note != 0){
        	        ctrls[idx].process( [ 0x90, note, 64], 0);
        	    }
            }
	    }
	}
	return false;
}

function keypress(event)
{
	event.preventDefault();
	event.stopPropagation();

	status("Keypress\n");

	return false;
}


function keyup(event)
{	var note;
    var idx;
    var ctrls;

	if( event.keyCode == 116||  event.keyCode == 123){
		return true;
	}

    ctrls = controls;
    if( ctrls != null){
        for(idx=0; ctrls[idx] != null; idx++){
            if( ctrls[idx].key( 1, event.keyCode)){
                note = keytonote( event);
                if( note != 0){
        	        ctrls[idx].process( [ 0x80, note, 64], 0);
        	    }
            }
	    }
	}
	event.preventDefault();
	event.stopPropagation();
	return false;
}

//////////////////////////////////////////////////////////////////////////////////

function findbynote(head, note)
{
	while( head != null){
		if( head.type() == "osc" && head.curnote == note){
			return head;
		}
		head = head.next;
	}
	return null;
}

//////////////////////////////////////////////////////////////////////////////////
//

repfilter.prototype = Object.create( sound.prototype);

function repfilter()
{	
	sound.call(this);
	this.map = Array(128);

	this.type = function()
	{
		return "repfilter";
	}

	this.doinput = function(data, port)
	{	var cmd = data[0] & 0xf0;

		if( cmd == 0x90 && data[2] == 0){
			cmd = 0x80;
		}

		if( cmd == 0x90){
			if( this.map[data[1]]== true ){	
				return true;
			}
			this.map[data[1]] = true;
		}else if( cmd == 0x80){
			this.map[ data[1] ] = false;
		}
		if( cmd == 0xb0){
			// CC
			return false;
		}
		if( cmd == 0xe0){
			// bend
			return false;
		}
		
		return false;	// pass on to rest of chain.
	}

}

////////////////////////////////////////////////////////////////////////////
//// process midi messages.
//// pass down the filter chain else display the data.
function doinput(evt, port)
{
	processmidi(evt.data, port);
}

////////////////////////////////////////////////////////////////////////////
// return true if data was used.

function runfilters(filt, data, port)
{	while( filt != null ){
		if( filt.doinput( data, port) ){
			return true;
		}
		filt = filt.next;
	}

	return false;
}

// send the midi data to the synths etc in the filter list.
// if the synth doinput returns true then that synth accepted the data.

function processmidi(data, port)
{	var mtype;
	var mchan;
	var note;
	var msg="";
	var filt;

	mtype = data[0]&0xf0;
	mchan = data[0]&0x0f;

//	if( mchan != 0){
//		port = mchan;
//		status2("Chan: "+mchan);
//	}

	filt = filters[port];

	if( runfilters(filt, data, port)){
		return true;
	}

	if( mtype == 0x90){
		if( data[2] != 0){
			// note on
			if( showmidiUI != null){
				showmidiUI.status("Note="+data[1]+" chan="+mchan+" port="+port);
			}
			return false;
		}else {
			// running status note off
			mtype = 0x80;
		}
	}

	if( mtype == 0x80){
		note = data[1];
		// note off
			if( showmidiUI != null){
				showmidiUI.status("Off="+data[1]+" chan="+mchan+" port="+port);
			}
	}

	if( mtype == 0xa0){
		// aftertouch
		status("Aftertouch: "+data[1]+" "+data[2]);
	}

	if( mtype == 0xb0){
		// control change
		if( data[1] & 0x20){
			if( showmidiUI != null){
				showmidiUI.status2("CC("+data[1]+" , "+data[2]+") "+mchan+" port="+port);
			}
		}else {
			if( showmidiUI != null){
				showmidiUI.status("CC("+data[1]+" , "+data[2]+") "+mchan+" port="+port);
			}
		}

	}

	if( mtype == 0xc0){
		// program change
		status("Program Change: "+data[1]);
	}


	if( mtype == 0xd0){
		// Channel pressure
		if( showmidiUI != null){
//			showmidiUI.status("Pressure="+data[1]);
		}

	}
	if( mtype == 0xe0){
		var low, high;
		var bend;
		// Bend
		low = data[1];
		high= data[2];
		bend = high*128+low;

		if( showmidiUI != null){
			showmidiUI.status("bend="+bend);
		}

	}
	return false;
}

var showxycontrols = false;
var showcontrols = false;

// static info..
function drawcontrols()
{	var msg="";

	msg += '<table>\n';
    msg += '  \n';
    msg += '      \n';
    msg += '         \n';
    msg += '  \n';
    if( showxycontrols){
        msg += '      \n';
        msg += '        Mouse X control:\n';
        msg += '        \n';
        msg += '          \n';
        msg += '        \n';
        msg += '        \n';
        msg += '      \n';
        msg += '      \n';
        msg += '        Mouse Y control:\n';
        msg += '        \n';
        msg += '          \n';
        msg += '        \n';
        msg += '        \n';
        msg += '      \n';
    }
    msg += '      \n';
	return msg;
}

function UIstart()
{   var d = document.getElementById("start");

    if( d != null){
	    if( initsynth() ){
		    status("Failed to init audio!");
		    return;
	    }

	    initkeytar();
        d.style.zIndex= -10;
        d.style.display = "none";
        playtune();
        
   	    if( !isfullscreen){
		    isfullscreen = true;
		    toggleFullScreen();
	    }
    }
}



function toggleFullScreen() {
  var doc = window.document;
  var docEl = doc.documentElement;

  var requestFullScreen = docEl.requestFullscreen || docEl.mozRequestFullScreen || docEl.webkitRequestFullScreen || docEl.msRequestFullscreen;
  var cancelFullScreen = doc.exitFullscreen || doc.mozCancelFullScreen || doc.webkitExitFullscreen || doc.msExitFullscreen;

  if(!doc.fullscreenElement && !doc.mozFullScreenElement && !doc.webkitFullscreenElement && !doc.msFullscreenElement) {
    requestFullScreen.call(docEl);
  }
//  else {
//    cancelFullScreen.call(doc);
//  }
}



////////////////////////////////////////////////////////////////////////////

var redraw=true;
var needredraw=true;
var needsplash=true;

function splashscreen()
{   var msg="";

    msg+= '<div id="start" style="z-index:10;position:absolute;top:50px;left:100px;background-color:white;border-style:solid;border-width:5px;border-color:green;padding:10px;margin:20px;" >\n';
    msg+= '<h1>Phone\'y Guitar/Keytar\n';
    msg+= '<input type="button" onclick="UIstart();" value="Let\'s have FUN!" >\n';
    msg+= '</div>\n';
    return msg;
}

function drawscreen()
{	var d = document.getElementById("main");
	var msg="";
	var w6 = Math.floor(screenwidth/6);
	var w3 = w6+w6;
	var h4 = Math.floor(screenheight/4);
	var h2 = h4+h4;
	var i;
	var k;

	var l=0;
	var t=0;
		// create a canvas tag
	msg += '<canvas class="fixed" id="display" width="'+screenwidth+'" height="'+screenheight+'" ';
	msg += 'style="left:'+0+'px;top:'+0+'px;width:'+screenwidth+'px;height:'+screenheight+'px;z-index:1;"';
	msg += '></canvas>\n';

	msg += drawcontrols();
	
	if( needsplash){
	    msg+= splashscreen();
	    needsplash = false;
	}

	if( d != null){
		d.innerHTML = msg;
	}
}


/////////////////////////////////////////////////////////////////////////////
var screenwidth=0;
var screenheight=0;
var lastchange = 0;
var hasfocus = 0;
var tick=0;

function objlist()
{   this.head = null;
}

function carrier()
{   this.next = null;
    this.obj = null;
}

function getcarrier(obj)
{   var ret;
    if( carrierlist.head != null){
        ret = carrierlist.head;
        carrierlist.head = ret.next;
    }else {
        ret = new carrier();        
    }
    ret.obj = obj;
    ret.next = null;
    return ret;
}

function freecarrier(c)
{   var x = carrierlist.head;
    
    while(x != null){
        if( x == c){
            // already free
            return;
        }
        x = x.next;
    }
    c.next = carrierlist.head;
    carrierlist.head = c;
}


function addlist(list, obj)
{   var c;

    c = list.head;
    while(c != null){
        if( c.obj == obj){
            return;
        }
        c = c.next;
    }
    c = getcarrier(obj);
    c.next = list.head;
    list.head = c;
}

// compare returns -1,0,+1
function addorderedlist(list, obj, compare)
{   var c,cn;
    var res;

    c = list.head;
    if( c == null){
        // first;
        c = getcarrier(obj);
        c.next = null;
        list.head = c;
        return 1;
    }
    ret = compare(c.obj, obj);
    if( ret < 0 ){
        // insert before
        c = getcarrier(obj);
        c.next = list.head;
        list.head = c;
        return 1;
    }
    if( ret == 0){
        return 0; // found
    }

    while(c.next != null){
        cn = c.next;
        ret = compare(cn.obj, obj);
        if( ret < 0){
            // insert before
            break;
        }
        if( ret == 0){
            return 0;
        }
        c = cn;
    }
    // insert before next
    cn = getcarrier(obj);
    cn.next = c.next;
    c.next = cn;
    return 1;
}

function dellist(list, obj)
{   var x = list.head;
    var xn;

    if( x == null){
        return;
    }

    if( x.obj == obj){
        list.head = x.next;
        freecarrier(x);
        return;
    }
    while(x.next != null){
        xn = x.next;
        if( xn.obj == obj){
            x.next = xn.next;
            freecarrier(xn);
            return;
        }
        x = xn;
    }
}



var timerlist = new objlist();
var carrierlist = new objlist();
var animatelist = new objlist();

// from above...
addlist(animatelist, guitarxyctrl.visual);


function addtimer(obj)
{   var c;

    addlist(timerlist, obj);
}

function deltimer(obj)
{   
    dellist(timerlist, obj);
}

var ticks=0;
function dotimer()
{   var x,xn;
    var now;
    var idx;

    ticks++;
    if( ticks >= 5){    // per second..
        ticks = 0;
	    if( (window.innerWidth != screenwidth ||
		    window.innerHeight != screenheight )
		    ){

		    screenheight = window.innerHeight;
		    screenwidth = window.innerWidth;
		    redraw=true;
	    }
	    if( redraw){
	        resizeall();
		    drawscreen();
            redraw = false;
            needredraw = true;
	    }

	    if(document.height < window.outerHeight)
	    {
		    document.body.style.height = (window.outerHeight + 50) + 'px';
//		    window.scrollTo(0, 1);
	    }
	    window.scrollTo(0, 1);
	}
	
	if(controls == null){
	    setcontrols();
	    resizeall();
	}
	
    if( needredraw && controls != null){
        for(idx=0; controls[idx] != null; idx++){
            controls[idx].draw();
        }
        needredraw = false;
    }

	// for each oscillator run their timer.
	if( context != null){
        now = context.currentTime;
        x = timerlist.head;
        while(x != null){
            xn = x.next;
            x.obj.timer(now);
            x = xn;
        }
    }
    x = animatelist.head;
    while(x != null){
        x.obj.animate();
        
        x = x.next;
    }
}

///////////////////////////////////////////////////////////////////////////
//

function pageloaded()
{

	setInterval(dotimer, 20);

}

//
var context = null;
var filters = [ null, null, null, null, null, null, null, null, null, null, null, null, null, null, null, null ];

function initsynth()
{
	window.AudioContext = window.AudioContext || window.webkitAudioContext;

	if( ! window.AudioContext){
		status("No audio support present in your browser..");
	}else {
		context = new AudioContext();
	}
}

//////////////////////////////////////////////////////////////////////////////////////////////////////////////
var playtime=0;

function playtune()
{
}

///////////////////////////////////////////////////
/// build a chain of components
//

var synths = [ null, null, null, null, null, null ];

function basicsynth()
{	var osc;
	var head;
	
	head = new cclforate(0, LFORATE);
	osc = head;

	osc.next = new cclfodepth(0, LFODEPTH);
	osc = osc.next;

	osc.next = new ccglide(0, GLIDE);
	osc = osc.next;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
	addtimer(osc);
	synths[0] = osc;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
	addtimer(osc);
	synths[1] = osc;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
 	synths[2] = osc;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
 	synths[3] = osc;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
 	synths[4] = osc;

	osc.next = new synth(0);
	osc = osc.next;
	osc.osc.type="sawtooth";
 	synths[5] = osc;
	return head;
}

/////////////////////////////////////////////////////////////////////
// basic object that will be used to form lists of sound objects.
//
// next, type(), doinput(), action()

function sound()
{	this.next = null;

	this.type = function()
	{
		return "none";
	}

	this.doinput = function(data, port){
		return false;
	}

	this.action = function(pos, data)
	{
	}

}

/////////////////////////////////////////////////////////////////////
// synthconfig object. 
// holds values that are set by the main script.
//

function synthcnfg()
{	this.bendrange = 24/8192;

}


/////////////////////////////////////////////////////////////////////
// control change basic object
// provides doinput that looks for CC messages.
//  [ 0xb0, ctrl, value ]
//

cc.prototype = Object.create( sound.prototype);

function cc(port, ctrl)
{	this.port = port;
	this.ctrl = ctrl;

	sound.call(this);

	this.type = function()
	{
		return "cc";
	}

	this.doinput = function(data, port){
		var func = data[0] & 0xf0;
		var pos;

		if( func != 0xb0){
			return false;
		}
		if( data[1] != this.ctrl){
			return false;
		}
		pos = this.next;
		while(pos != null){
			this.action(pos, data);
			pos = pos.next;
		}

		return true;
	}
}


// cc 1
cclfodepth.prototype = Object.create( cc.prototype);

function cclfodepth(port, ctrl)
{	this.port = port;
	this.ctrl = ctrl;

	cc.call(this, port, ctrl);

	this.action = function(pos, data)
	{
		if( pos.type() == "osc"){
			pos.doinput( data, 0);
		}
	}

}

ccvcfdepth.prototype = Object.create( cc.prototype);

function ccvcfdepth(port, ctrl)
{	this.port = port;
	this.ctrl = ctrl;

	cc.call(this, port, ctrl);

	this.action = function(pos, data)
	{
		if( pos.type() == "osc"){
			pos.doinput( data, 0);
		}
	}

}


// cc 17
cclforate.prototype = Object.create( cc.prototype);

function cclforate(port, ctrl)
{	this.port = port;
	this.ctrl = ctrl;

	cc.call(this, port, ctrl);

	this.action = function(pos, data)
	{
		if( pos.type() == "osc"){
			pos.doinput(data, 0);
		}
	}

}


ccglide.prototype = Object.create( cc.prototype);

function ccglide(port, ctrl)
{	this.port = port;
	this.ctrl = ctrl;

	cc.call(this, port, ctrl);

	this.action = function(pos, data)
	{
		if( pos.type() == "osc"){
			pos.doinput( data, 0);
		}
	}

}


//////////////////////////////////////////////////////////////////////////////
// doinput decodes note on and note off
//
var curwave = 0;

function changewave()
{
    curwave += 32;
    if( curwave >= 96){
        curwave = 0;
    }
    processmidi([0xb0, WAVE, curwave], 0);
}

var curgrind = 0;

function changedrive()
{
    curgrind += 32;
    if( curgrind >= 96){
        curgrind = 0;
    }
    processmidi([0xb0, DRIVE, curgrind], 0);
}

var nodeone = null;

function initnodeone(context)
{   var constantCurve;

    constantCurve = new Float32Array(2);

	nodeone = context.createWaveShaper();
	constantCurve[0] = 1.0;
	constantCurve[1] = 1.0;
	nodeone.curve = constantCurve;
}

function setdrive(node, val)
{   var constantCurve;

status2("Drive="+val);
    if( val >= 64){
        constantCurve = new Float32Array(4);
	    constantCurve[0] = -1.0;
	    constantCurve[1] = -0.1;
	    constantCurve[2] = 0.2;
	    constantCurve[3] = 1.0;
	}else if( val >= 32){
        constantCurve = new Float32Array(4);
	    constantCurve[0] = 0.0;
	    constantCurve[1] = 1.0;
	    constantCurve[2] = -1.0;
	    constantCurve[3] = 0.0;
	}else {
        constantCurve = new Float32Array(2);
	    constantCurve[0] = -1.0;
	    constantCurve[1] = 1.0;
	}
	node.curve = constantCurve;
}

function adsr(ctx)
{   this.attack=0.05;    // TC
    this.decay=0.2;     // TC
    this.sustain=0.1;   // value
    this.release=0.2;   // TC
    this.pause = 0.2;   // T
    this.time=0;
    this.ctx = ctx;
    this.repeat = false;
    this.state = 0;
    this.range = 0.7;   // target of attack 
    this.offset = 0;    // offset.
    this.active=false;  // key pressed?
    this.debug = false;
    this.mode = 0;
    this.node = ctx.createGain();
	this.offset = 0;
   
    this.connect = function( dest)
    {
        this.node.connect(dest);
        nodeone.connect(this.node);
    }
    
    this.start = function(val)
    {
       this.node.gain.setValueAtTime(0,0);
    }
    
    this.trigger = function(noteon)
    {
		this.node.gain.cancelScheduledValues(0);
        if( noteon == 1){
        // note on
            this.node.gain.setTargetAtTime(this.range+this.offset, 0, this.attack);
            this.time = this.ctx.currentTime + 3*this.attack;
            this.state = 0; // attack
            addtimer(this);
            this.active = true;
            if( this.debug){
                status("Attack");
            }
        }else {
        // note off
            this.node.gain.setTargetAtTime(this.offset, 0, this.release);
            this.state = 3; // release
            this.time = this.ctx.currentTime + 3*this.release;
            addtimer(this);
            this.active = false;
            if( this.debug){
                status("Release");
            }
        }
    }
    
    this.timer = function(now)
    {
        if( this.time < now){
            if( this.debug){
                status("now "+now+"<br />"+this.time);
            }
            if( this.state == 0){
                if( this.mode == 1){
                    // release
                    this.node.gain.setTargetAtTime(this.offset, 0, this.release);
                    this.state = 3; // release
                    this.time = this.ctx.currentTime + 3*this.release;
                    addtimer(this);
                    if( this.debug){
                        status("Release");
                    }
                    return;
                }
                // start decay
        		this.node.gain.cancelScheduledValues(0);
                this.node.gain.setTargetAtTime(this.sustain*(this.range+this.offset), 0, this.decay);
                this.time = now+3*this.decay;
                this.state = 1;
                if( this.debug){
                    status("Decay");
                }
            }else if( this.state ==1 ){
                // sustain level
                if( this.active || !this.repeat){
                    this.state = 2;
                    deltimer(this);
                    if( this.debug){
                        status("Sustain");
                    }
                }else {
                    // start release
                // note off
                    this.node.gain.setTargetAtTime(this.offset, 0, this.release);
                    this.state = 3; // release
                    this.time = this.ctx.currentTime + 3*this.release;
                    addtimer(this);
                    if( this.debug){
                        status("Release");
                    }
                }
            }else if( this.state == 2){
                deltimer(this);
            }else if( this.state == 3){
                deltimer(this);
                this.state = 0;
                if( this.repeat){
                    addtimer(this);
                    this.state = 4; // pause
                    this.time = now+this.pause;
                }
            }else if( this.state == 4){
            // repeat.
            // note on
                this.node.gain.setTargetAtTime(this.range+this.offset, 0, this.attack);
                this.time = this.ctx.currentTime + 3*this.attack;
                this.state = 0; // attack
                addtimer(this);
            }
        }
    }
}

synth.prototype = Object.create( sound.prototype);

function synth(port)
{	sound.call(this);
    var i;
	this.port = port;
	this.osc = null;
	this.env = null;
	this.env2 = null;
	this.vcf = null;

	this.attack=0.01;
	this.release=0.5;
	this.portmento=0.01;
	this.curnote=0;
	this.chan=0;
	this.vol = 0.2;
	this.lfo=null;
	this.lfoenv=null;
	this.lfovol = 0;
	this.lforate = 4.0;
	this.lfoglide = 0.01;
	this.moddelay = 0.01;
	this.pressure = 0.0;
	this.vcffreq = 96;
	this.vcfq = 0.9;
	this.vcfvol = 0.0;
	this.freq = 0.0;		// freq knob value.
	this.bend = 0.0;
	this.vcfmod = 4000;
	this.vcfattack = 0.1;
	this.vcfrelease = 0.3;
	this.vcfstate = 0;      // for adsr
	this.vcfnow = 0;
	this.grind = 33;         // drive...

	this.drive = context.createWaveShaper();
	this.osc = context.createOscillator();
	this.osc.frequency.setValueAtTime(110, 0);
	this.env = context.createGain();
	this.env2= context.createGain();
	this.vcf = context.createBiquadFilter();
	this.vcf.type="lowpass";

    setdrive(this.drive, this.grind);
	this.osc.connect(this.env2);		// pressure
	this.env2.connect(this.drive);
	this.drive.connect(this.vcf);
	this.vcf.connect(this.env);
	this.env.connect(context.destination);
	this.env.gain.setValueAtTime(0.0, 0);
	this.env2.gain.setValueAtTime(1.0, 0);
	
	this.lfo= context.createOscillator();
	this.lfo.frequency.setValueAtTime(110, 0);
	this.lfo.type = "triangle";
	this.lfoenv = context.createGain();
	this.lfoenv.gain.setValueAtTime(0.0, 0);
// vcfadsr
	this.vcfenv = context.createGain();
	this.vcfenv.gain.setValueAtTime(0.0, 0);
// vcflfomod
	this.vcfenv2 = context.createGain();
	this.vcfenv2.gain.setValueAtTime(0.0, 0);

	this.lfoenv.gain.cancelScheduledValues(0);
	this.lfoenv.gain.setTargetAtTime(this.lfovol,0,2.0);

    if( nodeone==null){
        initnodeone(context);
        this.lfo.connect( nodeone);
    }
	this.lfo.connect( this.lfoenv);
	this.lfo.connect( this.vcfenv2);

	this.lfoenv.connect( this.osc.detune);
	this.vcfenv.connect( this.vcf.detune);  // adsr
	this.vcfenv2.connect( this.vcf.detune); // lfo
	nodeone.connect(this.vcfenv);
	
	this.shaper = context.createGain();
	this.shaper.connect( this.osc.detune );
	nodeone.connect(this.shaper);

	this.mode = 1;
	
	this.lfo.frequency.cancelScheduledValues(0);
	this.lfo.frequency.setTargetAtTime( this.lforate, 0, this.portmento);
 
 // start the sources
	this.osc.start(0);
	this.lfo.start(0);
	
// glide?
	this.setbend = function( offset)
	{	this.offset = offset;
    	this.shaper.gain.cancelScheduledValues(0);
	    this.shaper.gain.setTargetAtTime( offset, 0, 0.02);
//	    status2("bend "+offset);
	}

	this.type = function()
	{
		return "osc";
	}

	this.setvcf = function()
	{	var f = this.freqof(this.vcffreq) ;

		if( f > 24000){
			f = 24000;
		}
		this.vcf.frequency.cancelScheduledValues(0);
		this.vcf.frequency.setTargetAtTime( f, 0, 0.02); 
		this.vcf.Q.cancelScheduledValues(0);
		this.vcf.Q.setTargetAtTime( this.vcfq, 0, 0.02); 

	}

// For a synth, process midi data
	this.doinput = function(data, port){
		var func = data[0] & 0xf0;
		var pos;
		var cc;
		var high,low;
		var bend;

		if( func == 0x90){	// NOTE ON
			if( data[2] != 0){
				if( this.curnote != 0){
					return false;
				}
				this.curnote = data[1];
				this.osc.frequency.cancelScheduledValues(0);
				this.osc.frequency.setTargetAtTime( this.freqof( data[1]), 0, this.portmento);

                this.env.gain.cancelScheduledValues(0);
                this.env.gain.setTargetAtTime( this.vol, 0, this.attack);

//				this.lfoenv.gain.cancelScheduledValues(0);
//				this.lfoenv.gain.setTargetAtTime(this.lfovol,0,this.moddelay);

// vcf adsr
                this.vcfenv.gain.cancelScheduledValues(0);
                this.vcfenv.gain.setTargetAtTime( this.vcfmod, 0, this.vcfattack);
                this.vcfstate = 1;
                this.vcfnow = context.currentTime+3*this.vcfattack;

				return true;
			}else {
				func = 0x80;
			}
		}
		if( func == 0x80){	// NOTE OFF
			if( this.curnote == 0 || this.curnote != data[1]){
				return false;
			}

			this.curnote = 0;
            this.env.gain.cancelScheduledValues(0);
            this.env.gain.setTargetAtTime( 0.0, 0, this.release);

//			this.lfoenv.gain.cancelScheduledValues(0);
//			this.lfoenv.gain.setTargetAtTime(0.0,0,this.release);

// vcfadsr
			this.vcfenv.gain.cancelScheduledValues(0);
			this.vcfenv.gain.setTargetAtTime(0.0,0,this.vcfrelease);
			this.vcfstate = 2;
            this.vcfnow = context.currentTime+3*this.vcfrelease;

//			this.setbend( 0 );
			return true;
		}

		if( func == 0xb0){
			cc = data[1];	// Control Change Number
			
			if( cc == LFORATE){
				this.lforate = data[2] / 4;
				this.lfo.frequency.cancelScheduledValues(0);
				this.lfo.frequency.setTargetAtTime( this.lforate, 0, this.lfoglide);
			}else if( cc == LFODEPTH){
				this.lfovol = data[2] *64;
				this.lfoenv.gain.cancelScheduledValues(0);
				this.lfoenv.gain.setTargetAtTime(this.lfovol,0,0.1);
			}else if( cc == FILTERMOD){
				this.vcfvol = data[2] *64;
				this.vcfenv2.gain.cancelScheduledValues(0);
				this.vcfenv2.gain.setTargetAtTime(this.vcfvol,0,0.1);
			}else if( cc == GLIDE){
				this.portmento = data[2] / 64 + 0.01;
			}else if( cc == LFOGLIDE){
				this.lfoglide = data[2] / 64 + 0.01;
			}else if( cc == MODDELAY){
				this.moddelay = data[2] / 64 + 0.01;
			}else if( cc == CUTOFF){
				this.vcffreq = data[2];
				this.setvcf();
			}else if( cc == LFOWAVE){
				if( data[2] >= 64){
					this.lfo.type = "sawtooth";
				}else if( data[2] >= 32){
					this.lfo.type = "square";
				}else {
					this.lfo.type = "triangle";
				}
			}else if( cc == WAVE){
				if( data[2] >= 64){
					this.osc.type = "sawtooth";
				}else if( data[2] >= 32){
					this.osc.type = "square";
				}else {
					this.osc.type = "triangle";
				}
				status2(this.osc.type+" "+this.grind);
			}else if( cc == RESONANCE){
				this.vcfq = data[2]/4;
				this.setvcf();
			}else if( cc == FREQ){
				this.freq = data[2]*8 - 64;
				this.setbend(this.bend+this.freq );
			}else if( cc == DRIVE){
			    setdrive(this.drive, data[2]);
			}
			return false;
		}

		if( func == 0xd0){
			// pressure
			this.pressure = data[1] / 256;
			this.env2.gain.cancelScheduledValues(0);
			this.env2.gain.setTargetAtTime(this.pressure,0,0.02);
		}

		if( func == 0xe0){
			low = data[1];
			high = data[2];
			bend = (high*127+low - 8192) * synthconfig.bendrange * 100;
			// status2("Bend: "+bend);
			this.bend = bend;
			this.setbend(this.bend+this.freq );
			return false;
		}
		
		return false;
	}

	this.freqof = function(note)
	{
		return 440 * Math.pow(2, (note-69)/12 );
	}
	
	this.timer = function(now)
	{
	    if( this.vcfstate != 0){
	        if( this.vcfnow < now){
	            if( this.vcfstate == 1){
			        this.vcfenv.gain.cancelScheduledValues(0);
			        this.vcfenv.gain.setTargetAtTime(0.0,0,this.vcfrelease);
			        this.vcfstate = 2;
                    this.vcfnow = context.currentTime+3*this.vcfrelease;
	            }else {
	                this.vcfstate = 0;
	            }
	        }
	    }
	}

}

// register a synth config object.
var synthconfig = new synthcnfg();

function UIselxchange()
{
    status("X change");
}


function UIselychange()
{
    status("Y change");
}