Permalink
Switch branches/tags
Nothing to show
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
1967 lines (1729 sloc) 68.6 KB
<html>
<!--
P I N K T R O M B O N E
Bare-handed procedural speech synthesis
version 1.1, March 2017
by Neil Thapen
venuspatrol.nfshost.com
MODIFIED in September 2017 by Alec Smecher to add MIDI control.
Bibliography
Julius O. Smith III, "Physical audio signal processing for virtual musical instruments and audio effects."
https://ccrma.stanford.edu/~jos/pasp/
Story, Brad H. "A parametric model of the vocal tract area function for vowel and consonant simulation."
The Journal of the Acoustical Society of America 117.5 (2005): 3231-3254.
Lu, Hui-Ling, and J. O. Smith. "Glottal source modeling for singing voice synthesis."
Proceedings of the 2000 International Computer Music Conference. 2000.
Mullen, Jack. Physical modelling of the vocal tract with the 2D digital waveguide mesh.
PhD thesis, University of York, 2006.
Copyright 2017 Neil Thapen
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
-->
<head>
<title>Pink Trombone</title>
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="mobile-web-app-capable" content="yes">
</head>
<body style="margin: 0; padding: 0; background-color:white">
<canvas id="tractCanvas" width="600" height="600"
style = "position: absolute; z-index: 1; background-color:transparent; margin: 0; padding: 0;">
</canvas>
<canvas id="backCanvas" width="600" height="600"
style = "position: absolute; z-index: 0; background-color:transparent; margin: 0; padding: 0;">
</canvas>
<script>
Math.clamp = function(number, min, max) {
if (number<min) return min;
else if (number>max) return max;
else return number;
}
Math.moveTowards = function(current, target, amount)
{
if (current<target) return Math.min(current+amount, target);
else return Math.max(current-amount, target);
}
Math.moveTowards = function(current, target, amountUp, amountDown)
{
if (current<target) return Math.min(current+amountUp, target);
else return Math.max(current-amountDown, target);
}
Math.gaussian = function()
{
var s = 0;
for (var c=0; c<16; c++) s+=Math.random();
return (s-8)/4;
}
var backCanvas = document.getElementById("backCanvas");
var backCtx = backCanvas.getContext("2d");
var tractCanvas = document.getElementById("tractCanvas");
var tractCtx = tractCanvas.getContext("2d");
var sampleRate;
var time = 0;
var temp = {a:0, b:0};
var alwaysVoice = true;
var autoWobble = true;
var noiseFreq = 500;
var noiseQ = 0.7;
var palePink = "#FFEEF5";
var isFirefox = false;
var browser=navigator.userAgent.toLowerCase();
if (browser.indexOf('firefox') > -1) isFirefox = true;
var UI =
{
width : 600,
top_margin : 5,
left_margin : 5,
inInstructionsScreen : false,
instructionsLine : 0,
debugText : "",
init : function()
{
this.touchesWithMouse = [];
this.mouseTouch = {alive: false, endTime: 0};
this.mouseDown = false;
this.alwaysVoiceButton = makeButton(460, 428, 140, 30, "always voice", true);
this.autoWobbleButton = makeButton(460, 464, 140, 30, "pitch wobble", true);
tractCanvas.addEventListener('touchstart', UI.startTouches);
tractCanvas.addEventListener('touchmove', UI.moveTouches);
tractCanvas.addEventListener('touchend', UI.endTouches);
tractCanvas.addEventListener('touchcancel', UI.endTouches);
document.addEventListener('touchstart', (function(event) {event.preventDefault();}) );
document.addEventListener('mousedown', (function(event)
{UI.mouseDown = true; event.preventDefault(); UI.startMouse(event);}));
document.addEventListener('mouseup', (function(event)
{UI.mouseDown = false; UI.endMouse(event);}));
document.addEventListener('mousemove', UI.moveMouse);
},
draw : function()
{
this.alwaysVoiceButton.draw(tractCtx);
this.autoWobbleButton.draw(tractCtx);
if (this.inInstructionsScreen) this.drawInstructionsScreen();
},
drawInstructionsScreen : function()
{
AudioSystem.mute();
var ctx = tractCtx;
ctx.globalAlpha = 0.85;
ctx.fillStyle = "white";
ctx.rect(0,0,600,600);
ctx.fill();
ctx.globalAlpha = 1.0;
ctx.fillStyle = "#C070C6";
ctx.strokeStyle = "#C070C6";
ctx.font="24px Arial";
ctx.lineWidth = 2;
ctx.textAlign = "center";
ctx.font = "19px Arial";
ctx.textAlign = "left";
this.instructionsLine = 0;
this.write("Sound is generated in the glottis (at the bottom left) then ");
this.write("filtered by the shape of the vocal tract. The voicebox ");
this.write("controls the pitch and intensity of the initial sound.");
this.write("");
this.write("Then, to talk:");
this.write("");
this.write("- move the body of the tongue to shape vowels");
this.write("");
this.write("- touch the oral cavity to narrow it, for fricative consonants");
this.write("");
this.write("- touch above the oral cavity to close it, for stop consonants");
this.write("");
this.write("- touch the nasal cavity to open the velum and let sound ");
this.write(" flow through the nose.");
this.write("");
this.write("");
this.write("(tap anywhere to continue)");
ctx.textAlign = "center";
ctx.fillText("[tap here to RESET]", 470, 535);
this.instructionsLine = 18.8;
ctx.textAlign = "left";
this.write("Pink Trombone v1.1");
this.write("by Neil Thapen");
ctx.fillStyle = "blue";
ctx.globalAlpha = 0.6;
this.write("venuspatrol.nfshost.com");
/*ctx.beginPath();
ctx.rect(35, 535, 230, 35);
ctx.rect(370, 505, 200, 50);
ctx.fill();*/
ctx.globalAlpha = 1.0;
},
instructionsScreenHandleTouch : function(x,y)
{
if ((x >=35 && x<=265) && (y>=535 && y<=570)) window.location.href = "http://venuspatrol.nfshost.com";
else if ((x>=370 && x<=570) && (y>=505 && y<=555)) location.reload(false);
else
{
UI.inInstructionsScreen = false;
AudioSystem.unmute();
}
},
write : function(text)
{
tractCtx.fillText(text, 50, 100 + this.instructionsLine*22);
this.instructionsLine += 1;
if (text == "") this.instructionsLine -= 0.3;
},
buttonsHandleTouchStart : function(touch)
{
this.alwaysVoiceButton.handleTouchStart(touch);
alwaysVoice = this.alwaysVoiceButton.switchedOn;
this.autoWobbleButton.handleTouchStart(touch);
autoWobble = this.autoWobbleButton.switchedOn;
},
startTouches : function(event)
{
event.preventDefault();
if (!AudioSystem.started)
{
AudioSystem.started = true;
AudioSystem.startSound();
}
if (UI.inInstructionsScreen)
{
var touches = event.changedTouches;
for (var j=0; j<touches.length; j++)
{
var x = (touches[j].pageX-UI.left_margin)/UI.width*600;
var y = (touches[j].pageY-UI.top_margin)/UI.width*600;
}
UI.instructionsScreenHandleTouch(x,y);
return;
}
var touches = event.changedTouches;
for (var j=0; j<touches.length; j++)
{
var touch = {};
touch.startTime = time;
touch.endTime = 0;
touch.fricative_intensity = 0;
touch.alive = true;
touch.id = touches[j].identifier;
touch.x = (touches[j].pageX-UI.left_margin)/UI.width*600;
touch.y = (touches[j].pageY-UI.top_margin)/UI.width*600;
touch.index = TractUI.getIndex(touch.x, touch.y);
touch.diameter = TractUI.getDiameter(touch.x, touch.y);
UI.touchesWithMouse.push(touch);
UI.buttonsHandleTouchStart(touch);
}
UI.handleTouches();
},
getTouchById : function(id)
{
for (var j=0; j<UI.touchesWithMouse.length; j++)
{
if (UI.touchesWithMouse[j].id == id && UI.touchesWithMouse[j].alive) return UI.touchesWithMouse[j];
}
return 0;
},
moveTouches : function(event)
{
var touches = event.changedTouches;
for (var j=0; j<touches.length; j++)
{
var touch = UI.getTouchById(touches[j].identifier);
if (touch != 0)
{
touch.x = (touches[j].pageX-UI.left_margin)/UI.width*600;
touch.y = (touches[j].pageY-UI.top_margin)/UI.width*600;
touch.index = TractUI.getIndex(touch.x, touch.y);
touch.diameter = TractUI.getDiameter(touch.x, touch.y);
}
}
UI.handleTouches();
},
endTouches : function(event)
{
var touches = event.changedTouches;
for (var j=0; j<touches.length; j++)
{
var touch = UI.getTouchById(touches[j].identifier);
if (touch != 0)
{
touch.alive = false;
touch.endTime = time;
}
}
UI.handleTouches();
},
startMouse : function(event)
{
if (!AudioSystem.started)
{
AudioSystem.started = true;
AudioSystem.startSound();
}
if (UI.inInstructionsScreen)
{
var x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
var y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
UI.instructionsScreenHandleTouch(x,y);
return;
}
var touch = {};
touch.startTime = time;
touch.fricative_intensity = 0;
touch.endTime = 0;
touch.alive = true;
touch.id = "mouse"+Math.random();
touch.x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
touch.y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
console.log(event.pageX + ", " + event.pageY);
touch.index = TractUI.getIndex(touch.x, touch.y);
touch.diameter = TractUI.getDiameter(touch.x, touch.y);
UI.mouseTouch = touch;
UI.touchesWithMouse.push(touch);
UI.buttonsHandleTouchStart(touch);
UI.handleTouches();
},
moveMouse : function(event)
{
var touch = UI.mouseTouch;
if (!touch.alive) return;
touch.x = (event.pageX-tractCanvas.offsetLeft)/UI.width*600;
touch.y = (event.pageY-tractCanvas.offsetTop)/UI.width*600;
touch.index = TractUI.getIndex(touch.x, touch.y);
touch.diameter = TractUI.getDiameter(touch.x, touch.y);
UI.handleTouches();
},
endMouse : function(event)
{
var touch = UI.mouseTouch;
if (!touch.alive) return;
touch.alive = false;
touch.endTime = time;
UI.handleTouches();
},
handleTouches : function(event)
{
TractUI.handleTouches();
Glottis.handleTouches();
},
updateTouches : function()
{
var fricativeAttackTime = 0.1;
for (var j=UI.touchesWithMouse.length-1; j >=0; j--)
{
var touch = UI.touchesWithMouse[j];
if (!(touch.alive) && (time > touch.endTime + 1))
{
UI.touchesWithMouse.splice(j,1);
}
else if (touch.alive)
{
touch.fricative_intensity = Math.clamp((time-touch.startTime)/fricativeAttackTime, 0, 1);
}
else
{
touch.fricative_intensity = Math.clamp(1-(time-touch.endTime)/fricativeAttackTime, 0, 1);
}
}
},
shapeToFitScreen : function()
{
if (window.innerWidth <= window.innerHeight)
{
this.width = window.innerWidth-10;
this.left_margin = 5;
this.top_margin = 0.5*(window.innerHeight-this.width);
}
else
{
this.width = window.innerHeight-10;
this.left_margin = 0.5*(window.innerWidth-this.width);
this.top_margin = 5;
}
document.body.style.marginLeft = this.left_margin;
document.body.style.marginTop = this.top_margin;
tractCanvas.style.width = this.width;
backCanvas.style.width = this.width;
}
}
var AudioSystem =
{
blockLength : 512,
blockTime : 1,
started : false,
soundOn : false,
init : function ()
{
window.AudioContext = window.AudioContext||window.webkitAudioContext;
this.audioContext = new window.AudioContext();
sampleRate = this.audioContext.sampleRate;
this.blockTime = this.blockLength/sampleRate;
},
startSound : function()
{
//scriptProcessor may need a dummy input channel on iOS
this.scriptProcessor = this.audioContext.createScriptProcessor(this.blockLength, 2, 1);
this.scriptProcessor.connect(this.audioContext.destination);
this.scriptProcessor.onaudioprocess = AudioSystem.doScriptProcessor;
var whiteNoise = this.createWhiteNoiseNode(2*sampleRate); // 2 seconds of noise
var aspirateFilter = this.audioContext.createBiquadFilter();
aspirateFilter.type = "bandpass";
aspirateFilter.frequency.value = 500;
aspirateFilter.Q.value = 0.5;
whiteNoise.connect(aspirateFilter);
aspirateFilter.connect(this.scriptProcessor);
var fricativeFilter = this.audioContext.createBiquadFilter();
fricativeFilter.type = "bandpass";
fricativeFilter.frequency.value = 1000;
fricativeFilter.Q.value = 0.5;
whiteNoise.connect(fricativeFilter);
fricativeFilter.connect(this.scriptProcessor);
whiteNoise.start(0);
},
createWhiteNoiseNode : function(frameCount)
{
var myArrayBuffer = this.audioContext.createBuffer(1, frameCount, sampleRate);
var nowBuffering = myArrayBuffer.getChannelData(0);
for (var i = 0; i < frameCount; i++)
{
nowBuffering[i] = Math.random();// gaussian();
}
var source = this.audioContext.createBufferSource();
source.buffer = myArrayBuffer;
source.loop = true;
return source;
},
doScriptProcessor : function(event)
{
var inputArray1 = event.inputBuffer.getChannelData(0);
var inputArray2 = event.inputBuffer.getChannelData(1);
var outArray = event.outputBuffer.getChannelData(0);
for (var j = 0, N = outArray.length; j < N; j++)
{
var lambda1 = j/N;
var lambda2 = (j+0.5)/N;
var glottalOutput = Glottis.runStep(lambda1, inputArray1[j]);
var vocalOutput = 0;
//Tract runs at twice the sample rate
Tract.runStep(glottalOutput, inputArray2[j], lambda1);
vocalOutput += Tract.lipOutput + Tract.noseOutput;
Tract.runStep(glottalOutput, inputArray2[j], lambda2);
vocalOutput += Tract.lipOutput + Tract.noseOutput;
outArray[j] = vocalOutput * 0.125;
}
Glottis.finishBlock();
Tract.finishBlock();
},
mute : function()
{
this.scriptProcessor.disconnect();
},
unmute : function()
{
this.scriptProcessor.connect(this.audioContext.destination);
}
}
var Glottis =
{
timeInWaveform : 0,
oldFrequency : 140,
newFrequency : 140,
UIFrequency : 140,
smoothFrequency : 140,
oldTenseness : 0.6,
newTenseness : 0.6,
UITenseness : 0.6,
totalTime : 0,
vibratoAmount : 0.005,
vibratoFrequency : 6,
intensity : 0,
loudness : 1,
isTouched : false,
ctx : backCtx,
touch : 0,
x : 240,
y : 530,
keyboardTop : 500,
keyboardLeft : 00,
keyboardWidth : 600,
keyboardHeight : 100,
semitones : 20,
marks : [0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0],
baseNote : 87.3071, //F
init : function()
{
this.setupWaveform(0);
this.drawKeyboard();
},
drawKeyboard : function()
{
this.ctx.strokeStyle = palePink;
this.ctx.fillStyle = palePink;
backCtx.globalAlpha = 1.0;
backCtx.lineCap = 'round';
backCtx.lineJoin = 'round';
var radius = 2;
this.drawBar(0.0, 0.4, 8);
backCtx.globalAlpha = 0.7;
this.drawBar(0.52, 0.72, 8);
backCtx.strokeStyle = "orchid";
backCtx.fillStyle = "orchid";
for (var i=0; i< this.semitones; i++)
{
var keyWidth = this.keyboardWidth/this.semitones;
var x = this.keyboardLeft+(i+1/2)*keyWidth;
var y = this.keyboardTop;
if (this.marks[(i+3)%12]==1)
{
backCtx.lineWidth = 4;
backCtx.globalAlpha = 0.4;
}
else
{
backCtx.lineWidth = 3;
backCtx.globalAlpha = 0.2;
}
backCtx.beginPath();
backCtx.moveTo(x,y+9);
backCtx.lineTo(x, y+this.keyboardHeight*0.4-9);
backCtx.stroke();
backCtx.lineWidth = 3;
backCtx.globalAlpha = 0.15;
backCtx.beginPath();
backCtx.moveTo(x,y+this.keyboardHeight*0.52+6);
backCtx.lineTo(x, y+this.keyboardHeight*0.72-6);
backCtx.stroke();
}
backCtx.fillStyle = "orchid";
backCtx.font="17px Arial";
backCtx.textAlign = "center";
backCtx.globalAlpha = 0.7;
backCtx.fillText("voicebox control", 300, 490);
backCtx.fillText("pitch", 300, 592);
backCtx.globalAlpha = 0.3;
backCtx.strokeStyle = "orchid";
backCtx.fillStyle = "orchid";
backCtx.save()
backCtx.translate(410, 587);
this.drawArrow(80, 2, 10);
backCtx.translate(-220, 0);
backCtx.rotate(Math.PI);
this.drawArrow(80, 2, 10);
backCtx.restore();
backCtx.globalAlpha=1.0;
},
drawBar : function(topFactor, bottomFactor, radius)
{
backCtx.lineWidth = radius*2;
backCtx.beginPath();
backCtx.moveTo(this.keyboardLeft+radius, this.keyboardTop+topFactor*this.keyboardHeight+radius);
backCtx.lineTo(this.keyboardLeft+this.keyboardWidth-radius, this.keyboardTop+topFactor*this.keyboardHeight+radius);
backCtx.lineTo(this.keyboardLeft+this.keyboardWidth-radius, this.keyboardTop+bottomFactor*this.keyboardHeight-radius);
backCtx.lineTo(this.keyboardLeft+radius, this.keyboardTop+bottomFactor*this.keyboardHeight-radius);
backCtx.closePath();
backCtx.stroke();
backCtx.fill();
},
drawArrow : function(l, ahw, ahl)
{
backCtx.lineWidth = 2;
backCtx.beginPath();
backCtx.moveTo(-l, 0);
backCtx.lineTo(0,0);
backCtx.lineTo(0, -ahw);
backCtx.lineTo(ahl, 0);
backCtx.lineTo(0, ahw);
backCtx.lineTo(0,0);
backCtx.closePath();
backCtx.stroke();
backCtx.fill();
},
handleTouches : function()
{
if (this.touch != 0 && !this.touch.alive) this.touch = 0;
if (this.touch == 0)
{
for (var j=0; j<UI.touchesWithMouse.length; j++)
{
var touch = UI.touchesWithMouse[j];
if (!touch.alive) continue;
if (touch.y<this.keyboardTop) continue;
this.touch = touch;
}
}
if (this.touch != 0)
{
var local_y = this.touch.y - this.keyboardTop-10;
var local_x = this.touch.x - this.keyboardLeft;
local_y = Math.clamp(local_y, 0, this.keyboardHeight-26);
var semitone = this.semitones * local_x / this.keyboardWidth + 0.5;
Glottis.UIFrequency = this.baseNote * Math.pow(2, semitone/12);
if (Glottis.intensity == 0) Glottis.smoothFrequency = Glottis.UIFrequency;
//Glottis.UIRd = 3*local_y / (this.keyboardHeight-20);
var t = Math.clamp(1-local_y / (this.keyboardHeight-28), 0, 1);
Glottis.UITenseness = 1-Math.cos(t*Math.PI*0.5);
Glottis.loudness = Math.pow(Glottis.UITenseness, 0.25);
this.x = this.touch.x;
this.y = local_y + this.keyboardTop+10;
}
Glottis.isTouched = (this.touch != 0);
},
runStep : function(lambda, noiseSource)
{
var timeStep = 1.0 / sampleRate;
this.timeInWaveform += timeStep;
this.totalTime += timeStep;
if (this.timeInWaveform>this.waveformLength)
{
this.timeInWaveform -= this.waveformLength;
this.setupWaveform(lambda);
}
var out = this.normalizedLFWaveform(this.timeInWaveform/this.waveformLength);
var aspiration = this.intensity*(1-Math.sqrt(this.UITenseness))*this.getNoiseModulator()*noiseSource;
aspiration *= 0.2 + 0.02*noise.simplex1(this.totalTime * 1.99);
out += aspiration;
return out;
},
getNoiseModulator : function()
{
var voiced = 0.1+0.2*Math.max(0,Math.sin(Math.PI*2*this.timeInWaveform/this.waveformLength));
//return 0.3;
return this.UITenseness* this.intensity * voiced + (1-this.UITenseness* this.intensity ) * 0.3;
},
finishBlock : function()
{
var vibrato = 0;
vibrato += this.vibratoAmount * Math.sin(2*Math.PI * this.totalTime *this.vibratoFrequency);
vibrato += 0.02 * noise.simplex1(this.totalTime * 4.07);
vibrato += 0.04 * noise.simplex1(this.totalTime * 2.15);
if (autoWobble)
{
vibrato += 0.2 * noise.simplex1(this.totalTime * 0.98);
vibrato += 0.4 * noise.simplex1(this.totalTime * 0.5);
}
if (this.UIFrequency>this.smoothFrequency)
this.smoothFrequency = Math.min(this.smoothFrequency * 1.1, this.UIFrequency);
if (this.UIFrequency<this.smoothFrequency)
this.smoothFrequency = Math.max(this.smoothFrequency / 1.1, this.UIFrequency);
this.oldFrequency = this.newFrequency;
this.newFrequency = this.smoothFrequency * (1+vibrato);
this.oldTenseness = this.newTenseness;
this.newTenseness = this.UITenseness
+ 0.1*noise.simplex1(this.totalTime*0.46)+0.05*noise.simplex1(this.totalTime*0.36);
if (!this.isTouched && alwaysVoice) this.newTenseness += (3-this.UITenseness)*(1-this.intensity);
if (this.isTouched || alwaysVoice) this.intensity += 0.13;
else this.intensity -= 0.05;
this.intensity = Math.clamp(this.intensity, 0, 1);
},
setupWaveform : function(lambda)
{
this.frequency = this.oldFrequency*(1-lambda) + this.newFrequency*lambda;
var tenseness = this.oldTenseness*(1-lambda) + this.newTenseness*lambda;
this.Rd = 3*(1-tenseness);
this.waveformLength = 1.0/this.frequency;
var Rd = this.Rd;
if (Rd<0.5) Rd = 0.5;
if (Rd>2.7) Rd = 2.7;
var output;
// normalized to time = 1, Ee = 1
var Ra = -0.01 + 0.048*Rd;
var Rk = 0.224 + 0.118*Rd;
var Rg = (Rk/4)*(0.5+1.2*Rk)/(0.11*Rd-Ra*(0.5+1.2*Rk));
var Ta = Ra;
var Tp = 1 / (2*Rg);
var Te = Tp + Tp*Rk; //
var epsilon = 1/Ta;
var shift = Math.exp(-epsilon * (1-Te));
var Delta = 1 - shift; //divide by this to scale RHS
var RHSIntegral = (1/epsilon)*(shift - 1) + (1-Te)*shift;
RHSIntegral = RHSIntegral/Delta;
var totalLowerIntegral = - (Te-Tp)/2 + RHSIntegral;
var totalUpperIntegral = -totalLowerIntegral;
var omega = Math.PI/Tp;
var s = Math.sin(omega*Te);
// need E0*e^(alpha*Te)*s = -1 (to meet the return at -1)
// and E0*e^(alpha*Tp/2) * Tp*2/pi = totalUpperIntegral
// (our approximation of the integral up to Tp)
// writing x for e^alpha,
// have E0*x^Te*s = -1 and E0 * x^(Tp/2) * Tp*2/pi = totalUpperIntegral
// dividing the second by the first,
// letting y = x^(Tp/2 - Te),
// y * Tp*2 / (pi*s) = -totalUpperIntegral;
var y = -Math.PI*s*totalUpperIntegral / (Tp*2);
var z = Math.log(y);
var alpha = z/(Tp/2 - Te);
var E0 = -1 / (s*Math.exp(alpha*Te));
this.alpha = alpha;
this.E0 = E0;
this.epsilon = epsilon;
this.shift = shift;
this.Delta = Delta;
this.Te=Te;
this.omega = omega;
},
normalizedLFWaveform: function(t)
{
if (t>this.Te) output = (-Math.exp(-this.epsilon * (t-this.Te)) + this.shift)/this.Delta;
else output = this.E0 * Math.exp(this.alpha*t) * Math.sin(this.omega * t);
return output * this.intensity * this.loudness;
}
}
var Tract =
{
n : 44,
bladeStart : 10,
tipStart : 32,
lipStart : 39,
R : [], //component going right
L : [], //component going left
reflection : [],
junctionOutputR : [],
junctionOutputL : [],
maxAmplitude : [],
diameter : [],
restDiameter : [],
targetDiameter : [],
newDiameter : [],
A : [],
glottalReflection : 0.75,
lipReflection : -0.85,
lastObstruction : -1,
fade : 1.0, //0.9999,
movementSpeed : 15, //cm per second
transients : [],
lipOutput : 0,
noseOutput : 0,
velumTarget : 0.01,
init : function()
{
this.bladeStart = Math.floor(this.bladeStart*this.n/44);
this.tipStart = Math.floor(this.tipStart*this.n/44);
this.lipStart = Math.floor(this.lipStart*this.n/44);
this.diameter = new Float64Array(this.n);
this.restDiameter = new Float64Array(this.n);
this.targetDiameter = new Float64Array(this.n);
this.newDiameter = new Float64Array(this.n);
for (var i=0; i<this.n; i++)
{
var diameter = 0;
if (i<7*this.n/44-0.5) diameter = 0.6;
else if (i<12*this.n/44) diameter = 1.1;
else diameter = 1.5;
this.diameter[i] = this.restDiameter[i] = this.targetDiameter[i] = this.newDiameter[i] = diameter;
}
this.R = new Float64Array(this.n);
this.L = new Float64Array(this.n);
this.reflection = new Float64Array(this.n+1);
this.newReflection = new Float64Array(this.n+1);
this.junctionOutputR = new Float64Array(this.n+1);
this.junctionOutputL = new Float64Array(this.n+1);
this.A =new Float64Array(this.n);
this.maxAmplitude = new Float64Array(this.n);
this.noseLength = Math.floor(28*this.n/44)
this.noseStart = this.n-this.noseLength + 1;
this.noseR = new Float64Array(this.noseLength);
this.noseL = new Float64Array(this.noseLength);
this.noseJunctionOutputR = new Float64Array(this.noseLength+1);
this.noseJunctionOutputL = new Float64Array(this.noseLength+1);
this.noseReflection = new Float64Array(this.noseLength+1);
this.noseDiameter = new Float64Array(this.noseLength);
this.noseA = new Float64Array(this.noseLength);
this.noseMaxAmplitude = new Float64Array(this.noseLength);
for (var i=0; i<this.noseLength; i++)
{
var diameter;
var d = 2*(i/this.noseLength);
if (d<1) diameter = 0.4+1.6*d;
else diameter = 0.5+1.5*(2-d);
diameter = Math.min(diameter, 1.9);
this.noseDiameter[i] = diameter;
}
this.newReflectionLeft = this.newReflectionRight = this.newReflectionNose = 0;
this.calculateReflections();
this.calculateNoseReflections();
this.noseDiameter[0] = this.velumTarget;
},
reshapeTract : function(deltaTime)
{
var amount = deltaTime * this.movementSpeed; ;
var newLastObstruction = -1;
for (var i=0; i<this.n; i++)
{
var diameter = this.diameter[i];
var targetDiameter = this.targetDiameter[i];
if (diameter <= 0) newLastObstruction = i;
var slowReturn;
if (i<this.noseStart) slowReturn = 0.6;
else if (i >= this.tipStart) slowReturn = 1.0;
else slowReturn = 0.6+0.4*(i-this.noseStart)/(this.tipStart-this.noseStart);
this.diameter[i] = Math.moveTowards(diameter, targetDiameter, slowReturn*amount, 2*amount);
}
if (this.lastObstruction>-1 && newLastObstruction == -1 && this.noseA[0]<0.05)
{
this.addTransient(this.lastObstruction);
}
this.lastObstruction = newLastObstruction;
amount = deltaTime * this.movementSpeed;
this.noseDiameter[0] = Math.moveTowards(this.noseDiameter[0], this.velumTarget,
amount*0.25, amount*0.1);
this.noseA[0] = this.noseDiameter[0]*this.noseDiameter[0];
},
calculateReflections : function()
{
for (var i=0; i<this.n; i++)
{
this.A[i] = this.diameter[i]*this.diameter[i]; //ignoring PI etc.
}
for (var i=1; i<this.n; i++)
{
this.reflection[i] = this.newReflection[i];
if (this.A[i] == 0) this.newReflection[i] = 0.999; //to prevent some bad behaviour if 0
else this.newReflection[i] = (this.A[i-1]-this.A[i]) / (this.A[i-1]+this.A[i]);
}
//now at junction with nose
this.reflectionLeft = this.newReflectionLeft;
this.reflectionRight = this.newReflectionRight;
this.reflectionNose = this.newReflectionNose;
var sum = this.A[this.noseStart]+this.A[this.noseStart+1]+this.noseA[0];
this.newReflectionLeft = (2*this.A[this.noseStart]-sum)/sum;
this.newReflectionRight = (2*this.A[this.noseStart+1]-sum)/sum;
this.newReflectionNose = (2*this.noseA[0]-sum)/sum;
},
calculateNoseReflections : function()
{
for (var i=0; i<this.noseLength; i++)
{
this.noseA[i] = this.noseDiameter[i]*this.noseDiameter[i];
}
for (var i=1; i<this.noseLength; i++)
{
this.noseReflection[i] = (this.noseA[i-1]-this.noseA[i]) / (this.noseA[i-1]+this.noseA[i]);
}
},
runStep : function(glottalOutput, turbulenceNoise, lambda)
{
var updateAmplitudes = (Math.random()<0.1);
//mouth
this.processTransients();
this.addTurbulenceNoise(turbulenceNoise);
//this.glottalReflection = -0.8 + 1.6 * Glottis.newTenseness;
this.junctionOutputR[0] = this.L[0] * this.glottalReflection + glottalOutput;
this.junctionOutputL[this.n] = this.R[this.n-1] * this.lipReflection;
for (var i=1; i<this.n; i++)
{
var r = this.reflection[i] * (1-lambda) + this.newReflection[i]*lambda;
var w = r * (this.R[i-1] + this.L[i]);
this.junctionOutputR[i] = this.R[i-1] - w;
this.junctionOutputL[i] = this.L[i] + w;
}
//now at junction with nose
var i = this.noseStart;
var r = this.newReflectionLeft * (1-lambda) + this.reflectionLeft*lambda;
this.junctionOutputL[i] = r*this.R[i-1]+(1+r)*(this.noseL[0]+this.L[i]);
r = this.newReflectionRight * (1-lambda) + this.reflectionRight*lambda;
this.junctionOutputR[i] = r*this.L[i]+(1+r)*(this.R[i-1]+this.noseL[0]);
r = this.newReflectionNose * (1-lambda) + this.reflectionNose*lambda;
this.noseJunctionOutputR[0] = r*this.noseL[0]+(1+r)*(this.L[i]+this.R[i-1]);
for (var i=0; i<this.n; i++)
{
this.R[i] = this.junctionOutputR[i]*0.999;
this.L[i] = this.junctionOutputL[i+1]*0.999;
//this.R[i] = Math.clamp(this.junctionOutputR[i] * this.fade, -1, 1);
//this.L[i] = Math.clamp(this.junctionOutputL[i+1] * this.fade, -1, 1);
if (updateAmplitudes)
{
var amplitude = Math.abs(this.R[i]+this.L[i]);
if (amplitude > this.maxAmplitude[i]) this.maxAmplitude[i] = amplitude;
else this.maxAmplitude[i] *= 0.999;
}
}
this.lipOutput = this.R[this.n-1];
//nose
this.noseJunctionOutputL[this.noseLength] = this.noseR[this.noseLength-1] * this.lipReflection;
for (var i=1; i<this.noseLength; i++)
{
var w = this.noseReflection[i] * (this.noseR[i-1] + this.noseL[i]);
this.noseJunctionOutputR[i] = this.noseR[i-1] - w;
this.noseJunctionOutputL[i] = this.noseL[i] + w;
}
for (var i=0; i<this.noseLength; i++)
{
this.noseR[i] = this.noseJunctionOutputR[i] * this.fade;
this.noseL[i] = this.noseJunctionOutputL[i+1] * this.fade;
//this.noseR[i] = Math.clamp(this.noseJunctionOutputR[i] * this.fade, -1, 1);
//this.noseL[i] = Math.clamp(this.noseJunctionOutputL[i+1] * this.fade, -1, 1);
if (updateAmplitudes)
{
var amplitude = Math.abs(this.noseR[i]+this.noseL[i]);
if (amplitude > this.noseMaxAmplitude[i]) this.noseMaxAmplitude[i] = amplitude;
else this.noseMaxAmplitude[i] *= 0.999;
}
}
this.noseOutput = this.noseR[this.noseLength-1];
},
finishBlock : function()
{
this.reshapeTract(AudioSystem.blockTime);
this.calculateReflections();
},
addTransient : function(position)
{
var trans = {}
trans.position = position;
trans.timeAlive = 0;
trans.lifeTime = 0.2;
trans.strength = 0.3;
trans.exponent = 200;
this.transients.push(trans);
},
processTransients : function()
{
for (var i = 0; i < this.transients.length; i++)
{
var trans = this.transients[i];
var amplitude = trans.strength * Math.pow(2, -trans.exponent * trans.timeAlive);
this.R[trans.position] += amplitude/2;
this.L[trans.position] += amplitude/2;
trans.timeAlive += 1.0/(sampleRate*2);
}
for (var i=this.transients.length-1; i>=0; i--)
{
var trans = this.transients[i];
if (trans.timeAlive > trans.lifeTime)
{
this.transients.splice(i,1);
}
}
},
addTurbulenceNoise : function(turbulenceNoise)
{
for (var j=0; j<UI.touchesWithMouse.length; j++)
{
var touch = UI.touchesWithMouse[j];
if (touch.index<2 || touch.index>Tract.n) continue;
if (touch.diameter<=0) continue;
var intensity = touch.fricative_intensity;
if (intensity == 0) continue;
this.addTurbulenceNoiseAtIndex(0.66*turbulenceNoise*intensity, touch.index, touch.diameter);
}
},
addTurbulenceNoiseAtIndex : function(turbulenceNoise, index, diameter)
{
var i = Math.floor(index);
var delta = index - i;
turbulenceNoise *= Glottis.getNoiseModulator();
var thinness0 = Math.clamp(8*(0.7-diameter),0,1);
var openness = Math.clamp(30*(diameter-0.3), 0, 1);
var noise0 = turbulenceNoise*(1-delta)*thinness0*openness;
var noise1 = turbulenceNoise*delta*thinness0*openness;
this.R[i+1] += noise0/2;
this.L[i+1] += noise0/2;
this.R[i+2] += noise1/2;
this.L[i+2] += noise1/2;
}
};
var TractUI =
{
originX : 340,
originY : 449,
radius : 298,
scale : 60,
tongueIndex : 12.9,
tongueDiameter : 2.43,
innerTongueControlRadius : 2.05,
outerTongueControlRadius : 3.5,
tongueTouch : 0,
angleScale : 0.64,
angleOffset : -0.24,
noseOffset : 0.8,
gridOffset : 1.7,
fillColour : 'pink',
lineColour : '#C070C6',
init : function()
{
this.ctx = tractCtx;
this.setRestDiameter();
for (var i=0; i<Tract.n; i++)
{
Tract.diameter[i] = Tract.targetDiameter[i] = Tract.restDiameter[i];
}
this.drawBackground();
this.tongueLowerIndexBound = Tract.bladeStart+2;
this.tongueUpperIndexBound = Tract.tipStart-3;
this.tongueIndexCentre = 0.5*(this.tongueLowerIndexBound+this.tongueUpperIndexBound);
},
moveTo : function(i,d)
{
var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
var wobble = (Tract.maxAmplitude[Tract.n-1]+Tract.noseMaxAmplitude[Tract.noseLength-1]);
wobble *= 0.03*Math.sin(2*i-50*time)*i/Tract.n;
angle += wobble;
var r = this.radius - this.scale*d + 100*wobble;
this.ctx.moveTo(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle));
},
lineTo : function(i,d)
{
var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
var wobble = (Tract.maxAmplitude[Tract.n-1]+Tract.noseMaxAmplitude[Tract.noseLength-1]);
wobble *= 0.03*Math.sin(2*i-50*time)*i/Tract.n;
angle += wobble;
var r = this.radius - this.scale*d + 100*wobble;
this.ctx.lineTo(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle));
},
drawText : function(i,d,text)
{
var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
var r = this.radius - this.scale*d;
this.ctx.save();
this.ctx.translate(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle)+2); //+8);
this.ctx.rotate(angle-Math.PI/2);
this.ctx.fillText(text, 0, 0);
this.ctx.restore();
},
drawTextStraight : function(i,d,text)
{
var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
var r = this.radius - this.scale*d;
this.ctx.save();
this.ctx.translate(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle)+2); //+8);
//this.ctx.rotate(angle-Math.PI/2);
this.ctx.fillText(text, 0, 0);
this.ctx.restore();
},
drawCircle : function(i,d,radius)
{
var angle = this.angleOffset + i * this.angleScale * Math.PI / (Tract.lipStart-1);
var r = this.radius - this.scale*d;
this.ctx.beginPath();
this.ctx.arc(this.originX-r*Math.cos(angle), this.originY-r*Math.sin(angle), radius, 0, 2*Math.PI);
this.ctx.fill();
},
getIndex : function(x,y)
{
var xx = x-this.originX; var yy = y-this.originY;
var angle = Math.atan2(yy, xx);
while (angle> 0) angle -= 2*Math.PI;
return (Math.PI + angle - this.angleOffset)*(Tract.lipStart-1) / (this.angleScale*Math.PI);
},
getDiameter : function(x,y)
{
var xx = x-this.originX; var yy = y-this.originY;
return (this.radius-Math.sqrt(xx*xx + yy*yy))/this.scale;
},
draw : function()
{
this.ctx.clearRect(0, 0, tractCanvas.width, tractCanvas.height);
this.ctx.lineCap = 'round';
this.ctx.lineJoin = 'round';
this.drawTongueControl();
this.drawPitchControl();
var velum = Tract.noseDiameter[0];
var velumAngle = velum * 4;
//first draw fill
this.ctx.beginPath();
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = this.fillColour;
this.ctx.fillStyle = this.fillColour;
this.moveTo(1,0);
for (var i = 1; i < Tract.n; i++) this.lineTo(i, Tract.diameter[i]);
for (var i = Tract.n-1; i >= 2; i--) this.lineTo(i, 0);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
//for nose
this.ctx.beginPath();
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = this.fillColour;
this.ctx.fillStyle = this.fillColour;
this.moveTo(Tract.noseStart, -this.noseOffset);
for (var i = 1; i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
for (var i = Tract.noseLength-1; i >= 1; i--) this.lineTo(i+Tract.noseStart, -this.noseOffset);
this.ctx.closePath();
//this.ctx.stroke();
this.ctx.fill();
//velum
this.ctx.beginPath();
this.ctx.lineWidth = 2;
this.ctx.strokeStyle = this.fillColour;
this.ctx.fillStyle = this.fillColour;
this.moveTo(Tract.noseStart-2, 0);
this.lineTo(Tract.noseStart, -this.noseOffset);
this.lineTo(Tract.noseStart+velumAngle, -this.noseOffset);
this.lineTo(Tract.noseStart+velumAngle-2, 0);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
//white text
this.ctx.fillStyle = "white";
this.ctx.font="20px Arial";
this.ctx.textAlign = "center";
this.ctx.globalAlpha = 1.0;
this.drawText(Tract.n*0.10, 0.425, "throat");
this.drawText(Tract.n*0.71, -1.8, "nasal");
this.drawText(Tract.n*0.71, -1.3, "cavity");
this.ctx.font="22px Arial";
this.drawText(Tract.n*0.6, 0.9, "oral");
this.drawText(Tract.n*0.7, 0.9, "cavity");
this.drawAmplitudes();
//then draw lines
this.ctx.beginPath();
this.ctx.lineWidth = 5;
this.ctx.strokeStyle = this.lineColour;
this.ctx.lineJoin = 'round';
this.ctx.lineCap = 'round';
this.moveTo(1, Tract.diameter[0]);
for (var i = 2; i < Tract.n; i++) this.lineTo(i, Tract.diameter[i]);
this.moveTo(1,0);
for (var i = 2; i <= Tract.noseStart-2; i++) this.lineTo(i, 0);
this.moveTo(Tract.noseStart+velumAngle-2,0);
for (var i = Tract.noseStart+Math.ceil(velumAngle)-2; i < Tract.n; i++) this.lineTo(i, 0);
this.ctx.stroke();
//for nose
this.ctx.beginPath();
this.ctx.lineWidth = 5;
this.ctx.strokeStyle = this.lineColour;
this.ctx.lineJoin = 'round';
this.moveTo(Tract.noseStart, -this.noseOffset);
for (var i = 1; i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
this.moveTo(Tract.noseStart+velumAngle, -this.noseOffset);
for (var i = Math.ceil(velumAngle); i < Tract.noseLength; i++) this.lineTo(i+Tract.noseStart, -this.noseOffset);
this.ctx.stroke();
//velum
this.ctx.globalAlpha = velum*5;
this.ctx.beginPath();
this.moveTo(Tract.noseStart-2, 0);
this.lineTo(Tract.noseStart, -this.noseOffset);
this.moveTo(Tract.noseStart+velumAngle-2, 0);
this.lineTo(Tract.noseStart+velumAngle, -this.noseOffset);
this.ctx.stroke();
this.ctx.fillStyle = "orchid";
this.ctx.font="20px Arial";
this.ctx.textAlign = "center";
this.ctx.globalAlpha = 0.7;
this.drawText(Tract.n*0.95, 0.8+0.8*Tract.diameter[Tract.n-1], " lip");
this.ctx.globalAlpha=1.0;
this.ctx.fillStyle = "black";
this.ctx.textAlign = "left";
this.ctx.fillText(UI.debugText, 20, 20);
//this.drawPositions();
},
drawBackground : function()
{
this.ctx = backCtx;
//text
this.ctx.fillStyle = "orchid";
this.ctx.font="20px Arial";
this.ctx.textAlign = "center";
this.ctx.globalAlpha = 0.7;
this.drawText(Tract.n*0.44, -0.28, "soft");
this.drawText(Tract.n*0.51, -0.28, "palate");
this.drawText(Tract.n*0.77, -0.28, "hard");
this.drawText(Tract.n*0.84, -0.28, "palate");
this.drawText(Tract.n*0.95, -0.28, " lip");
this.ctx.font="17px Arial";
this.drawTextStraight(Tract.n*0.18, 3, " tongue control");
this.ctx.textAlign = "left";
this.drawText(Tract.n*1.03, -1.07, "nasals");
this.drawText(Tract.n*1.03, -0.28, "stops");
this.drawText(Tract.n*1.03, 0.51, "fricatives");
//this.drawTextStraight(1.5, +0.8, "glottis")
this.ctx.strokeStyle = "orchid";
this.ctx.lineWidth = 2;
this.ctx.beginPath();
this.moveTo(Tract.n*1.03, 0); this.lineTo(Tract.n*1.07, 0);
this.moveTo(Tract.n*1.03, -this.noseOffset); this.lineTo(Tract.n*1.07, -this.noseOffset);
this.ctx.stroke();
this.ctx.globalAlpha = 0.9;
this.ctx.globalAlpha = 1.0;
this.ctx = tractCtx;
},
drawPositions : function()
{
this.ctx.fillStyle = "orchid";
this.ctx.font="24px Arial";
this.ctx.textAlign = "center";
this.ctx.globalAlpha = 0.6;
var a = 2;
var b = 1.5;
this.drawText(15, a+b*0.60, 'æ'); //pat
this.drawText(13, a+b*0.27, 'ɑ'); //part
this.drawText(12, a+b*0.00, 'ɒ'); //pot
this.drawText(17.7, a+b*0.05, '(ɔ)'); //port (rounded)
this.drawText(27, a+b*0.65, 'ɪ'); //pit
this.drawText(27.4, a+b*0.21, 'i'); //peat
this.drawText(20, a+b*1.00, 'e'); //pet
this.drawText(18.1, a+b*0.37, 'ʌ'); //putt
//put ʊ
this.drawText(23, a+b*0.1, '(u)'); //poot (rounded)
this.drawText(21, a+b*0.6, 'ə'); //pert [should be ɜ]
var nasals = -1.1;
var stops = -0.4;
var fricatives = 0.3;
var approximants = 1.1;
this.ctx.globalAlpha = 0.8;
//approximants
this.drawText(38, approximants, 'l');
this.drawText(41, approximants, 'w');
//?
this.drawText(4.5, 0.37, 'h');
if (Glottis.isTouched || alwaysVoice)
{
//voiced consonants
this.drawText(31.5, fricatives, 'ʒ');
this.drawText(36, fricatives, 'z');
this.drawText(41, fricatives, 'v');
this.drawText(22, stops, 'g');
this.drawText(36, stops, 'd');
this.drawText(41, stops, 'b');
this.drawText(22, nasals, 'ŋ');
this.drawText(36, nasals, 'n');
this.drawText(41, nasals, 'm');
}
else
{
//unvoiced consonants
this.drawText(31.5, fricatives, 'ʃ');
this.drawText(36, fricatives, 's');
this.drawText(41, fricatives, 'f');
this.drawText(22, stops, 'k');
this.drawText(36, stops, 't');
this.drawText(41, stops, 'p');
this.drawText(22, nasals, 'ŋ');
this.drawText(36, nasals, 'n');
this.drawText(41, nasals, 'm');
}
},
drawAmplitudes : function()
{
this.ctx.strokeStyle = "orchid";
this.ctx.lineCap = "butt";
this.ctx.globalAlpha = 0.3;
for (var i=2; i<Tract.n-1; i++)
{
this.ctx.beginPath();
this.ctx.lineWidth = Math.sqrt(Tract.maxAmplitude[i])*3;
this.moveTo(i, 0);
this.lineTo(i, Tract.diameter[i]);
this.ctx.stroke();
}
for (var i=1; i<Tract.noseLength-1; i++)
{
this.ctx.beginPath();
this.ctx.lineWidth = Math.sqrt(Tract.noseMaxAmplitude[i]) * 3;
this.moveTo(i+Tract.noseStart, -this.noseOffset);
this.lineTo(i+Tract.noseStart, -this.noseOffset - Tract.noseDiameter[i]*0.9);
this.ctx.stroke();
}
this.ctx.globalAlpha = 1;
},
drawTongueControl : function()
{
this.ctx.lineCap = "round";
this.ctx.lineJoin = "round";
this.ctx.strokeStyle = palePink;
this.ctx.fillStyle = palePink;
this.ctx.globalAlpha = 1.0;
this.ctx.beginPath();
this.ctx.lineWidth = 45;
//outline
this.moveTo(this.tongueLowerIndexBound, this.innerTongueControlRadius);
for (var i=this.tongueLowerIndexBound+1; i<=this.tongueUpperIndexBound; i++) this.lineTo(i, this.innerTongueControlRadius);
this.lineTo(this.tongueIndexCentre, this.outerTongueControlRadius);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.fill();
var a = this.innerTongueControlRadius;
var c = this.outerTongueControlRadius;
var b = 0.5*(a+c);
var r = 3;
this.ctx.fillStyle = "orchid";
this.ctx.globalAlpha = 0.3;
this.drawCircle(this.tongueIndexCentre, a, r);
this.drawCircle(this.tongueIndexCentre-4.25, a, r);
this.drawCircle(this.tongueIndexCentre-8.5, a, r);
this.drawCircle(this.tongueIndexCentre+4.25, a, r);
this.drawCircle(this.tongueIndexCentre+8.5, a, r);
this.drawCircle(this.tongueIndexCentre-6.1, b, r);
this.drawCircle(this.tongueIndexCentre+6.1, b, r);
this.drawCircle(this.tongueIndexCentre, b, r);
this.drawCircle(this.tongueIndexCentre, c, r);
this.ctx.globalAlpha = 1.0;
//circle for tongue position
var angle = this.angleOffset + this.tongueIndex * this.angleScale * Math.PI / (Tract.lipStart-1);
var r = this.radius - this.scale*(this.tongueDiameter);
var x = this.originX-r*Math.cos(angle);
var y = this.originY-r*Math.sin(angle);
this.ctx.lineWidth = 4;
this.ctx.strokeStyle = "orchid";
this.ctx.globalAlpha = 0.7;
this.ctx.beginPath();
this.ctx.arc(x,y, 18, 0, 2*Math.PI);
this.ctx.stroke();
this.ctx.globalAlpha = 0.15;
this.ctx.fill();
this.ctx.globalAlpha = 1.0;
this.ctx.fillStyle = "orchid";
},
drawPitchControl : function()
{
var w=9;
var h=15;
if (Glottis.x)
{
this.ctx.lineWidth = 4;
this.ctx.strokeStyle = "orchid";
this.ctx.globalAlpha = 0.7;
this.ctx.beginPath();
this.ctx.moveTo(Glottis.x-w, Glottis.y-h);
this.ctx.lineTo(Glottis.x+w, Glottis.y-h);
this.ctx.lineTo(Glottis.x+w, Glottis.y+h);
this.ctx.lineTo(Glottis.x-w, Glottis.y+h);
this.ctx.closePath();
this.ctx.stroke();
this.ctx.globalAlpha = 0.15;
this.ctx.fill();
this.ctx.globalAlpha = 1.0;
}
},
setRestDiameter : function()
{
for (var i=Tract.bladeStart; i<Tract.lipStart; i++)
{
var t = 1.1 * Math.PI*(this.tongueIndex - i)/(Tract.tipStart - Tract.bladeStart);
var fixedTongueDiameter = 2+(this.tongueDiameter-2)/1.5;
var curve = (1.5-fixedTongueDiameter+this.gridOffset)*Math.cos(t);
if (i == Tract.bladeStart-2 || i == Tract.lipStart-1) curve *= 0.8;
if (i == Tract.bladeStart || i == Tract.lipStart-2) curve *= 0.94;
Tract.restDiameter[i] = 1.5 - curve;
}
},
handleTouches : function()
{
if (this.tongueTouch != 0 && !this.tongueTouch.alive) this.tongueTouch = 0;
if (this.tongueTouch == 0)
{
for (var j=0; j<UI.touchesWithMouse.length; j++)
{
var touch = UI.touchesWithMouse[j];
if (!touch.alive) continue;
if (touch.fricative_intensity == 1) continue; //only new touches will pass this
var x = touch.x;
var y = touch.y;
var index = TractUI.getIndex(x,y);
var diameter = TractUI.getDiameter(x,y);
if (index >= this.tongueLowerIndexBound-4 && index<=this.tongueUpperIndexBound+4
&& diameter >= this.innerTongueControlRadius-0.5 && diameter <= this.outerTongueControlRadius+0.5)
{
this.tongueTouch = touch;
}
}
}
if (this.tongueTouch != 0)
{
var x = this.tongueTouch.x;
var y = this.tongueTouch.y;
var index = TractUI.getIndex(x,y);
var diameter = TractUI.getDiameter(x,y);
var fromPoint = (this.outerTongueControlRadius-diameter)/(this.outerTongueControlRadius-this.innerTongueControlRadius);
fromPoint = Math.clamp(fromPoint, 0, 1);
fromPoint = Math.pow(fromPoint, 0.58) - 0.2*(fromPoint*fromPoint-fromPoint); //horrible kludge to fit curve to straight line
this.tongueDiameter = Math.clamp(diameter, this.innerTongueControlRadius, this.outerTongueControlRadius);
//this.tongueIndex = Math.clamp(index, this.tongueLowerIndexBound, this.tongueUpperIndexBound);
var out = fromPoint*0.5*(this.tongueUpperIndexBound-this.tongueLowerIndexBound);
this.tongueIndex = Math.clamp(index, this.tongueIndexCentre-out, this.tongueIndexCentre+out);
}
this.setRestDiameter();
for (var i=0; i<Tract.n; i++) Tract.targetDiameter[i] = Tract.restDiameter[i];
//other constrictions and nose
Tract.velumTarget = 0.01;
for (var j=0; j<UI.touchesWithMouse.length; j++)
{
var touch = UI.touchesWithMouse[j];
if (!touch.alive) continue;
var x = touch.x;
var y = touch.y;
var index = TractUI.getIndex(x,y);
var diameter = TractUI.getDiameter(x,y);
if (index > Tract.noseStart && diameter < -this.noseOffset)
{
Tract.velumTarget = 0.4;
}
temp.a = index;
temp.b = diameter;
if (diameter < -0.85-this.noseOffset) continue;
diameter -= 0.3;
if (diameter<0) diameter = 0;
var width=2;
if (index<25) width = 10;
else if (index>=Tract.tipStart) width= 5;
else width = 10-5*(index-25)/(Tract.tipStart-25);
if (index >= 2 && index < Tract.n && y<tractCanvas.height && diameter < 3)
{
intIndex = Math.round(index);
for (var i=-Math.ceil(width)-1; i<width+1; i++)
{
if (intIndex+i<0 || intIndex+i>=Tract.n) continue;
var relpos = (intIndex+i) - index;
relpos = Math.abs(relpos)-0.5;
var shrink;
if (relpos <= 0) shrink = 0;
else if (relpos > width) shrink = 1;
else shrink = 0.5*(1-Math.cos(Math.PI * relpos / width));
if (diameter < Tract.targetDiameter[intIndex+i])
{
Tract.targetDiameter[intIndex+i] = diameter + (Tract.targetDiameter[intIndex+i]-diameter)*shrink;
}
}
}
}
},
}
function makeButton(x, y, width, height, text, switchedOn)
{
button = {};
button.x = x;
button.y = y;
button.width = width;
button.height = height;
button.text = text;
button.switchedOn = switchedOn;
button.draw = function(ctx)
{
var radius = 10;
ctx.strokeStyle = palePink;
ctx.fillStyle = palePink;
ctx.globalAlpha = 1.0;
ctx.lineCap = 'round';
ctx.lineJoin = 'round';
ctx.lineWidth = 2*radius;
ctx.beginPath();
ctx.moveTo(this.x+radius, this.y+radius);
ctx.lineTo(this.x+this.width-radius, this.y+radius);
ctx.lineTo(this.x+this.width-radius, this.y+this.height-radius);
ctx.lineTo(this.x+radius, this.y+this.height-radius);
ctx.closePath();
ctx.stroke();
ctx.fill();
ctx.font="16px Arial";
ctx.textAlign = "center";
if (this.switchedOn)
{
ctx.fillStyle = "orchid";
ctx.globalAlpha = 0.6;
}
else
{
ctx.fillStyle = "white";
ctx.globalAlpha = 1.0;
}
this.drawText(ctx);
};
button.drawText = function(ctx)
{
ctx.fillText(this.text, this.x+this.width/2, this.y+this.height/2+6);
};
button.handleTouchStart = function(touch)
{
if (touch.x>=this.x && touch.x <= this.x + this.width
&& touch.y >= this.y && touch.y <= this.y + this.height)
{
this.switchedOn = !this.switchedOn;
}
};
return button;
}
document.body.style.cursor = 'pointer';
AudioSystem.init();
UI.init();
Glottis.init();
Tract.init();
TractUI.init();
/**
* MIDI modifications begin
*/
var midi = null;
function onMIDIMessage(event) {
switch (event.data[0]) {
case 0x90: // Note press
var note = event.data[1];
// Rough mapping of note to pitch (excluding Vox Humana switch)
if (note != 97) Glottis.UIFrequency = 10 + (note * 4);
switch (true) {
case note==97: // Vox Humana electrically pressed
if (AudioSystem.started) {
AudioSystem.mute();
}
break;
case note%12==0: // C
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 283, pageY: 306});
UI.endMouse({});
UI.mouseDown = false;
break;
case note%12==1: // C#
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 280, pageY: 360});
UI.endMouse({});
UI.mouseDown = false;
break;
case note%12==2: // D
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 195, pageY: 350});
UI.endMouse({});
UI.startMouse({pageX: 446, pageY: 207});
UI.mouseDown = true;
break;
case note%12==3: // D#
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 513, pageY: 127});
UI.mouseDown = true;
break;
case note%12==4: // E
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 210, pageY: 323});
UI.endMouse({});
UI.startMouse({pageX: 308, pageY: 99});
UI.mouseDown = true;
break;
case note%12==5: // F
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 291, pageY: 349});
UI.endMouse({});
UI.startMouse({pageX: 465, pageY: 181});
UI.mouseDown = true;
break;
case note%12==6: // F#
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 227, pageY: 315});
UI.mouseDown = true;
break;
case note%12==7: // G
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 202, pageY: 346});
UI.endMouse({});
UI.startMouse({pageX: 274, pageY: 81});
UI.mouseDown = true;
break;
case note%12==8: // G#
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 707, pageY: 66});
UI.mouseDown = true;
break;
case note%12==9: // A
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 301, pageY: 293});
UI.endMouse({});
UI.mouseDown = false;
break;
case note%12==10: // A#
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 30, pageY: 440});
UI.mouseDown = true;
break;
case note%12==11: // B
if (UI.mouseDown) UI.endMouse({});
UI.startMouse({pageX: 465, pageY: 181});
UI.mouseDown = true;
break;
}
break;
case 0x80: // Note release
var note = event.data[1];
switch(true) {
case note==97: // Vox Humana electrically released
if (!AudioSystem.started) {
AudioSystem.started = true;
AudioSystem.startSound();
} else {
AudioSystem.unmute();
}
break;
case note%12==5:
case note%12==11:
if (UI.mouseDown) UI.endMouse({});
UI.mouseDown = false;
break;
default:
UI.mouseDown = false;
UI.endMouse({});
break;
}
break;
}
}
function onMIDISuccess(midiAccess) {
midi = midiAccess;
midiAccess.inputs.forEach(function(entry) {
entry.onmidimessage = onMIDIMessage;
});
}
function onMIDIFailure(msg) {
console.log( "Failed to open MIDI: " + msg );
}
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
/**
* MIDI modifications end
*/
requestAnimationFrame(redraw);
// UI.shapeToFitScreen();
function redraw(highResTimestamp)
{
//UI.shapeToFitScreen();
TractUI.draw();
UI.draw();
requestAnimationFrame(redraw);
time = Date.now()/1000;
UI.updateTouches();
}
/**********************************************************************************/
/**********************************************************************************/
/*
* A speed-improved perlin and simplex noise algorithms for 2D.
*
* Based on example code by Stefan Gustavson (stegu@itn.liu.se).
* Optimisations by Peter Eastman (peastman@drizzle.stanford.edu).
* Better rank ordering method by Stefan Gustavson in 2012.
* Converted to Javascript by Joseph Gentle.
*
* Version 2012-03-09
*
* This code was placed in the public domain by its original author,
* Stefan Gustavson. You may use it as you see fit, but
* attribution is appreciated.
*
*/
(function(global){
var module = global.noise = {};
function Grad(x, y, z) {
this.x = x; this.y = y; this.z = z;
}
Grad.prototype.dot2 = function(x, y) {
return this.x*x + this.y*y;
};
Grad.prototype.dot3 = function(x, y, z) {
return this.x*x + this.y*y + this.z*z;
};
var grad3 = [new Grad(1,1,0),new Grad(-1,1,0),new Grad(1,-1,0),new Grad(-1,-1,0),
new Grad(1,0,1),new Grad(-1,0,1),new Grad(1,0,-1),new Grad(-1,0,-1),
new Grad(0,1,1),new Grad(0,-1,1),new Grad(0,1,-1),new Grad(0,-1,-1)];
var p = [151,160,137,91,90,15,
131,13,201,95,96,53,194,233,7,225,140,36,103,30,69,142,8,99,37,240,21,10,23,
190, 6,148,247,120,234,75,0,26,197,62,94,252,219,203,117,35,11,32,57,177,33,
88,237,149,56,87,174,20,125,136,171,168, 68,175,74,165,71,134,139,48,27,166,
77,146,158,231,83,111,229,122,60,211,133,230,220,105,92,41,55,46,245,40,244,
102,143,54, 65,25,63,161, 1,216,80,73,209,76,132,187,208, 89,18,169,200,196,
135,130,116,188,159,86,164,100,109,198,173,186, 3,64,52,217,226,250,124,123,
5,202,38,147,118,126,255,82,85,212,207,206,59,227,47,16,58,17,182,189,28,42,
223,183,170,213,119,248,152, 2,44,154,163, 70,221,153,101,155,167, 43,172,9,
129,22,39,253, 19,98,108,110,79,113,224,232,178,185, 112,104,218,246,97,228,
251,34,242,193,238,210,144,12,191,179,162,241, 81,51,145,235,249,14,239,107,
49,192,214, 31,181,199,106,157,184, 84,204,176,115,121,50,45,127, 4,150,254,
138,236,205,93,222,114,67,29,24,72,243,141,128,195,78,66,215,61,156,180];
// To remove the need for index wrapping, double the permutation table length
var perm = new Array(512);
var gradP = new Array(512);
// This isn't a very good seeding function, but it works ok. It supports 2^16
// different seed values. Write something better if you need more seeds.
module.seed = function(seed) {
if(seed > 0 && seed < 1) {
// Scale the seed out
seed *= 65536;
}
seed = Math.floor(seed);
if(seed < 256) {
seed |= seed << 8;
}
for(var i = 0; i < 256; i++) {
var v;
if (i & 1) {
v = p[i] ^ (seed & 255);
} else {
v = p[i] ^ ((seed>>8) & 255);
}
perm[i] = perm[i + 256] = v;
gradP[i] = gradP[i + 256] = grad3[v % 12];
}
};
module.seed(Date.now());
/*
for(var i=0; i<256; i++) {
perm[i] = perm[i + 256] = p[i];
gradP[i] = gradP[i + 256] = grad3[perm[i] % 12];
}*/
// Skewing and unskewing factors for 2, 3, and 4 dimensions
var F2 = 0.5*(Math.sqrt(3)-1);
var G2 = (3-Math.sqrt(3))/6;
var F3 = 1/3;
var G3 = 1/6;
// 2D simplex noise
module.simplex2 = function(xin, yin) {
var n0, n1, n2; // Noise contributions from the three corners
// Skew the input space to determine which simplex cell we're in
var s = (xin+yin)*F2; // Hairy factor for 2D
var i = Math.floor(xin+s);
var j = Math.floor(yin+s);
var t = (i+j)*G2;
var x0 = xin-i+t; // The x,y distances from the cell origin, unskewed.
var y0 = yin-j+t;
// For the 2D case, the simplex shape is an equilateral triangle.
// Determine which simplex we are in.
var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords
if(x0>y0) { // lower triangle, XY order: (0,0)->(1,0)->(1,1)
i1=1; j1=0;
} else { // upper triangle, YX order: (0,0)->(0,1)->(1,1)
i1=0; j1=1;
}
// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and
// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where
// c = (3-sqrt(3))/6
var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords
var y1 = y0 - j1 + G2;
var x2 = x0 - 1 + 2 * G2; // Offsets for last corner in (x,y) unskewed coords
var y2 = y0 - 1 + 2 * G2;
// Work out the hashed gradient indices of the three simplex corners
i &= 255;
j &= 255;
var gi0 = gradP[i+perm[j]];
var gi1 = gradP[i+i1+perm[j+j1]];
var gi2 = gradP[i+1+perm[j+1]];
// Calculate the contribution from the three corners
var t0 = 0.5 - x0*x0-y0*y0;
if(t0<0) {
n0 = 0;
} else {
t0 *= t0;
n0 = t0 * t0 * gi0.dot2(x0, y0); // (x,y) of grad3 used for 2D gradient
}
var t1 = 0.5 - x1*x1-y1*y1;
if(t1<0) {
n1 = 0;
} else {
t1 *= t1;
n1 = t1 * t1 * gi1.dot2(x1, y1);
}
var t2 = 0.5 - x2*x2-y2*y2;
if(t2<0) {
n2 = 0;
} else {
t2 *= t2;
n2 = t2 * t2 * gi2.dot2(x2, y2);
}
// Add contributions from each corner to get the final noise value.
// The result is scaled to return values in the interval [-1,1].
return 70 * (n0 + n1 + n2);
};
module.simplex1 = function(x)
{
return module.simplex2(x*1.2, -x*0.7);
};
})(this);
</script>
</body>
</html>