UNPKG

pink-trombone

Version:

A headless programmable polyphonic version of Neil Thapen's Pink Trombone

860 lines (734 loc) 32.6 kB
/* 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 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 Copyright 2025 Gorka Egino 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. */ import { Noise } from "./Noise.js"; const lerp = (a, b, t) => a + (b - a) * t; const normalize = (value, min, max) => (value - min) / (max - min); const clamp = (number, min, max) => Math.max(min, Math.min(max, number)); const hertzToMidi = hertz => 12 * Math.log2(hertz / 440) + 69; const midiToHertz = midi => 440 * Math.pow(2, (midi - 69) / 12); const moveTowards = (current, target, amountUp, amountDown) => { if (current<target) return Math.min(current+amountUp, target); else return Math.max(current-amountDown, target); } var sampleRate; var noiseFreq = 500; var noiseQ = 0.7; export class PinkTrombone { isDisposed = false; constructor(audioContext) { this.glottis = new Glottis(); this.tract = new Tract(this.glottis); this.audioSystem = new AudioSystem(audioContext, this.glottis, this.tract); } dispose() { this.isDisposed = true; this.audioSystem.dispose(); this.audioSystem = null; this.glottis = null; this.tract = null; } get isVoiced() { return this.glottis.isTouched } set isVoiced(v) { this.glottis.isTouched = v } get voicedness() { return Math.acos(1-this.#tenseness) * 2 / Math.PI; // if loudness was set separately (diffeent relationship than pow(tenseness, 0.25)) it wouldn't work } set voicedness(v) { this.#tenseness = 1-Math.cos(v*Math.PI*0.5); this.#loudness = Math.pow(this.#tenseness, 0.25); } get #loudness() { return this.glottis.loudness } set #loudness(v) { this.glottis.loudness = v } get #tenseness() { return this.glottis.UITenseness } set #tenseness(v) { this.glottis.UITenseness = v } get frequency() { return this.glottis.UIFrequency } set frequency(v) { this.glottis.UIFrequency = v } get pitch() { return hertzToMidi(this.frequency) } set pitch(v) { this.frequency = midiToHertz(v) } get vibrato() { const glottis = this.glottis; return { get amount() { return glottis.vibratoAmount }, set amount(v) { glottis.vibratoAmount = v }, get frequency() { return glottis.vibratoFrequency }, set frequency(v) { glottis.vibratoFrequency = v }, } } get nasality() { return normalize(this.tract.velumTarget, 0.01, 0.4) } set nasality(v) { this.tract.velumTarget = lerp(0.01, 0.4, v) } #frontness = 0.5; #openness = 1; get vowel() { const trombone = this; return { get frontness() { return trombone.#frontness}, set frontness(v) { trombone.#frontness = clamp(v, 0, 1); const min = 1 - Math.sqrt(1-Math.pow(1-trombone.#frontness, 2)); trombone.#openness = clamp(trombone.#openness, min, 1); trombone.updateTongue(); }, get openness() { return trombone.#openness}, set openness(v) { trombone.#openness = clamp(v, 0, 1); const min = 1 - Math.sqrt(1-Math.pow(1-trombone.#openness, 2)); trombone.#frontness = clamp(trombone.#frontness, min, 1); trombone.updateTongue(); } } } updateTongue() { const theta = Math.atan2(1-this.#openness, 1-this.#frontness); const r = Math.sqrt(Math.pow(1-this.#openness,2)+Math.pow(1-this.#frontness,2)); this.tract.tongue.setPolarCoords(r, theta); } get constrictions() { return this.tract.constrictions } } class AudioSystem { blockLength = 512; blockTime = 1; isDisposed = false; constructor(audioContext, glottis, tract) { this.audioContext = audioContext; sampleRate = this.audioContext.sampleRate; this.blockTime = this.blockLength/sampleRate; this.glottis = glottis; this.tract = tract; //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 = (e) => this.doScriptProcessor(e); this.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; this.whiteNoise.connect(aspirateFilter); aspirateFilter.connect(this.scriptProcessor); var fricativeFilter = this.audioContext.createBiquadFilter(); fricativeFilter.type = "bandpass"; fricativeFilter.frequency.value = 1000; fricativeFilter.Q.value = 0.5; this.whiteNoise.connect(fricativeFilter); fricativeFilter.connect(this.scriptProcessor); this.filters = [aspirateFilter, fricativeFilter]; this.whiteNoise.start(0); } createWhiteNoiseNode(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(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 = this.glottis.runStep(lambda1, inputArray1[j]); var vocalOutput = 0; //Tract runs at twice the sample rate this.tract.runStep(glottalOutput, inputArray2[j], lambda1); vocalOutput +=this.tract.lipOutput +this.tract.noseOutput; this.tract.runStep(glottalOutput, inputArray2[j], lambda2); vocalOutput +=this.tract.lipOutput +this.tract.noseOutput; outArray[j] = vocalOutput * 0.125; } this.glottis.finishBlock(); this.tract.finishBlock(this.blockTime); } mute() { this.scriptProcessor.disconnect(); } unmute() { this.scriptProcessor.connect(this.audioContext.destination); } dispose() { this.isDisposed = true; this.scriptProcessor.disconnect(); this.scriptProcessor.onaudioprocess = null; this.scriptProcessor = null; this.whiteNoise.stop(); this.whiteNoise.disconnect(); this.whiteNoise.buffer = null; this.whiteNoise = null; this.filters.forEach(filter => filter.disconnect()); this.filters = null; this.glottis = null; this.tract = null; } } class 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; alwaysVoice = false; autoWobble = false; constructor() { this.setupWaveform(0); this.noise = new Noise(); } runStep(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*this.noise.simplex1(this.totalTime * 1.99); out += aspiration; return out; } getNoiseModulator() { 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() { const noise = this.noise; 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 (this.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 && this.alwaysVoice) this.newTenseness += (3-this.UITenseness)*(1-this.intensity); if (this.isTouched || this.alwaysVoice) this.intensity += 0.13; else this.intensity -= 0.05; this.intensity = clamp(this.intensity, 0, 1); } setupWaveform(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(t) { let output; 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; } } class Tongue { // high level setPolarCoords(r, theta) { const normTheta = theta / (Math.PI / 2); this.r = r; this.index = this.getIndexFromLocalNorm(r, normTheta); } // low level constructor(tract) { this.tract = tract; this.refTriangle = this.buildRefTriangle(); this._r = this.diameterToLocalR(); // why here } buildRefTriangle() { // angle is somewhat arbitrary, although constraints are set on // 'frontness' and 'openness' to behave as if they were half pi radians apart const ANGLE = Math.PI / 2; const GAMMA = (Math.PI*2 - ANGLE) / 2; const B = this.tract.__diameter - this.maxDiameter; const C = this.tract.__diameter - this.minDiameter; const BETA = Math.asin(B*Math.sin(GAMMA)/C); const ALPHA = Math.PI - GAMMA - BETA; const A = B * Math.sin(ALPHA) / Math.sin(BETA); return { A, // line from tongue 'triangle' open vertex to middle vertex B, // line from tract origin to tongue 'triangle' middle vertex C, // line from tract origin to tongue 'triangle' vertex ALPHA, BETA, GAMMA, // angles of the vertex opposite to the line }; } _index = 12.9; get index() { return this._index }; get minIndex() { return this.tract.bladeStart + 2 }; get maxIndex() { return this.tract.tipStart - 3 }; get meanIndex() { return (this.minIndex + this.maxIndex) * 0.5 }; set index(v) { this._index = v; this.tract.setRestDiameter()}; // here diameter refers to distance from tract, not to the deformation circle _diameter = 2.43; get diameter() { return this._diameter }; set diameter(v) { this._diameter = v; this._r = this.diameterToLocalR();this.tract.setRestDiameter() }; minDiameter = 2.05; maxDiameter = 3.5; // middle level, i guess ? // r is 'equivalent' for diameter. calculated when tract is received with constructor _r = undefined; get r() { return this._r }; set r(v) { this._r = v; this.diameter = this.localRToDiameter(v) }; localRToDiameter() { if (this.r === 0) return this.maxDiameter; if (this.r === 1) return this.minDiameter; const {A, B, GAMMA} = this.refTriangle; const a = this.r * A; const b = B; const gamma = GAMMA; const c = Math.sqrt(a**2 + b**2 - 2*a*b*Math.cos(gamma)); const diameter = this.tract.__diameter - c; return diameter; } diameterToLocalR() { if (this.diameter === this.maxDiameter) return 0; if (this.diameter === this.minDiameter) return 1; const {A, B, GAMMA} = this.refTriangle; const c = this.tract.__diameter - this.diameter; const b = B; const gamma = GAMMA; const beta = Math.asin(Math.sin(gamma)*b/c); const alpha = Math.PI - gamma - beta; const a = c * Math.sin(alpha) / Math.sin(gamma); const r = a / A; return r; } getIndexFromLocalNorm(r, localNorm) { const {A, ALPHA, GAMMA} = this.refTriangle; const a = r * A; const c = this.tract.__diameter - this.diameter; const gamma = GAMMA; const alpha = Math.asin(a * Math.sin(gamma) / c); const ratio = alpha / ALPHA; const maxOffset = this.maxIndex - this.meanIndex; const currentMaxOffset = maxOffset * ratio; const currentMinIndex = this.meanIndex - currentMaxOffset; const currentMaxIndex = this.meanIndex + currentMaxOffset; return lerp(currentMinIndex, currentMaxIndex, localNorm); } } // index and diameter will be used by Tract, while location ('normtheta') and strength ('r') will be used by PinkTrombone class Constriction { index; minIndex = 2; // maxIndex with constructor diameter; // not the diameter of the constriction but the distance from the tract 'surface' minDiameter = 0; // on surface maxDiameter = 3; fricativeAttackTime = 100; // ms constructor(location=1, strength=1, tractN, updateTract) { this.updateTract = updateTract; this.minIndex = 2; this.maxIndex = tractN; this.location = location; this.strength = strength; this.creationTime = Date.now(); } get location() { return normalize(this.index, this.minIndex, this.maxIndex) } set location(v) { this.index = lerp(this.minIndex, this.maxIndex, v); this.updateTract() } // strength will be inversely proportional to diameter, hence the 'reversal' of min and max get strength() { return normalize(this.diameter, this.maxDiameter, this.minDiameter) } set strength(v) { this.diameter = lerp(this.maxDiameter, this.minDiameter, v); this.updateTract() } get fricativeIntensity() { const time = Date.now(); const dividend = this.removeTime ? 1 - (time-this.removeTime) : time-this.creationTime; return clamp(dividend/this.fricativeAttackTime, 0, 1); } get isDestroyed() { return this.removeTime !== undefined } destroy(callback) { this.removeTime = Date.now(); callback(); // setTimeout(callback, 1000); } } class 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; gridOffset = 1.7; // Comes from old trombone TractUI's radius / scale // necessary to use though because things like tongue maxdiameter, mindiameter... depend on that __diameter = 4.966; constrictions = new Set(); tongue = new Tongue(this); constructor(glottis) { this.glottis = glottis; 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; this.setRestDiameter(); this.restDiameter.forEach((v, i) => this.diameter[i] = v); this.constrictions._add = this.constrictions.add; this.constrictions.add = (location, strength) => { const constriction = new Constriction(location, strength, this.n, this.handleConstrictions.bind(this)); this.constrictions._add(constriction); this.handleConstrictions(); return constriction; } this.constrictions.remove = (constriction) => { if (!(constriction instanceof Constriction)) return; constriction.destroy(() => { this.constrictions.delete(constriction); this.handleConstrictions(); }); } } handleConstrictions() { this.setRestDiameter(); this.constrictions.forEach(constriction => { const { index, diameter } = constriction; const intIndex = Math.round(index); // radial deformer's width is 10 at start (and less than 25), 5 at end (and more than 32), interpolation inbetween const width = clamp(10-5*(index-25)/(this.tipStart-25), 5, 10); for (var i=-Math.ceil(width)-1; i<width+1; i++) { if (intIndex+i<0 || intIndex+i>=this.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 < this.targetDiameter[intIndex+i]) { this.targetDiameter[intIndex+i] = diameter + (this.targetDiameter[intIndex+i]-diameter)*shrink; } } }) } // takes into account new position of tongue. also updates target diameter setRestDiameter() { for (var i=this.bladeStart; i<this.lipStart; i++) { var t = 1.1 * Math.PI*(this.tongue.index - i)/(this.tipStart - this.bladeStart); var fixedTongueDiameter = 2+(this.tongue.diameter-2)/1.5; var curve = (1.5-fixedTongueDiameter+this.gridOffset)*Math.cos(t); if (i == this.bladeStart-2 || i == this.lipStart-1) curve *= 0.8; if (i == this.bladeStart || i == this.lipStart-2) curve *= 0.94; this.restDiameter[i] = 1.5 - curve; } this.restDiameter.forEach((v, i) => this.targetDiameter[i] = v); } reshapeTract(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] = 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] = moveTowards(this.noseDiameter[0], this.velumTarget, amount*0.25, amount*0.1); this.noseA[0] = this.noseDiameter[0]*this.noseDiameter[0]; } calculateReflections() { 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() { 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(glottalOutput, turbulenceNoise, lambda) { var updateAmplitudes = (Math.random()<0.1); //mouth this.processTransients(); this.addTurbulenceNoise(turbulenceNoise); //this.glottalReflection = -0.8 + 1.6 * this.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] = clamp(this.junctionOutputR[i] * this.fade, -1, 1); //this.L[i] = 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] = clamp(this.noseJunctionOutputR[i] * this.fade, -1, 1); //this.noseL[i] = 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(blockTime) { this.reshapeTract(blockTime); this.calculateReflections(); } addTransient(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() { 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(turbulenceNoise) { this.constrictions.forEach(constriction => { const {index, diameter} = constriction; const intensity = constriction.fricativeIntensity; if (intensity === 0) return; this.addTurbulenceNoiseAtIndex(0.66*turbulenceNoise*intensity, index, diameter); }) } addTurbulenceNoiseAtIndex(turbulenceNoise, index, diameter) { var i = Math.floor(index); var delta = index - i; turbulenceNoise *= this.glottis.getNoiseModulator(); var thinness0 = clamp(8*(0.7-diameter),0,1); var openness = 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; } }