// 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 += ' Pitch Bend \n';
msg += ' Filter cutoff \n';
msg += ' LFO Modulation Depth \n';
msg += ' \n';
msg += ' \n';
msg += ' \n';
msg += ' \n';
msg += ' \n';
msg += ' Mouse Y control: \n';
msg += ' \n';
msg += ' \n';
msg += ' Pitch Bend \n';
msg += ' Filter cutoff \n';
msg += ' LFO Modulation Depth \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");
}