UNPKG

crossbrowdy

Version:

A Multimedia JavaScript framework to create real cross-platform and hybrid game engines, games, emulators, multimedia libraries and apps.

1,115 lines (958 loc) 28 kB
var _jsfxInitializer = function(jsfx) //CrossBrowdy modification to be able to initialize the library after an user-driven event (as onClick, etc.). { 'use strict'; var chr = String.fromCharCode; var TAU = +Math.PI*2; var bitsPerSample = 16|0; var numChannels = 1|0; var sin = Math.sin; var pow = Math.pow; var abs = Math.abs; var EPSILON = 0.000001; jsfx.SampleRate = 0|0; jsfx.Sec = 0|0; jsfx.SetSampleRate = function(sampleRate){ jsfx.SampleRate = sampleRate|0; jsfx.Sec = sampleRate|0; }; jsfx.SetSampleRate(getDefaultSampleRate()); // MAIN API // Creates a new Audio object based on the params // params can be a params generating function or the actual parameters jsfx.Sound = function(params){ var processor = new Processor(params, jsfx.DefaultModules); var block = createFloatArray(processor.getSamplesLeft()); processor.generate(block); return CreateAudio(block); }; // Same as Sounds, but avoids locking the browser for too long // in case you have a large amount of sounds to generate jsfx.Sounds = function(library, ondone, onprogress){ var audio = {}; var player = {}; player._audio = audio; var toLoad = []; // create playing functions map_object(library, function(_, name){ player[name] = function(){ if(typeof audio[name] !== "undefined"){ audio[name].currentTime = 0.0; audio[name].play(); } }; toLoad.push(name); }); var loaded = 0, total = toLoad.length; function next(){ if(toLoad.length == 0){ ondone && ondone(sounds); return; } var name = toLoad.shift(); audio[name] = jsfx.Sound(library[name]); loaded++; onprogress && onprogress(name, loaded, total); window.setTimeout(next, 30); } next(); return player; } // SoundsImmediate takes a named set of params, and generates multiple // sound objects at once. jsfx.SoundsImmediate = function(library){ var audio = {}; var player = {}; player._audio = audio; map_object(library, function(params, name){ audio[name] = jsfx.Sound(params); player[name] = function(){ if(typeof audio[name] !== "undefined"){ audio[name].currentTime = 0.0; audio[name].play(); } }; }) return player; }; if(typeof AudioContext !== "undefined"){ // Node creates a new AudioContext ScriptProcessor that outputs the // sound. It will automatically disconnect, unless otherwise specified. jsfx.Node = function(audioContext, params, modules, bufferSize, stayConnected){ var node = audioContext.createScriptProcessor(bufferSize, 0, 1); var gen = new Processor(params, modules || jsfx.DefaultModules); node.onaudioprocess = function(ev){ var block = ev.outputBuffer.getChannelData(0); gen.generate(block); if(!stayConnected && gen.finished){ // we need to do an async disconnect, otherwise Chrome may // glitch setTimeout(function(){ node.disconnect(); }, 30); } } return node; } // Live creates an managed AudioContext for playing. // This is useful, when you want to use procedurally generated sounds. jsfx.Live = function(library, modules, BufferSize){ //TODO: add limit for number of notes played at the same time BufferSize = BufferSize || 2048; var player = {}; var context = new AudioContext(); var volume = context.createGain(); volume.connect(context.destination); player._context = context; player._volume = volume; map_object(library, function(params, name){ player[name] = function(){ var node = jsfx.Node(context, params, modules, BufferSize); node.connect(volume); }; }); player._close = function(){ context.close(); }; player._play = function(params){ var node = jsfx.Node(context, params, modules, BufferSize); node.connect(volume); }; return player; } } else { jsfx.Live = jsfx.Sounds; } // SOUND GENERATION jsfx.Module = {}; // generators jsfx.G = {}; var stage = jsfx.stage = { PhaseSpeed : 0, PhaseSpeedMod : 10, Generator : 20, SampleMod : 30, Volume : 40 }; function byStage(a,b){ return a.stage - b.stage; } jsfx.InitDefaultParams = InitDefaultParams; function InitDefaultParams(params, modules){ // setup modules for(var i = 0; i < modules.length; i += 1){ var M = modules[i]; var P = params[M.name] || {}; // add missing parameters map_object(M.params, function(def, name){ if(typeof P[name] === 'undefined'){ P[name] = def.D; } }); params[M.name] = P; } } // Generates a stateful sound effect processor // params can be a function that creates a parameter set jsfx.Processor = Processor; function Processor(params, modules){ params = params || {}; modules = modules || jsfx.DefaultModules; if(typeof params === 'function'){ params = params(); } else { params = JSON.parse(JSON.stringify(params)) } this.finished = false; this.state = { SampleRate: params.SampleRate || jsfx.SampleRate }; // sort modules modules = modules.slice(); modules.sort(byStage) this.modules = modules; // init missing params InitDefaultParams(params, modules); // setup modules for(var i = 0; i < this.modules.length; i += 1){ var M = this.modules[i]; this.modules[i].setup(this.state, params[M.name]); } } Processor.prototype = { //TODO: see whether this can be converted to a module generate: function(block){ for(var i = 0|0; i < block.length; i += 1){ block[i] = 0; } if(this.finished){ return; } var $ = this.state, N = block.length|0; for(var i = 0; i < this.modules.length; i += 1){ var M = this.modules[i]; var n = M.process($, block.subarray(0,N))|0; N = Math.min(N, n); } if(N < block.length){ this.finished = true; } for(var i = N; i < block.length; i++){ block[i] = 0; } }, getSamplesLeft: function(){ var samples = 0; for(var i = 0; i < this.state.envelopes.length; i += 1){ samples += this.state.envelopes[i].N; } if(samples === 0){ samples = 3*this.state.SampleRate; } return samples; } }; // Frequency jsfx.Module.Frequency = { name: 'Frequency', params: { Start: { L:30, H:1800, D:440 }, Min: { L:30, H:1800, D:30 }, Max: { L:30, H:1800, D:1800 }, Slide: { L:-1, H:1, D:0 }, DeltaSlide: { L:-1, H:1, D:0 }, RepeatSpeed: { L:0, H: 3.0, D: 0 }, ChangeAmount: { L:-12, H:12, D:0 }, ChangeSpeed : { L: 0, H:1, D:0 } }, stage: stage.PhaseSpeed, setup: function($, P){ var SR = $.SampleRate; $.phaseParams = P; $.phaseSpeed = P.Start * TAU / SR; $.phaseSpeedMax = P.Max * TAU / SR; $.phaseSpeedMin = P.Min * TAU / SR; $.phaseSpeedMin = Math.min($.phaseSpeedMin, $.phaseSpeed); $.phaseSpeedMax = Math.max($.phaseSpeedMax, $.phaseSpeed); $.phaseSlide = 1.0 + pow(P.Slide, 3.0) * 64.0 / SR; $.phaseDeltaSlide = pow(P.DeltaSlide, 3.0) / (SR * 1000); $.repeatTime = 0; $.repeatLimit = Infinity; if(P.RepeatSpeed > 0){ $.repeatLimit = P.RepeatSpeed * SR; } $.arpeggiatorTime = 0; $.arpeggiatorLimit = P.ChangeSpeed * SR; if(P.ChangeAmount == 0){ $.arpeggiatorLimit = Infinity; } $.arpeggiatorMod = 1 + P.ChangeAmount / 12.0; }, process: function($, block){ var speed = +$.phaseSpeed, min = +$.phaseSpeedMin, max = +$.phaseSpeedMax, slide = +$.phaseSlide, deltaSlide = +$.phaseDeltaSlide; var repeatTime = $.repeatTime, repeatLimit = $.repeatLimit; var arpTime = $.arpeggiatorTime, arpLimit = $.arpeggiatorLimit, arpMod = $.arpeggiatorMod; for(var i = 0; i < block.length; i++){ slide += deltaSlide; speed *= slide; speed = speed < min ? min : speed > max ? max : speed; if(repeatTime > repeatLimit){ this.setup($, $.phaseParams); return i + this.process($, block.subarray(i)) - 1; } repeatTime++; if(arpTime > arpLimit){ speed *= arpMod; arpTime = 0; arpLimit = Infinity; } arpTime++; block[i] += speed; } $.repeatTime = repeatTime; $.arpeggiatorTime = arpTime; $.arpeggiatorLimit = arpLimit; $.phaseSpeed = speed; $.phaseSlide = slide; return block.length; } }; // Vibrato jsfx.Module.Vibrato = { name: 'Vibrato', params: { Depth: {L: 0, H:1, D:0}, DepthSlide: {L:-1, H:1, D:0}, Frequency: {L: 0.01, H:48, D:0}, FrequencySlide: {L: -1.00, H: 1, D:0} }, stage: stage.PhaseSpeedMod, setup: function($, P){ var SR = $.SampleRate; $.vibratoPhase = 0; $.vibratoDepth = P.Depth; $.vibratoPhaseSpeed = P.Frequency * TAU / SR; $.vibratoPhaseSpeedSlide = 1.0 + pow(P.FrequencySlide, 3.0) * 3.0 / SR; $.vibratoDepthSlide = P.DepthSlide / SR; }, process: function($, block){ var phase = +$.vibratoPhase, depth = +$.vibratoDepth, speed = +$.vibratoPhaseSpeed, slide = +$.vibratoPhaseSpeedSlide, depthSlide = +$.vibratoDepthSlide; if((depth == 0) && (depthSlide <= 0)){ return block.length; } for(var i = 0; i < block.length; i++){ phase += speed; if(phase > TAU){phase -= TAU}; block[i] += block[i] * sin(phase) * depth; speed *= slide; depth += depthSlide; depth = clamp1(depth); } $.vibratoPhase = phase; $.vibratoDepth = depth; $.vibratoPhaseSpeed = speed; return block.length; } }; // Generator jsfx.Module.Generator = { name: 'Generator', params: { // C = choose Func: {C: jsfx.G, D:'square'}, A: {L: 0, H: 1, D: 0}, B: {L: 0, H: 1, D: 0}, ASlide: {L: -1, H: 1, D: 0}, BSlide: {L: -1, H: 1, D: 0} }, stage: stage.Generator, setup: function($, P){ $.generatorPhase = 0; if(typeof P.Func === 'string'){ $.generator = jsfx.G[P.Func]; } else { $.generator = P.Func; } if(typeof $.generator === 'object'){ $.generator = $.generator.create(); } assert(typeof $.generator === 'function', 'generator must be a function') $.generatorA = P.A; $.generatorASlide = P.ASlide; $.generatorB = P.B; $.generatorBSlide = P.BSlide; }, process: function($, block){ return $.generator($, block); } }; // Karplus Strong algorithm for string sound var GuitarBufferSize = 1 << 16; jsfx.Module.Guitar = { name: 'Guitar', params: { A: {L:0.0, H:1.0, D: 1}, B: {L:0.0, H:1.0, D: 1}, C: {L:0.0, H:1.0, D: 1}, }, stage: stage.Generator, setup: function($, P){ $.guitarA = P.A; $.guitarB = P.B; $.guitarC = P.C; $.guitarBuffer = createFloatArray(GuitarBufferSize); $.guitarHead = 0; var B = $.guitarBuffer; for(var i = 0; i < B.length; i++){ B[i] = Math.random()*2 - 1; } }, process: function($, block){ var BS = GuitarBufferSize, BM = BS - 1; var A = +$.guitarA, B = +$.guitarB, C = +$.guitarC; var T = A + B + C; var h = $.guitarHead; var buffer = $.guitarBuffer; for(var i = 0; i < block.length; i++){ // buffer size var n = (TAU / block[i])|0; n = n > BS ? BS : n; // tail var t = ((h - n) + BS) & BM; buffer[h] = (buffer[(t-0+BS)&BM]*A + buffer[(t-1+BS)&BM]*B + buffer[(t-2+BS)&BM]*C) / T; block[i] = buffer[h]; h = (h + 1) & BM; } $.guitarHead = h; return block.length; } } // Low/High-Pass Filter jsfx.Module.Filter = { name: 'Filter', params: { LP: {L: 0, H:1, D:1}, LPSlide: {L:-1, H:1, D:0}, LPResonance: {L: 0, H:1, D:0}, HP: {L: 0, H:1, D:0}, HPSlide: {L:-1, H:1, D:0} }, stage: stage.SampleMod + 0, setup: function($, P){ $.FilterEnabled = (P.HP > EPSILON) || (P.LP < 1 - EPSILON); $.LPEnabled = P.LP < 1 - EPSILON; $.LP = pow(P.LP, 3.0) / 10; $.LPSlide = 1.0 + P.LPSlide * 100 / $.SampleRate; $.LPPos = 0; $.LPPosSlide = 0; $.LPDamping = 5.0 / (1.0 + pow(P.LPResonance, 2) * 20) * (0.01 + P.LP); $.LPDamping = 1.0 - Math.min($.LPDamping, 0.8); $.HP = pow(P.HP, 2.0) / 10; $.HPPos = 0; $.HPSlide = 1.0 + P.HPSlide * 100 / $.SampleRate; }, enabled: function($){ return $.FilterEnabled; }, process: function($, block){ if(!this.enabled($)){ return block.length; } var lp = +$.LP; var lpPos = +$.LPPos; var lpPosSlide = +$.LPPosSlide; var lpSlide = +$.LPSlide; var lpDamping = +$.LPDamping; var lpEnabled = +$.LPEnabled; var hp = +$.HP; var hpPos = +$.HPPos; var hpSlide = +$.HPSlide; for(var i = 0; i < block.length; i++){ if((hp > EPSILON) || (hp < -EPSILON)){ hp *= hpSlide; hp = hp < EPSILON ? EPSILON: hp > 0.1 ? 0.1 : hp; } var lpPos_ = lpPos; lp *= lpSlide; lp = lp < 0 ? lp = 0 : lp > 0.1 ? 0.1 : lp; var sample = block[i]; if(lpEnabled){ lpPosSlide += (sample - lpPos) * lp; lpPosSlide *= lpDamping; } else { lpPos = sample; lpPosSlide = 0; } lpPos += lpPosSlide; hpPos += lpPos - lpPos_; hpPos *= 1.0 - hp; block[i] = hpPos; } $.LPPos = lpPos; $.LPPosSlide = lpPosSlide; $.LP = lp; $.HP = hp; $.HPPos = hpPos; return block.length; } }; // Phaser Effect var PhaserBufferSize = 1 << 10; jsfx.Module.Phaser = { name: 'Phaser', params: { Offset: {L:-1, H:1, D:0}, Sweep: {L:-1, H:1, D:0} }, stage: stage.SampleMod + 1, setup: function($, P){ $.phaserBuffer = createFloatArray(PhaserBufferSize); $.phaserPos = 0; $.phaserOffset = pow(P.Offset, 2.0) * (PhaserBufferSize - 4); $.phaserOffsetSlide = pow(P.Sweep, 3.0) * 4000 / $.SampleRate; }, enabled: function($){ return (abs($.phaserOffsetSlide) > EPSILON) || (abs($.phaserOffset) > EPSILON); }, process: function($, block){ if(!this.enabled($)){ return block.length; } var BS = PhaserBufferSize, BM = BS - 1; var buffer = $.phaserBuffer, pos = $.phaserPos|0, offset = +$.phaserOffset, offsetSlide = +$.phaserOffsetSlide; for(var i = 0; i < block.length; i++){ offset += offsetSlide; //TODO: check whether this is correct if(offset < 0){ offset = -offset; offsetSlide = -offsetSlide; } if(offset > BM){ offset = BM; offsetSlide = 0; } buffer[pos] = block[i]; var p = (pos - (offset|0) + BS) & BM; block[i] += buffer[p]; pos = ((pos + 1) & BM)|0; } $.phaserPos = pos; $.phaserOffset = offset; return block.length; } }; // Volume dynamic control with Attack-Sustain-Decay // ATTACK | 0 - Volume + Punch // SUSTAIN | Volume + Punch - Volume // DECAY | Volume - 0 jsfx.Module.Volume = { name: 'Volume', params: { Master: { L: 0, H: 1, D: 0.5 }, Attack: { L: 0.001, H: 1, D: 0.01 }, Sustain: { L: 0, H: 2, D: 0.3 }, Punch: { L: 0, H: 3, D: 1.0 }, Decay: { L: 0.001, H: 2, D: 1.0 } }, stage: stage.Volume, setup: function($, P){ var SR = $.SampleRate; var V = P.Master; var VP = V * (1 + P.Punch); $.envelopes = [ // S = start volume, E = end volume, N = duration in samples {S: 0, E: V, N: (P.Attack * SR)|0 }, // Attack {S:VP, E: V, N: (P.Sustain * SR)|0 }, // Sustain {S: V, E: 0, N: (P.Decay * SR)|0 } // Decay ]; // G = volume gradient for(var i = 0; i < $.envelopes.length; i += 1){ var e = $.envelopes[i]; e.G = (e.E - e.S) / e.N; } }, process: function($, block){ var i = 0; while(($.envelopes.length > 0) && (i < block.length)){ var E = $.envelopes[0]; var vol = E.S, grad = E.G; var N = Math.min(block.length - i, E.N)|0; var end = (i+N)|0; for(; i < end; i += 1){ block[i] *= vol; vol += grad; vol = clamp(vol, 0, 10); } E.S = vol; E.N -= N; if(E.N <= 0){ $.envelopes.shift(); } } return i; } }; // PRESETS jsfx.DefaultModules = [ jsfx.Module.Frequency, jsfx.Module.Vibrato, jsfx.Module.Generator, jsfx.Module.Filter, jsfx.Module.Phaser, jsfx.Module.Volume ]; jsfx.DefaultModules.sort(byStage); jsfx.EmptyParams = EmptyParams; function EmptyParams(){ return map_object(jsfx.Module, function(){ return {} }); } jsfx._RemoveEmptyParams = RemoveEmptyParams; function RemoveEmptyParams(params){ for(var name in params){ if(Object_keys(params[name]).length == 0){ delete params[name]; } } }; jsfx.Preset = { Reset: function(){ return EmptyParams(); }, Coin: function(){ var p = EmptyParams(); p.Frequency.Start = runif(880, 660); p.Volume.Sustain = runif(0.1); p.Volume.Decay = runif(0.4, 0.1); p.Volume.Punch = runif(0.3, 0.3); if(runif() < 0.5){ p.Frequency.ChangeSpeed = runif(0.15, 0.1); p.Frequency.ChangeAmount = runif(8, 4); } RemoveEmptyParams(p); return p; }, Laser: function(){ var p = EmptyParams(); p.Generator.Func = rchoose(['square', 'saw', 'sine']); if(runif() < 0.33){ p.Frequency.Start = runif(880, 440); p.Frequency.Min = runif(0.1); p.Frequency.Slide = runif(0.3, -0.8); } else { p.Frequency.Start = runif(1200, 440); p.Frequency.Min = p.Frequency.Start - runif(880, 440); if(p.Frequency.Min < 110){ p.Frequency.Min = 110; } p.Frequency.Slide = runif(0.3, -1); } if(runif() < 0.5){ p.Generator.A = runif(0.5); p.Generator.ASlide = runif(0.2); } else { p.Generator.A = runif(0.5, 0.4); p.Generator.ASlide = runif(0.7); } p.Volume.Sustain = runif(0.2, 0.1); p.Volume.Decay = runif(0.4); if(runif() < 0.5){ p.Volume.Punch = runif(0.3); } if(runif() < 0.33){ p.Phaser.Offset = runif(0.2); p.Phaser.Sweep = runif(0.2); } if(runif() < 0.5){ p.Filter.HP = runif(0.3); } RemoveEmptyParams(p); return p; }, Explosion: function(){ var p = EmptyParams(); p.Generator.Func = 'noise'; if(runif() < 0.5){ p.Frequency.Start = runif(440, 40); p.Frequency.Slide = runif(0.4, -0.1); } else { p.Frequency.Start = runif(1600, 220); p.Frequency.Slide = runif(-0.2, -0.2); } if(runif() < 0.2){ p.Frequency.Slide = 0; } if(runif() < 0.3){ p.Frequency.RepeatSpeed = runif(0.5, 0.3); } p.Volume.Sustain = runif(0.3, 0.1); p.Volume.Decay = runif(0.5); p.Volume.Punch = runif(0.6, 0.2); if(runif() < 0.5){ p.Phaser.Offset = runif(0.9, -0.3); p.Phaser.Sweep = runif(-0.3); } if(runif() < 0.33){ p.Frequency.ChangeSpeed = runif(0.3, 0.6); p.Frequency.ChangeAmount = runif(24, -12); } RemoveEmptyParams(p); return p; }, Powerup: function(){ var p = EmptyParams(); if(runif() < 0.5){ p.Generator.Func = 'saw'; } else { p.Generator.A = runif(0.6); } p.Frequency.Start = runif(220, 440); if(runif() < 0.5){ p.Frequency.Slide = runif(0.5, 0.2); p.Frequency.RepeatSpeed = runif(0.4, 0.4); } else { p.Frequency.Slide = runif(0.2, 0.05); if(runif() < 0.5){ p.Vibrato.Depth = runif(0.6, 0.1); p.Vibrato.Frequency = runif(30, 10); } } p.Volume.Sustain = runif(0.4); p.Volume.Decay = runif(0.4, 0.1); RemoveEmptyParams(p); return p; }, Hit: function(){ var p = EmptyParams(); p.Generator.Func = rchoose(['square', 'saw', 'noise']); p.Generator.A = runif(0.6); p.Generator.ASlide = runif(1, -0.5); p.Frequency.Start = runif(880, 220); p.Frequency.Slide = -runif(0.4, 0.3); p.Volume.Sustain = runif(0.1); p.Volume.Decay = runif(0.2, 0.1); if(runif() < 0.5){ p.Filter.HP = runif(0.3); } RemoveEmptyParams(p); return p; }, Jump: function(){ var p = EmptyParams(); p.Generator.Func = 'square'; p.Generator.A = runif(0.6); p.Frequency.Start = runif(330, 330); p.Frequency.Slide = runif(0.4, 0.2); p.Volume.Sustain = runif(0.3, 0.1); p.Volume.Decay = runif(0.2, 0.1); if(runif() < 0.5){ p.Filter.HP = runif(0.3); } if(runif() < 0.3){ p.Filter.LP = runif(-0.6, 1); } RemoveEmptyParams(p); return p; }, Select: function(){ var p = EmptyParams(); p.Generator.Func = rchoose(['square', 'saw']); p.Generator.A = runif(0.6); p.Frequency.Start = runif(660, 220); p.Volume.Sustain = runif(0.1, 0.1); p.Volume.Decay = runif(0.2); p.Filter.HP = 0.2; RemoveEmptyParams(p); return p; }, Lucky: function(){ var p = EmptyParams(); map_object(p, function(out, moduleName){ var defs = jsfx.Module[moduleName].params; map_object(defs, function(def, name){ if(def.C){ var values = Object_keys(def.C); out[name] = values[(values.length * Math.random()) | 0]; } else { out[name] = Math.random() * (def.H - def.L) + def.L; } }); }); p.Volume.Master = 0.4; p.Filter = {}; // disable filter, as it usually will clip everything RemoveEmptyParams(p); return p; } }; // GENERATORS // uniform noise jsfx.G.unoise = newGenerator("sample = Math.random();"); // sine wave jsfx.G.sine = newGenerator("sample = Math.sin(phase);"); // saw wave jsfx.G.saw = newGenerator("sample = 2*(phase/TAU - ((phase/TAU + 0.5)|0));"); // triangle wave jsfx.G.triangle = newGenerator("sample = Math.abs(4 * ((phase/TAU - 0.25)%1) - 2) - 1;"); // square wave jsfx.G.square = newGenerator("var s = Math.sin(phase); sample = s > A ? 1.0 : s < A ? -1.0 : A;"); // simple synth jsfx.G.synth = newGenerator("sample = Math.sin(phase) + .5*Math.sin(phase/2) + .3*Math.sin(phase/4);"); // STATEFUL var __noiseLast = 0; jsfx.G.noise = newGenerator("if(phase % TAU < 4){__noiseLast = Math.random() * 2 - 1;} sample = __noiseLast;"); // Karplus-Strong string jsfx.G.string = { create: function(){ var BS = 1 << 16; var BM = BS-1; var buffer = createFloatArray(BS); for(var i = 0; i < buffer.length; i++){ buffer[i] = Math.random()*2-1; } var head = 0; return function($, block){ var TAU = Math.PI * 2; var A = +$.generatorA, ASlide = +$.generatorASlide, B = +$.generatorB, BSlide = +$.generatorBSlide; var buf = buffer; for(var i = 0; i < block.length; i++){ var phaseSpeed = block[i]; var n = (TAU/phaseSpeed)|0; A += ASlide; B += BSlide; A = A < 0 ? 0 : A > 1 ? 1 : A; B = B < 0 ? 0 : B > 1 ? 1 : B; var t = ((head - n) + BS) & BM; var sample = ( buf[(t-0+BS)&BM]*1 + buf[(t-1+BS)&BM]*A + buf[(t-2+BS)&BM]*B) / (1+A+B); buf[head] = sample; block[i] = buf[head]; head = (head + 1) & BM; } $.generatorA = A; $.generatorB = B; return block.length; } } }; // Generates samples using given frequency and generator function newGenerator(line){ return new Function("$", "block", "" + "var TAU = Math.PI * 2;\n" + "var sample;\n" + "var phase = +$.generatorPhase,\n"+ " A = +$.generatorA, ASlide = +$.generatorASlide,\n"+ " B = +$.generatorB, BSlide = +$.generatorBSlide;\n"+ "\n"+ "for(var i = 0; i < block.length; i++){\n"+ " var phaseSpeed = block[i];\n"+ " phase += phaseSpeed;\n"+ " if(phase > TAU){ phase -= TAU };\n"+ " A += ASlide; B += BSlide;\n"+ " A = A < 0 ? 0 : A > 1 ? 1 : A;\n"+ " B = B < 0 ? 0 : B > 1 ? 1 : B;\n"+ line + " block[i] = sample;\n"+ "}\n"+ "\n"+ "$.generatorPhase = phase;\n"+ "$.generatorA = A;\n"+ "$.generatorB = B;\n"+ "return block.length;\n" + ""); } // WAVE SUPPORT // Creates an Audio element from audio data [-1.0 .. 1.0] jsfx.CreateAudio = CreateAudio; function CreateAudio(data){ if(typeof Float32Array !== "undefined"){ assert(data instanceof Float32Array, 'data must be an Float32Array'); } var blockAlign = numChannels * bitsPerSample >> 3; var byteRate = jsfx.SampleRate * blockAlign; var output = createByteArray(8 + 36 + data.length * 2); var p = 0; // emits string to output function S(value){ for(var i = 0; i < value.length; i += 1){ output[p] = value.charCodeAt(i); p++; } } // emits integer value to output function V(value, nBytes){ if(nBytes <= 0){ return; } output[p] = value & 0xFF; p++; V(value >> 8, nBytes - 1); } S('RIFF'); V(36 + data.length * 2, 4); S('WAVEfmt '); V(16, 4); V(1, 2); V(numChannels, 2); V(jsfx.SampleRate, 4); V(byteRate, 4); V(blockAlign, 2); V(bitsPerSample, 2); S('data'); V(data.length * 2, 4); CopyFToU8(output.subarray(p), data); return new Audio('data:audio/wav;base64,' + U8ToB64(output)); }; jsfx.DownloadAsFile = function(audio){ assert(audio instanceof Audio, 'input must be an Audio object'); document.location.href = audio.src; }; // HELPERS jsfx.Util = {}; // Copies array of Floats to a Uint8Array with 16bits per sample jsfx.Util.CopyFToU8 = CopyFToU8; function CopyFToU8(into, floats){ assert(into.length/2 == floats.length, 'the target buffer must be twice as large as the iinput'); var k = 0; for(var i = 0; i < floats.length; i++){ var v = +floats[i]; var a = (v * 0x7FFF)|0; a = a < -0x8000 ? -0x8000 : 0x7FFF < a ? 0x7FFF : a; a += a < 0 ? 0x10000 : 0; into[k] = a & 0xFF; k++; into[k] = a >> 8; k++; } } function U8ToB64(data){ var CHUNK = 0x8000; var result = ''; for(var start = 0; start < data.length; start += CHUNK){ var end = Math.min(start + CHUNK, data.length); result += String.fromCharCode.apply(null, data.subarray(start, end)); } return btoa(result); } // uses AudioContext sampleRate or 44100; function getDefaultSampleRate(){ if(typeof AudioContext !== 'undefined'){ return (new AudioContext()).sampleRate; } return 44100; } // for checking pre/post conditions function assert(condition, message){ if(!condition){ throw new Error(message); } } function clamp(v, min, max){ v = +v; min = +min; max = +max; if(v < min){ return +min; } if(v > max){ return +max; } return +v; } function clamp1(v){ v = +v; if(v < +0.0){ return +0.0; } if(v > +1.0){ return +1.0; } return +v; } function map_object(obj, fn){ var r = {}; for(var name in obj){ if(obj.hasOwnProperty(name)){ r[name] = fn(obj[name], name); } } return r; } // uniform random function runif(scale, offset){ var a = Math.random(); if(scale !== undefined) a *= scale; if(offset !== undefined) a += offset; return a; } function rchoose(gens){ return gens[(gens.length*Math.random())|0]; } function Object_keys(obj){ var r = []; for(var name in obj){ r.push(name); } return r; } jsfx._createFloatArray = createFloatArray; function createFloatArray(N){ if(typeof Float32Array === "undefined") { var r = new Array(N); for(var i = 0; i < r.length; i++){ r[i] = 0.0; } } return new Float32Array(N); } function createByteArray(N){ if(typeof Uint8Array === "undefined") { var r = new Array(N); for(var i = 0; i < r.length; i++){ r[i] = 0|0; } } return new Uint8Array(N); } };