├── .DS_Store ├── .gitignore ├── LICENSE ├── README.md ├── compileWASM.bat ├── compileWASM.sh ├── cpp ├── dft ├── dft.cpp └── webdsp.cpp ├── demo.js ├── gulpfile.js ├── img ├── forward1.svg ├── loop1.svg ├── noloop1.svg ├── pause1.svg ├── play1.svg └── rewind1.svg ├── index.html ├── lib ├── .DS_Store ├── webdsp.js ├── webdsp_c.js ├── webdsp_c.wasm └── webdsp_polyfill.js ├── package.json ├── server.js ├── smoothie.js ├── style.css └── test ├── lib └── catch.hpp ├── test.cpp ├── test.js └── testCpp /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamadee/web-dsp-demo/97a57c6000ce5f803b45a0deaa483123b6e3b04b/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | media/ 3 | node_modules/ 4 | .DS_Store 5 | npm-debug.log 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## A client-side DSP library utilizing the power of WebAssembly (.wasm) 2 | 3 | This is the demo repo, to find the library itself please visit https://github.com/shamadee/web-dsp. The README below is the same as the library's. 4 | 5 | WebDSP is a collection of highly performant algorithms, which are designed to be building blocks for web applications that aim to operate on media data. The methods are written in C++ and compiled to WASM, and exposed as simple vanilla Javascript functions developers can run on the client side.
6 |
7 | WebAssembly is very young, and this is the first .wasm based library designed to be dropped in to existing production level JS code bases. With that in mind, there was an explicit goal to optimize and simplify how JS developers can load and use .wasm functionality. Just as important, was our goal to lay the groundwork for future open source WebAssembly module developers. 8 | 9 | ### Demo & Starter Kit 10 | 11 | Check out the [demo video editor](http://tiny.cc/webdsp) and [corresponding repo](https://github.com/shamadee/web-dsp-demo).
12 | 13 | To quickly start working with WebAssembly and to build your own modules, please see our started WebAssembly work environment you can npm install and launch [wasm-init](https://www.npmjs.com/package/wasm-init). 14 | 15 | ### Install 16 | 17 | Clone this repo and drop only the 'lib' folder into your project. Simply load our library file in a script tag. You can also get the module via `npm install web-dsp`, which comes with a built-in npm executable (`get-dsp`), which will copy the lib folder into your project directory. 18 | ```html 19 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 65 | -------------------------------------------------------------------------------- /lib/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamadee/web-dsp-demo/97a57c6000ce5f803b45a0deaa483123b6e3b04b/lib/.DS_Store -------------------------------------------------------------------------------- /lib/webdsp.js: -------------------------------------------------------------------------------- 1 | var Module = {}; 2 | 3 | function isImageData(element) { 4 | return element instanceof ImageData; 5 | }; 6 | 7 | function isCanvasImageSource(element) { 8 | return ( 9 | element instanceof HTMLCanvasElement || 10 | element instanceof HTMLImageElement || 11 | element instanceof HTMLVideoElement || 12 | element instanceof ImageBitmap 13 | ); 14 | }; 15 | 16 | function toCanvas(source) { 17 | console.assert(isCanvasImageSource(source)); 18 | if (source instanceof HTMLCanvasElement) { 19 | return source; 20 | } 21 | var canvas = document.createElement('canvas'); // draw to a temp canvas 22 | canvas.width = source.videoWidth || source.naturalWidth || source.width; 23 | canvas.height = source.videoHeight || source.naturalHeight || source.height; 24 | canvas.getContext('2d').drawImage(source, 0, 0, canvas.width, canvas.height); 25 | return canvas; 26 | }; 27 | 28 | // Reads image data from ImageData or CanvasImageSource(HTMLCanvasElement or HTMLImageElement or HTMLVideoElement or ImageBitmap) 29 | // TODO: maybe CanvasRenderingContext2D and Blob also? 30 | function readImageData(source) { 31 | console.assert(source); 32 | if (isImageData(source)) { 33 | return source; 34 | } 35 | var canvas = toCanvas(source); 36 | return canvas.getContext('2d').getImageData(0, 0, canvas.width, canvas.height); 37 | }; 38 | 39 | function writeImageDataToCanvas(canvas, data, width, height) { 40 | canvas.width = width; 41 | canvas.height = height; 42 | var context = canvas.getContext('2d'); 43 | var imageData = context.createImageData(width, height); 44 | imageData.data.set(data); 45 | context.putImageData(imageData, 0, 0); 46 | return canvas; 47 | }; 48 | 49 | // Writes image data into ImageData, HTMLCanvasElement, HTMLImageElement or creates a new canvas and appends it 50 | function writeImageData(dest, data, width, height) { 51 | console.assert(dest); 52 | 53 | if (typeof dest === 'function') { 54 | dest(data, width, height); 55 | return; 56 | } 57 | 58 | if (isImageData(dest)) { 59 | console.assert(dest.width === width, dest.height === height); 60 | dest.data.set(data); 61 | return; 62 | } 63 | 64 | console.assert(dest instanceof HTMLElement); 65 | console.assert(!(dest instanceof HTMLVideoElement), 'Cannot write to video element'); 66 | var canvas = (dest instanceof HTMLCanvasElement) ? dest : document.createElement('canvas'); 67 | writeImageDataToCanvas(canvas, data, width, height); 68 | 69 | if (!(dest instanceof HTMLCanvasElement)) { 70 | if (dest instanceof HTMLImageElement) { 71 | dest.src = canvas.toDataURL(); 72 | } else { 73 | dest.appendChild(canvas); 74 | } 75 | } 76 | }; 77 | 78 | function drawVideo(element, destination) { 79 | const canvas = document.createElement('canvas'); 80 | canvas.width = element.videoWidth; 81 | canvas.height = element.videoHeight; 82 | context = canvas.getContext('2d'); 83 | context.drawImage(element, 0, 0, canvas.width, canvas.height); 84 | pixels = context.getImageData(0, 0, element.width, element.height); 85 | pixels.data.set(filter(pixels.data)); 86 | // context.putImageData(pixels, 0, 0); 87 | if (!destination) { 88 | const dest = document.querySelector('#wasm-canvas'); 89 | if (!dest) { 90 | const newCanvas = document.createElement('canvas'); 91 | newCanvas.id = 'wasm-canvas'; 92 | const newContext = newCanvas.getContext('2d'); 93 | } 94 | } 95 | } 96 | 97 | function createFilter(filter) { 98 | 99 | return function(element, destination) { 100 | if (element instanceof HTMLVideoElement) { 101 | if (!element.paused) requestAnimationFrame(drawVideo(element, destination)); 102 | return; 103 | } 104 | if (element instanceof HTMLCanvasElement) { 105 | const context = element.getContext('2d'); 106 | const pixels = context.getImageData(0, 0, element.width, element.height); 107 | pixels.data.set(filter(pixels.data)); 108 | context.putImageData(pixels, 0, 0); 109 | } 110 | } 111 | } 112 | 113 | function loadWASM() { 114 | return new Promise((resolve, reject) => { 115 | if (!('WebAssembly' in window)) { 116 | console.log('Couldn\'t load WASM, loading JS Fallback'); 117 | const fbScript = document.createElement('script'); 118 | fbScript.src = './lib/webdsp_polyfill.js'; 119 | fbScript.onload = function() { 120 | return resolve(jsFallback()); 121 | } 122 | document.body.appendChild(fbScript); 123 | } else { 124 | // TODO: use xmlhttprequest where fetch not supported 125 | fetch('./lib/webdsp_c.wasm') 126 | .then(response => response.arrayBuffer()) 127 | .then(buffer => { 128 | Module.wasmBinary = buffer; 129 | // GLOBAL -- create custom event for complete glue script execution 130 | script = document.createElement('script'); 131 | doneEvent = new Event('done'); 132 | script.addEventListener('done', buildWam); 133 | // END GLOBAL 134 | 135 | // TODO: IN EMSCRIPTEN GLUE INSERT 136 | // else{doRun()} ... 137 | // script.dispatchEvent(doneEvent); 138 | // ... }Module["run"] 139 | 140 | script.src = './lib/webdsp_c.js'; 141 | document.body.appendChild(script); 142 | 143 | function buildWam() { 144 | console.log('Emscripten boilerplate loaded.'); 145 | const wam = {}; 146 | 147 | // filters 148 | wam['grayScale'] = function (pixelData) { 149 | const len = pixelData.length 150 | const mem = _malloc(len); 151 | HEAPU8.set(pixelData, mem); 152 | _grayScale(mem, len); 153 | const filtered = HEAPU8.subarray(mem, mem + len); 154 | _free(mem); 155 | return filtered; 156 | }; 157 | wam['brighten'] = function (pixelData, brightness=25) { 158 | const len = pixelData.length; 159 | const mem = _malloc(len); 160 | HEAPU8.set(pixelData, mem); 161 | _brighten(mem, len, brightness); 162 | const filtered = HEAPU8.subarray(mem, mem + len); 163 | _free(mem); 164 | return filtered; 165 | }; 166 | wam['invert'] = function (pixelData) { 167 | const len = pixelData.length; 168 | const mem = _malloc(len); 169 | HEAPU8.set(pixelData, mem); 170 | _invert(mem, len); 171 | const filtered = HEAPU8.subarray(mem, mem + len); 172 | _free(mem); 173 | return filtered; 174 | }; 175 | wam['noise'] = function (pixelData) { 176 | const len = pixelData.length; 177 | const mem = _malloc(len * Float32Array.BYTES_PER_ELEMENT); 178 | HEAPF32.set(pixelData, mem / Float32Array.BYTES_PER_ELEMENT); 179 | _noise(mem, len); 180 | const filtered = HEAPF32.subarray(mem / Float32Array.BYTES_PER_ELEMENT, mem / Float32Array.BYTES_PER_ELEMENT + len); 181 | _free(mem); 182 | return filtered; 183 | }; 184 | //MultiFilter Family of Functions 185 | wam['multiFilter'] = function (pixelData, width, filterType, mag, multiplier, adj) { 186 | const len = pixelData.length; 187 | const mem = _malloc(len); 188 | HEAPU8.set(pixelData, mem); 189 | _multiFilter(mem, len, width, filterType, mag, multiplier, adj); 190 | const filtered = HEAPU8.subarray(mem, mem + len); 191 | _free(mem); 192 | return filtered; 193 | }; 194 | wam['multiFilterFloat'] = function (pixelData, width, filterType, mag, multiplier, adj) { 195 | const len = pixelData.length; 196 | const mem = _malloc(len * Float32Array.BYTES_PER_ELEMENT); 197 | HEAPF32.set(pixelData, mem / Float32Array.BYTES_PER_ELEMENT); 198 | _multiFilterFloat(mem, len, width, filterType, mag, multiplier, adj); 199 | const filtered = HEAPF32.subarray(mem / Float32Array.BYTES_PER_ELEMENT, mem / Float32Array.BYTES_PER_ELEMENT + len); 200 | _free(mem); 201 | return filtered; 202 | }; 203 | //bindLastArgs is defined and hoisted from below the module load 204 | let mag = 127, mult = 2, adj = 4; 205 | let filt = wam['multiFilter']; 206 | let filtFloat = wam['multiFilterFloat']; 207 | wam['sunset'] = bindLastArgs(filt, 4, mag, mult, adj); 208 | wam['analogTV'] = bindLastArgs(filt, 7, mag, mult, adj); 209 | wam['emboss'] = bindLastArgs(filt, 1, mag, mult, adj); 210 | wam['urple'] = bindLastArgs(filt, 2, mag, mult, adj); 211 | wam['forest'] = bindLastArgs(filtFloat, 5, mag, 3, 1); 212 | wam['romance'] = bindLastArgs(filtFloat, 8, mag, 3, 2); 213 | wam['hippo'] = bindLastArgs(filtFloat, 2, 80, 3, 2); 214 | wam['longhorn'] = bindLastArgs(filtFloat, 2, 27, 3, 2); 215 | wam['underground'] = bindLastArgs(filt, 8, mag, 1, 4); 216 | wam['rooster'] = bindLastArgs(filt, 8, 60, 1, 4); 217 | wam['moss'] = bindLastArgs(filtFloat, 1, mag, 1, 1); 218 | wam['mist'] = bindLastArgs(filt, 1, mag, 1, 1); 219 | wam['kaleidoscope'] = bindLastArgs(filt, 1, 124, 4, 3); 220 | wam['tingle'] = bindLastArgs(filtFloat, 1, 124, 4, 3); 221 | wam['bacteria'] = bindLastArgs(filt, 4, 0, 2, 4); 222 | wam['hulk'] = bindLastArgs(filt, 2, 10, 2, 4); 223 | wam['ghost'] = bindLastArgs(filt, 1, 5, 2, 4); 224 | wam['swamp'] = bindLastArgs(filtFloat, 1, 40, 2, 3); 225 | wam['twisted'] = bindLastArgs(filt, 1, 40, 2, 3); 226 | wam['security'] = bindLastArgs(filt, 1, 120, 1, 0); 227 | wam['robbery'] = bindLastArgs(filtFloat, 1, 120, 1, 0); 228 | //end filters from multiFilter family 229 | 230 | wam['sobelFilter'] = function (pixelData, width, height, invert=false) { 231 | const len = pixelData.length; 232 | const mem = _malloc(len); 233 | HEAPU8.set(pixelData, mem); 234 | _sobelFilter(mem, width, height, invert); 235 | const filtered = HEAPU8.subarray(mem, mem + len); 236 | _free(mem); 237 | return filtered; 238 | }; 239 | //convFilter family of filters 240 | wam['convFilter'] = function(pixelData, width, height, kernel, divisor, bias=0, count=1) { 241 | const arLen = pixelData.length; 242 | const memData = _malloc(arLen * Float32Array.BYTES_PER_ELEMENT); 243 | HEAPF32.set(pixelData, memData / Float32Array.BYTES_PER_ELEMENT); 244 | const kerWidth = kernel[0].length; 245 | const kerHeight = kernel.length; 246 | const kerLen = kerWidth * kerHeight; 247 | const flatKernel = kernel.reduce((acc, cur) => acc.concat(cur)); 248 | const memKernel = _malloc(kerLen * Float32Array.BYTES_PER_ELEMENT); 249 | HEAPF32.set(flatKernel, memKernel / Float32Array.BYTES_PER_ELEMENT); 250 | _convFilter(memData, width, height, memKernel, 3, 3, divisor, bias, count); 251 | const filtered = HEAPF32.subarray(memData / Float32Array.BYTES_PER_ELEMENT, memData / Float32Array.BYTES_PER_ELEMENT + arLen); 252 | _free(memData); 253 | _free(memKernel); 254 | return filtered; 255 | } 256 | //defining kernel and other parameters before each function definition 257 | let kernel = [[1, 1, 1], [1, 1, 1], [1, 1, 1]], divisor = 9, bias = 0, count = 1; 258 | let conv = wam['convFilter']; 259 | wam['blur'] = bindLastArgs(conv, kernel, divisor, bias, 3); 260 | kernel = [[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]], divisor = 1; 261 | wam['strongSharpen'] = bindLastArgs(conv, kernel, divisor); 262 | kernel = [[0, -1, 0], [-1, 5, -1], [0, -1, 0]], divisor = 2; 263 | wam['sharpen'] = bindLastArgs(conv, kernel, divisor); 264 | kernel = [[1, -1, -1], [-1, 8, -1], [-1, -1, 1]], divisor = 3; 265 | wam['clarity'] = bindLastArgs(conv, kernel, divisor); 266 | kernel = [[-1, -1, 1], [-1, 14, -1], [1, -1, -1]], divisor = 3; 267 | wam['goodMorning'] = bindLastArgs(conv, kernel, divisor); 268 | kernel = [[4, -1, -1], [-1, 4, -1], [0, -1, 4]], divisor = 3; 269 | wam['acid'] = bindLastArgs(conv, kernel, divisor); 270 | kernel = [[0, 0, -1], [-1, 12, -1], [0, -1, -1]], divisor = 2; 271 | wam['dewdrops'] = bindLastArgs(conv, kernel, divisor); 272 | kernel = [[-1, -1, 4], [-1, 9, -1], [0, -1, 0]], divisor = 2; 273 | wam['destruction'] = bindLastArgs(conv, kernel, divisor); 274 | //end convFilter family of filters 275 | // for (let prop in wam) { 276 | // wam[prop] = createFilter(returnsOnceMallocedFilter(prop)); 277 | // } 278 | resolve(wam); 279 | }; 280 | }); 281 | } 282 | }); 283 | } 284 | //to bind arguments in the right order 285 | function bindLastArgs (func, ...boundArgs){ 286 | return function (...baseArgs) { 287 | return func(...baseArgs, ...boundArgs); 288 | } 289 | } 290 | -------------------------------------------------------------------------------- /lib/webdsp_c.js: -------------------------------------------------------------------------------- 1 | var Module;if(!Module)Module=(typeof Module!=="undefined"?Module:null)||{};var moduleOverrides={};for(var key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;if(Module["ENVIRONMENT"]){if(Module["ENVIRONMENT"]==="WEB"){ENVIRONMENT_IS_WEB=true}else if(Module["ENVIRONMENT"]==="WORKER"){ENVIRONMENT_IS_WORKER=true}else if(Module["ENVIRONMENT"]==="NODE"){ENVIRONMENT_IS_NODE=true}else if(Module["ENVIRONMENT"]==="SHELL"){ENVIRONMENT_IS_SHELL=true}else{throw new Error("The provided Module['ENVIRONMENT'] value is not valid. It must be one of: WEB|WORKER|NODE|SHELL.")}}else{ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof require==="function"&&!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_WORKER;ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER}if(ENVIRONMENT_IS_NODE){if(!Module["print"])Module["print"]=console.log;if(!Module["printErr"])Module["printErr"]=console.warn;var nodeFS;var nodePath;Module["read"]=function read(filename,binary){if(!nodeFS)nodeFS=require("fs");if(!nodePath)nodePath=require("path");filename=nodePath["normalize"](filename);var ret=nodeFS["readFileSync"](filename);return binary?ret:ret.toString()};Module["readBinary"]=function readBinary(filename){var ret=Module["read"](filename,true);if(!ret.buffer){ret=new Uint8Array(ret)}assert(ret.buffer);return ret};Module["load"]=function load(f){globalEval(read(f))};if(!Module["thisProgram"]){if(process["argv"].length>1){Module["thisProgram"]=process["argv"][1].replace(/\\/g,"/")}else{Module["thisProgram"]="unknown-program"}}Module["arguments"]=process["argv"].slice(2);if(typeof module!=="undefined"){module["exports"]=Module}process["on"]("uncaughtException",(function(ex){if(!(ex instanceof ExitStatus)){throw ex}}));Module["inspect"]=(function(){return"[Emscripten Module object]"})}else if(ENVIRONMENT_IS_SHELL){if(!Module["print"])Module["print"]=print;if(typeof printErr!="undefined")Module["printErr"]=printErr;if(typeof read!="undefined"){Module["read"]=read}else{Module["read"]=function read(){throw"no read() available"}}Module["readBinary"]=function readBinary(f){if(typeof readbuffer==="function"){return new Uint8Array(readbuffer(f))}var data=read(f,"binary");assert(typeof data==="object");return data};if(typeof scriptArgs!="undefined"){Module["arguments"]=scriptArgs}else if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof quit==="function"){Module["quit"]=(function(status,toThrow){quit(status)})}}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){Module["read"]=function read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){Module["readBinary"]=function read(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return xhr.response}}Module["readAsync"]=function readAsync(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function xhr_onload(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response)}else{onerror()}};xhr.onerror=onerror;xhr.send(null)};if(typeof arguments!="undefined"){Module["arguments"]=arguments}if(typeof console!=="undefined"){if(!Module["print"])Module["print"]=function print(x){console.log(x)};if(!Module["printErr"])Module["printErr"]=function printErr(x){console.warn(x)}}else{var TRY_USE_DUMP=false;if(!Module["print"])Module["print"]=TRY_USE_DUMP&&typeof dump!=="undefined"?(function(x){dump(x)}):(function(x){})}if(ENVIRONMENT_IS_WORKER){Module["load"]=importScripts}if(typeof Module["setWindowTitle"]==="undefined"){Module["setWindowTitle"]=(function(title){document.title=title})}}else{throw"Unknown runtime environment. Where are we?"}function globalEval(x){eval.call(null,x)}if(!Module["load"]&&Module["read"]){Module["load"]=function load(f){globalEval(Module["read"](f))}}if(!Module["print"]){Module["print"]=(function(){})}if(!Module["printErr"]){Module["printErr"]=Module["print"]}if(!Module["arguments"]){Module["arguments"]=[]}if(!Module["thisProgram"]){Module["thisProgram"]="./this.program"}if(!Module["quit"]){Module["quit"]=(function(status,toThrow){throw toThrow})}Module.print=Module["print"];Module.printErr=Module["printErr"];Module["preRun"]=[];Module["postRun"]=[];for(var key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=undefined;var Runtime={setTempRet0:(function(value){tempRet0=value;return value}),getTempRet0:(function(){return tempRet0}),stackSave:(function(){return STACKTOP}),stackRestore:(function(stackTop){STACKTOP=stackTop}),getNativeTypeSize:(function(type){switch(type){case"i1":case"i8":return 1;case"i16":return 2;case"i32":return 4;case"i64":return 8;case"float":return 4;case"double":return 8;default:{if(type[type.length-1]==="*"){return Runtime.QUANTUM_SIZE}else if(type[0]==="i"){var bits=parseInt(type.substr(1));assert(bits%8===0);return bits/8}else{return 0}}}}),getNativeFieldSize:(function(type){return Math.max(Runtime.getNativeTypeSize(type),Runtime.QUANTUM_SIZE)}),STACK_ALIGN:16,prepVararg:(function(ptr,type){if(type==="double"||type==="i64"){if(ptr&7){assert((ptr&7)===4);ptr+=4}}else{assert((ptr&3)===0)}return ptr}),getAlignSize:(function(type,size,vararg){if(!vararg&&(type=="i64"||type=="double"))return 8;if(!type)return Math.min(size,8);return Math.min(size||(type?Runtime.getNativeFieldSize(type):0),Runtime.QUANTUM_SIZE)}),dynCall:(function(sig,ptr,args){if(args&&args.length){return Module["dynCall_"+sig].apply(null,[ptr].concat(args))}else{return Module["dynCall_"+sig].call(null,ptr)}}),functionPointers:[],addFunction:(function(func){for(var i=0;i>2];var end=(ret+size+15|0)&-16;HEAP32[DYNAMICTOP_PTR>>2]=end;if(end>=TOTAL_MEMORY){var success=enlargeMemory();if(!success){HEAP32[DYNAMICTOP_PTR>>2]=ret;return 0}}return ret}),alignMemory:(function(size,quantum){var ret=size=Math.ceil(size/(quantum?quantum:16))*(quantum?quantum:16);return ret}),makeBigInt:(function(low,high,unsigned){var ret=unsigned?+(low>>>0)+ +(high>>>0)*4294967296:+(low>>>0)+ +(high|0)*4294967296;return ret}),GLOBAL_BASE:1024,QUANTUM_SIZE:4,__dummy__:0};Module["Runtime"]=Runtime;var ABORT=0;var EXITSTATUS=0;function assert(condition,text){if(!condition){abort("Assertion failed: "+text)}}function getCFunc(ident){var func=Module["_"+ident];if(!func){try{func=eval("_"+ident)}catch(e){}}assert(func,"Cannot call unknown function "+ident+" (perhaps LLVM optimizations or closure removed it?)");return func}var cwrap,ccall;((function(){var JSfuncs={"stackSave":(function(){Runtime.stackSave()}),"stackRestore":(function(){Runtime.stackRestore()}),"arrayToC":(function(arr){var ret=Runtime.stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}),"stringToC":(function(str){var ret=0;if(str!==null&&str!==undefined&&str!==0){var len=(str.length<<2)+1;ret=Runtime.stackAlloc(len);stringToUTF8(str,ret,len)}return ret})};var toC={"string":JSfuncs["stringToC"],"array":JSfuncs["arrayToC"]};ccall=function ccallFunc(ident,returnType,argTypes,args,opts){var func=getCFunc(ident);var cArgs=[];var stack=0;if(args){for(var i=0;i>0]=value;break;case"i8":HEAP8[ptr>>0]=value;break;case"i16":HEAP16[ptr>>1]=value;break;case"i32":HEAP32[ptr>>2]=value;break;case"i64":tempI64=[value>>>0,(tempDouble=value,+Math_abs(tempDouble)>=1?tempDouble>0?(Math_min(+Math_floor(tempDouble/4294967296),4294967295)|0)>>>0:~~+Math_ceil((tempDouble- +(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[ptr>>2]=tempI64[0],HEAP32[ptr+4>>2]=tempI64[1];break;case"float":HEAPF32[ptr>>2]=value;break;case"double":HEAPF64[ptr>>3]=value;break;default:abort("invalid type for setValue: "+type)}}Module["setValue"]=setValue;function getValue(ptr,type,noSafe){type=type||"i8";if(type.charAt(type.length-1)==="*")type="i32";switch(type){case"i1":return HEAP8[ptr>>0];case"i8":return HEAP8[ptr>>0];case"i16":return HEAP16[ptr>>1];case"i32":return HEAP32[ptr>>2];case"i64":return HEAP32[ptr>>2];case"float":return HEAPF32[ptr>>2];case"double":return HEAPF64[ptr>>3];default:abort("invalid type for setValue: "+type)}return null}Module["getValue"]=getValue;var ALLOC_NORMAL=0;var ALLOC_STACK=1;var ALLOC_STATIC=2;var ALLOC_DYNAMIC=3;var ALLOC_NONE=4;Module["ALLOC_NORMAL"]=ALLOC_NORMAL;Module["ALLOC_STACK"]=ALLOC_STACK;Module["ALLOC_STATIC"]=ALLOC_STATIC;Module["ALLOC_DYNAMIC"]=ALLOC_DYNAMIC;Module["ALLOC_NONE"]=ALLOC_NONE;function allocate(slab,types,allocator,ptr){var zeroinit,size;if(typeof slab==="number"){zeroinit=true;size=slab}else{zeroinit=false;size=slab.length}var singleType=typeof types==="string"?types:null;var ret;if(allocator==ALLOC_NONE){ret=ptr}else{ret=[typeof _malloc==="function"?_malloc:Runtime.staticAlloc,Runtime.stackAlloc,Runtime.staticAlloc,Runtime.dynamicAlloc][allocator===undefined?ALLOC_STATIC:allocator](Math.max(size,singleType?1:types.length))}if(zeroinit){var ptr=ret,stop;assert((ret&3)==0);stop=ret+(size&~3);for(;ptr>2]=0}stop=ret+size;while(ptr>0]=0}return ret}if(singleType==="i8"){if(slab.subarray||slab.slice){HEAPU8.set(slab,ret)}else{HEAPU8.set(new Uint8Array(slab),ret)}return ret}var i=0,type,typeSize,previousType;while(i>0];hasUtf|=t;if(t==0&&!length)break;i++;if(length&&i==length)break}if(!length)length=i;var ret="";if(hasUtf<128){var MAX_CHUNK=1024;var curr;while(length>0){curr=String.fromCharCode.apply(String,HEAPU8.subarray(ptr,ptr+Math.min(length,MAX_CHUNK)));ret=ret?ret+curr:curr;ptr+=MAX_CHUNK;length-=MAX_CHUNK}return ret}return Module["UTF8ToString"](ptr)}Module["Pointer_stringify"]=Pointer_stringify;function AsciiToString(ptr){var str="";while(1){var ch=HEAP8[ptr++>>0];if(!ch)return str;str+=String.fromCharCode(ch)}}Module["AsciiToString"]=AsciiToString;function stringToAscii(str,outPtr){return writeAsciiToMemory(str,outPtr,false)}Module["stringToAscii"]=stringToAscii;var UTF8Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf8"):undefined;function UTF8ArrayToString(u8Array,idx){var endPtr=idx;while(u8Array[endPtr])++endPtr;if(endPtr-idx>16&&u8Array.subarray&&UTF8Decoder){return UTF8Decoder.decode(u8Array.subarray(idx,endPtr))}else{var u0,u1,u2,u3,u4,u5;var str="";while(1){u0=u8Array[idx++];if(!u0)return str;if(!(u0&128)){str+=String.fromCharCode(u0);continue}u1=u8Array[idx++]&63;if((u0&224)==192){str+=String.fromCharCode((u0&31)<<6|u1);continue}u2=u8Array[idx++]&63;if((u0&240)==224){u0=(u0&15)<<12|u1<<6|u2}else{u3=u8Array[idx++]&63;if((u0&248)==240){u0=(u0&7)<<18|u1<<12|u2<<6|u3}else{u4=u8Array[idx++]&63;if((u0&252)==248){u0=(u0&3)<<24|u1<<18|u2<<12|u3<<6|u4}else{u5=u8Array[idx++]&63;u0=(u0&1)<<30|u1<<24|u2<<18|u3<<12|u4<<6|u5}}}if(u0<65536){str+=String.fromCharCode(u0)}else{var ch=u0-65536;str+=String.fromCharCode(55296|ch>>10,56320|ch&1023)}}}}Module["UTF8ArrayToString"]=UTF8ArrayToString;function UTF8ToString(ptr){return UTF8ArrayToString(HEAPU8,ptr)}Module["UTF8ToString"]=UTF8ToString;function stringToUTF8Array(str,outU8Array,outIdx,maxBytesToWrite){if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){if(outIdx>=endIdx)break;outU8Array[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;outU8Array[outIdx++]=192|u>>6;outU8Array[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;outU8Array[outIdx++]=224|u>>12;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=2097151){if(outIdx+3>=endIdx)break;outU8Array[outIdx++]=240|u>>18;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else if(u<=67108863){if(outIdx+4>=endIdx)break;outU8Array[outIdx++]=248|u>>24;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}else{if(outIdx+5>=endIdx)break;outU8Array[outIdx++]=252|u>>30;outU8Array[outIdx++]=128|u>>24&63;outU8Array[outIdx++]=128|u>>18&63;outU8Array[outIdx++]=128|u>>12&63;outU8Array[outIdx++]=128|u>>6&63;outU8Array[outIdx++]=128|u&63}}outU8Array[outIdx]=0;return outIdx-startIdx}Module["stringToUTF8Array"]=stringToUTF8Array;function stringToUTF8(str,outPtr,maxBytesToWrite){return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)}Module["stringToUTF8"]=stringToUTF8;function lengthBytesUTF8(str){var len=0;for(var i=0;i=55296&&u<=57343)u=65536+((u&1023)<<10)|str.charCodeAt(++i)&1023;if(u<=127){++len}else if(u<=2047){len+=2}else if(u<=65535){len+=3}else if(u<=2097151){len+=4}else if(u<=67108863){len+=5}else{len+=6}}return len}Module["lengthBytesUTF8"]=lengthBytesUTF8;var UTF16Decoder=typeof TextDecoder!=="undefined"?new TextDecoder("utf-16le"):undefined;function demangle(func){var __cxa_demangle_func=Module["___cxa_demangle"]||Module["__cxa_demangle"];if(__cxa_demangle_func){try{var s=func.substr(1);var len=lengthBytesUTF8(s)+1;var buf=_malloc(len);stringToUTF8(s,buf,len);var status=_malloc(4);var ret=__cxa_demangle_func(buf,0,0,status);if(getValue(status,"i32")===0&&ret){return Pointer_stringify(ret)}}catch(e){}finally{if(buf)_free(buf);if(status)_free(status);if(ret)_free(ret)}return func}Runtime.warnOnce("warning: build with -s DEMANGLE_SUPPORT=1 to link in libcxxabi demangling");return func}function demangleAll(text){var regex=/__Z[\w\d_]+/g;return text.replace(regex,(function(x){var y=demangle(x);return x===y?x:x+" ["+y+"]"}))}function jsStackTrace(){var err=new Error;if(!err.stack){try{throw new Error(0)}catch(e){err=e}if(!err.stack){return"(no stack trace available)"}}return err.stack.toString()}function stackTrace(){var js=jsStackTrace();if(Module["extraStackTrace"])js+="\n"+Module["extraStackTrace"]();return demangleAll(js)}Module["stackTrace"]=stackTrace;var WASM_PAGE_SIZE=65536;var ASMJS_PAGE_SIZE=16777216;var MIN_TOTAL_MEMORY=16777216;function alignUp(x,multiple){if(x%multiple>0){x+=multiple-x%multiple}return x}var HEAP;var buffer;var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBuffer(buf){Module["buffer"]=buffer=buf}function updateGlobalBufferViews(){Module["HEAP8"]=HEAP8=new Int8Array(buffer);Module["HEAP16"]=HEAP16=new Int16Array(buffer);Module["HEAP32"]=HEAP32=new Int32Array(buffer);Module["HEAPU8"]=HEAPU8=new Uint8Array(buffer);Module["HEAPU16"]=HEAPU16=new Uint16Array(buffer);Module["HEAPU32"]=HEAPU32=new Uint32Array(buffer);Module["HEAPF32"]=HEAPF32=new Float32Array(buffer);Module["HEAPF64"]=HEAPF64=new Float64Array(buffer)}var STATIC_BASE,STATICTOP,staticSealed;var STACK_BASE,STACKTOP,STACK_MAX;var DYNAMIC_BASE,DYNAMICTOP_PTR;STATIC_BASE=STATICTOP=STACK_BASE=STACKTOP=STACK_MAX=DYNAMIC_BASE=DYNAMICTOP_PTR=0;staticSealed=false;function abortOnCannotGrowMemory(){abort("Cannot enlarge memory arrays. Either (1) compile with -s TOTAL_MEMORY=X with X higher than the current value "+TOTAL_MEMORY+", (2) compile with -s ALLOW_MEMORY_GROWTH=1 which allows increasing the size at runtime, or (3) if you want malloc to return NULL (0) instead of this abort, compile with -s ABORTING_MALLOC=0 ")}if(!Module["reallocBuffer"])Module["reallocBuffer"]=(function(size){var ret;try{if(ArrayBuffer.transfer){ret=ArrayBuffer.transfer(buffer,size)}else{var oldHEAP8=HEAP8;ret=new ArrayBuffer(size);var temp=new Int8Array(ret);temp.set(oldHEAP8)}}catch(e){return false}var success=_emscripten_replace_memory(ret);if(!success)return false;return ret});function enlargeMemory(){var PAGE_MULTIPLE=Module["usingWasm"]?WASM_PAGE_SIZE:ASMJS_PAGE_SIZE;var LIMIT=2147483648-PAGE_MULTIPLE;if(HEAP32[DYNAMICTOP_PTR>>2]>LIMIT){return false}TOTAL_MEMORY=Math.max(TOTAL_MEMORY,MIN_TOTAL_MEMORY);while(TOTAL_MEMORY>2]){if(TOTAL_MEMORY<=536870912){TOTAL_MEMORY=alignUp(2*TOTAL_MEMORY,PAGE_MULTIPLE)}else{TOTAL_MEMORY=Math.min(alignUp((3*TOTAL_MEMORY+2147483648)/4,PAGE_MULTIPLE),LIMIT)}}var replacement=Module["reallocBuffer"](TOTAL_MEMORY);if(!replacement||replacement.byteLength!=TOTAL_MEMORY){return false}updateGlobalBuffer(replacement);updateGlobalBufferViews();return true}var byteLength;try{byteLength=Function.prototype.call.bind(Object.getOwnPropertyDescriptor(ArrayBuffer.prototype,"byteLength").get);byteLength(new ArrayBuffer(4))}catch(e){byteLength=(function(buffer){return buffer.byteLength})}var TOTAL_STACK=Module["TOTAL_STACK"]||5242880;var TOTAL_MEMORY=Module["TOTAL_MEMORY"]||16777216;if(TOTAL_MEMORY0){var callback=callbacks.shift();if(typeof callback=="function"){callback();continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){Module["dynCall_v"](func)}else{Module["dynCall_vi"](func,callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATMAIN__=[];var __ATEXIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;var runtimeExited=false;function preRun(){if(Module["preRun"]){if(typeof Module["preRun"]=="function")Module["preRun"]=[Module["preRun"]];while(Module["preRun"].length){addOnPreRun(Module["preRun"].shift())}}callRuntimeCallbacks(__ATPRERUN__)}function ensureInitRuntime(){if(runtimeInitialized)return;runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}function preMain(){callRuntimeCallbacks(__ATMAIN__)}function exitRuntime(){callRuntimeCallbacks(__ATEXIT__);runtimeExited=true}function postRun(){if(Module["postRun"]){if(typeof Module["postRun"]=="function")Module["postRun"]=[Module["postRun"]];while(Module["postRun"].length){addOnPostRun(Module["postRun"].shift())}}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}Module["addOnPreRun"]=addOnPreRun;function addOnInit(cb){__ATINIT__.unshift(cb)}Module["addOnInit"]=addOnInit;function addOnPreMain(cb){__ATMAIN__.unshift(cb)}Module["addOnPreMain"]=addOnPreMain;function addOnExit(cb){__ATEXIT__.unshift(cb)}Module["addOnExit"]=addOnExit;function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}Module["addOnPostRun"]=addOnPostRun;function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}Module["intArrayFromString"]=intArrayFromString;function intArrayToString(array){var ret=[];for(var i=0;i255){chr&=255}ret.push(String.fromCharCode(chr))}return ret.join("")}Module["intArrayToString"]=intArrayToString;function writeStringToMemory(string,buffer,dontAddNull){Runtime.warnOnce("writeStringToMemory is deprecated and should not be called! Use stringToUTF8() instead!");var lastChar,end;if(dontAddNull){end=buffer+lengthBytesUTF8(string);lastChar=HEAP8[end]}stringToUTF8(string,buffer,Infinity);if(dontAddNull)HEAP8[end]=lastChar}Module["writeStringToMemory"]=writeStringToMemory;function writeArrayToMemory(array,buffer){HEAP8.set(array,buffer)}Module["writeArrayToMemory"]=writeArrayToMemory;function writeAsciiToMemory(str,buffer,dontAddNull){for(var i=0;i>0]=str.charCodeAt(i)}if(!dontAddNull)HEAP8[buffer>>0]=0}Module["writeAsciiToMemory"]=writeAsciiToMemory;if(!Math["imul"]||Math["imul"](4294967295,5)!==-5)Math["imul"]=function imul(a,b){var ah=a>>>16;var al=a&65535;var bh=b>>>16;var bl=b&65535;return al*bl+(ah*bl+al*bh<<16)|0};Math.imul=Math["imul"];if(!Math["fround"]){var froundBuffer=new Float32Array(1);Math["fround"]=(function(x){froundBuffer[0]=x;return froundBuffer[0]})}Math.fround=Math["fround"];if(!Math["clz32"])Math["clz32"]=(function(x){x=x>>>0;for(var i=0;i<32;i++){if(x&1<<31-i)return i}return 32});Math.clz32=Math["clz32"];if(!Math["trunc"])Math["trunc"]=(function(x){return x<0?Math.ceil(x):Math.floor(x)});Math.trunc=Math["trunc"];var Math_abs=Math.abs;var Math_cos=Math.cos;var Math_sin=Math.sin;var Math_tan=Math.tan;var Math_acos=Math.acos;var Math_asin=Math.asin;var Math_atan=Math.atan;var Math_atan2=Math.atan2;var Math_exp=Math.exp;var Math_log=Math.log;var Math_sqrt=Math.sqrt;var Math_ceil=Math.ceil;var Math_floor=Math.floor;var Math_pow=Math.pow;var Math_imul=Math.imul;var Math_fround=Math.fround;var Math_round=Math.round;var Math_min=Math.min;var Math_clz32=Math.clz32;var Math_trunc=Math.trunc;var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}Module["addRunDependency"]=addRunDependency;function removeRunDependency(id){runDependencies--;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}Module["removeRunDependency"]=removeRunDependency;Module["preloadedImages"]={};Module["preloadedAudios"]={};var memoryInitializer=null;function integrateWasmJS(Module){var method=Module["wasmJSMethod"]||"native-wasm";Module["wasmJSMethod"]=method;var wasmTextFile=Module["wasmTextFile"]||"webdsp_c.wast";var wasmBinaryFile=Module["wasmBinaryFile"]||"webdsp_c.wasm";var asmjsCodeFile=Module["asmjsCodeFile"]||"webdsp_c.temp.asm.js";var wasmPageSize=64*1024;var asm2wasmImports={"f64-rem":(function(x,y){return x%y}),"f64-to-int":(function(x){return x|0}),"i32s-div":(function(x,y){return(x|0)/(y|0)|0}),"i32u-div":(function(x,y){return(x>>>0)/(y>>>0)>>>0}),"i32s-rem":(function(x,y){return(x|0)%(y|0)|0}),"i32u-rem":(function(x,y){return(x>>>0)%(y>>>0)>>>0}),"debugger":(function(){debugger})};var info={"global":null,"env":null,"asm2wasm":asm2wasmImports,"parent":Module};var exports=null;function lookupImport(mod,base){var lookup=info;if(mod.indexOf(".")<0){lookup=(lookup||{})[mod]}else{var parts=mod.split(".");lookup=(lookup||{})[parts[0]];lookup=(lookup||{})[parts[1]]}if(base){lookup=(lookup||{})[base]}if(lookup===undefined){abort("bad lookupImport to ("+mod+")."+base)}return lookup}function mergeMemory(newBuffer){var oldBuffer=Module["buffer"];if(newBuffer.byteLength=0||Module["wasmJSMethod"].indexOf("interpret-asm2wasm")>=0?"webdsp_c.js.mem":null;var STATIC_BUMP=3024;Module["STATIC_BASE"]=STATIC_BASE;Module["STATIC_BUMP"]=STATIC_BUMP;var tempDoublePtr=STATICTOP;STATICTOP+=16;function ___setErrNo(value){if(Module["___errno_location"])HEAP32[Module["___errno_location"]()>>2]=value;return value}Module["_sbrk"]=_sbrk;Module["_memset"]=_memset;function ___lock(){}function _emscripten_memcpy_big(dest,src,num){HEAPU8.set(HEAPU8.subarray(src,src+num),dest);return dest}Module["_memcpy"]=_memcpy;function _abort(){Module["abort"]()}var SYSCALLS={varargs:0,get:(function(varargs){SYSCALLS.varargs+=4;var ret=HEAP32[SYSCALLS.varargs-4>>2];return ret}),getStr:(function(){var ret=Pointer_stringify(SYSCALLS.get());return ret}),get64:(function(){var low=SYSCALLS.get(),high=SYSCALLS.get();if(low>=0)assert(high===0);else assert(high===-1);return low}),getZero:(function(){assert(SYSCALLS.get()===0)})};function ___syscall140(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(),offset_high=SYSCALLS.get(),offset_low=SYSCALLS.get(),result=SYSCALLS.get(),whence=SYSCALLS.get();var offset=offset_low;assert(offset_high===0);FS.llseek(stream,offset,whence);HEAP32[result>>2]=stream.position;if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS==="undefined"||!(e instanceof FS.ErrnoError))abort(e);return-e.errno}}function ___syscall146(which,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.get(),iov=SYSCALLS.get(),iovcnt=SYSCALLS.get();var ret=0;if(!___syscall146.buffer){___syscall146.buffers=[null,[],[]];___syscall146.printChar=(function(stream,curr){var buffer=___syscall146.buffers[stream];assert(buffer);if(curr===0||curr===10){(stream===1?Module["print"]:Module["printErr"])(UTF8ArrayToString(buffer,0));buffer.length=0}else{buffer.push(curr)}})}for(var i=0;i>2];var len=HEAP32[iov+(i*8+4)>>2];for(var j=0;j>2]=DYNAMIC_BASE;staticSealed=true;Module["wasmTableSize"]=6;Module["wasmMaxTableSize"]=6;function invoke_ii(index,a1){try{return Module["dynCall_ii"](index,a1)}catch(e){if(typeof e!=="number"&&e!=="longjmp")throw e;Module["setThrew"](1,0)}}function invoke_iiii(index,a1,a2,a3){try{return Module["dynCall_iiii"](index,a1,a2,a3)}catch(e){if(typeof e!=="number"&&e!=="longjmp")throw e;Module["setThrew"](1,0)}}Module.asmGlobalArg={"Math":Math,"Int8Array":Int8Array,"Int16Array":Int16Array,"Int32Array":Int32Array,"Uint8Array":Uint8Array,"Uint16Array":Uint16Array,"Uint32Array":Uint32Array,"Float32Array":Float32Array,"Float64Array":Float64Array,"NaN":NaN,"Infinity":Infinity,"byteLength":byteLength};Module.asmLibraryArg={"abort":abort,"assert":assert,"enlargeMemory":enlargeMemory,"getTotalMemory":getTotalMemory,"abortOnCannotGrowMemory":abortOnCannotGrowMemory,"invoke_ii":invoke_ii,"invoke_iiii":invoke_iiii,"___lock":___lock,"___syscall6":___syscall6,"___setErrNo":___setErrNo,"_abort":_abort,"___syscall140":___syscall140,"_emscripten_memcpy_big":_emscripten_memcpy_big,"___syscall54":___syscall54,"___unlock":___unlock,"___syscall146":___syscall146,"DYNAMICTOP_PTR":DYNAMICTOP_PTR,"tempDoublePtr":tempDoublePtr,"ABORT":ABORT,"STACKTOP":STACKTOP,"STACK_MAX":STACK_MAX};var asm=Module["asm"](Module.asmGlobalArg,Module.asmLibraryArg,buffer);Module["asm"]=asm;var _grayScale=Module["_grayScale"]=(function(){return Module["asm"]["_grayScale"].apply(null,arguments)});var _invert=Module["_invert"]=(function(){return Module["asm"]["_invert"].apply(null,arguments)});var setThrew=Module["setThrew"]=(function(){return Module["asm"]["setThrew"].apply(null,arguments)});var _multiFilterFloat=Module["_multiFilterFloat"]=(function(){return Module["asm"]["_multiFilterFloat"].apply(null,arguments)});var _noise=Module["_noise"]=(function(){return Module["asm"]["_noise"].apply(null,arguments)});var _fflush=Module["_fflush"]=(function(){return Module["asm"]["_fflush"].apply(null,arguments)});var ___errno_location=Module["___errno_location"]=(function(){return Module["asm"]["___errno_location"].apply(null,arguments)});var _memset=Module["_memset"]=(function(){return Module["asm"]["_memset"].apply(null,arguments)});var _sbrk=Module["_sbrk"]=(function(){return Module["asm"]["_sbrk"].apply(null,arguments)});var _memcpy=Module["_memcpy"]=(function(){return Module["asm"]["_memcpy"].apply(null,arguments)});var stackAlloc=Module["stackAlloc"]=(function(){return Module["asm"]["stackAlloc"].apply(null,arguments)});var getTempRet0=Module["getTempRet0"]=(function(){return Module["asm"]["getTempRet0"].apply(null,arguments)});var setTempRet0=Module["setTempRet0"]=(function(){return Module["asm"]["setTempRet0"].apply(null,arguments)});var _emscripten_get_global_libc=Module["_emscripten_get_global_libc"]=(function(){return Module["asm"]["_emscripten_get_global_libc"].apply(null,arguments)});var _sobelFilter=Module["_sobelFilter"]=(function(){return Module["asm"]["_sobelFilter"].apply(null,arguments)});var stackSave=Module["stackSave"]=(function(){return Module["asm"]["stackSave"].apply(null,arguments)});var _multiFilter=Module["_multiFilter"]=(function(){return Module["asm"]["_multiFilter"].apply(null,arguments)});var _free=Module["_free"]=(function(){return Module["asm"]["_free"].apply(null,arguments)});var runPostSets=Module["runPostSets"]=(function(){return Module["asm"]["runPostSets"].apply(null,arguments)});var establishStackSpace=Module["establishStackSpace"]=(function(){return Module["asm"]["establishStackSpace"].apply(null,arguments)});var stackRestore=Module["stackRestore"]=(function(){return Module["asm"]["stackRestore"].apply(null,arguments)});var _malloc=Module["_malloc"]=(function(){return Module["asm"]["_malloc"].apply(null,arguments)});var _emscripten_replace_memory=Module["_emscripten_replace_memory"]=(function(){return Module["asm"]["_emscripten_replace_memory"].apply(null,arguments)});var _convFilter=Module["_convFilter"]=(function(){return Module["asm"]["_convFilter"].apply(null,arguments)});var _brighten=Module["_brighten"]=(function(){return Module["asm"]["_brighten"].apply(null,arguments)});var dynCall_ii=Module["dynCall_ii"]=(function(){return Module["asm"]["dynCall_ii"].apply(null,arguments)});var dynCall_iiii=Module["dynCall_iiii"]=(function(){return Module["asm"]["dynCall_iiii"].apply(null,arguments)});Runtime.stackAlloc=Module["stackAlloc"];Runtime.stackSave=Module["stackSave"];Runtime.stackRestore=Module["stackRestore"];Runtime.establishStackSpace=Module["establishStackSpace"];Runtime.setTempRet0=Module["setTempRet0"];Runtime.getTempRet0=Module["getTempRet0"];Module["asm"]=asm;if(memoryInitializer){if(typeof Module["locateFile"]==="function"){memoryInitializer=Module["locateFile"](memoryInitializer)}else if(Module["memoryInitializerPrefixURL"]){memoryInitializer=Module["memoryInitializerPrefixURL"]+memoryInitializer}if(ENVIRONMENT_IS_NODE||ENVIRONMENT_IS_SHELL){var data=Module["readBinary"](memoryInitializer);HEAPU8.set(data,Runtime.GLOBAL_BASE)}else{addRunDependency("memory initializer");var applyMemoryInitializer=(function(data){if(data.byteLength)data=new Uint8Array(data);HEAPU8.set(data,Runtime.GLOBAL_BASE);if(Module["memoryInitializerRequest"])delete Module["memoryInitializerRequest"].response;removeRunDependency("memory initializer")});function doBrowserLoad(){Module["readAsync"](memoryInitializer,applyMemoryInitializer,(function(){throw"could not load memory initializer "+memoryInitializer}))}if(Module["memoryInitializerRequest"]){function useRequest(){var request=Module["memoryInitializerRequest"];if(request.status!==200&&request.status!==0){console.warn("a problem seems to have happened with Module.memoryInitializerRequest, status: "+request.status+", retrying "+memoryInitializer);doBrowserLoad();return}applyMemoryInitializer(request.response)}if(Module["memoryInitializerRequest"].response){setTimeout(useRequest,0)}else{Module["memoryInitializerRequest"].addEventListener("load",useRequest)}}else{doBrowserLoad()}}}function ExitStatus(status){this.name="ExitStatus";this.message="Program terminated with exit("+status+")";this.status=status}ExitStatus.prototype=new Error;ExitStatus.prototype.constructor=ExitStatus;var initialStackTop;var preloadStartTime=null;var calledMain=false;dependenciesFulfilled=function runCaller(){if(!Module["calledRun"])run();if(!Module["calledRun"])dependenciesFulfilled=runCaller};Module["callMain"]=Module.callMain=function callMain(args){args=args||[];ensureInitRuntime();var argc=args.length+1;function pad(){for(var i=0;i<4-1;i++){argv.push(0)}}var argv=[allocate(intArrayFromString(Module["thisProgram"]),"i8",ALLOC_NORMAL)];pad();for(var i=0;i0){return}preRun();if(runDependencies>0)return;if(Module["calledRun"])return;function doRun(){if(Module["calledRun"])return;Module["calledRun"]=true;if(ABORT)return;ensureInitRuntime();preMain();if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();if(Module["_main"]&&shouldRunNow)Module["callMain"](args);postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout((function(){setTimeout((function(){Module["setStatus"]("")}),1);doRun()}),1)}else{doRun()}script.dispatchEvent(doneEvent);}Module["run"]=Module.run=run;function exit(status,implicit){if(implicit&&Module["noExitRuntime"]){return}if(Module["noExitRuntime"]){}else{ABORT=true;EXITSTATUS=status;STACKTOP=initialStackTop;exitRuntime();if(Module["onExit"])Module["onExit"](status)}if(ENVIRONMENT_IS_NODE){process["exit"](status)}Module["quit"](status,new ExitStatus(status))}Module["exit"]=Module.exit=exit;var abortDecorators=[];function abort(what){if(what!==undefined){Module.print(what);Module.printErr(what);what=JSON.stringify(what)}else{what=""}ABORT=true;EXITSTATUS=1;var extra="\nIf this abort() is unexpected, build with -s ASSERTIONS=1 which can give more information.";var output="abort("+what+") at "+stackTrace()+extra;if(abortDecorators){abortDecorators.forEach((function(decorator){output=decorator(output,what)}))}throw output}Module["abort"]=Module.abort=abort;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}var shouldRunNow=true;if(Module["noInitialRun"]){shouldRunNow=false}run() 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /lib/webdsp_c.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamadee/web-dsp-demo/97a57c6000ce5f803b45a0deaa483123b6e3b04b/lib/webdsp_c.wasm -------------------------------------------------------------------------------- /lib/webdsp_polyfill.js: -------------------------------------------------------------------------------- 1 | // fallback extravaganza 2 | jsFallback = function() { 3 | const wam = {}; 4 | let mag = 127, mult = 2, adj = 4; 5 | wam['grayScale'] = js_grayScale; 6 | wam['brighten'] = js_brighten; 7 | wam['invert'] = js_invert; 8 | wam['noise'] = js_noise; 9 | wam['multiFilter'] = js_multiFilter; 10 | wam['sunset'] = js_sunset; 11 | wam['analogTV'] = js_analog; 12 | wam['emboss'] = js_emboss; 13 | wam['sobelFilter'] = js_sobelFilter; 14 | wam['convFilter'] = js_convFilter; 15 | wam['blur'] = js_blur; 16 | wam['sharpen'] = js_sharpen; 17 | wam['strongSharpen'] = js_strongSharpen; 18 | wam['urple'] = js_urple; 19 | wam['forest'] = js_forest; 20 | wam['romance'] = js_romance; 21 | wam['hippo'] = js_hippo; 22 | wam['longhorn'] = js_longhorn; 23 | wam['underground'] = js_underground; 24 | wam['rooster'] = js_rooster; 25 | wam['mist'] = js_mist; 26 | wam['moss'] = js_mist; 27 | wam['tingle'] = js_tingle; 28 | wam['kaleidoscope'] = js_tingle; 29 | wam['bacteria'] = js_bacteria; 30 | wam['clarity'] = js_clarity; 31 | wam['goodMorning'] = js_goodMorning; 32 | wam['acid'] = js_acid; 33 | wam['dewdrops'] = js_dewdrops; 34 | wam['destruction'] = js_destruction; 35 | wam['hulk'] = js_hulk; 36 | wam['ghost'] = js_ghost; 37 | wam['twisted'] = js_twisted; 38 | wam['swamp'] = js_twisted; 39 | wam['security'] = js_security; 40 | wam['robbery'] = js_security; 41 | 42 | return wam; 43 | } 44 | function js_grayScale(data) { 45 | for (let i = 0; i < data.length; i += 4) { 46 | let r = data[i]; 47 | let g = data[i+1]; 48 | let b = data[i+2]; 49 | let a = data[i+3]; 50 | // let brightness = (r*.21+g*.72+b*.07); 51 | 52 | data[i] = r; 53 | data[i+1] = r; 54 | data[i+2] = r; 55 | data[i+3] = a; 56 | } 57 | return data; 58 | } 59 | function js_brighten(data, brightness=25) { 60 | for (let i = 0; i < data.length; i += 4) { 61 | data[i] + data[i] + brightness > 255 ? 255 : data[i] += brightness; 62 | data[i+1] + data[i+1] + brightness > 255 ? 255 : data[i+1] += brightness; 63 | data[i+2] + data[i+2] + brightness > 255 ? 255 : data[i+2] += brightness; 64 | } 65 | return data; 66 | } 67 | function js_invert(data) { 68 | for (let i = 0; i < data.length; i += 4) { 69 | data[i] = 255 - data[i]; //r 70 | data[i+1] = 255 - data[i+1]; //g 71 | data[i+2] = 255 - data[i+2]; //b 72 | } 73 | return data; 74 | } 75 | function js_noise(data) { 76 | let random; 77 | for (let i = 0; i < data.length; i += 4) { 78 | random = (Math.random() - 0.5) * 70; 79 | data[i] = data[i] + random; //r 80 | data[i+1] = data[i+1] + random; //g 81 | data[i+2] = data[i+2] + random; //b 82 | } 83 | return data; 84 | } 85 | function js_multiFilter(data, width, filterType, mag, mult, adj) { 86 | for (let i = 0; i < data.length; i += filterType) { 87 | if (i % 4 != 3) { 88 | data[i] = mag + mult * data[i] - data[i + adj] - data[i + width * 4]; 89 | } 90 | } 91 | return data; 92 | } 93 | const js_sunset = bindLastArgs(js_multiFilter, 4, 127, 2, 4); 94 | const js_analog = bindLastArgs(js_multiFilter, 7, 127, 2, 4); 95 | const js_emboss = bindLastArgs(js_multiFilter, 1, 127, 2, 4); 96 | const js_urple = bindLastArgs(js_multiFilter, 2, 127, 2, 4); 97 | const js_forest = bindLastArgs(js_multiFilter, 5, 127, 3, 1); 98 | const js_romance = bindLastArgs(js_multiFilter, 8, 127, 3, 2); 99 | const js_hippo = bindLastArgs(js_multiFilter, 2, 80, 3, 2); 100 | const js_longhorn = bindLastArgs(js_multiFilter, 2, 27, 3, 2); 101 | const js_underground = bindLastArgs(js_multiFilter, 8, 127, 1, 4); 102 | const js_rooster = bindLastArgs(js_multiFilter, 8, 60, 1, 4); 103 | const js_mist = bindLastArgs(js_multiFilter, 1, 127, 1, 1); 104 | const js_tingle = bindLastArgs(js_multiFilter, 1, 124, 4, 3); 105 | const js_bacteria = bindLastArgs(js_multiFilter, 4, 0, 2, 4); 106 | const js_hulk = bindLastArgs(js_multiFilter, 2, 10, 2, 4); 107 | const js_ghost = bindLastArgs(js_multiFilter, 1, 5, 2, 4); 108 | const js_twisted = bindLastArgs(js_multiFilter, 1, 40, 2, 3); 109 | const js_security = bindLastArgs(js_multiFilter, 1, 120, 1, 0); 110 | function js_convFilter(data, width, height, kernel, divisor, bias=0, count=1) { 111 | const w = kernel[0].length; 112 | const h = kernel.length; 113 | const half = Math.floor(h / 2); 114 | for (let i = 0; i < count; i += 1) { 115 | for (let y = 1; y < height - 1; y += 1) { 116 | for (let x = 1; x < width - 1; x += 1) { 117 | const px = (y * width + x) * 4; // pixel index 118 | let r = 0, g = 0, b = 0; 119 | 120 | for (let cy = 0; cy < h; ++cy) { 121 | for (let cx = 0; cx < w; ++cx) { 122 | const cpx = ((y + (cy - half)) * width + (x + (cx - half))) * 4; 123 | r += data[cpx + 0] * kernel[cy][cx]; 124 | g += data[cpx + 1] * kernel[cy][cx]; 125 | b += data[cpx + 2] * kernel[cy][cx]; 126 | } 127 | } 128 | 129 | data[px + 0] = (1 / divisor) * r + bias; 130 | data[px + 1] = (1 / divisor) * g + bias; 131 | data[px + 2] = (1 / divisor) * b + bias; 132 | } 133 | } 134 | } 135 | return data; 136 | } 137 | const js_blur = bindLastArgs(js_convFilter, [[1, 1, 1], [1, 1, 1], [1, 1, 1]],9,0,3) 138 | const js_sharpen = bindLastArgs(js_convFilter,[[0, -1, 0], [-1, 5, -1], [0, -1, 0]],2) 139 | const js_strongSharpen = bindLastArgs(js_convFilter,[[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]],1) 140 | const js_clarity = bindLastArgs(js_convFilter, [[1, -1, -1], [-1, 8, -1], [-1, -1, 1]], 3); 141 | const js_goodMorning = bindLastArgs(js_convFilter, [[-1, -1, 1], [-1, 14, -1], [1, -1, -1]], 3); 142 | const js_acid = bindLastArgs(js_convFilter, [[4, -1, -1], [-1, 4, -1], [0, -1, 4]], 3); 143 | const js_dewdrops = bindLastArgs(js_convFilter, [[0, 0, -1], [-1, 12, -1], [0, -1, -1]], 2); 144 | const js_destruction = bindLastArgs(js_convFilter, [[-1, -1, 4], [-1, 9, -1], [0, -1, 0]], 2); 145 | function js_sobelFilter(data, width, height, invert=false) { 146 | const out = []; 147 | let wid = width; 148 | let hei = height; 149 | var grayData = new Int32Array(wid * hei); 150 | 151 | function getPixel(x, y) { 152 | if (x < 0 || y < 0) return 0; 153 | if (x >= (wid) || y >= (hei)) return 0; 154 | return (grayData[((wid * y) + x)]); 155 | } 156 | //Grayscale 157 | for (var y = 0; y < height; y++) { 158 | for (var x = 0; x < width; x++) { 159 | var goffset = ((wid * y) + x) << 2; //multiply by 4 160 | var r = data[goffset]; 161 | var g = data[goffset + 1]; 162 | var b = data[goffset + 2]; 163 | var avg = (r >> 2) + (g >> 1) + (b >> 3); 164 | grayData[((wid * y) + x)] = avg; 165 | var doffset = ((wid * y) + x) << 2; 166 | data[doffset] = avg; 167 | data[doffset + 1] = avg; 168 | data[doffset + 2] = avg; 169 | data[doffset + 3] = 255; 170 | } 171 | } 172 | //Sobel 173 | for (var y = 0; y < height; y++) { 174 | for (var x = 0; x < width; x++) { 175 | 176 | var newX; 177 | var newY; 178 | if ((x >= width - 1) || (y >= height - 1)) { 179 | newX = 0; 180 | newY = 0; 181 | } else { 182 | //sobel Filter use surrounding pixels and matrix multiply by sobel 183 | newX = ( 184 | (-1 * getPixel(x - 1, y - 1)) + 185 | (getPixel(x + 1, y - 1)) + 186 | (-1 * (getPixel(x - 1, y) << 1)) + 187 | (getPixel(x + 1, y) << 1) + 188 | (-1 * getPixel(x - 1, y + 1)) + 189 | (getPixel(x + 1, y + 1)) 190 | ); 191 | newY = ( 192 | (-1 * getPixel(x - 1, y - 1)) + 193 | (-1 * (getPixel(x, y - 1) << 1)) + 194 | (-1 * getPixel(x + 1, y - 1)) + 195 | (getPixel(x - 1, y + 1)) + 196 | (getPixel(x, y + 1) << 1) + 197 | (getPixel(x + 1, y + 1)) 198 | ); 199 | var mag = Math.floor(Math.sqrt((newX * newX) + (newY * newY)) >>> 0); 200 | if (mag > 255) mag = 255; 201 | if (invert) mag = 255 - mag; 202 | data[((wid * y) + x) * 4] = mag; 203 | data[((wid * y) + x) * 4 + 1] = mag; 204 | data[((wid * y) + x) * 4 + 2] = mag; 205 | data[((wid * y) + x) * 4 + 3] = 255; 206 | } 207 | } 208 | } 209 | return data; //sobelData; 210 | } 211 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "load-wasm", 3 | "version": "1.0.0", 4 | "description": "This is a sample library using WebAssembly. To compile the C library download emscripten and run in terminal:", 5 | "main": "test.js", 6 | "scripts": { 7 | "test": "npm run testCPP && npm run testJS", 8 | "testCPP": "g++ ./test/test.cpp -o ./test/testCpp && ./test/testCpp", 9 | "testJS": "mocha || true", 10 | "start": "gulp", 11 | "cpp": "echo \"compiling C/C++ to WASM\" && . ./compileWASM.sh" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/matzewagner/load-wasm.git" 16 | }, 17 | "author": "", 18 | "license": "ISC", 19 | "bugs": { 20 | "url": "https://github.com/matzewagner/load-wasm/issues" 21 | }, 22 | "homepage": "https://github.com/matzewagner/load-wasm#readme", 23 | "dependencies": { 24 | "express": "^4.15.0", 25 | "nodemon": "^1.11.0", 26 | "path": "^0.12.7" 27 | }, 28 | "devDependencies": { 29 | "browser-sync": "^2.18.8", 30 | "chai": "^3.5.0", 31 | "gulp": "^3.9.1", 32 | "gulp-livereload": "^3.8.1", 33 | "mocha": "^3.2.0", 34 | "zombie": "^5.0.5" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const express = require('express'); 4 | 5 | const app = express(); 6 | 7 | app.use('/', express.static(path.join(__dirname, '/'))); 8 | 9 | app.listen(3000, () => { 10 | console.log('Server listening on port 3000'); 11 | }); 12 | -------------------------------------------------------------------------------- /smoothie.js: -------------------------------------------------------------------------------- 1 | // MIT License: 2 | // 3 | // Copyright (c) 2010-2013, Joe Walnes 4 | // 2013-2014, Drew Noakes 5 | // 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy 7 | // of this software and associated documentation files (the "Software"), to deal 8 | // in the Software without restriction, including without limitation the rights 9 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | // copies of the Software, and to permit persons to whom the Software is 11 | // furnished to do so, subject to the following conditions: 12 | // 13 | // The above copyright notice and this permission notice shall be included in 14 | // all copies or substantial portions of the Software. 15 | // 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | // THE SOFTWARE. 23 | 24 | /** 25 | * Smoothie Charts - http://smoothiecharts.org/ 26 | * (c) 2010-2013, Joe Walnes 27 | * 2013-2014, Drew Noakes 28 | * 29 | * v1.0: Main charting library, by Joe Walnes 30 | * v1.1: Auto scaling of axis, by Neil Dunn 31 | * v1.2: fps (frames per second) option, by Mathias Petterson 32 | * v1.3: Fix for divide by zero, by Paul Nikitochkin 33 | * v1.4: Set minimum, top-scale padding, remove timeseries, add optional timer to reset bounds, by Kelley Reynolds 34 | * v1.5: Set default frames per second to 50... smoother. 35 | * .start(), .stop() methods for conserving CPU, by Dmitry Vyal 36 | * options.interpolation = 'bezier' or 'line', by Dmitry Vyal 37 | * options.maxValue to fix scale, by Dmitry Vyal 38 | * v1.6: minValue/maxValue will always get converted to floats, by Przemek Matylla 39 | * v1.7: options.grid.fillStyle may be a transparent color, by Dmitry A. Shashkin 40 | * Smooth rescaling, by Kostas Michalopoulos 41 | * v1.8: Set max length to customize number of live points in the dataset with options.maxDataSetLength, by Krishna Narni 42 | * v1.9: Display timestamps along the bottom, by Nick and Stev-io 43 | * (https://groups.google.com/forum/?fromgroups#!topic/smoothie-charts/-Ywse8FCpKI%5B1-25%5D) 44 | * Refactored by Krishna Narni, to support timestamp formatting function 45 | * v1.10: Switch to requestAnimationFrame, removed the now obsoleted options.fps, by Gergely Imreh 46 | * v1.11: options.grid.sharpLines option added, by @drewnoakes 47 | * Addressed warning seen in Firefox when seriesOption.fillStyle undefined, by @drewnoakes 48 | * v1.12: Support for horizontalLines added, by @drewnoakes 49 | * Support for yRangeFunction callback added, by @drewnoakes 50 | * v1.13: Fixed typo (#32), by @alnikitich 51 | * v1.14: Timer cleared when last TimeSeries removed (#23), by @davidgaleano 52 | * Fixed diagonal line on chart at start/end of data stream, by @drewnoakes 53 | * v1.15: Support for npm package (#18), by @dominictarr 54 | * Fixed broken removeTimeSeries function (#24) by @davidgaleano 55 | * Minor performance and tidying, by @drewnoakes 56 | * v1.16: Bug fix introduced in v1.14 relating to timer creation/clearance (#23), by @drewnoakes 57 | * TimeSeries.append now deals with out-of-order timestamps, and can merge duplicates, by @zacwitte (#12) 58 | * Documentation and some local variable renaming for clarity, by @drewnoakes 59 | * v1.17: Allow control over font size (#10), by @drewnoakes 60 | * Timestamp text won't overlap, by @drewnoakes 61 | * v1.18: Allow control of max/min label precision, by @drewnoakes 62 | * Added 'borderVisible' chart option, by @drewnoakes 63 | * Allow drawing series with fill but no stroke (line), by @drewnoakes 64 | * v1.19: Avoid unnecessary repaints, and fixed flicker in old browsers having multiple charts in document (#40), by @asbai 65 | * v1.20: Add SmoothieChart.getTimeSeriesOptions and SmoothieChart.bringToFront functions, by @drewnoakes 66 | * v1.21: Add 'step' interpolation mode, by @drewnoakes 67 | * v1.22: Add support for different pixel ratios. Also add optional y limit formatters, by @copacetic 68 | * v1.23: Fix bug introduced in v1.22 (#44), by @drewnoakes 69 | * v1.24: Fix bug introduced in v1.23, re-adding parseFloat to y-axis formatter defaults, by @siggy_sf 70 | * v1.25: Fix bug seen when adding a data point to TimeSeries which is older than the current data, by @Nking92 71 | * Draw time labels on top of series, by @comolosabia 72 | * Add TimeSeries.clear function, by @drewnoakes 73 | * v1.26: Add support for resizing on high device pixel ratio screens, by @copacetic 74 | * v1.27: Fix bug introduced in v1.26 for non whole number devicePixelRatio values, by @zmbush 75 | * v1.28: Add 'minValueScale' option, by @megawac 76 | * Fix 'labelPos' for different size of 'minValueString' 'maxValueString', by @henryn 77 | */ 78 | 79 | ;(function(exports) { 80 | 81 | var Util = { 82 | extend: function() { 83 | arguments[0] = arguments[0] || {}; 84 | for (var i = 1; i < arguments.length; i++) 85 | { 86 | for (var key in arguments[i]) 87 | { 88 | if (arguments[i].hasOwnProperty(key)) 89 | { 90 | if (typeof(arguments[i][key]) === 'object') { 91 | if (arguments[i][key] instanceof Array) { 92 | arguments[0][key] = arguments[i][key]; 93 | } else { 94 | arguments[0][key] = Util.extend(arguments[0][key], arguments[i][key]); 95 | } 96 | } else { 97 | arguments[0][key] = arguments[i][key]; 98 | } 99 | } 100 | } 101 | } 102 | return arguments[0]; 103 | } 104 | }; 105 | 106 | /** 107 | * Initialises a new TimeSeries with optional data options. 108 | * 109 | * Options are of the form (defaults shown): 110 | * 111 | *
112 |    * {
113 |    *   resetBounds: true,        // enables/disables automatic scaling of the y-axis
114 |    *   resetBoundsInterval: 3000 // the period between scaling calculations, in millis
115 |    * }
116 |    * 
117 | * 118 | * Presentation options for TimeSeries are specified as an argument to SmoothieChart.addTimeSeries. 119 | * 120 | * @constructor 121 | */ 122 | function TimeSeries(options) { 123 | this.options = Util.extend({}, TimeSeries.defaultOptions, options); 124 | this.clear(); 125 | } 126 | 127 | TimeSeries.defaultOptions = { 128 | resetBoundsInterval: 3000, 129 | resetBounds: true 130 | }; 131 | 132 | /** 133 | * Clears all data and state from this TimeSeries object. 134 | */ 135 | TimeSeries.prototype.clear = function() { 136 | this.data = []; 137 | this.maxValue = Number.NaN; // The maximum value ever seen in this TimeSeries. 138 | this.minValue = Number.NaN; // The minimum value ever seen in this TimeSeries. 139 | }; 140 | 141 | /** 142 | * Recalculate the min/max values for this TimeSeries object. 143 | * 144 | * This causes the graph to scale itself in the y-axis. 145 | */ 146 | TimeSeries.prototype.resetBounds = function() { 147 | if (this.data.length) { 148 | // Walk through all data points, finding the min/max value 149 | this.maxValue = this.data[0][1]; 150 | this.minValue = this.data[0][1]; 151 | for (var i = 1; i < this.data.length; i++) { 152 | var value = this.data[i][1]; 153 | if (value > this.maxValue) { 154 | this.maxValue = value; 155 | } 156 | if (value < this.minValue) { 157 | this.minValue = value; 158 | } 159 | } 160 | } else { 161 | // No data exists, so set min/max to NaN 162 | this.maxValue = Number.NaN; 163 | this.minValue = Number.NaN; 164 | } 165 | }; 166 | 167 | /** 168 | * Adds a new data point to the TimeSeries, preserving chronological order. 169 | * 170 | * @param timestamp the position, in time, of this data point 171 | * @param value the value of this data point 172 | * @param sumRepeatedTimeStampValues if timestamp has an exact match in the series, this flag controls 173 | * whether it is replaced, or the values summed (defaults to false.) 174 | */ 175 | TimeSeries.prototype.append = function(timestamp, value, sumRepeatedTimeStampValues) { 176 | // Rewind until we hit an older timestamp 177 | var i = this.data.length - 1; 178 | while (i >= 0 && this.data[i][0] > timestamp) { 179 | i--; 180 | } 181 | 182 | if (i === -1) { 183 | // This new item is the oldest data 184 | this.data.splice(0, 0, [timestamp, value]); 185 | } else if (this.data.length > 0 && this.data[i][0] === timestamp) { 186 | // Update existing values in the array 187 | if (sumRepeatedTimeStampValues) { 188 | // Sum this value into the existing 'bucket' 189 | this.data[i][1] += value; 190 | value = this.data[i][1]; 191 | } else { 192 | // Replace the previous value 193 | this.data[i][1] = value; 194 | } 195 | } else if (i < this.data.length - 1) { 196 | // Splice into the correct position to keep timestamps in order 197 | this.data.splice(i + 1, 0, [timestamp, value]); 198 | } else { 199 | // Add to the end of the array 200 | this.data.push([timestamp, value]); 201 | } 202 | 203 | this.maxValue = isNaN(this.maxValue) ? value : Math.max(this.maxValue, value); 204 | this.minValue = isNaN(this.minValue) ? value : Math.min(this.minValue, value); 205 | }; 206 | 207 | TimeSeries.prototype.dropOldData = function(oldestValidTime, maxDataSetLength) { 208 | // We must always keep one expired data point as we need this to draw the 209 | // line that comes into the chart from the left, but any points prior to that can be removed. 210 | var removeCount = 0; 211 | while (this.data.length - removeCount >= maxDataSetLength && this.data[removeCount + 1][0] < oldestValidTime) { 212 | removeCount++; 213 | } 214 | if (removeCount !== 0) { 215 | this.data.splice(0, removeCount); 216 | } 217 | }; 218 | 219 | /** 220 | * Initialises a new SmoothieChart. 221 | * 222 | * Options are optional, and should be of the form below. Just specify the values you 223 | * need and the rest will be given sensible defaults as shown: 224 | * 225 | *
226 |    * {
227 |    *   minValue: undefined,                      // specify to clamp the lower y-axis to a given value
228 |    *   maxValue: undefined,                      // specify to clamp the upper y-axis to a given value
229 |    *   maxValueScale: 1,                         // allows proportional padding to be added above the chart. for 10% padding, specify 1.1.
230 |    *   minValueScale: 1,                         // allows proportional padding to be added below the chart. for 10% padding, specify 1.1.
231 |    *   yRangeFunction: undefined,                // function({min: , max: }) { return {min: , max: }; }
232 |    *   scaleSmoothing: 0.125,                    // controls the rate at which y-value zoom animation occurs
233 |    *   millisPerPixel: 20,                       // sets the speed at which the chart pans by
234 |    *   enableDpiScaling: true,                   // support rendering at different DPI depending on the device
235 |    *   yMinFormatter: function(min, precision) { // callback function that formats the min y value label
236 |    *     return parseFloat(min).toFixed(precision);
237 |    *   },
238 |    *   yMaxFormatter: function(max, precision) { // callback function that formats the max y value label
239 |    *     return parseFloat(max).toFixed(precision);
240 |    *   },
241 |    *   maxDataSetLength: 2,
242 |    *   interpolation: 'bezier'                   // one of 'bezier', 'linear', or 'step'
243 |    *   timestampFormatter: null,                 // optional function to format time stamps for bottom of chart
244 |    *                                             // you may use SmoothieChart.timeFormatter, or your own: function(date) { return ''; }
245 |    *   scrollBackwards: false,                   // reverse the scroll direction of the chart
246 |    *   horizontalLines: [],                      // [ { value: 0, color: '#ffffff', lineWidth: 1 } ]
247 |    *   grid:
248 |    *   {
249 |    *     fillStyle: '#000000',                   // the background colour of the chart
250 |    *     lineWidth: 1,                           // the pixel width of grid lines
251 |    *     strokeStyle: '#777777',                 // colour of grid lines
252 |    *     millisPerLine: 1000,                    // distance between vertical grid lines
253 |    *     sharpLines: false,                      // controls whether grid lines are 1px sharp, or softened
254 |    *     verticalSections: 2,                    // number of vertical sections marked out by horizontal grid lines
255 |    *     borderVisible: true                     // whether the grid lines trace the border of the chart or not
256 |    *   },
257 |    *   labels
258 |    *   {
259 |    *     disabled: false,                        // enables/disables labels showing the min/max values
260 |    *     fillStyle: '#ffffff',                   // colour for text of labels,
261 |    *     fontSize: 15,
262 |    *     fontFamily: 'sans-serif',
263 |    *     precision: 2
264 |    *   }
265 |    * }
266 |    * 
267 | * 268 | * @constructor 269 | */ 270 | function SmoothieChart(options) { 271 | this.options = Util.extend({}, SmoothieChart.defaultChartOptions, options); 272 | this.seriesSet = []; 273 | this.currentValueRange = 1; 274 | this.currentVisMinValue = 0; 275 | this.lastRenderTimeMillis = 0; 276 | } 277 | 278 | SmoothieChart.defaultChartOptions = { 279 | millisPerPixel: 20, 280 | enableDpiScaling: true, 281 | yMinFormatter: function(min, precision) { 282 | return parseFloat(min).toFixed(precision); 283 | }, 284 | yMaxFormatter: function(max, precision) { 285 | return parseFloat(max).toFixed(precision); 286 | }, 287 | maxValueScale: 1, 288 | minValueScale: 1, 289 | interpolation: 'bezier', 290 | scaleSmoothing: 0.125, 291 | maxDataSetLength: 2, 292 | scrollBackwards: false, 293 | grid: { 294 | fillStyle: '#000000', 295 | strokeStyle: '#777777', 296 | lineWidth: 1, 297 | sharpLines: false, 298 | millisPerLine: 1000, 299 | verticalSections: 2, 300 | borderVisible: true 301 | }, 302 | labels: { 303 | fillStyle: '#ffffff', 304 | disabled: false, 305 | fontSize: 10, 306 | fontFamily: 'monospace', 307 | precision: 2 308 | }, 309 | horizontalLines: [] 310 | }; 311 | 312 | // Based on http://inspirit.github.com/jsfeat/js/compatibility.js 313 | SmoothieChart.AnimateCompatibility = (function() { 314 | var requestAnimationFrame = function(callback, element) { 315 | var requestAnimationFrame = 316 | window.requestAnimationFrame || 317 | window.webkitRequestAnimationFrame || 318 | window.mozRequestAnimationFrame || 319 | window.oRequestAnimationFrame || 320 | window.msRequestAnimationFrame || 321 | function(callback) { 322 | return window.setTimeout(function() { 323 | callback(new Date().getTime()); 324 | }, 16); 325 | }; 326 | return requestAnimationFrame.call(window, callback, element); 327 | }, 328 | cancelAnimationFrame = function(id) { 329 | var cancelAnimationFrame = 330 | window.cancelAnimationFrame || 331 | function(id) { 332 | clearTimeout(id); 333 | }; 334 | return cancelAnimationFrame.call(window, id); 335 | }; 336 | 337 | return { 338 | requestAnimationFrame: requestAnimationFrame, 339 | cancelAnimationFrame: cancelAnimationFrame 340 | }; 341 | })(); 342 | 343 | SmoothieChart.defaultSeriesPresentationOptions = { 344 | lineWidth: 1, 345 | strokeStyle: '#ffffff' 346 | }; 347 | 348 | /** 349 | * Adds a TimeSeries to this chart, with optional presentation options. 350 | * 351 | * Presentation options should be of the form (defaults shown): 352 | * 353 | *
354 |    * {
355 |    *   lineWidth: 1,
356 |    *   strokeStyle: '#ffffff',
357 |    *   fillStyle: undefined
358 |    * }
359 |    * 
360 | */ 361 | SmoothieChart.prototype.addTimeSeries = function(timeSeries, options) { 362 | this.seriesSet.push({timeSeries: timeSeries, options: Util.extend({}, SmoothieChart.defaultSeriesPresentationOptions, options)}); 363 | if (timeSeries.options.resetBounds && timeSeries.options.resetBoundsInterval > 0) { 364 | timeSeries.resetBoundsTimerId = setInterval( 365 | function() { 366 | timeSeries.resetBounds(); 367 | }, 368 | timeSeries.options.resetBoundsInterval 369 | ); 370 | } 371 | }; 372 | 373 | /** 374 | * Removes the specified TimeSeries from the chart. 375 | */ 376 | SmoothieChart.prototype.removeTimeSeries = function(timeSeries) { 377 | // Find the correct timeseries to remove, and remove it 378 | var numSeries = this.seriesSet.length; 379 | for (var i = 0; i < numSeries; i++) { 380 | if (this.seriesSet[i].timeSeries === timeSeries) { 381 | this.seriesSet.splice(i, 1); 382 | break; 383 | } 384 | } 385 | // If a timer was operating for that timeseries, remove it 386 | if (timeSeries.resetBoundsTimerId) { 387 | // Stop resetting the bounds, if we were 388 | clearInterval(timeSeries.resetBoundsTimerId); 389 | } 390 | }; 391 | 392 | /** 393 | * Gets render options for the specified TimeSeries. 394 | * 395 | * As you may use a single TimeSeries in multiple charts with different formatting in each usage, 396 | * these settings are stored in the chart. 397 | */ 398 | SmoothieChart.prototype.getTimeSeriesOptions = function(timeSeries) { 399 | // Find the correct timeseries to remove, and remove it 400 | var numSeries = this.seriesSet.length; 401 | for (var i = 0; i < numSeries; i++) { 402 | if (this.seriesSet[i].timeSeries === timeSeries) { 403 | return this.seriesSet[i].options; 404 | } 405 | } 406 | }; 407 | 408 | /** 409 | * Brings the specified TimeSeries to the top of the chart. It will be rendered last. 410 | */ 411 | SmoothieChart.prototype.bringToFront = function(timeSeries) { 412 | // Find the correct timeseries to remove, and remove it 413 | var numSeries = this.seriesSet.length; 414 | for (var i = 0; i < numSeries; i++) { 415 | if (this.seriesSet[i].timeSeries === timeSeries) { 416 | var set = this.seriesSet.splice(i, 1); 417 | this.seriesSet.push(set[0]); 418 | break; 419 | } 420 | } 421 | }; 422 | 423 | /** 424 | * Instructs the SmoothieChart to start rendering to the provided canvas, with specified delay. 425 | * 426 | * @param canvas the target canvas element 427 | * @param delayMillis an amount of time to wait before a data point is shown. This can prevent the end of the series 428 | * from appearing on screen, with new values flashing into view, at the expense of some latency. 429 | */ 430 | SmoothieChart.prototype.streamTo = function(canvas, delayMillis) { 431 | this.canvas = canvas; 432 | this.delay = delayMillis; 433 | this.start(); 434 | }; 435 | 436 | /** 437 | * Make sure the canvas has the optimal resolution for the device's pixel ratio. 438 | */ 439 | SmoothieChart.prototype.resize = function() { 440 | // TODO this function doesn't handle the value of enableDpiScaling changing during execution 441 | if (!this.options.enableDpiScaling || !window || window.devicePixelRatio === 1) 442 | return; 443 | 444 | var dpr = window.devicePixelRatio; 445 | var width = parseInt(this.canvas.getAttribute('width')); 446 | var height = parseInt(this.canvas.getAttribute('height')); 447 | 448 | if (!this.originalWidth || (Math.floor(this.originalWidth * dpr) !== width)) { 449 | this.originalWidth = width; 450 | this.canvas.setAttribute('width', (Math.floor(width * dpr)).toString()); 451 | this.canvas.style.width = width + 'px'; 452 | this.canvas.getContext('2d').scale(dpr, dpr); 453 | } 454 | 455 | if (!this.originalHeight || (Math.floor(this.originalHeight * dpr) !== height)) { 456 | this.originalHeight = height; 457 | this.canvas.setAttribute('height', (Math.floor(height * dpr)).toString()); 458 | this.canvas.style.height = height + 'px'; 459 | this.canvas.getContext('2d').scale(dpr, dpr); 460 | } 461 | }; 462 | 463 | /** 464 | * Starts the animation of this chart. 465 | */ 466 | SmoothieChart.prototype.start = function() { 467 | if (this.frame) { 468 | // We're already running, so just return 469 | return; 470 | } 471 | 472 | // Renders a frame, and queues the next frame for later rendering 473 | var animate = function() { 474 | this.frame = SmoothieChart.AnimateCompatibility.requestAnimationFrame(function() { 475 | this.render(); 476 | animate(); 477 | }.bind(this)); 478 | }.bind(this); 479 | 480 | animate(); 481 | }; 482 | 483 | /** 484 | * Stops the animation of this chart. 485 | */ 486 | SmoothieChart.prototype.stop = function() { 487 | if (this.frame) { 488 | SmoothieChart.AnimateCompatibility.cancelAnimationFrame(this.frame); 489 | delete this.frame; 490 | } 491 | }; 492 | 493 | SmoothieChart.prototype.updateValueRange = function() { 494 | // Calculate the current scale of the chart, from all time series. 495 | var chartOptions = this.options, 496 | chartMaxValue = Number.NaN, 497 | chartMinValue = Number.NaN; 498 | 499 | for (var d = 0; d < this.seriesSet.length; d++) { 500 | // TODO(ndunn): We could calculate / track these values as they stream in. 501 | var timeSeries = this.seriesSet[d].timeSeries; 502 | if (!isNaN(timeSeries.maxValue)) { 503 | chartMaxValue = !isNaN(chartMaxValue) ? Math.max(chartMaxValue, timeSeries.maxValue) : timeSeries.maxValue; 504 | } 505 | 506 | if (!isNaN(timeSeries.minValue)) { 507 | chartMinValue = !isNaN(chartMinValue) ? Math.min(chartMinValue, timeSeries.minValue) : timeSeries.minValue; 508 | } 509 | } 510 | 511 | // Scale the chartMaxValue to add padding at the top if required 512 | if (chartOptions.maxValue != null) { 513 | chartMaxValue = chartOptions.maxValue; 514 | } else { 515 | chartMaxValue *= chartOptions.maxValueScale; 516 | } 517 | 518 | // Set the minimum if we've specified one 519 | if (chartOptions.minValue != null) { 520 | chartMinValue = chartOptions.minValue; 521 | } else { 522 | chartMinValue -= Math.abs(chartMinValue * chartOptions.minValueScale - chartMinValue); 523 | } 524 | 525 | // If a custom range function is set, call it 526 | if (this.options.yRangeFunction) { 527 | var range = this.options.yRangeFunction({min: chartMinValue, max: chartMaxValue}); 528 | chartMinValue = range.min; 529 | chartMaxValue = range.max; 530 | } 531 | 532 | if (!isNaN(chartMaxValue) && !isNaN(chartMinValue)) { 533 | var targetValueRange = chartMaxValue - chartMinValue; 534 | var valueRangeDiff = (targetValueRange - this.currentValueRange); 535 | var minValueDiff = (chartMinValue - this.currentVisMinValue); 536 | this.isAnimatingScale = Math.abs(valueRangeDiff) > 0.1 || Math.abs(minValueDiff) > 0.1; 537 | this.currentValueRange += chartOptions.scaleSmoothing * valueRangeDiff; 538 | this.currentVisMinValue += chartOptions.scaleSmoothing * minValueDiff; 539 | } 540 | 541 | this.valueRange = { min: chartMinValue, max: chartMaxValue }; 542 | }; 543 | 544 | SmoothieChart.prototype.render = function(canvas, time) { 545 | var nowMillis = new Date().getTime(); 546 | 547 | if (!this.isAnimatingScale) { 548 | // We're not animating. We can use the last render time and the scroll speed to work out whether 549 | // we actually need to paint anything yet. If not, we can return immediately. 550 | 551 | // Render at least every 1/6th of a second. The canvas may be resized, which there is 552 | // no reliable way to detect. 553 | var maxIdleMillis = Math.min(1000/6, this.options.millisPerPixel); 554 | 555 | if (nowMillis - this.lastRenderTimeMillis < maxIdleMillis) { 556 | return; 557 | } 558 | } 559 | 560 | this.resize(); 561 | 562 | this.lastRenderTimeMillis = nowMillis; 563 | 564 | canvas = canvas || this.canvas; 565 | time = time || nowMillis - (this.delay || 0); 566 | 567 | // Round time down to pixel granularity, so motion appears smoother. 568 | time -= time % this.options.millisPerPixel; 569 | 570 | var context = canvas.getContext('2d'), 571 | chartOptions = this.options, 572 | dimensions = { top: 0, left: 0, width: canvas.clientWidth, height: canvas.clientHeight }, 573 | // Calculate the threshold time for the oldest data points. 574 | oldestValidTime = time - (dimensions.width * chartOptions.millisPerPixel), 575 | valueToYPixel = function(value) { 576 | var offset = value - this.currentVisMinValue; 577 | return this.currentValueRange === 0 578 | ? dimensions.height 579 | : dimensions.height - (Math.round((offset / this.currentValueRange) * dimensions.height)); 580 | }.bind(this), 581 | timeToXPixel = function(t) { 582 | if(chartOptions.scrollBackwards) { 583 | return Math.round((time - t) / chartOptions.millisPerPixel); 584 | } 585 | return Math.round(dimensions.width - ((time - t) / chartOptions.millisPerPixel)); 586 | }; 587 | 588 | this.updateValueRange(); 589 | 590 | context.font = chartOptions.labels.fontSize + 'px ' + chartOptions.labels.fontFamily; 591 | 592 | // Save the state of the canvas context, any transformations applied in this method 593 | // will get removed from the stack at the end of this method when .restore() is called. 594 | context.save(); 595 | 596 | // Move the origin. 597 | context.translate(dimensions.left, dimensions.top); 598 | 599 | // Create a clipped rectangle - anything we draw will be constrained to this rectangle. 600 | // This prevents the occasional pixels from curves near the edges overrunning and creating 601 | // screen cheese (that phrase should need no explanation). 602 | context.beginPath(); 603 | context.rect(0, 0, dimensions.width, dimensions.height); 604 | context.clip(); 605 | 606 | // Clear the working area. 607 | context.save(); 608 | context.fillStyle = chartOptions.grid.fillStyle; 609 | context.clearRect(0, 0, dimensions.width, dimensions.height); 610 | context.fillRect(0, 0, dimensions.width, dimensions.height); 611 | context.restore(); 612 | 613 | // Grid lines... 614 | context.save(); 615 | context.lineWidth = chartOptions.grid.lineWidth; 616 | context.strokeStyle = chartOptions.grid.strokeStyle; 617 | // Vertical (time) dividers. 618 | if (chartOptions.grid.millisPerLine > 0) { 619 | context.beginPath(); 620 | for (var t = time - (time % chartOptions.grid.millisPerLine); 621 | t >= oldestValidTime; 622 | t -= chartOptions.grid.millisPerLine) { 623 | var gx = timeToXPixel(t); 624 | if (chartOptions.grid.sharpLines) { 625 | gx -= 0.5; 626 | } 627 | context.moveTo(gx, 0); 628 | context.lineTo(gx, dimensions.height); 629 | } 630 | context.stroke(); 631 | context.closePath(); 632 | } 633 | 634 | // Horizontal (value) dividers. 635 | for (var v = 1; v < chartOptions.grid.verticalSections; v++) { 636 | var gy = Math.round(v * dimensions.height / chartOptions.grid.verticalSections); 637 | if (chartOptions.grid.sharpLines) { 638 | gy -= 0.5; 639 | } 640 | context.beginPath(); 641 | context.moveTo(0, gy); 642 | context.lineTo(dimensions.width, gy); 643 | context.stroke(); 644 | context.closePath(); 645 | } 646 | // Bounding rectangle. 647 | if (chartOptions.grid.borderVisible) { 648 | context.beginPath(); 649 | context.strokeRect(0, 0, dimensions.width, dimensions.height); 650 | context.closePath(); 651 | } 652 | context.restore(); 653 | 654 | // Draw any horizontal lines... 655 | if (chartOptions.horizontalLines && chartOptions.horizontalLines.length) { 656 | for (var hl = 0; hl < chartOptions.horizontalLines.length; hl++) { 657 | var line = chartOptions.horizontalLines[hl], 658 | hly = Math.round(valueToYPixel(line.value)) - 0.5; 659 | context.strokeStyle = line.color || '#ffffff'; 660 | context.lineWidth = line.lineWidth || 1; 661 | context.beginPath(); 662 | context.moveTo(0, hly); 663 | context.lineTo(dimensions.width, hly); 664 | context.stroke(); 665 | context.closePath(); 666 | } 667 | } 668 | 669 | // For each data set... 670 | for (var d = 0; d < this.seriesSet.length; d++) { 671 | context.save(); 672 | var timeSeries = this.seriesSet[d].timeSeries, 673 | dataSet = timeSeries.data, 674 | seriesOptions = this.seriesSet[d].options; 675 | 676 | // Delete old data that's moved off the left of the chart. 677 | timeSeries.dropOldData(oldestValidTime, chartOptions.maxDataSetLength); 678 | 679 | // Set style for this dataSet. 680 | context.lineWidth = seriesOptions.lineWidth; 681 | context.strokeStyle = seriesOptions.strokeStyle; 682 | // Draw the line... 683 | context.beginPath(); 684 | // Retain lastX, lastY for calculating the control points of bezier curves. 685 | var firstX = 0, lastX = 0, lastY = 0; 686 | for (var i = 0; i < dataSet.length && dataSet.length !== 1; i++) { 687 | var x = timeToXPixel(dataSet[i][0]), 688 | y = valueToYPixel(dataSet[i][1]); 689 | 690 | if (i === 0) { 691 | firstX = x; 692 | context.moveTo(x, y); 693 | } else { 694 | switch (chartOptions.interpolation) { 695 | case "linear": 696 | case "line": { 697 | context.lineTo(x,y); 698 | break; 699 | } 700 | case "bezier": 701 | default: { 702 | // Great explanation of Bezier curves: http://en.wikipedia.org/wiki/Bezier_curve#Quadratic_curves 703 | // 704 | // Assuming A was the last point in the line plotted and B is the new point, 705 | // we draw a curve with control points P and Q as below. 706 | // 707 | // A---P 708 | // | 709 | // | 710 | // | 711 | // Q---B 712 | // 713 | // Importantly, A and P are at the same y coordinate, as are B and Q. This is 714 | // so adjacent curves appear to flow as one. 715 | // 716 | context.bezierCurveTo( // startPoint (A) is implicit from last iteration of loop 717 | Math.round((lastX + x) / 2), lastY, // controlPoint1 (P) 718 | Math.round((lastX + x)) / 2, y, // controlPoint2 (Q) 719 | x, y); // endPoint (B) 720 | break; 721 | } 722 | case "step": { 723 | context.lineTo(x,lastY); 724 | context.lineTo(x,y); 725 | break; 726 | } 727 | } 728 | } 729 | 730 | lastX = x; lastY = y; 731 | } 732 | 733 | if (dataSet.length > 1) { 734 | if (seriesOptions.fillStyle) { 735 | // Close up the fill region. 736 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, lastY); 737 | context.lineTo(dimensions.width + seriesOptions.lineWidth + 1, dimensions.height + seriesOptions.lineWidth + 1); 738 | context.lineTo(firstX, dimensions.height + seriesOptions.lineWidth); 739 | context.fillStyle = seriesOptions.fillStyle; 740 | context.fill(); 741 | } 742 | 743 | if (seriesOptions.strokeStyle && seriesOptions.strokeStyle !== 'none') { 744 | context.stroke(); 745 | } 746 | context.closePath(); 747 | } 748 | context.restore(); 749 | } 750 | 751 | // Draw the axis values on the chart. 752 | if (!chartOptions.labels.disabled && !isNaN(this.valueRange.min) && !isNaN(this.valueRange.max)) { 753 | var maxValueString = chartOptions.yMaxFormatter(this.valueRange.max, chartOptions.labels.precision), 754 | minValueString = chartOptions.yMinFormatter(this.valueRange.min, chartOptions.labels.precision), 755 | maxLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(maxValueString).width - 2, 756 | minLabelPos = chartOptions.scrollBackwards ? 0 : dimensions.width - context.measureText(minValueString).width - 2; 757 | context.fillStyle = chartOptions.labels.fillStyle; 758 | context.fillText(maxValueString, maxLabelPos, chartOptions.labels.fontSize); 759 | context.fillText(minValueString, minLabelPos, dimensions.height - 2); 760 | } 761 | 762 | // Display timestamps along x-axis at the bottom of the chart. 763 | if (chartOptions.timestampFormatter && chartOptions.grid.millisPerLine > 0) { 764 | var textUntilX = chartOptions.scrollBackwards 765 | ? context.measureText(minValueString).width 766 | : dimensions.width - context.measureText(minValueString).width + 4; 767 | for (var t = time - (time % chartOptions.grid.millisPerLine); 768 | t >= oldestValidTime; 769 | t -= chartOptions.grid.millisPerLine) { 770 | var gx = timeToXPixel(t); 771 | // Only draw the timestamp if it won't overlap with the previously drawn one. 772 | if ((!chartOptions.scrollBackwards && gx < textUntilX) || (chartOptions.scrollBackwards && gx > textUntilX)) { 773 | // Formats the timestamp based on user specified formatting function 774 | // SmoothieChart.timeFormatter function above is one such formatting option 775 | var tx = new Date(t), 776 | ts = chartOptions.timestampFormatter(tx), 777 | tsWidth = context.measureText(ts).width; 778 | 779 | textUntilX = chartOptions.scrollBackwards 780 | ? gx + tsWidth + 2 781 | : gx - tsWidth - 2; 782 | 783 | context.fillStyle = chartOptions.labels.fillStyle; 784 | if(chartOptions.scrollBackwards) { 785 | context.fillText(ts, gx, dimensions.height - 2); 786 | } else { 787 | context.fillText(ts, gx - tsWidth, dimensions.height - 2); 788 | } 789 | } 790 | } 791 | } 792 | 793 | context.restore(); // See .save() above. 794 | }; 795 | 796 | // Sample timestamp formatting function 797 | SmoothieChart.timeFormatter = function(date) { 798 | function pad2(number) { return (number < 10 ? '0' : '') + number } 799 | return pad2(date.getHours()) + ':' + pad2(date.getMinutes()) + ':' + pad2(date.getSeconds()); 800 | }; 801 | 802 | exports.TimeSeries = TimeSeries; 803 | exports.SmoothieChart = SmoothieChart; 804 | 805 | })(typeof exports === 'undefined' ? this : exports); 806 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | font-family: "Montserrat", sans-serif; 4 | background-color: #232323; 5 | color: grey; 6 | /*border: 1px solid #333333;*/ 7 | } 8 | header h1, .wasmCheck { 9 | display: inline; 10 | border: none; 11 | } 12 | 13 | header h1 { 14 | font-size: 36px; 15 | padding-top: 10px; 16 | } 17 | 18 | header { 19 | margin-top: 20px; 20 | text-align: center; 21 | } 22 | 23 | .wasmCheck { 24 | float: right; 25 | margin-right: 30px; 26 | border: none; 27 | } 28 | 29 | #git-link { 30 | margin-top: 7px; 31 | text-align: center; 32 | } 33 | #git-link, a { 34 | font-size: 24px; 35 | color: silver; 36 | text-decoration: none; 37 | } 38 | 39 | #git-link, a:hover { 40 | color: white; 41 | text-decoration: none; 42 | } 43 | 44 | #webcamButton { 45 | /*font-family: "Cochin", serif;*/ 46 | margin-top: 3px; 47 | margin-left: 20px; 48 | font-size: 20px; 49 | color: silver; 50 | border: 2px solid; 51 | border-radius: 2px; 52 | } 53 | 54 | #editor { 55 | display: flex; 56 | flex-direction: row; 57 | padding: 20px; 58 | width: 100%; 59 | border: none; 60 | } 61 | #c { 62 | border: 2px solid black; 63 | } 64 | #filters { 65 | min-width: 200px; 66 | width: 200px; 67 | height: 480px; 68 | overflow-y: scroll; 69 | overflow-x: hidden; 70 | padding-left: 5px; 71 | border: 2px solid black; 72 | } 73 | .indFilter { 74 | width: 200px; 75 | border: 1px solid #333333; 76 | } 77 | .indFilter:hover { 78 | cursor: pointer; 79 | background-color: darkslategray; 80 | color: silver; 81 | } 82 | .selectedFilter { 83 | background-color: #1d2544; 84 | } 85 | 86 | #controls, #timing { 87 | display: flex; 88 | justify-content: space-between; 89 | width: 725px; 90 | } 91 | 92 | #jsButton, #jsCanvas { 93 | margin-top: 10px; 94 | margin-right: 3px; 95 | font-size: 20px; 96 | border: 2px solid; 97 | border-radius: 2px; 98 | } 99 | #playButton, #rewind, #fastForward, #loopButton { 100 | margin-top: 3px; 101 | border-radius: 3px; 102 | border: 2px solid; 103 | height: 40px; 104 | width: 80px; 105 | } 106 | #slowButton, #fastButton { 107 | border: 2px solid; 108 | } 109 | 110 | #vidTime { 111 | margin-top: 0px; 112 | font-size: 48px; 113 | color: #EFEFEF; 114 | border: none; 115 | } 116 | 117 | #middle-col { 118 | margin-left: 10px; 119 | margin-right: 10px; 120 | border: none; 121 | } 122 | #duration, #slowButton, #fastButton { 123 | margin-bottom: 10px; 124 | display: block; 125 | } 126 | #statsContainer { 127 | width: 400px; 128 | margin: 5px; 129 | border: 2px solid black; 130 | padding: 5px; 131 | } 132 | #jsButtonDiv { 133 | text-align: center; 134 | border: none; 135 | } 136 | 137 | #statsCanvas { 138 | display: block; 139 | border: none; 140 | } 141 | .header { 142 | display: inline; 143 | } 144 | 145 | 146 | -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include 3 | #include "./lib/catch.hpp" 4 | #include "../cpp/webdsp.cpp" 5 | 6 | TEST_CASE("Testing simple doubler function", "[doubler]") { 7 | 8 | SECTION("Doubling simple positive and negative integers") { 9 | REQUIRE( doubler(1) == 2 ); 10 | REQUIRE( doubler(100) == 200); 11 | } 12 | SECTION("Testing edge cases") { 13 | REQUIRE( doubler(0) == 0 ); 14 | REQUIRE( doubler(-1) == -2); 15 | REQUIRE( doubler(12.75) == 24); 16 | } 17 | } -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | const expect = require('chai').expect; 2 | const assert = require('assert'); 3 | const Browser = require('zombie'); 4 | 5 | Browser.localhost('example.com', 3000); 6 | 7 | describe('Should get to the page and', function () { 8 | let app, browser; 9 | before(function() { 10 | app = require('./../server'); 11 | browser = new Browser(); 12 | return browser.visit('/'); 13 | }); 14 | it('should have a title element with text "WASM Example"', function () { 15 | browser.assert.text('title', 'WASM Example'); 16 | }); 17 | it('should have the loadWASM function', function () { 18 | assert(browser.window.document._global.loadWASM); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/testCpp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shamadee/web-dsp-demo/97a57c6000ce5f803b45a0deaa483123b6e3b04b/test/testCpp --------------------------------------------------------------------------------