/* Copyright (c) 2012 DinahMoe AB 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. Bitcrusher & Moog Filter by Zach Denton */ // https://developer.mozilla.org/en-US/docs/Web_Audio_API/Porting_webkitAudioContext_code_to_standards_based_AudioContext // Originally written by Alessandro Saccoia, Chris Coniglio and Oskar Eriksson (function (window) { var userContext; var userInstance; var Tuna = function (context) { if (!window.AudioContext) { window.AudioContext = window.webkitAudioContext; } if (!context) { console.log("tuna.js: Missing audio context! Creating a new context for you."); context = window.AudioContext && (new window.AudioContext()); } userContext = context; userInstance = this; }, version = "0.1", set = "setValueAtTime", linear = "linearRampToValueAtTime", pipe = function (param, val) { param.value = val; }, Super = Object.create(null, { activate: { writable: true, value: function (doActivate) { if (doActivate) { this.input.disconnect(); this.input.connect(this.activateNode); if (this.activateCallback) { this.activateCallback(doActivate); } } else { this.input.disconnect(); this.input.connect(this.output); } } }, bypass: { get: function () { return this._bypass; }, set: function (value) { if (this._lastBypassValue === value) { return; } this._bypass = value; this.activate(!value); this._lastBypassValue = value; } }, connect: { value: function (target) { this.output.connect(target); } }, disconnect: { value: function (target) { this.output.disconnect(target); } }, connectInOrder: { value: function (nodeArray) { var i = nodeArray.length - 1; while(i--) { if (!nodeArray[i].connect) { return console.error("AudioNode.connectInOrder: TypeError: Not an AudioNode.", nodeArray[i]); } if (nodeArray[i + 1].input) { nodeArray[i].connect(nodeArray[i + 1].input); } else { nodeArray[i].connect(nodeArray[i + 1]); } } } }, getDefaults: { value: function () { var result = {}; for(var key in this.defaults) { result[key] = this.defaults[key].value; } return result; } }, getValues: { value: function () { var result = {}; for(var key in this.defaults) { result[key] = this[key]; } return result; } }, automate: { value: function (property, value, duration, startTime) { var start = startTime ? ~~ (startTime / 1000) : userContext.currentTime, dur = duration ? ~~ (duration / 1000) : 0, _is = this.defaults[property], param = this[property], method; if (param) { if (_is.automatable) { if (!duration) { method = set; } else { method = linear; param.cancelScheduledValues(start); param.setValueAtTime(param.value, start); } param[method](value, dur + start); } else { param = value; } } else { console.error("Invalid Property for " + this.name); } } } }), FLOAT = "float", BOOLEAN = "boolean", STRING = "string", INT = "int"; function dbToWAVolume(db) { return Math.max(0, Math.round(100 * Math.pow(2, db / 6)) / 100); } function fmod(x, y) { // http://kevin.vanzonneveld.net // + original by: Onno Marsman // + input by: Brett Zamir (http://brett-zamir.me) // + bugfixed by: Kevin van Zonneveld (http://kevin.vanzonneveld.net) // * example 1: fmod(5.7, 1.3); // * returns 1: 0.5 var tmp, tmp2, p = 0, pY = 0, l = 0.0, l2 = 0.0; tmp = x.toExponential().match(/^.\.?(.*)e(.+)$/); p = parseInt(tmp[2], 10) - (tmp[1] + '').length; tmp = y.toExponential().match(/^.\.?(.*)e(.+)$/); pY = parseInt(tmp[2], 10) - (tmp[1] + '').length; if (pY > p) { p = pY; } tmp2 = (x % y); if (p < -100 || p > 20) { // toFixed will give an out of bound error so we fix it like this: l = Math.round(Math.log(tmp2) / Math.log(10)); l2 = Math.pow(10, l); return(tmp2 / l2).toFixed(l - p) * l2; } else { return parseFloat(tmp2.toFixed(-p)); } } function sign(x) { if (x === 0) { return 1; } else { return Math.abs(x) / x; } } function tanh(n) { return(Math.exp(n) - Math.exp(-n)) / (Math.exp(n) + Math.exp(-n)); } Tuna.toString = Tuna.prototype.toString = function () { return "You are running Tuna version " + version + " by Dinahmoe!"; }; Tuna.prototype.Filter = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.filter = userContext.createBiquadFilter(); this.output = userContext.createGain(); this.activateNode.connect(this.filter); this.filter.connect(this.output); this.frequency = properties.frequency || this.defaults.frequency.value; this.Q = properties.resonance || this.defaults.Q.value; this.filterType = properties.filterType || this.defaults.filterType.value; this.gain = properties.gain || this.defaults.gain.value; this.bypass = properties.bypass || false; }; Tuna.prototype.Filter.prototype = Object.create(Super, { name: { value: "Filter" }, defaults: { writable:true, value: { frequency: { value: 800, min: 20, max: 22050, automatable: true }, Q: { value: 1, min: 0.001, max: 100, automatable: true }, gain: { value: 0, min: -40, max: 40, automatable: true }, bypass: { value: true, automatable: false, type: BOOLEAN }, filterType: { value: 1, min: 0, max: 7, automatable: false, type: INT } } }, filterType: { enumerable: true, get: function () { return this.filter.type; }, set: function (value) { this.filter.type = value; } }, Q: { enumerable: true, get: function () { return this.filter.Q; }, set: function (value) { this.filter.Q.value = value; } }, gain: { enumerable: true, get: function () { return this.filter.gain; }, set: function (value) { this.filter.gain.value = value; } }, frequency: { enumerable: true, get: function () { return this.filter.frequency; }, set: function (value) { this.filter.frequency.value = value; } } }); Tuna.prototype.Bitcrusher = function (properties) { if (!properties) { properties = this.getDefaults(); } this.bufferSize = properties.bufferSize || this.defaults.bufferSize.value; this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.processor = userContext.createScriptProcessor(this.bufferSize, 1, 1); this.output = userContext.createGain(); this.activateNode.connect(this.processor); this.processor.connect(this.output); var phaser = 0, last = 0; this.processor.onaudioprocess = function (e) { var input = e.inputBuffer.getChannelData(0), output = e.outputBuffer.getChannelData(0), step = Math.pow(1/2, this.bits); for(var i = 0; i < input.length; i++) { phaser += this.normfreq; if (phaser >= 1.0) { phaser -= 1.0; last = step * Math.floor(input[i] / step + 0.5); } output[i] = last; } }; this.bits = properties.bits || this.defaults.bits.value; this.normfreq = properties.normfreq || this.defaults.normfreq.value; this.bypass = properties.bypass || false; }; Tuna.prototype.Bitcrusher.prototype = Object.create(Super, { name: { value: "Bitcrusher" }, defaults: { writable: true, value: { bits: { value: 4, min: 1, max: 16, automatable: false, type: INT }, bufferSize: { value: 4096, min: 256, max: 16384, automatable: false, type: INT }, bypass: { value: false, automatable: false, type: BOOLEAN }, normfreq: { value: 0.1, min: 0.0001, max: 1.0, automatable: false, } } }, bits: { enumerable: true, get: function () { return this.processor.bits; }, set: function (value) { this.processor.bits = value; } }, normfreq: { enumerable: true, get: function () { return this.processor.normfreq; }, set: function (value) { this.processor.normfreq = value; } } }); Tuna.prototype.Cabinet = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.convolver = this.newConvolver(properties.impulsePath || "../impulses/impulse_guitar.wav"); this.makeupNode = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.convolver.input); this.convolver.output.connect(this.makeupNode); this.makeupNode.connect(this.output); this.makeupGain = properties.makeupGain || this.defaults.makeupGain; this.bypass = properties.bypass || false; }; Tuna.prototype.Cabinet.prototype = Object.create(Super, { name: { value: "Cabinet" }, defaults: { writable:true, value: { makeupGain: { value: 1, min: 0, max: 20, automatable: true }, bypass: { value: false, automatable: false, type: BOOLEAN } } }, makeupGain: { enumerable: true, get: function () { return this.makeupNode.gain; }, set: function (value) { this.makeupNode.gain.value = value; } }, newConvolver: { value: function (impulsePath) { return new userInstance.Convolver({ impulse: impulsePath, dryLevel: 0, wetLevel: 1 }); } } }); Tuna.prototype.Chorus = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.attenuator = this.activateNode = userContext.createGain(); this.splitter = userContext.createChannelSplitter(2); this.delayL = userContext.createDelayNode(); this.delayR = userContext.createDelayNode(); this.feedbackGainNodeLR = userContext.createGain(); this.feedbackGainNodeRL = userContext.createGain(); this.merger = userContext.createChannelMerger(2); this.output = userContext.createGain(); this.lfoL = new userInstance.LFO({ target: this.delayL.delayTime, callback: pipe }); this.lfoR = new userInstance.LFO({ target: this.delayR.delayTime, callback: pipe }); this.input.connect(this.attenuator); this.attenuator.connect(this.output); this.attenuator.connect(this.splitter); this.splitter.connect(this.delayL, 0); this.splitter.connect(this.delayR, 1); this.delayL.connect(this.feedbackGainNodeLR); this.delayR.connect(this.feedbackGainNodeRL); this.feedbackGainNodeLR.connect(this.delayR); this.feedbackGainNodeRL.connect(this.delayL); this.delayL.connect(this.merger, 0, 0); this.delayR.connect(this.merger, 0, 1); this.merger.connect(this.output); this.feedback = properties.feedback || this.defaults.feedback.value; this.rate = properties.rate || this.defaults.rate.value; this.delay = properties.delay || this.defaults.delay.value; this.depth = properties.depth || this.defaults.depth.value; this.lfoR.phase = Math.PI / 2; this.attenuator.gain.value = 0.6934; // 1 / (10 ^ (((20 * log10(3)) / 3) / 20)) this.lfoL.activate(true); this.lfoR.activate(true); this.bypass = properties.bypass || false; }; Tuna.prototype.Chorus.prototype = Object.create(Super, { name: { value: "Chorus" }, defaults: { writable:true, value: { feedback: { value: 0.4, min: 0, max: 0.95, automatable: false, }, delay: { value: 0.0045, min: 0, max: 1, automatable: false, }, depth: { value: 0.7, min: 0, max: 1, automatable: false, }, rate: { value: 1.5, min: 0, max: 8, automatable: false, }, bypass: { value: true, automatable: false, type: BOOLEAN } } }, delay: { enumerable: true, get: function () { return this._delay; }, set: function (value) { this._delay = 0.0002 * (Math.pow(10, value) * 2); this.lfoL.offset = this._delay; this.lfoR.offset = this._delay; this._depth = this._depth; } }, depth: { enumerable: true, get: function () { return this._depth; }, set: function (value) { this._depth = value; this.lfoL.oscillation = this._depth * this._delay; this.lfoR.oscillation = this._depth * this._delay; } }, feedback: { enumerable: true, get: function () { return this._feedback; }, set: function (value) { this._feedback = value; this.feedbackGainNodeLR.gain.value = this._feedback; this.feedbackGainNodeRL.gain.value = this._feedback; } }, rate: { enumerable: true, get: function () { return this._rate; }, set: function (value) { this._rate = value; this.lfoL.frequency = this._rate; this.lfoR.frequency = this._rate; } } }); Tuna.prototype.Compressor = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.compNode = this.activateNode = userContext.createDynamicsCompressor(); this.makeupNode = userContext.createGain(); this.output = userContext.createGain(); this.compNode.connect(this.makeupNode); this.makeupNode.connect(this.output); this.automakeup = properties.automakeup || this.defaults.automakeup.value; this.makeupGain = properties.makeupGain || this.defaults.makeupGain.value; this.threshold = properties.threshold || this.defaults.threshold.value; this.release = properties.release || this.defaults.release.value; this.attack = properties.attack || this.defaults.attack.value; this.ratio = properties.ratio || this.defaults.ratio.value; this.knee = properties.knee || this.defaults.knee.value; this.bypass = properties.bypass || false; }; Tuna.prototype.Compressor.prototype = Object.create(Super, { name: { value: "Compressor" }, defaults: { writable:true, value: { threshold: { value: -20, min: -60, max: 0, automatable: true }, release: { value: 250, min: 10, max: 2000, automatable: true }, makeupGain: { value: 1, min: 1, max: 100, automatable: true }, attack: { value: 1, min: 0, max: 1000, automatable: true }, ratio: { value: 4, min: 1, max: 50, automatable: true }, knee: { value: 5, min: 0, max: 40, automatable: true }, automakeup: { value: false, automatable: false, type: BOOLEAN }, bypass: { value: true, automatable: false, type: BOOLEAN } } }, computeMakeup: { value: function () { var magicCoefficient = 4, // raise me if the output is too hot c = this.compNode; return -(c.threshold.value - c.threshold.value / c.ratio.value) / magicCoefficient; } }, automakeup: { enumerable: true, get: function () { return this._automakeup; }, set: function (value) { this._automakeup = value; if (this._automakeup) this.makeupGain = this.computeMakeup(); } }, threshold: { enumerable: true, get: function () { return this.compNode.threshold; }, set: function (value) { this.compNode.threshold.value = value; if (this._automakeup) this.makeupGain = this.computeMakeup(); } }, ratio: { enumerable: true, get: function () { return this.compNode.ratio; }, set: function (value) { this.compNode.ratio.value = value; if (this._automakeup) this.makeupGain = this.computeMakeup(); } }, knee: { enumerable: true, get: function () { return this.compNode.knee; }, set: function (value) { this.compNode.knee.value = value; if (this._automakeup) this.makeupGain = this.computeMakeup(); } }, attack: { enumerable: true, get: function () { return this.compNode.attack; }, set: function (value) { this.compNode.attack.value = value / 1000; } }, release: { enumerable: true, get: function () { return this.compNode.release; }, set: function (value) { this.compNode.release = value / 1000; } }, makeupGain: { enumerable: true, get: function () { return this.makeupNode.gain; }, set: function (value) { this.makeupNode.gain.value = dbToWAVolume(value); } } }); Tuna.prototype.Convolver = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.convolver = userContext.createConvolver(); this.dry = userContext.createGain(); this.filterLow = userContext.createBiquadFilter(); this.filterHigh = userContext.createBiquadFilter(); this.wet = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.filterLow); this.activateNode.connect(this.dry); this.filterLow.connect(this.filterHigh); this.filterHigh.connect(this.convolver); this.convolver.connect(this.wet); this.wet.connect(this.output); this.dry.connect(this.output); this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value; this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value; this.highCut = properties.highCut || this.defaults.highCut.value; this.buffer = properties.impulse || "../impulses/ir_rev_short.wav"; this.lowCut = properties.lowCut || this.defaults.lowCut.value; this.level = properties.level || this.defaults.level.value; this.filterHigh.type = this.filterHigh.LOWPASS; this.filterLow.type = this.filterHigh.HIGHPASS; this.bypass = properties.bypass || false; }; Tuna.prototype.Convolver.prototype = Object.create(Super, { name: { value: "Convolver" }, defaults: { writable:true, value: { highCut: { value: 22050, min: 20, max: 22050, automatable: true }, lowCut: { value: 20, min: 20, max: 22050, automatable: true }, dryLevel: { value: 1, min: 0, max: 1, automatable: true }, wetLevel: { value: 1, min: 0, max: 1, automatable: true }, level: { value: 1, min: 0, max: 1, automatable: true } } }, lowCut: { get: function () { return this.filterLow.frequency; }, set: function (value) { this.filterLow.frequency.value = value; } }, highCut: { get: function () { return this.filterHigh.frequency; }, set: function (value) { this.filterHigh.frequency.value = value; } }, level: { get: function () { return this.output.gain; }, set: function (value) { this.output.gain.value = value; } }, dryLevel: { get: function () { return this.dry.gain }, set: function (value) { this.dry.gain.value = value; } }, wetLevel: { get: function () { return this.wet.gain; }, set: function (value) { this.wet.gain.value = value; } }, buffer: { enumerable: false, get: function () { return this.convolver.buffer; }, set: function (impulse) { var convolver = this.convolver, xhr = new XMLHttpRequest(); if (!impulse) { console.log("Tuna.Convolver.setBuffer: Missing impulse path!"); return; } xhr.open("GET", impulse, true); xhr.responseType = "arraybuffer"; xhr.onreadystatechange = function () { if (xhr.readyState === 4) { if (xhr.status < 300 && xhr.status > 199 || xhr.status === 302) { userContext.decodeAudioData(xhr.response, function (buffer) { convolver.buffer = buffer; }, function (e) { if (e) console.log("Tuna.Convolver.setBuffer: Error decoding data" + e); }); } } }; xhr.send(null); } } }); Tuna.prototype.Delay = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.dry = userContext.createGain(); this.wet = userContext.createGain(); this.filter = userContext.createBiquadFilter(); this.delay = userContext.createDelayNode(); this.feedbackNode = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.delay); this.activateNode.connect(this.dry); this.delay.connect(this.filter); this.filter.connect(this.feedbackNode); this.feedbackNode.connect(this.delay); this.feedbackNode.connect(this.wet); this.wet.connect(this.output); this.dry.connect(this.output); this.delayTime = properties.delayTime || this.defaults.delayTime.value; this.feedback = properties.feedback || this.defaults.feedback.value; this.wetLevel = properties.wetLevel || this.defaults.wetLevel.value; this.dryLevel = properties.dryLevel || this.defaults.dryLevel.value; this.cutoff = properties.cutoff || this.defaults.cutoff.value; this.filter.type = this.filter.LOWPASS; this.bypass = properties.bypass || false; }; Tuna.prototype.Delay.prototype = Object.create(Super, { name: { value: "Delay" }, defaults: { writable:true, value: { delayTime: { value: 100, min: 20, max: 1000, automatable: false, }, feedback: { value: 0.45, min: 0, max: 0.9, automatable: true }, cutoff: { value: 20000, min: 20, max: 20000, automatable: true }, wetLevel: { value: 0.5, min: 0, max: 1, automatable: true }, dryLevel: { value: 1, min: 0, max: 1, automatable: true } } }, delayTime: { enumerable: true, get: function () { return this.delay.delayTime; }, set: function (value) { this.delay.delayTime.value = value / 1000; } }, wetLevel: { enumerable: true, get: function () { return this.wet.gain; }, set: function (value) { this.wet.gain.value = value; } }, dryLevel: { enumerable: true, get: function () { return this.dry.gain; }, set: function (value) { this.dry.gain.value = value; } }, feedback: { enumerable: true, get: function () { return this.feedbackNode.gain; }, set: function (value) { this.feedbackNode.gain.value = value; } }, cutoff: { enumerable: true, get: function () { return this.filter.frequency; }, set: function (value) { this.filter.frequency.value = value; } } }); Tuna.prototype.MoogFilter = function (properties) { if (!properties) { properties = this.getDefaults(); } this.bufferSize = properties.bufferSize || this.defaults.bufferSize.value; this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.processor = userContext.createScriptProcessor(this.bufferSize, 1, 1); this.output = userContext.createGain(); this.activateNode.connect(this.processor); this.processor.connect(this.output); var in1, in2, in3, in4, out1, out2, out3, out4; in1 = in2 = in3 = in4 = out1 = out2 = out3 = out4 = 0.0; this.processor.onaudioprocess = function (e) { var input = e.inputBuffer.getChannelData(0), output = e.outputBuffer.getChannelData(0), f = this.cutoff * 1.16, fb = this.resonance * (1.0 - 0.15 * f * f); for(var i = 0; i < input.length; i++) { input[i] -= out4 * fb; input[i] *= 0.35013 * (f*f)*(f*f); out1 = input[i] + 0.3 * in1 + (1 - f) * out1; // Pole 1 in1 = input[i]; out2 = out1 + 0.3 * in2 + (1 - f) * out2; // Pole 2 in2 = out1; out3 = out2 + 0.3 * in3 + (1 - f) * out3; // Pole 3 in3 = out2; out4 = out3 + 0.3 * in4 + (1 - f) * out4; // Pole 4 in4 = out3; output[i] = out4; } }; this.cutoff = properties.cutoff || this.defaults.cutoff.value; this.resonance = properties.resonance || this.defaults.resonance.value; this.bypass = properties.bypass || false; }; Tuna.prototype.MoogFilter.prototype = Object.create(Super, { name: { value: "MoogFilter" }, defaults: { writable: true, value: { bufferSize: { value: 4096, min: 256, max: 16384, automatable: false, type: INT }, bypass: { value: false, automatable: false, type: BOOLEAN }, cutoff: { value: 0.065, min: 0.0001, max: 1.0, automatable: false, }, resonance: { value: 3.5, min: 0.0, max: 4.0, automatable: false, } } }, cutoff: { enumerable: true, get: function () { return this.processor.cutoff; }, set: function (value) { this.processor.cutoff = value; } }, resonance: { enumerable: true, get: function () { return this.processor.resonance; }, set: function (value) { this.processor.resonance = value; } } }); Tuna.prototype.Overdrive = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.inputDrive = userContext.createGain(); this.waveshaper = userContext.createWaveShaper(); this.outputDrive = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.inputDrive); this.inputDrive.connect(this.waveshaper); this.waveshaper.connect(this.outputDrive); this.outputDrive.connect(this.output); this.ws_table = new Float32Array(this.k_nSamples); this.drive = properties.drive || this.defaults.drive.value; this.outputGain = properties.outputGain || this.defaults.outputGain.value; this.curveAmount = properties.curveAmount || this.defaults.curveAmount.value; this.algorithmIndex = properties.algorithmIndex || this.defaults.algorithmIndex.value; this.bypass = properties.bypass || false; }; Tuna.prototype.Overdrive.prototype = Object.create(Super, { name: { value: "Overdrive" }, defaults: { writable:true, value: { drive: { value: 1, min: 0, max: 1, automatable: true, type: FLOAT, scaled: true }, outputGain: { value: 1, min: 0, max: 1, automatable: true, type: FLOAT, scaled: true }, curveAmount: { value: 0.725, min: 0, max: 1, automatable: false, }, algorithmIndex: { value: 0, min: 0, max: 5, automatable: false, type: INT } } }, k_nSamples: { value: 8192 }, drive: { get: function () { return this.inputDrive.gain; }, set: function (value) { this._drive = value; } }, curveAmount: { get: function () { return this._curveAmount; }, set: function (value) { this._curveAmount = value; if (this._algorithmIndex === undefined) { this._algorithmIndex = 0; } this.waveshaperAlgorithms[this._algorithmIndex](this._curveAmount, this.k_nSamples, this.ws_table); this.waveshaper.curve = this.ws_table; } }, outputGain: { get: function () { return this.outputDrive.gain; }, set: function (value) { this._outputGain = dbToWAVolume(value); } }, algorithmIndex: { get: function () { return this._algorithmIndex; }, set: function (value) { this._algorithmIndex = value>>0; this.curveAmount = this._curveAmount; } }, waveshaperAlgorithms: { value: [ function (amount, n_samples, ws_table) { amount = Math.min(amount, 0.9999); var k = 2 * amount / (1 - amount), i, x; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; ws_table[i] = (1 + k) * x / (1 + k * Math.abs(x)); } }, function (amount, n_samples, ws_table) { var i, x, y; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; y = ((0.5 * Math.pow((x + 1.4), 2)) - 1) * y >= 0 ? 5.8 : 1.2; ws_table[i] = tanh(y); } }, function (amount, n_samples, ws_table) { var i, x, y, a = 1 - amount; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; y = x < 0 ? -Math.pow(Math.abs(x), a + 0.04) : Math.pow(x, a); ws_table[i] = tanh(y * 2); } }, function (amount, n_samples, ws_table) { var i, x, y, abx, a = 1 - amount > 0.99 ? 0.99 : 1 - amount; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; abx = Math.abs(x); if (abx < a) y = abx; else if (abx > a) y = a + (abx - a) / (1 + Math.pow((abx - a) / (1 - a), 2)); else if (abx > 1) y = abx; ws_table[i] = sign(x) * y * (1 / ((a + 1) / 2)); } }, function (amount, n_samples, ws_table) { // fixed curve, amount doesn't do anything, the distortion is just from the drive var i, x; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; if (x < -0.08905) { ws_table[i] = (-3 / 4) * (1 - (Math.pow((1 - (Math.abs(x) - 0.032857)), 12)) + (1 / 3) * (Math.abs(x) - 0.032847)) + 0.01; } else if (x >= -0.08905 && x < 0.320018) { ws_table[i] = (-6.153 * (x * x)) + 3.9375 * x; } else { ws_table[i] = 0.630035; } } }, function (amount, n_samples, ws_table) { var a = 2 + Math.round(amount * 14), // we go from 2 to 16 bits, keep in mind for the UI bits = Math.round(Math.pow(2, a - 1)), // real number of quantization steps divided by 2 i, x; for(i = 0; i < n_samples; i++) { x = i * 2 / n_samples - 1; ws_table[i] = Math.round(x * bits) / bits; } }] } }); Tuna.prototype.Phaser = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.splitter = this.activateNode = userContext.createChannelSplitter(2); this.filtersL = []; this.filtersR = []; this.feedbackGainNodeL = userContext.createGain(); this.feedbackGainNodeR = userContext.createGain(); this.merger = userContext.createChannelMerger(2); this.filteredSignal = userContext.createGain(); this.output = userContext.createGain(); this.lfoL = new userInstance.LFO({ target: this.filtersL, callback: this.callback }); this.lfoR = new userInstance.LFO({ target: this.filtersR, callback: this.callback }); var i = this.stage; while(i--) { this.filtersL[i] = userContext.createBiquadFilter(); this.filtersR[i] = userContext.createBiquadFilter(); this.filtersL[i].type = this.filtersL[i].ALLPASS; this.filtersR[i].type = this.filtersR[i].ALLPASS; } this.input.connect(this.splitter); this.input.connect(this.output); this.splitter.connect(this.filtersL[0], 0, 0); this.splitter.connect(this.filtersR[0], 1, 0); this.connectInOrder(this.filtersL); this.connectInOrder(this.filtersR); this.filtersL[this.stage - 1].connect(this.feedbackGainNodeL); this.filtersL[this.stage - 1].connect(this.merger, 0, 0); this.filtersR[this.stage - 1].connect(this.feedbackGainNodeR); this.filtersR[this.stage - 1].connect(this.merger, 0, 1); this.feedbackGainNodeL.connect(this.filtersL[0]); this.feedbackGainNodeR.connect(this.filtersR[0]); this.merger.connect(this.output); this.rate = properties.rate || this.defaults.rate.value; this.baseModulationFrequency = properties.baseModulationFrequency || this.defaults.baseModulationFrequency.value; this.depth = properties.depth || this.defaults.depth.value; this.feedback = properties.feedback || this.defaults.feedback.value; this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value; this.lfoL.activate(true); this.lfoR.activate(true); this.bypass = properties.bypass || false; }; Tuna.prototype.Phaser.prototype = Object.create(Super, { name: { value: "Phaser" }, stage: { value: 4 }, defaults: { writable:true, value: { rate: { value: 0.1, min: 0, max: 8, automatable: false, }, depth: { value: 0.6, min: 0, max: 1, automatable: false, }, feedback: { value: 0.7, min: 0, max: 1, automatable: false, }, stereoPhase: { value: 40, min: 0, max: 180, automatable: false, }, baseModulationFrequency: { value: 700, min: 500, max: 1500, automatable: false, } } }, callback: { value: function (filters, value) { for(var stage = 0; stage < 4; stage++) { filters[stage].frequency.value = value; } } }, depth: { get: function () { return this._depth; }, set: function (value) { this._depth = value; this.lfoL.oscillation = this._baseModulationFrequency * this._depth; this.lfoR.oscillation = this._baseModulationFrequency * this._depth; } }, rate: { get: function () { return this._rate; }, set: function (value) { this._rate = value; this.lfoL.frequency = this._rate; this.lfoR.frequency = this._rate; } }, baseModulationFrequency: { enumerable: true, get: function () { return this._baseModulationFrequency; }, set: function (value) { this._baseModulationFrequency = value; this.lfoL.offset = this._baseModulationFrequency; this.lfoR.offset = this._baseModulationFrequency; this._depth = this._depth; } }, feedback: { get: function () { return this._feedback; }, set: function (value) { this._feedback = value; this.feedbackGainNodeL.gain.value = this._feedback; this.feedbackGainNodeR.gain.value = this._feedback; } }, stereoPhase: { get: function () { return this._stereoPhase; }, set: function (value) { this._stereoPhase = value; var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180; newPhase = fmod(newPhase, 2 * Math.PI); this.lfoR._phase = newPhase; } } }); Tuna.prototype.Tremolo = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.splitter = this.activateNode = userContext.createChannelSplitter(2), this.amplitudeL = userContext.createGain(), this.amplitudeR = userContext.createGain(), this.merger = userContext.createChannelMerger(2), this.output = userContext.createGain(); this.lfoL = new userInstance.LFO({ target: this.amplitudeL.gain, callback: pipe }); this.lfoR = new userInstance.LFO({ target: this.amplitudeR.gain, callback: pipe }); this.input.connect(this.splitter); this.splitter.connect(this.amplitudeL, 0); this.splitter.connect(this.amplitudeR, 1); this.amplitudeL.connect(this.merger, 0, 0); this.amplitudeR.connect(this.merger, 0, 1); this.merger.connect(this.output); this.rate = properties.rate || this.defaults.rate.value; this.intensity = properties.intensity || this.defaults.intensity.value; this.stereoPhase = properties.stereoPhase || this.defaults.stereoPhase.value; this.lfoL.offset = 1 - (this.intensity / 2); this.lfoR.offset = 1 - (this.intensity / 2); this.lfoL.phase = this.stereoPhase * Math.PI / 180; this.lfoL.activate(true); this.lfoR.activate(true); this.bypass = properties.bypass || false; }; Tuna.prototype.Tremolo.prototype = Object.create(Super, { name: { value: "Tremolo" }, defaults: { writable:true, value: { intensity: { value: 0.3, min: 0, max: 1, automatable: false, }, stereoPhase: { value: 0, min: 0, max: 180, automatable: false, }, rate: { value: 5, min: 0.1, max: 11, automatable: false, } } }, intensity: { enumerable: true, get: function () { return this._intensity; }, set: function (value) { this._intensity = value; this.lfoL.offset = 1 - this._intensity / 2; this.lfoR.offset = 1 - this._intensity / 2; this.lfoL.oscillation = this._intensity; this.lfoR.oscillation = this._intensity; } }, rate: { enumerable: true, get: function () { return this._rate; }, set: function (value) { this._rate = value; this.lfoL.frequency = this._rate; this.lfoR.frequency = this._rate; } }, stereoPhase: { enumerable: true, get: function () { return this._rate; }, set: function (value) { this._stereoPhase = value; var newPhase = this.lfoL._phase + this._stereoPhase * Math.PI / 180; newPhase = fmod(newPhase, 2 * Math.PI); this.lfoR.phase = newPhase; } } }); Tuna.prototype.WahWah = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.envelopeFollower = new userInstance.EnvelopeFollower({ target: this, callback: function (context, value) { context.sweep = value; } }); this.filterBp = userContext.createBiquadFilter(); this.filterPeaking = userContext.createBiquadFilter(); this.output = userContext.createGain(); //Connect AudioNodes this.activateNode.connect(this.filterBp); this.filterBp.connect(this.filterPeaking); this.filterPeaking.connect(this.output); //Set Properties this.init(); this.automode = properties.enableAutoMode || this.defaults.automode.value; this.resonance = properties.resonance || this.defaults.resonance.value; this.sensitivity = properties.sensitivity || this.defaults.sensitivity.value; this.baseFrequency = properties.baseFrequency || this.defaults.baseFrequency.value; this.excursionOctaves = properties.excursionOctaves || this.defaults.excursionOctaves.value; this.sweep = properties.sweep || this.defaults.sweep.value; this.activateNode.gain.value = 2; this.envelopeFollower.activate(true); this.bypass = properties.bypass || false; }; Tuna.prototype.WahWah.prototype = Object.create(Super, { name: { value: "WahWah" }, defaults: { writable:true, value: { automode: { value: true, automatable: false, type: BOOLEAN }, baseFrequency: { value: 0.5, min: 0, max: 1, automatable: false, }, excursionOctaves: { value: 2, min: 1, max: 6, automatable: false, }, sweep: { value: 0.2, min: 0, max: 1, automatable: false, }, resonance: { value: 10, min: 1, max: 100, automatable: false, }, sensitivity: { value: 0.5, min: -1, max: 1, automatable: false, } } }, activateCallback: { value: function (value) { this.automode = value; } }, automode: { get: function () { return this._automode; }, set: function (value) { this._automode = value; if (value) { this.activateNode.connect(this.envelopeFollower.input); this.envelopeFollower.activate(true); } else { this.envelopeFollower.activate(false); this.activateNode.disconnect(); this.activateNode.connect(this.filterBp); } } }, sweep: { enumerable: true, get: function () { return this._sweep.value; }, set: function (value) { this._sweep = Math.pow(value > 1 ? 1 : value < 0 ? 0 : value, this._sensitivity); this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; } }, baseFrequency: { enumerable: true, get: function () { return this._baseFrequency; }, set: function (value) { this._baseFrequency = 50 * Math.pow(10, value * 2); this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves)); this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; } }, excursionOctaves: { enumerable: true, get: function () { return this._excursionOctaves; }, set: function (value) { this._excursionOctaves = value; this._excursionFrequency = Math.min(this.sampleRate / 2, this.baseFrequency * Math.pow(2, this._excursionOctaves)); this.filterBp.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; this.filterPeaking.frequency.value = this._baseFrequency + this._excursionFrequency * this._sweep; } }, sensitivity: { enumerable: true, get: function () { return this._sensitivity; }, set: function (value) { this._sensitivity = Math.pow(10, value); } }, resonance: { enumerable: true, get: function () { return this._resonance; }, set: function (value) { this._resonance = value; this.filterPeaking.Q = this._resonance; } }, init: { value: function () { this.output.gain.value = 1; this.filterPeaking.type = 5; this.filterBp.type = 2; this.filterPeaking.frequency.value = 100; this.filterPeaking.gain.value = 20; this.filterPeaking.Q.value = 5; this.filterBp.frequency.value = 100; this.filterBp.Q.value = 1; this.sampleRate = userContext.sampleRate; } } }); Tuna.prototype.EnvelopeFollower = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.jsNode = this.output = userContext.createScriptProcessor(this.buffersize, 1, 1); this.input.connect(this.output); this.attackTime = properties.attackTime || this.defaults.attackTime.value; this.releaseTime = properties.releaseTime || this.defaults.releaseTime.value; this._envelope = 0; this.target = properties.target || {}; this.callback = properties.callback || function () {}; }; Tuna.prototype.EnvelopeFollower.prototype = Object.create(Super, { name: { value: "EnvelopeFollower" }, defaults: { value: { attackTime: { value: 0.003, min: 0, max: 0.5, automatable: false, }, releaseTime: { value: 0.5, min: 0, max: 0.5, automatable: false, } } }, buffersize: { value: 256 }, envelope: { value: 0 }, sampleRate: { value: 44100 }, attackTime: { enumerable: true, get: function () { return this._attackTime; }, set: function (value) { this._attackTime = value; this._attackC = Math.exp(-1 / this._attackTime * this.sampleRate / this.buffersize); } }, releaseTime: { enumerable: true, get: function () { return this._releaseTime; }, set: function (value) { this._releaseTime = value; this._releaseC = Math.exp(-1 / this._releaseTime * this.sampleRate / this.buffersize); } }, callback: { get: function () { return this._callback; }, set: function (value) { if (typeof value === "function") { this._callback = value; } else { console.error("tuna.js: " + this.name + ": Callback must be a function!"); } } }, target: { get: function () { return this._target; }, set: function (value) { this._target = value; } }, activate: { value: function (doActivate) { this.activated = doActivate; if (doActivate) { this.jsNode.connect(userContext.destination); this.jsNode.onaudioprocess = this.returnCompute(this); } else { this.jsNode.disconnect(); this.jsNode.onaudioprocess = null; } } }, returnCompute: { value: function (instance) { return function (event) { instance.compute(event); }; } }, compute: { value: function (event) { var count = event.inputBuffer.getChannelData(0).length, channels = event.inputBuffer.numberOfChannels, current, chan, rms, i; chan = rms = i = 0; if (channels > 1) { //need to mixdown for(i = 0; i < count; ++i) { for(; chan < channels; ++chan) { current = event.inputBuffer.getChannelData(chan)[i]; rms += (current * current) / channels; } } } else { for(i = 0; i < count; ++i) { current = event.inputBuffer.getChannelData(0)[i]; rms += (current * current); } } rms = Math.sqrt(rms); if (this._envelope < rms) { this._envelope *= this._attackC; this._envelope += (1 - this._attackC) * rms; } else { this._envelope *= this._releaseC; this._envelope += (1 - this._releaseC) * rms; } this._callback(this._target, this._envelope); } } }); // Low-frequency oscillation Tuna.prototype.LFO = function (properties) { //Instantiate AudioNode this.output = userContext.createScriptProcessor(256, 1, 1); this.activateNode = userContext.destination; //Set Properties this.frequency = properties.frequency || this.defaults.frequency.value; this.offset = properties.offset || this.defaults.offset.value; this.oscillation = properties.oscillation || this.defaults.oscillation.value; this.phase = properties.phase || this.defaults.phase.value; this.target = properties.target || {}; this.output.onaudioprocess = this.callback(properties.callback || function () {}); this.bypass = properties.bypass || false; }; Tuna.prototype.LFO.prototype = Object.create(Super, { name: { value: "LFO" }, bufferSize: { value: 256 }, sampleRate: { value: 44100 }, defaults: { value: { frequency: { value: 1, min: 0, max: 20, automatable: false, }, offset: { value: 0.85, min: 0, max: 22049, automatable: false, }, oscillation: { value: 0.3, min: -22050, max: 22050, automatable: false, }, phase: { value: 0, min: 0, max: 2 * Math.PI, automatable: false, } } }, frequency: { get: function () { return this._frequency; }, set: function (value) { this._frequency = value; this._phaseInc = 2 * Math.PI * this._frequency * this.bufferSize / this.sampleRate; } }, offset: { get: function () { return this._offset; }, set: function (value) { this._offset = value; } }, oscillation: { get: function () { return this._oscillation; }, set: function (value) { this._oscillation = value; } }, phase: { get: function () { return this._phase; }, set: function (value) { this._phase = value; } }, target: { get: function () { return this._target; }, set: function (value) { this._target = value; } }, activate: { value: function (doActivate) { if (!doActivate) { this.output.disconnect(userContext.destination); } else { this.output.connect(userContext.destination); } } }, callback: { value: function (callback) { var that = this; return function () { that._phase += that._phaseInc; if (that._phase > 2 * Math.PI) { that._phase = 0; } callback(that._target, that._offset + that._oscillation * Math.sin(that._phase)); }; } } }); /* Panner ------------------------------------------------*/ Tuna.prototype.Panner = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.output = userContext.createGain(); this.panner = userContext.createPanner(); this.activateNode.connect(this.panner); this.panner.connect(this.output); this.bypass = properties.bypass || false; this.x = properties.x || 0; this.y = properties.y || 1; this.z = properties.z || 1; this.panningModel = properties.panningModel || 0; this.distanceModel = properties.distanceModel || 0; }; var PannerPosition = function(type) { return { enumerable: true, get: function () { return this["_" + type]; }, set: function (value) { this["_" + type] = value; this.panner.setPosition(this._x || 0, this._y || 0, this._z || 0); } } }; var PannerModel = function(type) { return { enumerable: true, get: function () { return this["_" + type]; }, set: function (value) { this["_" + type] = value; this.panner[type] = value; } } }; var Clamp = function(value, min, max, automatable) { return { value: value, min: min, max: max, automatable: automatable }; }; Tuna.prototype.Panner.prototype = Object.create(Super, { name: { value: "Panner" }, defaults: { writable:true, value: { x: Clamp(0, -20, 20, false), y: Clamp(0, -20, 20, false), z: Clamp(0, -20, 20, false), distanceModel: Clamp(0, 0, 2, false), panningModel: Clamp(0, 0, 2, false) } }, x: PannerPosition("x"), y: PannerPosition("y"), z: PannerPosition("z"), panningModel: PannerModel("panningModel"), distanceModel: PannerModel("distanceModel") }); /* Volume ------------------------------------------------*/ Tuna.prototype.Volume = function (properties) { if (!properties) { properties = this.getDefaults(); } this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.output); this.bypass = properties.bypass || false; this.amount = properties.amount || this.defaults.amount.value; }; Tuna.prototype.Volume.prototype = Object.create(Super, { name: { value: "Volume" }, defaults: { writable:true, value: { amount: Clamp(0, 0, 2, false) } }, amount: { enumerable: true, get: function () { return this._volume; }, set: function (value) { this._volume = value; this.activateNode.gain.value = value; } } }); /* Frequency ------------------------------------------------*/ Tuna.prototype.Frequency = function (properties) { if (!properties) { properties = this.getDefaults(); } this.trebleFilter = userContext.createBiquadFilter(); this.trebleFilter.type = this.trebleFilter.HIGHSHELF; this.trebleFilter.frequency.value = 8000; // 1k+ this.trebleFilter.Q.value = 0; this.midtoneFilter = userContext.createBiquadFilter(); this.midtoneFilter.type = this.midtoneFilter.PEAKING; this.midtoneFilter.frequency.value = 1000; // 200-1k this.midtoneFilter.Q.value = 0; this.bassFilter = userContext.createBiquadFilter(); this.bassFilter.type = this.bassFilter.LOWSHELF; this.bassFilter.frequency.value = 200; // 60-200k this.bassFilter.Q.value = 0; this.input = userContext.createGain(); this.activateNode = userContext.createGain(); this.output = userContext.createGain(); this.activateNode.connect(this.bassFilter); this.bassFilter.connect(this.midtoneFilter); this.midtoneFilter.connect(this.trebleFilter); this.trebleFilter.connect(this.output); this.bypass = properties.bypass || false; this.volume = properties.volume || false; this.treble = properties.treble || false; this.midtone = properties.midtone || false; this.bass = properties.bass || false; }; var GainValue = function(type, nodeId) { return { enumerable: true, get: function () { return this["_" + type]; }, set: function (value) { this["_" + type] = value; this[nodeId || type + "Filter"].gain.value = value; } }; }; Tuna.prototype.Frequency.prototype = Object.create(Super, { name: { value: "Frequency" }, defaults: { writable:true, value: { volume: Clamp(1, 0, 2, false), treble: Clamp(0, -20, 20, false), midtone: Clamp(0, -20, 20, false), bass: Clamp(0, -20, 20, false) } }, volume: GainValue("volume", "activateNode"), treble: GainValue("treble"), midtone: GainValue("midtone"), bass: GainValue("bass") }); if (typeof define === "function") { define("Tuna", [], function () { return Tuna; }); } else { window.Tuna = Tuna; } })(this);