├── .gitignore ├── jsfx.png ├── .editorconfig ├── package.json ├── music.html ├── LICENSE-MIT ├── synth.html ├── README.markdown ├── jsmusic.js ├── index.html └── jsfx.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.~* 2 | ~* -------------------------------------------------------------------------------- /jsfx.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/loov/jsfx/HEAD/jsfx.png -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | max_line_length = 110 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [*.js] 11 | charset = utf-8 12 | indent_size = 4 13 | indent_style = tab 14 | 15 | [*.css] 16 | charset = utf-8 17 | indent_size = 4 18 | indent_style = tab 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loov-jsfx", 3 | "version": "1.2.0", 4 | "description": "Package for sound effect generation.", 5 | "main": "jsfx.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/loov/jsfx.git" 12 | }, 13 | "keywords": [ 14 | "audio", 15 | "sound", 16 | "effects", 17 | "jsfx", 18 | "gamedev" 19 | ], 20 | "author": "Egon Elbre", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/loov/jsfx/issues" 24 | }, 25 | "homepage": "http://loov.io/jsfx" 26 | } 27 | -------------------------------------------------------------------------------- /music.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 30 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Egon Elbre 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /synth.html: -------------------------------------------------------------------------------- 1 | Note this is broken on Chrome ATM. 2 |

Play with AWSEDFTGYHUJK keys.

3 | 4 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | ![](https://raw.githubusercontent.com/loov/jsfx/master/jsfx.png) 2 | 3 | This is a JavaScript library for sound effect generation and is supported on 4 | [most current browsers](http://caniuse.com/#feat=audio). 5 | 6 | Generation speed is approximately 1s audio = 10ms processing. Of course that 7 | value can vary a lot, depending on the settings or browser that you use. 8 | 9 | ### How to use it? 10 | 11 | Open [index.html](http://loov.github.io/jsfx/) - this helps to pick out your samples. 12 | 13 | Try clicking the presets and tweaking all the options. Once you are satisified 14 | with your result click add button at top-right. 15 | 16 | Enter a name for the sound e.g. "select", repeat that as many times as you like. Tip: You can save your settings by making a bookmark of the page. 17 | 18 | At the bottom of the page there is a Library section. There you can relisten 19 | or remove sounds that you do not like. 20 | 21 | Once you are satisfied with your selection copy the JSON description 22 | (it's inside the input box). 23 | 24 | It will look something like: 25 | 26 | {"select":{"Volume":{"Sustain":0.1,"Decay":0.15,"Punch":0.55}}} 27 | 28 | To use that library, you need to include `jsfx.js` in your code and use `jsfx.Sounds(libarry)` to initialize it. For example: 29 | 30 | ```html 31 | 32 | 39 | 40 | 41 | ``` 42 | 43 | Note that it will load with a delay to avoid blocking the page load for too 44 | long, so calling those function immediately may result in silence. 45 | 46 | ### Using with AudioContext (experimental) 47 | 48 | You can use AudioContext to procedurally generate the sounds, for example: 49 | 50 | ```html 51 | 52 | 62 | 63 | 64 | 65 | ``` 66 | 67 | ### Few notes... 68 | 69 | It's recommended to copy the jsfx.js to your own project instead of 70 | automatically downloading the latest version. Since every slight adjustment 71 | to the audio generation code can affect the resulting audio significantly. 72 | 73 | The stable API is what is described in the README, everything else is 74 | subject to change. 75 | 76 | ### Thanks to 77 | 78 | This project was inspired by [sfxr](http://www.drpetter.se/project_sfxr.html) 79 | and was used as a reference for some algorithms and modes. -------------------------------------------------------------------------------- /jsmusic.js: -------------------------------------------------------------------------------- 1 | (function(jsmusic){ 2 | var scale = "c d ef g ha bC D EF G HA B"; 3 | 4 | jsmusic.Simple = Simple; 5 | function Simple(songtext, instrument, bpm, modules){ 6 | var tokens = songtext.split(""); 7 | var notes = []; 8 | for(var i = 0; i < tokens.length; i+=1){ 9 | var note = scale.indexOf(tokens[i]) + 3; 10 | 11 | var more = true; 12 | while(more){ 13 | switch(tokens[i+1]){ 14 | case "+": i++; note += 1; break; 15 | case "-": i++; note -= 1; break; 16 | case "^": i++; note += 12; break; 17 | case ".": i++; note -= 12; break; 18 | default: 19 | more = false; 20 | } 21 | } 22 | var freq = 220 * Math.pow(2, 1 + note/12); 23 | notes.push({Frequency: { Start: freq }}); 24 | } 25 | return GenerateSong(notes, instrument, bpm, modules); 26 | }; 27 | 28 | 29 | function GenerateSong(notes, instrument, bpm, modules){ 30 | bpm = bpm || 120; 31 | var processor = new jsfx.Processor(instrument, modules); 32 | var sampleRate = processor.state.SampleRate; 33 | 34 | var beatSamples = sampleRate / (bpm / 60); 35 | var songSamples = notes.length * beatSamples; 36 | 37 | var beatBuffer = jsfx._createFloatArray(beatSamples); 38 | var playing = []; 39 | 40 | var songBuffer = jsfx._createFloatArray(songSamples + processor.getSamplesLeft()); 41 | var currentStart = 0; 42 | 43 | // fill the beats 44 | for(var i = 0; i < notes.length; i += 1){ 45 | var note = notes[i]; 46 | var params = MergeParams(instrument, note); 47 | var proc = new jsfx.Processor(params, modules); 48 | playing.push(proc); 49 | 50 | playing.map(function(proc){ 51 | proc.generate(beatBuffer); 52 | for(var i = 0; i < beatBuffer.length; i++){ 53 | songBuffer[currentStart + i] += beatBuffer[i]; 54 | }; 55 | }); 56 | currentStart += beatBuffer.length; 57 | playing = playing.filter(function(proc){ return !proc.finished; }); 58 | } 59 | 60 | // fill the decay 61 | while(currentStart < songBuffer.length){ 62 | if(playing.length == 0){break;} 63 | playing.map(function(proc){ 64 | proc.generate(beatBuffer); 65 | var N = Math.min(beatBuffer.length, songBuffer.length - currentStart); 66 | for(var i = 0; i < N; i++){ 67 | songBuffer[currentStart + i] += beatBuffer[i]; 68 | }; 69 | }); 70 | currentStart += beatBuffer.length; 71 | playing = playing.filter(function(proc){ return !proc.finished; }); 72 | } 73 | 74 | return jsfx.CreateAudio(songBuffer); 75 | } 76 | 77 | 78 | // modifies the override 79 | function MergeParams(base, override){ 80 | if(typeof base === 'function'){ 81 | base = base(); 82 | } else { 83 | base = JSON.parse(JSON.stringify(base)); 84 | } 85 | 86 | if(typeof override === 'function'){ 87 | override = override(); 88 | } 89 | 90 | for(var name in override){ 91 | if(typeof base[name] === 'undefined'){ 92 | base[name] = {}; 93 | } 94 | for(var param in override[name]){ 95 | base[name][param] = override[name][param]; 96 | } 97 | } 98 | 99 | return base; 100 | } 101 | 102 | 103 | })(this.jsmusic = {}); 104 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | jsfx 8 | 14 | 15 | 16 | 17 |
18 |

jsfx

19 |
See github.com/loov/jsfx for more information.
20 |
21 |
22 |
Control
23 | 24 | 25 | 26 | Download 27 |
28 |
29 |
30 |
31 |
Presets
32 |
33 |
34 |
35 |
36 |
37 | 38 |
39 |
Library
40 | 41 |
42 |
43 |
44 | 285 | 442 | 443 | -------------------------------------------------------------------------------- /jsfx.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof module === "object" && typeof module.exports === "object") { 3 | module.exports = factory(); 4 | } else { 5 | root.jsfx = factory(); 6 | } 7 | }(this, function() { 8 | 'use strict'; 9 | 10 | var chr = String.fromCharCode; 11 | var TAU = +Math.PI * 2; 12 | var bitsPerSample = 16 | 0; 13 | var numChannels = 1 | 0; 14 | var sin = Math.sin; 15 | var pow = Math.pow; 16 | var abs = Math.abs; 17 | var EPSILON = 0.000001; 18 | 19 | var jsfx = {}; 20 | var AudioContext = window.AudioContext || window.webkitAudioContext; 21 | 22 | jsfx.SampleRate = 0 | 0; 23 | jsfx.Sec = 0 | 0; 24 | 25 | jsfx.SetSampleRate = function(sampleRate) { 26 | jsfx.SampleRate = sampleRate | 0; 27 | jsfx.Sec = sampleRate | 0; 28 | }; 29 | jsfx.SetSampleRate(getDefaultSampleRate()); 30 | 31 | // MAIN API 32 | 33 | // Creates a new Audio object based on the params 34 | // params can be a params generating function or the actual parameters 35 | jsfx.Sound = function(params) { 36 | var processor = new Processor(params, jsfx.DefaultModules); 37 | var block = createFloatArray(processor.getSamplesLeft()); 38 | processor.generate(block); 39 | return CreateAudio(block); 40 | }; 41 | 42 | // Same as Sounds, but avoids locking the browser for too long 43 | // in case you have a large amount of sounds to generate 44 | jsfx.Sounds = function(library, ondone, onprogress) { 45 | var audio = {}; 46 | var player = {}; 47 | player._audio = audio; 48 | 49 | var toLoad = []; 50 | 51 | // create playing functions 52 | map_object(library, function(_, name) { 53 | player[name] = function() { 54 | if (typeof audio[name] !== "undefined") { 55 | audio[name].currentTime = 0.0; 56 | audio[name].play(); 57 | } 58 | }; 59 | toLoad.push(name); 60 | }); 61 | 62 | var loaded = 0, 63 | total = toLoad.length; 64 | 65 | function next() { 66 | if (toLoad.length == 0) { 67 | ondone && ondone(sounds); 68 | return; 69 | } 70 | var name = toLoad.shift(); 71 | audio[name] = jsfx.Sound(library[name]); 72 | loaded++; 73 | onprogress && onprogress(name, loaded, total); 74 | 75 | window.setTimeout(next, 30); 76 | } 77 | next(); 78 | 79 | return player; 80 | } 81 | 82 | // SoundsImmediate takes a named set of params, and generates multiple 83 | // sound objects at once. 84 | jsfx.SoundsImmediate = function(library) { 85 | var audio = {}; 86 | var player = {}; 87 | player._audio = audio; 88 | map_object(library, function(params, name) { 89 | audio[name] = jsfx.Sound(params); 90 | player[name] = function() { 91 | if (typeof audio[name] !== "undefined") { 92 | audio[name].currentTime = 0.0; 93 | audio[name].play(); 94 | } 95 | }; 96 | }) 97 | return player; 98 | }; 99 | 100 | // FloatBuffer creates a FloatArray filled with audio 101 | jsfx.FloatBuffer = function(params, modules) { 102 | var processor = new Processor(params, jsfx.DefaultModules); 103 | var block = createFloatArray(processor.getSamplesLeft()); 104 | processor.generate(block); 105 | return block; 106 | }; 107 | 108 | if (typeof AudioContext !== "undefined") { 109 | // Node creates a new AudioContext ScriptProcessor that outputs the 110 | // sound. It will automatically disconnect, unless otherwise specified. 111 | jsfx.Node = function(audioContext, params, modules, bufferSize, stayConnected) { 112 | var node = audioContext.createScriptProcessor(bufferSize, 0, 1); 113 | var gen = new Processor(params, modules || jsfx.DefaultModules); 114 | node.onaudioprocess = function(ev) { 115 | var block = ev.outputBuffer.getChannelData(0); 116 | gen.generate(block); 117 | if (!stayConnected && gen.finished) { 118 | // we need to do an async disconnect, otherwise Chrome may 119 | // glitch 120 | setTimeout(function() { 121 | node.disconnect(); 122 | }, 30); 123 | } 124 | } 125 | return node; 126 | } 127 | 128 | // AudioBuffer creates a buffer filled with the proper audio 129 | // This is useful, when you want to use AudioContext.BufferSource 130 | jsfx.AudioBuffer = function(audioContext, params, modules) { 131 | var processor = new Processor(params, modules || jsfx.DefaultModules); 132 | var buffer = audioContext.createBuffer(numChannels, processor.getSamplesLeft(), jsfx.SampleRate); 133 | var block = buffer.getChannelData(0); 134 | processor.generate(block); 135 | return buffer; 136 | }; 137 | 138 | // Live creates an managed AudioContext for playing. 139 | // This is useful, when you want to use procedurally generated sounds. 140 | jsfx.Live = function(library, modules, BufferSize) { 141 | //TODO: add limit for number of notes played at the same time 142 | BufferSize = BufferSize || 2048; 143 | var player = {}; 144 | 145 | var context = new AudioContext(); 146 | var volume = context.createGain(); 147 | volume.connect(context.destination); 148 | 149 | player._context = context; 150 | player._volume = volume; 151 | 152 | map_object(library, function(params, name) { 153 | player[name] = function() { 154 | var node = jsfx.Node(context, params, modules, BufferSize); 155 | node.connect(volume); 156 | }; 157 | }); 158 | 159 | player._close = function() { 160 | context.close(); 161 | }; 162 | 163 | player._play = function(params) { 164 | var node = jsfx.Node(context, params, modules, BufferSize); 165 | node.connect(volume); 166 | }; 167 | 168 | return player; 169 | } 170 | } else { 171 | jsfx.Live = jsfx.Sounds; 172 | } 173 | 174 | // SOUND GENERATION 175 | jsfx.Module = {}; 176 | 177 | // generators 178 | jsfx.G = {}; 179 | 180 | var stage = jsfx.stage = { 181 | PhaseSpeed: 0, 182 | PhaseSpeedMod: 10, 183 | Generator: 20, 184 | SampleMod: 30, 185 | Volume: 40 186 | }; 187 | 188 | function byStage(a, b) { 189 | return a.stage - b.stage; 190 | } 191 | 192 | jsfx.InitDefaultParams = InitDefaultParams; 193 | 194 | function InitDefaultParams(params, modules) { 195 | // setup modules 196 | for (var i = 0; i < modules.length; i += 1) { 197 | var M = modules[i]; 198 | var P = params[M.name] || {}; 199 | 200 | // add missing parameters 201 | map_object(M.params, function(def, name) { 202 | if (typeof P[name] === 'undefined') { 203 | P[name] = def.D; 204 | } 205 | }); 206 | 207 | params[M.name] = P; 208 | } 209 | } 210 | 211 | // Generates a stateful sound effect processor 212 | // params can be a function that creates a parameter set 213 | jsfx.Processor = Processor; 214 | 215 | function Processor(params, modules) { 216 | params = params || {}; 217 | modules = modules || jsfx.DefaultModules; 218 | 219 | if (typeof params === 'function') { 220 | params = params(); 221 | } else { 222 | params = JSON.parse(JSON.stringify(params)) 223 | } 224 | this.finished = false; 225 | 226 | this.state = { 227 | SampleRate: params.SampleRate || jsfx.SampleRate 228 | }; 229 | 230 | // sort modules 231 | modules = modules.slice(); 232 | modules.sort(byStage) 233 | this.modules = modules; 234 | 235 | // init missing params 236 | InitDefaultParams(params, modules); 237 | 238 | // setup modules 239 | for (var i = 0; i < this.modules.length; i += 1) { 240 | var M = this.modules[i]; 241 | this.modules[i].setup(this.state, params[M.name]); 242 | } 243 | } 244 | Processor.prototype = { 245 | //TODO: see whether this can be converted to a module 246 | generate: function(block) { 247 | for (var i = 0 | 0; i < block.length; i += 1) { 248 | block[i] = 0; 249 | } 250 | if (this.finished) { 251 | return; 252 | } 253 | 254 | var $ = this.state, 255 | N = block.length | 0; 256 | for (var i = 0; i < this.modules.length; i += 1) { 257 | var M = this.modules[i]; 258 | var n = M.process($, block.subarray(0, N)) | 0; 259 | N = Math.min(N, n); 260 | } 261 | if (N < block.length) { 262 | this.finished = true; 263 | } 264 | for (var i = N; i < block.length; i++) { 265 | block[i] = 0; 266 | } 267 | }, 268 | getSamplesLeft: function() { 269 | var samples = 0; 270 | for (var i = 0; i < this.state.envelopes.length; i += 1) { 271 | samples += this.state.envelopes[i].N; 272 | } 273 | if (samples === 0) { 274 | samples = 3 * this.state.SampleRate; 275 | } 276 | return samples; 277 | } 278 | }; 279 | 280 | // Frequency 281 | jsfx.Module.Frequency = { 282 | name: 'Frequency', 283 | params: { 284 | /* beautify preserve:start */ 285 | Start: { L:30, H:1800, D:440 }, 286 | 287 | Min: { L:30, H:1800, D:30 }, 288 | Max: { L:30, H:1800, D:1800 }, 289 | 290 | Slide: { L:-1, H:1, D:0 }, 291 | DeltaSlide: { L:-1, H:1, D:0 }, 292 | 293 | RepeatSpeed: { L:0, H: 3.0, D: 0 }, 294 | 295 | ChangeAmount: { L:-12, H:12, D:0 }, 296 | ChangeSpeed : { L: 0, H:1, D:0 } 297 | /* beautify preserve:end */ 298 | }, 299 | stage: stage.PhaseSpeed, 300 | setup: function($, P) { 301 | var SR = $.SampleRate; 302 | 303 | $.phaseParams = P; 304 | 305 | $.phaseSpeed = P.Start * TAU / SR; 306 | $.phaseSpeedMax = P.Max * TAU / SR; 307 | $.phaseSpeedMin = P.Min * TAU / SR; 308 | 309 | $.phaseSpeedMin = Math.min($.phaseSpeedMin, $.phaseSpeed); 310 | $.phaseSpeedMax = Math.max($.phaseSpeedMax, $.phaseSpeed); 311 | 312 | $.phaseSlide = 1.0 + pow(P.Slide, 3.0) * 64.0 / SR; 313 | $.phaseDeltaSlide = pow(P.DeltaSlide, 3.0) / (SR * 1000); 314 | 315 | $.repeatTime = 0; 316 | $.repeatLimit = Infinity; 317 | if (P.RepeatSpeed > 0) { 318 | $.repeatLimit = P.RepeatSpeed * SR; 319 | } 320 | 321 | $.arpeggiatorTime = 0; 322 | $.arpeggiatorLimit = P.ChangeSpeed * SR; 323 | if (P.ChangeAmount == 0) { 324 | $.arpeggiatorLimit = Infinity; 325 | } 326 | $.arpeggiatorMod = 1 + P.ChangeAmount / 12.0; 327 | }, 328 | process: function($, block) { 329 | var speed = +$.phaseSpeed, 330 | min = +$.phaseSpeedMin, 331 | max = +$.phaseSpeedMax, 332 | slide = +$.phaseSlide, 333 | deltaSlide = +$.phaseDeltaSlide; 334 | 335 | var repeatTime = $.repeatTime, 336 | repeatLimit = $.repeatLimit; 337 | 338 | var arpTime = $.arpeggiatorTime, 339 | arpLimit = $.arpeggiatorLimit, 340 | arpMod = $.arpeggiatorMod; 341 | 342 | for (var i = 0; i < block.length; i++) { 343 | slide += deltaSlide; 344 | speed *= slide; 345 | speed = speed < min ? min : speed > max ? max : speed; 346 | 347 | if (repeatTime > repeatLimit) { 348 | this.setup($, $.phaseParams); 349 | return i + this.process($, block.subarray(i)) - 1; 350 | } 351 | repeatTime++; 352 | 353 | if (arpTime > arpLimit) { 354 | speed *= arpMod; 355 | arpTime = 0; 356 | arpLimit = Infinity; 357 | } 358 | arpTime++; 359 | 360 | block[i] += speed; 361 | } 362 | 363 | $.repeatTime = repeatTime; 364 | $.arpeggiatorTime = arpTime; 365 | $.arpeggiatorLimit = arpLimit; 366 | 367 | $.phaseSpeed = speed; 368 | $.phaseSlide = slide; 369 | 370 | return block.length; 371 | } 372 | }; 373 | 374 | // Vibrato 375 | jsfx.Module.Vibrato = { 376 | name: 'Vibrato', 377 | params: { 378 | /* beautify preserve:start */ 379 | Depth: {L: 0, H:1, D:0}, 380 | DepthSlide: {L:-1, H:1, D:0}, 381 | 382 | Frequency: {L: 0.01, H:48, D:0}, 383 | FrequencySlide: {L: -1.00, H: 1, D:0} 384 | /* beautify preserve:end */ 385 | }, 386 | stage: stage.PhaseSpeedMod, 387 | setup: function($, P) { 388 | var SR = $.SampleRate; 389 | $.vibratoPhase = 0; 390 | $.vibratoDepth = P.Depth; 391 | $.vibratoPhaseSpeed = P.Frequency * TAU / SR; 392 | 393 | $.vibratoPhaseSpeedSlide = 1.0 + pow(P.FrequencySlide, 3.0) * 3.0 / SR; 394 | $.vibratoDepthSlide = P.DepthSlide / SR; 395 | }, 396 | process: function($, block) { 397 | var phase = +$.vibratoPhase, 398 | depth = +$.vibratoDepth, 399 | speed = +$.vibratoPhaseSpeed, 400 | slide = +$.vibratoPhaseSpeedSlide, 401 | depthSlide = +$.vibratoDepthSlide; 402 | 403 | if ((depth == 0) && (depthSlide <= 0)) { 404 | return block.length; 405 | } 406 | 407 | for (var i = 0; i < block.length; i++) { 408 | phase += speed; 409 | if (phase > TAU) { 410 | phase -= TAU 411 | }; 412 | block[i] += block[i] * sin(phase) * depth; 413 | 414 | speed *= slide; 415 | depth += depthSlide; 416 | depth = clamp1(depth); 417 | } 418 | 419 | $.vibratoPhase = phase; 420 | $.vibratoDepth = depth; 421 | $.vibratoPhaseSpeed = speed; 422 | return block.length; 423 | } 424 | }; 425 | 426 | // Generator 427 | jsfx.Module.Generator = { 428 | name: 'Generator', 429 | params: { 430 | /* beautify preserve:start */ 431 | // C = choose 432 | Func: {C: jsfx.G, D:'square'}, 433 | 434 | A: {L: 0, H: 1, D: 0}, 435 | B: {L: 0, H: 1, D: 0}, 436 | 437 | ASlide: {L: -1, H: 1, D: 0}, 438 | BSlide: {L: -1, H: 1, D: 0} 439 | /* beautify preserve:end */ 440 | }, 441 | stage: stage.Generator, 442 | setup: function($, P) { 443 | $.generatorPhase = 0; 444 | 445 | if (typeof P.Func === 'string') { 446 | $.generator = jsfx.G[P.Func]; 447 | } else { 448 | $.generator = P.Func; 449 | } 450 | if (typeof $.generator === 'object') { 451 | $.generator = $.generator.create(); 452 | } 453 | assert(typeof $.generator === 'function', 'generator must be a function') 454 | 455 | $.generatorA = P.A; 456 | $.generatorASlide = P.ASlide; 457 | $.generatorB = P.B; 458 | $.generatorBSlide = P.BSlide; 459 | }, 460 | process: function($, block) { 461 | return $.generator($, block); 462 | } 463 | }; 464 | 465 | // Karplus Strong algorithm for string sound 466 | var GuitarBufferSize = 1 << 16; 467 | jsfx.Module.Guitar = { 468 | name: 'Guitar', 469 | params: { 470 | /* beautify preserve:start */ 471 | A: {L:0.0, H:1.0, D: 1}, 472 | B: {L:0.0, H:1.0, D: 1}, 473 | C: {L:0.0, H:1.0, D: 1} 474 | /* beautify preserve:end */ 475 | }, 476 | stage: stage.Generator, 477 | setup: function($, P) { 478 | $.guitarA = P.A; 479 | $.guitarB = P.B; 480 | $.guitarC = P.C; 481 | 482 | $.guitarBuffer = createFloatArray(GuitarBufferSize); 483 | $.guitarHead = 0; 484 | var B = $.guitarBuffer; 485 | for (var i = 0; i < B.length; i++) { 486 | B[i] = Math.random() * 2 - 1; 487 | } 488 | }, 489 | process: function($, block) { 490 | var BS = GuitarBufferSize, 491 | BM = BS - 1; 492 | 493 | var A = +$.guitarA, 494 | B = +$.guitarB, 495 | C = +$.guitarC; 496 | var T = A + B + C; 497 | var h = $.guitarHead; 498 | 499 | var buffer = $.guitarBuffer; 500 | for (var i = 0; i < block.length; i++) { 501 | // buffer size 502 | var n = (TAU / block[i]) | 0; 503 | n = n > BS ? BS : n; 504 | 505 | // tail 506 | var t = ((h - n) + BS) & BM; 507 | buffer[h] = 508 | (buffer[(t - 0 + BS) & BM] * A + 509 | buffer[(t - 1 + BS) & BM] * B + 510 | buffer[(t - 2 + BS) & BM] * C) / T; 511 | 512 | block[i] = buffer[h]; 513 | h = (h + 1) & BM; 514 | } 515 | 516 | $.guitarHead = h; 517 | return block.length; 518 | } 519 | } 520 | 521 | // Low/High-Pass Filter 522 | jsfx.Module.Filter = { 523 | name: 'Filter', 524 | params: { 525 | /* beautify preserve:start */ 526 | LP: {L: 0, H:1, D:1}, 527 | LPSlide: {L:-1, H:1, D:0}, 528 | LPResonance: {L: 0, H:1, D:0}, 529 | HP: {L: 0, H:1, D:0}, 530 | HPSlide: {L:-1, H:1, D:0} 531 | /* beautify preserve:end */ 532 | }, 533 | stage: stage.SampleMod + 0, 534 | setup: function($, P) { 535 | $.FilterEnabled = (P.HP > EPSILON) || (P.LP < 1 - EPSILON); 536 | 537 | $.LPEnabled = P.LP < 1 - EPSILON; 538 | $.LP = pow(P.LP, 3.0) / 10; 539 | $.LPSlide = 1.0 + P.LPSlide * 100 / $.SampleRate; 540 | $.LPPos = 0; 541 | $.LPPosSlide = 0; 542 | 543 | $.LPDamping = 5.0 / (1.0 + pow(P.LPResonance, 2) * 20) * (0.01 + P.LP); 544 | $.LPDamping = 1.0 - Math.min($.LPDamping, 0.8); 545 | 546 | $.HP = pow(P.HP, 2.0) / 10; 547 | $.HPPos = 0; 548 | $.HPSlide = 1.0 + P.HPSlide * 100 / $.SampleRate; 549 | }, 550 | enabled: function($) { 551 | return $.FilterEnabled; 552 | }, 553 | process: function($, block) { 554 | if (!this.enabled($)) { 555 | return block.length; 556 | } 557 | 558 | var lp = +$.LP; 559 | var lpPos = +$.LPPos; 560 | var lpPosSlide = +$.LPPosSlide; 561 | var lpSlide = +$.LPSlide; 562 | var lpDamping = +$.LPDamping; 563 | var lpEnabled = +$.LPEnabled; 564 | 565 | var hp = +$.HP; 566 | var hpPos = +$.HPPos; 567 | var hpSlide = +$.HPSlide; 568 | 569 | for (var i = 0; i < block.length; i++) { 570 | if ((hp > EPSILON) || (hp < -EPSILON)) { 571 | hp *= hpSlide; 572 | hp = hp < EPSILON ? EPSILON : hp > 0.1 ? 0.1 : hp; 573 | } 574 | 575 | var lpPos_ = lpPos; 576 | 577 | lp *= lpSlide; 578 | lp = lp < 0 ? lp = 0 : lp > 0.1 ? 0.1 : lp; 579 | 580 | var sample = block[i]; 581 | if (lpEnabled) { 582 | lpPosSlide += (sample - lpPos) * lp; 583 | lpPosSlide *= lpDamping; 584 | } else { 585 | lpPos = sample; 586 | lpPosSlide = 0; 587 | } 588 | lpPos += lpPosSlide; 589 | 590 | hpPos += lpPos - lpPos_; 591 | hpPos *= 1.0 - hp; 592 | 593 | block[i] = hpPos; 594 | } 595 | 596 | $.LPPos = lpPos; 597 | $.LPPosSlide = lpPosSlide; 598 | $.LP = lp; 599 | $.HP = hp; 600 | $.HPPos = hpPos; 601 | 602 | return block.length; 603 | } 604 | }; 605 | 606 | // Phaser Effect 607 | var PhaserBufferSize = 1 << 10; 608 | jsfx.Module.Phaser = { 609 | name: 'Phaser', 610 | params: { 611 | /* beautify preserve:start */ 612 | Offset: {L:-1, H:1, D:0}, 613 | Sweep: {L:-1, H:1, D:0} 614 | /* beautify preserve:end */ 615 | }, 616 | stage: stage.SampleMod + 1, 617 | setup: function($, P) { 618 | $.phaserBuffer = createFloatArray(PhaserBufferSize); 619 | $.phaserPos = 0; 620 | $.phaserOffset = pow(P.Offset, 2.0) * (PhaserBufferSize - 4); 621 | $.phaserOffsetSlide = pow(P.Sweep, 3.0) * 4000 / $.SampleRate; 622 | }, 623 | enabled: function($) { 624 | return (abs($.phaserOffsetSlide) > EPSILON) || 625 | (abs($.phaserOffset) > EPSILON); 626 | }, 627 | process: function($, block) { 628 | if (!this.enabled($)) { 629 | return block.length; 630 | } 631 | 632 | var BS = PhaserBufferSize, 633 | BM = BS - 1; 634 | 635 | var buffer = $.phaserBuffer, 636 | pos = $.phaserPos | 0, 637 | offset = +$.phaserOffset, 638 | offsetSlide = +$.phaserOffsetSlide; 639 | 640 | for (var i = 0; i < block.length; i++) { 641 | offset += offsetSlide; 642 | //TODO: check whether this is correct 643 | if (offset < 0) { 644 | offset = -offset; 645 | offsetSlide = -offsetSlide; 646 | } 647 | if (offset > BM) { 648 | offset = BM; 649 | offsetSlide = 0; 650 | } 651 | 652 | buffer[pos] = block[i]; 653 | var p = (pos - (offset | 0) + BS) & BM; 654 | block[i] += buffer[p]; 655 | 656 | pos = ((pos + 1) & BM) | 0; 657 | } 658 | 659 | $.phaserPos = pos; 660 | $.phaserOffset = offset; 661 | return block.length; 662 | } 663 | }; 664 | 665 | // Volume dynamic control with Attack-Sustain-Decay 666 | // ATTACK | 0 - Volume + Punch 667 | // SUSTAIN | Volume + Punch - Volume 668 | // DECAY | Volume - 0 669 | jsfx.Module.Volume = { 670 | name: 'Volume', 671 | params: { 672 | /* beautify preserve:start */ 673 | Master: { L: 0, H: 1, D: 0.5 }, 674 | Attack: { L: 0.001, H: 1, D: 0.01 }, 675 | Sustain: { L: 0, H: 2, D: 0.3 }, 676 | Punch: { L: 0, H: 3, D: 1.0 }, 677 | Decay: { L: 0.001, H: 2, D: 1.0 } 678 | /* beautify preserve:end */ 679 | }, 680 | stage: stage.Volume, 681 | setup: function($, P) { 682 | var SR = $.SampleRate; 683 | var V = P.Master; 684 | var VP = V * (1 + P.Punch); 685 | $.envelopes = [ 686 | // S = start volume, E = end volume, N = duration in samples 687 | { 688 | S: 0, 689 | E: V, 690 | N: (P.Attack * SR) | 0 691 | }, // Attack 692 | { 693 | S: VP, 694 | E: V, 695 | N: (P.Sustain * SR) | 0 696 | }, // Sustain 697 | { 698 | S: V, 699 | E: 0, 700 | N: (P.Decay * SR) | 0 701 | } // Decay 702 | ]; 703 | // G = volume gradient 704 | for (var i = 0; i < $.envelopes.length; i += 1) { 705 | var e = $.envelopes[i]; 706 | e.G = (e.E - e.S) / e.N; 707 | } 708 | }, 709 | process: function($, block) { 710 | var i = 0; 711 | while (($.envelopes.length > 0) && (i < block.length)) { 712 | var E = $.envelopes[0]; 713 | var vol = E.S, 714 | grad = E.G; 715 | 716 | var N = Math.min(block.length - i, E.N) | 0; 717 | var end = (i + N) | 0; 718 | for (; i < end; i += 1) { 719 | block[i] *= vol; 720 | vol += grad; 721 | vol = clamp(vol, 0, 10); 722 | } 723 | E.S = vol; 724 | E.N -= N; 725 | if (E.N <= 0) { 726 | $.envelopes.shift(); 727 | } 728 | } 729 | return i; 730 | } 731 | }; 732 | 733 | // PRESETS 734 | 735 | jsfx.DefaultModules = [ 736 | jsfx.Module.Frequency, 737 | jsfx.Module.Vibrato, 738 | jsfx.Module.Generator, 739 | jsfx.Module.Filter, 740 | jsfx.Module.Phaser, 741 | jsfx.Module.Volume 742 | ]; 743 | jsfx.DefaultModules.sort(byStage); 744 | 745 | jsfx.EmptyParams = EmptyParams; 746 | 747 | function EmptyParams() { 748 | return map_object(jsfx.Module, function() { 749 | return {} 750 | }); 751 | } 752 | 753 | jsfx._RemoveEmptyParams = RemoveEmptyParams; 754 | 755 | function RemoveEmptyParams(params) { 756 | for (var name in params) { 757 | if (Object_keys(params[name]).length == 0) { 758 | delete params[name]; 759 | } 760 | } 761 | }; 762 | 763 | jsfx.Preset = { 764 | Reset: function() { 765 | return EmptyParams(); 766 | }, 767 | Coin: function() { 768 | var p = EmptyParams(); 769 | p.Frequency.Start = runif(880, 660); 770 | p.Volume.Sustain = runif(0.1); 771 | p.Volume.Decay = runif(0.4, 0.1); 772 | p.Volume.Punch = runif(0.3, 0.3); 773 | if (runif() < 0.5) { 774 | p.Frequency.ChangeSpeed = runif(0.15, 0.1); 775 | p.Frequency.ChangeAmount = runif(8, 4); 776 | } 777 | RemoveEmptyParams(p); 778 | return p; 779 | }, 780 | Laser: function() { 781 | var p = EmptyParams(); 782 | p.Generator.Func = rchoose(['square', 'saw', 'sine']); 783 | 784 | if (runif() < 0.33) { 785 | p.Frequency.Start = runif(880, 440); 786 | p.Frequency.Min = runif(0.1); 787 | p.Frequency.Slide = runif(0.3, -0.8); 788 | } else { 789 | p.Frequency.Start = runif(1200, 440); 790 | p.Frequency.Min = p.Frequency.Start - runif(880, 440); 791 | if (p.Frequency.Min < 110) { 792 | p.Frequency.Min = 110; 793 | } 794 | p.Frequency.Slide = runif(0.3, -1); 795 | } 796 | 797 | if (runif() < 0.5) { 798 | p.Generator.A = runif(0.5); 799 | p.Generator.ASlide = runif(0.2); 800 | } else { 801 | p.Generator.A = runif(0.5, 0.4); 802 | p.Generator.ASlide = runif(0.7); 803 | } 804 | 805 | p.Volume.Sustain = runif(0.2, 0.1); 806 | p.Volume.Decay = runif(0.4); 807 | if (runif() < 0.5) { 808 | p.Volume.Punch = runif(0.3); 809 | } 810 | if (runif() < 0.33) { 811 | p.Phaser.Offset = runif(0.2); 812 | p.Phaser.Sweep = runif(0.2); 813 | } 814 | if (runif() < 0.5) { 815 | p.Filter.HP = runif(0.3); 816 | } 817 | RemoveEmptyParams(p); 818 | return p; 819 | }, 820 | Explosion: function() { 821 | var p = EmptyParams(); 822 | p.Generator.Func = 'noise'; 823 | if (runif() < 0.5) { 824 | p.Frequency.Start = runif(440, 40); 825 | p.Frequency.Slide = runif(0.4, -0.1); 826 | } else { 827 | p.Frequency.Start = runif(1600, 220); 828 | p.Frequency.Slide = runif(-0.2, -0.2); 829 | } 830 | 831 | if (runif() < 0.2) { 832 | p.Frequency.Slide = 0; 833 | } 834 | if (runif() < 0.3) { 835 | p.Frequency.RepeatSpeed = runif(0.5, 0.3); 836 | } 837 | 838 | p.Volume.Sustain = runif(0.3, 0.1); 839 | p.Volume.Decay = runif(0.5); 840 | p.Volume.Punch = runif(0.6, 0.2); 841 | 842 | if (runif() < 0.5) { 843 | p.Phaser.Offset = runif(0.9, -0.3); 844 | p.Phaser.Sweep = runif(-0.3); 845 | } 846 | 847 | if (runif() < 0.33) { 848 | p.Frequency.ChangeSpeed = runif(0.3, 0.6); 849 | p.Frequency.ChangeAmount = runif(24, -12); 850 | } 851 | RemoveEmptyParams(p); 852 | return p; 853 | }, 854 | Powerup: function() { 855 | var p = EmptyParams(); 856 | if (runif() < 0.5) { 857 | p.Generator.Func = 'saw'; 858 | } else { 859 | p.Generator.A = runif(0.6); 860 | } 861 | 862 | p.Frequency.Start = runif(220, 440); 863 | if (runif() < 0.5) { 864 | p.Frequency.Slide = runif(0.5, 0.2); 865 | p.Frequency.RepeatSpeed = runif(0.4, 0.4); 866 | } else { 867 | p.Frequency.Slide = runif(0.2, 0.05); 868 | if (runif() < 0.5) { 869 | p.Vibrato.Depth = runif(0.6, 0.1); 870 | p.Vibrato.Frequency = runif(30, 10); 871 | } 872 | } 873 | 874 | p.Volume.Sustain = runif(0.4); 875 | p.Volume.Decay = runif(0.4, 0.1); 876 | 877 | RemoveEmptyParams(p); 878 | return p; 879 | }, 880 | Hit: function() { 881 | var p = EmptyParams(); 882 | p.Generator.Func = rchoose(['square', 'saw', 'noise']); 883 | p.Generator.A = runif(0.6); 884 | p.Generator.ASlide = runif(1, -0.5); 885 | 886 | p.Frequency.Start = runif(880, 220); 887 | p.Frequency.Slide = -runif(0.4, 0.3); 888 | 889 | p.Volume.Sustain = runif(0.1); 890 | p.Volume.Decay = runif(0.2, 0.1); 891 | 892 | if (runif() < 0.5) { 893 | p.Filter.HP = runif(0.3); 894 | } 895 | 896 | RemoveEmptyParams(p); 897 | return p; 898 | }, 899 | Jump: function() { 900 | var p = EmptyParams(); 901 | p.Generator.Func = 'square'; 902 | p.Generator.A = runif(0.6); 903 | 904 | p.Frequency.Start = runif(330, 330); 905 | p.Frequency.Slide = runif(0.4, 0.2); 906 | 907 | p.Volume.Sustain = runif(0.3, 0.1); 908 | p.Volume.Decay = runif(0.2, 0.1); 909 | 910 | if (runif() < 0.5) { 911 | p.Filter.HP = runif(0.3); 912 | } 913 | if (runif() < 0.3) { 914 | p.Filter.LP = runif(-0.6, 1); 915 | } 916 | 917 | RemoveEmptyParams(p); 918 | return p; 919 | }, 920 | Select: function() { 921 | var p = EmptyParams(); 922 | p.Generator.Func = rchoose(['square', 'saw']); 923 | p.Generator.A = runif(0.6); 924 | 925 | p.Frequency.Start = runif(660, 220); 926 | 927 | p.Volume.Sustain = runif(0.1, 0.1); 928 | p.Volume.Decay = runif(0.2); 929 | 930 | p.Filter.HP = 0.2; 931 | RemoveEmptyParams(p); 932 | return p; 933 | }, 934 | Lucky: function() { 935 | var p = EmptyParams(); 936 | map_object(p, function(out, moduleName) { 937 | var defs = jsfx.Module[moduleName].params; 938 | map_object(defs, function(def, name) { 939 | if (def.C) { 940 | var values = Object_keys(def.C); 941 | out[name] = values[(values.length * Math.random()) | 0]; 942 | } else { 943 | out[name] = Math.random() * (def.H - def.L) + def.L; 944 | } 945 | }); 946 | }); 947 | p.Volume.Master = 0.4; 948 | p.Filter = {}; // disable filter, as it usually will clip everything 949 | RemoveEmptyParams(p); 950 | return p; 951 | } 952 | }; 953 | 954 | // GENERATORS 955 | 956 | // uniform noise 957 | jsfx.G.unoise = newGenerator("sample = Math.random();"); 958 | // sine wave 959 | jsfx.G.sine = newGenerator("sample = Math.sin(phase);"); 960 | // saw wave 961 | jsfx.G.saw = newGenerator("sample = 2*(phase/TAU - ((phase/TAU + 0.5)|0));"); 962 | // triangle wave 963 | jsfx.G.triangle = newGenerator("sample = Math.abs(4 * ((phase/TAU - 0.25)%1) - 2) - 1;"); 964 | // square wave 965 | jsfx.G.square = newGenerator("var s = Math.sin(phase); sample = s > A ? 1.0 : s < A ? -1.0 : A;"); 966 | // simple synth 967 | jsfx.G.synth = newGenerator("sample = Math.sin(phase) + .5*Math.sin(phase/2) + .3*Math.sin(phase/4);"); 968 | 969 | // STATEFUL 970 | var __noiseLast = 0; 971 | jsfx.G.noise = newGenerator("if(phase % TAU < 4){__noiseLast = Math.random() * 2 - 1;} sample = __noiseLast;"); 972 | 973 | // Karplus-Strong string 974 | jsfx.G.string = { 975 | create: function() { 976 | var BS = 1 << 16; 977 | var BM = BS - 1; 978 | 979 | var buffer = createFloatArray(BS); 980 | for (var i = 0; i < buffer.length; i++) { 981 | buffer[i] = Math.random() * 2 - 1; 982 | } 983 | 984 | var head = 0; 985 | return function($, block) { 986 | var TAU = Math.PI * 2; 987 | var A = +$.generatorA, 988 | ASlide = +$.generatorASlide, 989 | B = +$.generatorB, 990 | BSlide = +$.generatorBSlide; 991 | var buf = buffer; 992 | 993 | for (var i = 0; i < block.length; i++) { 994 | var phaseSpeed = block[i]; 995 | var n = (TAU / phaseSpeed) | 0; 996 | A += ASlide; 997 | B += BSlide; 998 | A = A < 0 ? 0 : A > 1 ? 1 : A; 999 | B = B < 0 ? 0 : B > 1 ? 1 : B; 1000 | 1001 | var t = ((head - n) + BS) & BM; 1002 | var sample = ( 1003 | buf[(t - 0 + BS) & BM] * 1 + 1004 | buf[(t - 1 + BS) & BM] * A + 1005 | buf[(t - 2 + BS) & BM] * B) / (1 + A + B); 1006 | 1007 | buf[head] = sample; 1008 | block[i] = buf[head]; 1009 | head = (head + 1) & BM; 1010 | } 1011 | 1012 | $.generatorA = A; 1013 | $.generatorB = B; 1014 | return block.length; 1015 | } 1016 | } 1017 | }; 1018 | 1019 | // Generates samples using given frequency and generator 1020 | function newGenerator(line) { 1021 | return new Function("$", "block", "" + 1022 | "var TAU = Math.PI * 2;\n" + 1023 | "var sample;\n" + 1024 | "var phase = +$.generatorPhase,\n" + 1025 | " A = +$.generatorA, ASlide = +$.generatorASlide,\n" + 1026 | " B = +$.generatorB, BSlide = +$.generatorBSlide;\n" + 1027 | "\n" + 1028 | "for(var i = 0; i < block.length; i++){\n" + 1029 | " var phaseSpeed = block[i];\n" + 1030 | " phase += phaseSpeed;\n" + 1031 | " if(phase > TAU){ phase -= TAU };\n" + 1032 | " A += ASlide; B += BSlide;\n" + 1033 | " A = A < 0 ? 0 : A > 1 ? 1 : A;\n" + 1034 | " B = B < 0 ? 0 : B > 1 ? 1 : B;\n" + 1035 | line + 1036 | " block[i] = sample;\n" + 1037 | "}\n" + 1038 | "\n" + 1039 | "$.generatorPhase = phase;\n" + 1040 | "$.generatorA = A;\n" + 1041 | "$.generatorB = B;\n" + 1042 | "return block.length;\n" + 1043 | ""); 1044 | } 1045 | 1046 | // WAVE SUPPORT 1047 | 1048 | // Creates an Wave byte array from audio data [-1.0 .. 1.0] 1049 | jsfx.CreateWave = CreateWave; 1050 | 1051 | function CreateWave(data) { 1052 | if (typeof Float32Array !== "undefined") { 1053 | assert(data instanceof Float32Array, 'data must be an Float32Array'); 1054 | } 1055 | 1056 | var blockAlign = numChannels * bitsPerSample >> 3; 1057 | var byteRate = jsfx.SampleRate * blockAlign; 1058 | 1059 | var output = createByteArray(8 + 36 + data.length * 2); 1060 | var p = 0; 1061 | 1062 | // emits string to output 1063 | function S(value) { 1064 | for (var i = 0; i < value.length; i += 1) { 1065 | output[p] = value.charCodeAt(i); 1066 | p++; 1067 | } 1068 | } 1069 | 1070 | // emits integer value to output 1071 | function V(value, nBytes) { 1072 | if (nBytes <= 0) { 1073 | return; 1074 | } 1075 | output[p] = value & 0xFF; 1076 | p++; 1077 | V(value >> 8, nBytes - 1); 1078 | } 1079 | /* beautify preserve:start */ 1080 | S('RIFF'); V(36 + data.length * 2, 4); 1081 | 1082 | S('WAVEfmt '); V(16, 4); V(1, 2); 1083 | V(numChannels, 2); V(jsfx.SampleRate, 4); 1084 | V(byteRate, 4); V(blockAlign, 2); V(bitsPerSample, 2); 1085 | 1086 | S('data'); V(data.length * 2, 4); 1087 | CopyFToU8(output.subarray(p), data); 1088 | /* beautify preserve:end */ 1089 | 1090 | return output; 1091 | }; 1092 | 1093 | // Creates an Audio element from audio data [-1.0 .. 1.0] 1094 | jsfx.CreateAudio = CreateAudio; 1095 | 1096 | function CreateAudio(data) { 1097 | var wave = CreateWave(data); 1098 | return new Audio('data:audio/wav;base64,' + U8ToB64(wave)); 1099 | }; 1100 | 1101 | jsfx.DownloadAsFile = function(audio) { 1102 | assert(audio instanceof Audio, 'input must be an Audio object'); 1103 | document.location.href = audio.src; 1104 | }; 1105 | 1106 | // HELPERS 1107 | jsfx.Util = {}; 1108 | 1109 | // Copies array of Floats to a Uint8Array with 16bits per sample 1110 | jsfx.Util.CopyFToU8 = CopyFToU8; 1111 | 1112 | function CopyFToU8(into, floats) { 1113 | assert(into.length / 2 == floats.length, 1114 | 'the target buffer must be twice as large as the iinput'); 1115 | 1116 | var k = 0; 1117 | for (var i = 0; i < floats.length; i++) { 1118 | var v = +floats[i]; 1119 | var a = (v * 0x7FFF) | 0; 1120 | a = a < -0x8000 ? -0x8000 : 0x7FFF < a ? 0x7FFF : a; 1121 | a += a < 0 ? 0x10000 : 0; 1122 | into[k] = a & 0xFF; 1123 | k++; 1124 | into[k] = a >> 8; 1125 | k++; 1126 | } 1127 | } 1128 | 1129 | // Encodes Uint8Array with base64 1130 | jsfx.Util.U8ToB64 = U8ToB64; 1131 | 1132 | function U8ToB64(data) { 1133 | var CHUNK = 0x8000; 1134 | var result = ''; 1135 | for (var start = 0; start < data.length; start += CHUNK) { 1136 | var end = Math.min(start + CHUNK, data.length); 1137 | result += String.fromCharCode.apply(null, data.subarray(start, end)); 1138 | } 1139 | return btoa(result); 1140 | } 1141 | 1142 | // uses AudioContext sampleRate or 44100; 1143 | function getDefaultSampleRate() { 1144 | if (typeof AudioContext !== 'undefined') { 1145 | return (new AudioContext()).sampleRate; 1146 | } 1147 | return 44100; 1148 | } 1149 | 1150 | // for checking pre/post conditions 1151 | function assert(condition, message) { 1152 | if (!condition) { 1153 | throw new Error(message); 1154 | } 1155 | } 1156 | 1157 | function clamp(v, min, max) { 1158 | v = +v; 1159 | min = +min; 1160 | max = +max; 1161 | if (v < min) { 1162 | return +min; 1163 | } 1164 | if (v > max) { 1165 | return +max; 1166 | } 1167 | return +v; 1168 | } 1169 | 1170 | function clamp1(v) { 1171 | v = +v; 1172 | if (v < +0.0) { 1173 | return +0.0; 1174 | } 1175 | if (v > +1.0) { 1176 | return +1.0; 1177 | } 1178 | return +v; 1179 | } 1180 | 1181 | function map_object(obj, fn) { 1182 | var r = {}; 1183 | for (var name in obj) { 1184 | if (obj.hasOwnProperty(name)) { 1185 | r[name] = fn(obj[name], name); 1186 | } 1187 | } 1188 | return r; 1189 | } 1190 | 1191 | // uniform random 1192 | function runif(scale, offset) { 1193 | var a = Math.random(); 1194 | if (scale !== undefined) 1195 | a *= scale; 1196 | if (offset !== undefined) 1197 | a += offset; 1198 | return a; 1199 | } 1200 | 1201 | function rchoose(gens) { 1202 | return gens[(gens.length * Math.random()) | 0]; 1203 | } 1204 | 1205 | function Object_keys(obj) { 1206 | var r = []; 1207 | for (var name in obj) { 1208 | r.push(name); 1209 | } 1210 | return r; 1211 | } 1212 | 1213 | jsfx._createFloatArray = createFloatArray; 1214 | 1215 | function createFloatArray(N) { 1216 | if (typeof Float32Array === "undefined") { 1217 | var r = new Array(N); 1218 | for (var i = 0; i < r.length; i++) { 1219 | r[i] = 0.0; 1220 | } 1221 | } 1222 | return new Float32Array(N); 1223 | } 1224 | 1225 | function createByteArray(N) { 1226 | if (typeof Uint8Array === "undefined") { 1227 | var r = new Array(N); 1228 | for (var i = 0; i < r.length; i++) { 1229 | r[i] = 0 | 0; 1230 | } 1231 | } 1232 | return new Uint8Array(N); 1233 | } 1234 | 1235 | return jsfx; 1236 | })); 1237 | --------------------------------------------------------------------------------