This is a test for a JavaScript graphical filter editor, created by me
150 | (Carlos Rafael Gimenes das Neves - @carlosrafaelgn, ), based on my old C++ graphic equalizer.
Please, load files with a sample rate of 44100Hz or 48000Hz (the filter itself supports any sample rate, this is just due to AudioContext).
153 |
Check out the functions main() and updateConnections() to see how to have stereo output using two analyzers!
154 |
If running this locally in Chrome, it must be started with the command-line option --allow-file-access-from-files otherwise you will not be able to load any files!
155 |
* Playing files via streaming was tested on Chrome v29.0.1547.76 and later, and on Firefox Nightly v27.0a1 (2013-10-04). If any error happens, or you hear no difference when changing the filter, please, load the entire file into memory before playing.
156 |
** Chrome v30 has apparently stopped to support loading large files into memory. If any error happens, or only a small portion of the file is played/filtered, please, use either Chrome v29/v32+ or Firefox v26/v27+.
157 |
158 |
564 |
565 |
576 |
599 |
600 |
601 |
--------------------------------------------------------------------------------
/lib/lib.js:
--------------------------------------------------------------------------------
1 |
2 | var CLib = (function() {
3 | var _scriptDir = typeof document !== 'undefined' && document.currentScript ? document.currentScript.src : undefined;
4 |
5 | return (
6 | function(CLib) {
7 | CLib = CLib || {};
8 |
9 | var Module=typeof CLib!=="undefined"?CLib:{};var readyPromiseResolve,readyPromiseReject;Module["ready"]=new Promise(function(resolve,reject){readyPromiseResolve=resolve;readyPromiseReject=reject});var moduleOverrides={};var key;for(key in Module){if(Module.hasOwnProperty(key)){moduleOverrides[key]=Module[key]}}var arguments_=[];var thisProgram="./this.program";var quit_=function(status,toThrow){throw toThrow};var ENVIRONMENT_IS_WEB=false;var ENVIRONMENT_IS_WORKER=false;var ENVIRONMENT_IS_NODE=false;var ENVIRONMENT_IS_SHELL=false;ENVIRONMENT_IS_WEB=typeof window==="object";ENVIRONMENT_IS_WORKER=typeof importScripts==="function";ENVIRONMENT_IS_NODE=typeof process==="object"&&typeof process.versions==="object"&&typeof process.versions.node==="string";ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var read_,readAsync,readBinary,setWindowTitle;if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!=="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptDir){scriptDirectory=_scriptDir}if(scriptDirectory.indexOf("blob:")!==0){scriptDirectory=scriptDirectory.substr(0,scriptDirectory.lastIndexOf("/")+1)}else{scriptDirectory=""}{read_=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.send(null);return xhr.responseText};if(ENVIRONMENT_IS_WORKER){readBinary=function(url){var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=function(url,onload,onerror){var xhr=new XMLHttpRequest;xhr.open("GET",url,true);xhr.responseType="arraybuffer";xhr.onload=function(){if(xhr.status==200||xhr.status==0&&xhr.response){onload(xhr.response);return}onerror()};xhr.onerror=onerror;xhr.send(null)}}setWindowTitle=function(title){document.title=title}}else{}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.warn.bind(console);for(key in moduleOverrides){if(moduleOverrides.hasOwnProperty(key)){Module[key]=moduleOverrides[key]}}moduleOverrides=null;if(Module["arguments"])arguments_=Module["arguments"];if(Module["thisProgram"])thisProgram=Module["thisProgram"];if(Module["quit"])quit_=Module["quit"];var wasmBinary;if(Module["wasmBinary"])wasmBinary=Module["wasmBinary"];var noExitRuntime=Module["noExitRuntime"]||true;if(typeof WebAssembly!=="object"){abort("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;var buffer,HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateGlobalBufferAndViews(buf){buffer=buf;Module["HEAP8"]=HEAP8=new Int8Array(buf);Module["HEAP16"]=HEAP16=new Int16Array(buf);Module["HEAP32"]=HEAP32=new Int32Array(buf);Module["HEAPU8"]=HEAPU8=new Uint8Array(buf);Module["HEAPU16"]=HEAPU16=new Uint16Array(buf);Module["HEAPU32"]=HEAPU32=new Uint32Array(buf);Module["HEAPF32"]=HEAPF32=new Float32Array(buf);Module["HEAPF64"]=HEAPF64=new Float64Array(buf)}var INITIAL_MEMORY=Module["INITIAL_MEMORY"]||327680;var wasmTable;var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=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 initRuntime(){runtimeInitialized=true;callRuntimeCallbacks(__ATINIT__)}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)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;function addRunDependency(id){runDependencies++;if(Module["monitorRunDependencies"]){Module["monitorRunDependencies"](runDependencies)}}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["preloadedImages"]={};Module["preloadedAudios"]={};function abort(what){if(Module["onAbort"]){Module["onAbort"](what)}what+="";err(what);ABORT=true;EXITSTATUS=1;what="abort("+what+"). Build with -s ASSERTIONS=1 for more info.";var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";function isDataURI(filename){return filename.startsWith(dataURIPrefix)}function isFileURI(filename){return filename.startsWith("file://")}var wasmBinaryFile="lib.wasm";if(!isDataURI(wasmBinaryFile)){wasmBinaryFile=locateFile(wasmBinaryFile)}function getBinary(file){try{if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}else{throw"both async and sync fetching of the wasm failed"}}catch(err){abort(err)}}function getBinaryPromise(){if(!wasmBinary&&(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER)){if(typeof fetch==="function"&&!isFileURI(wasmBinaryFile)){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){if(!response["ok"]){throw"failed to load wasm binary file at '"+wasmBinaryFile+"'"}return response["arrayBuffer"]()}).catch(function(){return getBinary(wasmBinaryFile)})}else{if(readAsync){return new Promise(function(resolve,reject){readAsync(wasmBinaryFile,function(response){resolve(new Uint8Array(response))},reject)})}}}return Promise.resolve().then(function(){return getBinary(wasmBinaryFile)})}function createWasm(){var info={"a":asmLibraryArg};function receiveInstance(instance,module){var exports=instance.exports;Module["asm"]=exports;wasmMemory=Module["asm"]["c"];updateGlobalBufferAndViews(wasmMemory.buffer);wasmTable=Module["asm"]["H"];addOnInit(Module["asm"]["d"]);removeRunDependency("wasm-instantiate")}addRunDependency("wasm-instantiate");function receiveInstantiationResult(result){receiveInstance(result["instance"])}function instantiateArrayBuffer(receiver){return getBinaryPromise().then(function(binary){var result=WebAssembly.instantiate(binary,info);return result}).then(receiver,function(reason){err("failed to asynchronously prepare wasm: "+reason);abort(reason)})}function instantiateAsync(){if(!wasmBinary&&typeof WebAssembly.instantiateStreaming==="function"&&!isDataURI(wasmBinaryFile)&&!isFileURI(wasmBinaryFile)&&typeof fetch==="function"){return fetch(wasmBinaryFile,{credentials:"same-origin"}).then(function(response){var result=WebAssembly.instantiateStreaming(response,info);return result.then(receiveInstantiationResult,function(reason){err("wasm streaming compile failed: "+reason);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(receiveInstantiationResult)})})}else{return instantiateArrayBuffer(receiveInstantiationResult)}}if(Module["instantiateWasm"]){try{var exports=Module["instantiateWasm"](info,receiveInstance);return exports}catch(e){err("Module.instantiateWasm callback failed with error: "+e);return false}}instantiateAsync().catch(readyPromiseReject);return{}}function callRuntimeCallbacks(callbacks){while(callbacks.length>0){var callback=callbacks.shift();if(typeof callback=="function"){callback(Module);continue}var func=callback.func;if(typeof func==="number"){if(callback.arg===undefined){wasmTable.get(func)()}else{wasmTable.get(func)(callback.arg)}}else{func(callback.arg===undefined?null:callback.arg)}}}function _emscripten_memcpy_big(dest,src,num){HEAPU8.copyWithin(dest,src,src+num)}function abortOnCannotGrowMemory(requestedSize){abort("OOM")}function _emscripten_resize_heap(requestedSize){var oldSize=HEAPU8.length;requestedSize=requestedSize>>>0;abortOnCannotGrowMemory(requestedSize)}var asmLibraryArg={"a":_emscripten_memcpy_big,"b":_emscripten_resize_heap};var asm=createWasm();var ___wasm_call_ctors=Module["___wasm_call_ctors"]=function(){return(___wasm_call_ctors=Module["___wasm_call_ctors"]=Module["asm"]["d"]).apply(null,arguments)};var _fftSizeOf=Module["_fftSizeOf"]=function(){return(_fftSizeOf=Module["_fftSizeOf"]=Module["asm"]["e"]).apply(null,arguments)};var _fftInit=Module["_fftInit"]=function(){return(_fftInit=Module["_fftInit"]=Module["asm"]["f"]).apply(null,arguments)};var _fftAlloc=Module["_fftAlloc"]=function(){return(_fftAlloc=Module["_fftAlloc"]=Module["asm"]["g"]).apply(null,arguments)};var _fftFree=Module["_fftFree"]=function(){return(_fftFree=Module["_fftFree"]=Module["asm"]["h"]).apply(null,arguments)};var _fftChangeN=Module["_fftChangeN"]=function(){return(_fftChangeN=Module["_fftChangeN"]=Module["asm"]["i"]).apply(null,arguments)};var _fft=Module["_fft"]=function(){return(_fft=Module["_fft"]=Module["asm"]["j"]).apply(null,arguments)};var _ffti=Module["_ffti"]=function(){return(_ffti=Module["_ffti"]=Module["asm"]["k"]).apply(null,arguments)};var _fftSizeOff=Module["_fftSizeOff"]=function(){return(_fftSizeOff=Module["_fftSizeOff"]=Module["asm"]["l"]).apply(null,arguments)};var _fftInitf=Module["_fftInitf"]=function(){return(_fftInitf=Module["_fftInitf"]=Module["asm"]["m"]).apply(null,arguments)};var _fftAllocf=Module["_fftAllocf"]=function(){return(_fftAllocf=Module["_fftAllocf"]=Module["asm"]["n"]).apply(null,arguments)};var _fftFreef=Module["_fftFreef"]=function(){return(_fftFreef=Module["_fftFreef"]=Module["asm"]["o"]).apply(null,arguments)};var _fftChangeNf=Module["_fftChangeNf"]=function(){return(_fftChangeNf=Module["_fftChangeNf"]=Module["asm"]["p"]).apply(null,arguments)};var _fftf=Module["_fftf"]=function(){return(_fftf=Module["_fftf"]=Module["asm"]["q"]).apply(null,arguments)};var _fftif=Module["_fftif"]=function(){return(_fftif=Module["_fftif"]=Module["asm"]["r"]).apply(null,arguments)};var _allocBuffer=Module["_allocBuffer"]=function(){return(_allocBuffer=Module["_allocBuffer"]=Module["asm"]["s"]).apply(null,arguments)};var _freeBuffer=Module["_freeBuffer"]=function(){return(_freeBuffer=Module["_freeBuffer"]=Module["asm"]["t"]).apply(null,arguments)};var _graphicalFilterEditorAlloc=Module["_graphicalFilterEditorAlloc"]=function(){return(_graphicalFilterEditorAlloc=Module["_graphicalFilterEditorAlloc"]=Module["asm"]["u"]).apply(null,arguments)};var _graphicalFilterEditorGetFilterKernelBuffer=Module["_graphicalFilterEditorGetFilterKernelBuffer"]=function(){return(_graphicalFilterEditorGetFilterKernelBuffer=Module["_graphicalFilterEditorGetFilterKernelBuffer"]=Module["asm"]["v"]).apply(null,arguments)};var _graphicalFilterEditorGetChannelCurve=Module["_graphicalFilterEditorGetChannelCurve"]=function(){return(_graphicalFilterEditorGetChannelCurve=Module["_graphicalFilterEditorGetChannelCurve"]=Module["asm"]["w"]).apply(null,arguments)};var _graphicalFilterEditorGetActualChannelCurve=Module["_graphicalFilterEditorGetActualChannelCurve"]=function(){return(_graphicalFilterEditorGetActualChannelCurve=Module["_graphicalFilterEditorGetActualChannelCurve"]=Module["asm"]["x"]).apply(null,arguments)};var _graphicalFilterEditorGetVisibleFrequencies=Module["_graphicalFilterEditorGetVisibleFrequencies"]=function(){return(_graphicalFilterEditorGetVisibleFrequencies=Module["_graphicalFilterEditorGetVisibleFrequencies"]=Module["asm"]["y"]).apply(null,arguments)};var _graphicalFilterEditorGetEquivalentZones=Module["_graphicalFilterEditorGetEquivalentZones"]=function(){return(_graphicalFilterEditorGetEquivalentZones=Module["_graphicalFilterEditorGetEquivalentZones"]=Module["asm"]["z"]).apply(null,arguments)};var _graphicalFilterEditorGetEquivalentZonesFrequencyCount=Module["_graphicalFilterEditorGetEquivalentZonesFrequencyCount"]=function(){return(_graphicalFilterEditorGetEquivalentZonesFrequencyCount=Module["_graphicalFilterEditorGetEquivalentZonesFrequencyCount"]=Module["asm"]["A"]).apply(null,arguments)};var _graphicalFilterEditorUpdateFilter=Module["_graphicalFilterEditorUpdateFilter"]=function(){return(_graphicalFilterEditorUpdateFilter=Module["_graphicalFilterEditorUpdateFilter"]=Module["asm"]["B"]).apply(null,arguments)};var _graphicalFilterEditorUpdateActualChannelCurve=Module["_graphicalFilterEditorUpdateActualChannelCurve"]=function(){return(_graphicalFilterEditorUpdateActualChannelCurve=Module["_graphicalFilterEditorUpdateActualChannelCurve"]=Module["asm"]["C"]).apply(null,arguments)};var _graphicalFilterEditorChangeFilterLength=Module["_graphicalFilterEditorChangeFilterLength"]=function(){return(_graphicalFilterEditorChangeFilterLength=Module["_graphicalFilterEditorChangeFilterLength"]=Module["asm"]["D"]).apply(null,arguments)};var _graphicalFilterEditorFree=Module["_graphicalFilterEditorFree"]=function(){return(_graphicalFilterEditorFree=Module["_graphicalFilterEditorFree"]=Module["asm"]["E"]).apply(null,arguments)};var _plainAnalyzer=Module["_plainAnalyzer"]=function(){return(_plainAnalyzer=Module["_plainAnalyzer"]=Module["asm"]["F"]).apply(null,arguments)};var _waveletAnalyzer=Module["_waveletAnalyzer"]=function(){return(_waveletAnalyzer=Module["_waveletAnalyzer"]=Module["asm"]["G"]).apply(null,arguments)};var calledRun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function run(args){args=args||arguments_;if(runDependencies>0){return}preRun();if(runDependencies>0){return}function doRun(){if(calledRun)return;calledRun=true;Module["calledRun"]=true;if(ABORT)return;initRuntime();readyPromiseResolve(Module);if(Module["onRuntimeInitialized"])Module["onRuntimeInitialized"]();postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(function(){setTimeout(function(){Module["setStatus"]("")},1);doRun()},1)}else{doRun()}}Module["run"]=run;if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();
10 |
11 |
12 | return CLib.ready
13 | }
14 | );
15 | })();
16 | if (typeof exports === 'object' && typeof module === 'object')
17 | module.exports = CLib;
18 | else if (typeof define === 'function' && define['amd'])
19 | define([], function() { return CLib; });
20 | else if (typeof exports === 'object')
21 | exports["CLib"] = CLib;
22 |
--------------------------------------------------------------------------------
/lib/lib.js.mem:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/lib/lib.js.mem
--------------------------------------------------------------------------------
/lib/lib.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | interface CLib {
28 | HEAP8: Uint8Array;
29 | HEAPF32: Float32Array;
30 |
31 | _allocBuffer(size: number): number;
32 | _freeBuffer(ptr: number): void;
33 |
34 | _fftSizeOf(n: number): number;
35 | _fftInit(fft4gPtr: number, n: number): number;
36 | _fftAlloc(n: number): number;
37 | _fftFree(fft4gPtr: number): void;
38 | _fft(fft4gPtr: number, dataPtr: number): void;
39 | _ffti(fft4gPtr: number, dataPtr: number): void;
40 |
41 | _fftSizeOff(n: number): number;
42 | _fftInitf(fft4gfPtr: number, n: number): number;
43 | _fftAllocf(n: number): number;
44 | _fftFreef(fft4gfPtr: number): void;
45 | _fftf(fft4gfPtr: number, dataPtr: number): void;
46 | _fftif(fft4gfPtr: number, dataPtr: number): void;
47 |
48 | _graphicalFilterEditorAlloc(filterLength: number, sampleRate: number): number;
49 | _graphicalFilterEditorGetFilterKernelBuffer(editorPtr: number): number;
50 | _graphicalFilterEditorGetChannelCurve(editorPtr: number, channel: number): number;
51 | _graphicalFilterEditorGetActualChannelCurve(editorPtr: number): number;
52 | _graphicalFilterEditorGetVisibleFrequencies(editorPtr: number): number;
53 | _graphicalFilterEditorGetEquivalentZones(editorPtr: number): number;
54 | _graphicalFilterEditorGetEquivalentZonesFrequencyCount(editorPtr: number): number;
55 | _graphicalFilterEditorUpdateFilter(editorPtr: number, channelIndex: number, isNormalized: boolean): void;
56 | _graphicalFilterEditorUpdateActualChannelCurve(editorPtr: number, channelIndex: number): void;
57 | _graphicalFilterEditorChangeFilterLength(editorPtr: number, newFilterLength: number): void;
58 | _graphicalFilterEditorFree(editorPtr: number): void;
59 |
60 | _plainAnalyzer(fft4gfPtr: number, windowPtr: number, dataPtr: number, tmpPtr: number): void;
61 | _waveletAnalyzer(dataLPtr: number, dataRPtr: number, tmpPtr: number, oL1Ptr: number, oR1Ptr: number): void;
62 | }
63 |
--------------------------------------------------------------------------------
/lib/lib.wasm:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/lib/lib.wasm
--------------------------------------------------------------------------------
/lib/src/common.h:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | // Must be in sync with scripts/graphicalFilterEditor/graphicalFilterEditor.ts
28 | // Sorry, but due to the frequency mapping I created, this class will only work with
29 | // 500 visible bins... in order to change this, a new frequency mapping must be created...
30 | #define VisibleBinCount 500
31 | #define ValidYRangeHeight 321
32 | #define ZeroChannelValueY (ValidYRangeHeight >> 1)
33 | #define MaximumChannelValue ZeroChannelValueY
34 | #define MinimumChannelValue -ZeroChannelValueY
35 | #define MinusInfiniteChannelValue (MinimumChannelValue - 1)
36 | #define MaximumChannelValueY 0
37 | #define MinimumChannelValueY (ValidYRangeHeight - 1)
38 | #define MaximumFilterLength 8192
39 | #define EquivalentZoneCount 10
40 |
41 | extern double lerp(double x0, double y0, double x1, double y1, double x);
42 | extern float lerpf(float x0, float y0, float x1, float y1, float x);
43 |
--------------------------------------------------------------------------------
/lib/src/fft4g.h:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | #include "common.h"
28 |
29 | typedef struct FFT4gStruct {
30 | int n, maxN;
31 | // According to the spec: length of ip >= 2+sqrt(n/2)
32 | // Since MaximumFilterLength = 8192, length of ip >= 2+sqrt(8192/2) = 66
33 | int ip[70];
34 | // 70 + 2 = 72
35 | // 72 * 4 bytes = 288 bytes = 100100000 (w is aligned in a 32-byte boundary)
36 | double w[0];
37 | } FFT4g;
38 |
39 | typedef struct FFT4gfStruct {
40 | int n, maxN;
41 | // According to the spec: length of ip >= 2+sqrt(n/2)
42 | // Since MaximumFilterLength = 8192, length of ip >= 2+sqrt(8192/2) = 66
43 | int ip[70];
44 | // 70 + 2 = 72
45 | // 72 * 4 bytes = 288 bytes = 100100000 (w is aligned in a 32-byte boundary)
46 | float w[0];
47 | } FFT4gf;
48 |
49 | extern size_t fftSizeOf(int n);
50 | extern FFT4g* fftInit(FFT4g* fft4g, int n);
51 | extern FFT4g* fftAlloc(int n);
52 | extern void fftFree(FFT4g* fft4g);
53 | extern void fftChangeN(FFT4g* fft4g, int n);
54 |
55 | extern size_t fftSizeOff(int n);
56 | extern FFT4gf* fftInitf(FFT4gf* fft4gf, int n);
57 | extern FFT4gf* fftAllocf(int n);
58 | extern void fftFreef(FFT4gf* fft4gf);
59 | extern void fftChangeNf(FFT4gf* fft4gf, int n);
60 |
61 | // Ordering of data
62 | // time [0] | Real [bin 0]
63 | // time [1] | Real [bin n / 2]
64 | // time [2] | Real [bin 1]
65 | // time [3] | Imag [bin 1]
66 | // time [...] | Real [bin ...]
67 | // time [...] | Imag [bin ...]
68 | // time [n - 2] | Real [bin (n / 2) - 1]
69 | // time [n - 1] | Imag [bin (n / 2) - 1]
70 |
71 | extern void fft(FFT4g* fft4g, double* data);
72 | extern void ffti(FFT4g* fft4g, double* data);
73 | extern void fftf(FFT4gf* fft4gf, float* data);
74 | extern void fftif(FFT4gf* fft4gf, float* data);
75 |
--------------------------------------------------------------------------------
/lib/src/graphicalFilterEditor.c:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include "fft4g.h"
32 |
33 | void* allocBuffer(size_t size) {
34 | return malloc(size);
35 | }
36 |
37 | void freeBuffer(void* ptr) {
38 | if (ptr)
39 | free(ptr);
40 | }
41 |
42 | typedef struct GraphicalFilterEditorStruct {
43 | double filterKernelBuffer[MaximumFilterLength];
44 | double tmp[MaximumFilterLength];
45 | double visibleFrequencies[VisibleBinCount];
46 | int channelCurves[2][VisibleBinCount];
47 | int actualChannelCurve[VisibleBinCount];
48 | int equivalentZones[EquivalentZoneCount];
49 | int equivalentZonesFrequencyCount[EquivalentZoneCount + 1];
50 |
51 | int filterLength, sampleRate, binCount;
52 |
53 | // Must be last member
54 | FFT4g fft4g;
55 | } GraphicalFilterEditor;
56 |
57 | double lerp(double x0, double y0, double x1, double y1, double x) {
58 | return ((x - x0) * (y1 - y0) / (x1 - x0)) + y0;
59 | }
60 |
61 | float lerpf(float x0, float y0, float x1, float y1, float x) {
62 | return ((x - x0) * (y1 - y0) / (x1 - x0)) + y0;
63 | }
64 |
65 | GraphicalFilterEditor* graphicalFilterEditorAlloc(int filterLength, int sampleRate) {
66 | const size_t size = sizeof(GraphicalFilterEditor) - sizeof(FFT4g) + fftSizeOf(MaximumFilterLength);
67 | GraphicalFilterEditor* editor = (GraphicalFilterEditor*)malloc(size);
68 | memset(editor, 0, size);
69 |
70 | fftInit(&(editor->fft4g), MaximumFilterLength);
71 | fftChangeN(&(editor->fft4g), filterLength);
72 |
73 | editor->filterLength = filterLength;
74 | editor->sampleRate = sampleRate;
75 | editor->binCount = (filterLength >> 1) + 1;
76 |
77 | // First frequency mapping (512 bins - Original 2013)
78 | // const int freqSteps[] = { 5, 5, 5, 5, 10, 10, 20, 40, 80, 89 };
79 | // const int firstFreqs[] = { 5, 50, 95, 185, 360, 720, 1420, 2860, 5740, 11498 };
80 | // const int equivalentZones[] = { 31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 };
81 | // const int equivalentZonesFrequencyCount[] = { 0, 9, 9 + 9, 18 + 9 + 9, 35 + 18 + 9 + 9, 36 + 35 + 18 + 9 + 9, 70 + 36 + 35 + 18 + 9 + 9, 72 + 70 + 36 + 35 + 18 + 9 + 9, 72 + 72 + 70 + 36 + 35 + 18 + 9 + 9, 72 + 72 + 72 + 70 + 36 + 35 + 18 + 9 + 9, VisibleBinCount };
82 | //
83 | // Second frequency mapping (500 bins - logarithmic divisions/zones with linear inner frequencies - 03-2021)
84 | // Equivalent zone 31.25 | 62.5 | 125 | 250 | 500 | 1000 | 2000 | 4000 | 8000 | 16000
85 | // First frequency 0 | 46.875 | 93.75 | 187.5 | 375 | 750 | 1500 | 3000 | 6000 | 12000
86 | // Last frequency 46.875 | 93.75 | 187.5 | 375 | 750 | 1500 | 3000 | 6000 | 12000 | 24000
87 | // Steps 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50
88 | //const float freqSteps[] = { 0.9375f, 0.9375f, 1.875f, 3.75f, 7.5f, 15.0f, 30.0f, 60.0f, 120.0f, 240.0f };
89 | //const float firstFreqs[] = { 0.0f, 46.875f, 93.75f, 187.5f, 375.0f, 750.0f, 1500.0f, 3000.0f, 6000.0f, 12000.0f };
90 | //
91 | // Third frequency mapping (500 bins - logarithmic divisions/zones with logarithmic inner frequencies - 06-2021)
92 | // Equivalent zone 31.25 | 62.5 | 125 | 250 | 500 | 1000 | 2000 | 4000 | 8000 | 16000
93 | // First frequency 0 | 46.875 | 93.75 | 187.5 | 375 | 750 | 1500 | 3000 | 6000 | 12000
94 | // Last frequency 46.875 | 93.75 | 187.5 | 375 | 750 | 1500 | 3000 | 6000 | 12000 | 24000
95 | // Steps 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50 | 50
96 | // With the exception of the frequencies belonging to the first zone, all other frequencies
97 | // were generated using an exponential step of 2^(1/50) =
98 | const int equivalentZones[] = { 31, 62, 125, 250, 500, 1000, 2000, 4000, 8000, 16000 };
99 | const int equivalentZonesFrequencyCount[] = { 0, 50, 100, 150, 200, 250, 300, 350, 400, 450, VisibleBinCount };
100 | const double firstFreqs[] = { 0.0, 46.875, 93.75, 187.5, 375.0, 750.0, 1500.0, 3000.0, 6000.0, 12000.0 };
101 | const double step = 1.0139594797900291386901659996282;
102 |
103 | memcpy(editor->equivalentZones, equivalentZones, sizeof(int) * EquivalentZoneCount);
104 | memcpy(editor->equivalentZonesFrequencyCount, equivalentZonesFrequencyCount, sizeof(int) * (EquivalentZoneCount + 1));
105 |
106 | double* const visibleFrequencies = editor->visibleFrequencies;
107 |
108 | double f = 0.0;
109 | for (int i = 0; i < equivalentZonesFrequencyCount[1]; i++) {
110 | visibleFrequencies[i] = f;
111 | f += 0.9375;
112 | }
113 |
114 | for (int z = 1; z < EquivalentZoneCount; z++) {
115 | int i = equivalentZonesFrequencyCount[z];
116 | const int e = equivalentZonesFrequencyCount[z + 1];
117 | f = firstFreqs[z];
118 | while (i < e) {
119 | visibleFrequencies[i] = f;
120 | f *= step;
121 | i++;
122 | }
123 | }
124 |
125 | int* const channelCurves0 = editor->channelCurves[0];
126 | int* const channelCurves1 = editor->channelCurves[1];
127 | int* const actualChannelCurve = editor->actualChannelCurve;
128 | for (int i = VisibleBinCount - 1; i >= 0; i--) {
129 | channelCurves0[i] = ZeroChannelValueY;
130 | channelCurves1[i] = ZeroChannelValueY;
131 | actualChannelCurve[i] = ZeroChannelValueY;
132 | }
133 |
134 | return editor;
135 | }
136 |
137 | double* graphicalFilterEditorGetFilterKernelBuffer(GraphicalFilterEditor* editor) {
138 | return editor->filterKernelBuffer;
139 | }
140 |
141 | int* graphicalFilterEditorGetChannelCurve(GraphicalFilterEditor* editor, int channel) {
142 | return editor->channelCurves[channel];
143 | }
144 |
145 | int* graphicalFilterEditorGetActualChannelCurve(GraphicalFilterEditor* editor) {
146 | return editor->actualChannelCurve;
147 | }
148 |
149 | double* graphicalFilterEditorGetVisibleFrequencies(GraphicalFilterEditor* editor) {
150 | return editor->visibleFrequencies;
151 | }
152 |
153 | int* graphicalFilterEditorGetEquivalentZones(GraphicalFilterEditor* editor) {
154 | return editor->equivalentZones;
155 | }
156 |
157 | int* graphicalFilterEditorGetEquivalentZonesFrequencyCount(GraphicalFilterEditor* editor) {
158 | return editor->equivalentZonesFrequencyCount;
159 | }
160 |
161 | double yToMagnitude(double y) {
162 | // 40dB = 100
163 | // -40dB = 0.01
164 | // magnitude = 10 ^ (dB/20)
165 | // log a (x^p) = p * log a (x)
166 | // x^p = a ^ (p * log a (x))
167 | // 10^p = e ^ (p * log e (10))
168 | return ((y <= MaximumChannelValueY) ? 100.0 :
169 | ((y > MinimumChannelValueY) ? 0.0 :
170 | // 2 = 40dB/20
171 | // 2.302585092994046 = LN10
172 | exp(lerp(MaximumChannelValueY, 2, MinimumChannelValueY, -2, y) * 2.302585092994046)));
173 | }
174 |
175 | int magnitudeToY(double magnitude) {
176 | // 40dB = 100
177 | // -40dB = 0.01 (we are using 0.009 due to float point errors)
178 | return ((magnitude >= 100.0) ? MaximumChannelValueY :
179 | ((magnitude < 0.009) ? (ValidYRangeHeight + 1) :
180 | // 2.302585092994046 = LN10
181 | (int)round((ZeroChannelValueY - (ZeroChannelValueY * log(magnitude) / 2.302585092994046 * 0.5)) - 0.4)));
182 | }
183 |
184 | double applyWindowAndComputeActualMagnitudes(GraphicalFilterEditor* editor, const double* filter) {
185 | const int filterLength = editor->filterLength;
186 | const int M = (filterLength >> 1);
187 | const double PI2_M = 6.283185307179586476925286766559 / (double)M;
188 |
189 | double* const tmp = editor->tmp;
190 |
191 | int i;
192 | double ii, rval, ival, maxMag, mag;
193 |
194 | // It is not possible to know what kind of window the browser will use,
195 | // so make an assumption here... Blackman window!
196 | // ...at least it is the one I used, back in C++ times :)
197 | for (i = M; i >= 0; i--) {
198 | // Hanning window
199 | // tmp[i] = filter[i] * (0.5 - (0.5 * cos(PI2_M * (double)i)));
200 | // Hamming window
201 | // tmp[i] = filter[i] * (0.54 - (0.46 * cos(PI2_M * (double)i)));
202 | // Blackman window
203 | tmp[i] = filter[i] * (0.42 - (0.5 * cos(PI2_M * (double)i)) + (0.08 * cos(2.0 * PI2_M * (double)i)));
204 | }
205 |
206 | for (i = filterLength - 1; i > M; i--)
207 | tmp[i] = 0;
208 |
209 | // Calculate the spectrum
210 | fft(&(editor->fft4g), tmp);
211 |
212 | // Save Nyquist for later
213 | ii = tmp[1];
214 | maxMag = (tmp[0] > ii ? tmp[0] : ii);
215 | for (i = 2; i < filterLength; i += 2) {
216 | rval = tmp[i];
217 | ival = tmp[i + 1];
218 | mag = sqrt((rval * rval) + (ival * ival));
219 | tmp[i >> 1] = mag;
220 | if (mag > maxMag) maxMag = mag;
221 | }
222 |
223 | // Restore Nyquist in its new position
224 | tmp[M] = ii;
225 |
226 | return maxMag;
227 | }
228 |
229 | void graphicalFilterEditorUpdateFilter(GraphicalFilterEditor* editor, int channelIndex, int isNormalized) {
230 | const int filterLength = editor->filterLength;
231 | const int filterLength2 = (filterLength >> 1);
232 | const double bw = (double)editor->sampleRate / (double)filterLength;
233 | // M = filterLength2, so, M_HALF_PI_FFTLEN2 = (filterLength2 * 0.5 * Math.PI) / filterLength2
234 | const double M_HALF_PI_FFTLEN2 = 1.5707963267948966192313216916398;
235 |
236 | double* const filter = editor->filterKernelBuffer;
237 | double* const tmp = editor->tmp;
238 | const int* const curve = editor->channelCurves[channelIndex];
239 | const double* const visibleFrequencies = editor->visibleFrequencies;
240 |
241 | int i, ii, avgCount, repeat = (isNormalized ? 2 : 1);
242 | double k, mag, freq, avg, invMaxMag = 1.0;
243 |
244 | // Fill in all filter points, either averaging or interpolating them as necessary
245 | do {
246 | repeat--;
247 | i = 1;
248 | ii = 0;
249 | for (; ;) {
250 | freq = bw * (double)i;
251 | if (freq >= visibleFrequencies[0]) break;
252 | mag = yToMagnitude((double)curve[0]);
253 | filter[i << 1] = mag * invMaxMag;
254 | i++;
255 | }
256 |
257 | while (bw > (visibleFrequencies[ii + 1] - visibleFrequencies[ii]) && i < filterLength2 && ii < (VisibleBinCount - 1)) {
258 | freq = bw * (double)i;
259 | avg = 0.0;
260 | avgCount = 0;
261 | do {
262 | avg += (double)curve[ii];
263 | avgCount++;
264 | ii++;
265 | } while (freq > visibleFrequencies[ii] && ii < (VisibleBinCount - 1));
266 | mag = yToMagnitude(avg / (double)avgCount);
267 | filter[i << 1] = mag * invMaxMag;
268 | i++;
269 | }
270 |
271 | for (; i < filterLength2; i++) {
272 | freq = bw * (double)i;
273 | if (freq >= visibleFrequencies[VisibleBinCount - 1]) {
274 | mag = yToMagnitude((double)curve[VisibleBinCount - 1]);
275 | } else {
276 | while (ii < (VisibleBinCount - 1) && freq > visibleFrequencies[ii + 1])
277 | ii++;
278 | mag = yToMagnitude(lerp(visibleFrequencies[ii], (double)curve[ii], visibleFrequencies[ii + 1], (double)curve[ii + 1], freq));
279 | }
280 | filter[i << 1] = mag * invMaxMag;
281 | }
282 |
283 | // Since DC and Nyquist are purely real, do not bother with them in the for loop,
284 | // just make sure neither one has a gain greater than 0 dB
285 | filter[0] = (filter[2] >= 1.0 ? 1.0 : filter[2]);
286 | filter[1] = (filter[filterLength - 2] >= 1.0 ? 1.0 : filter[filterLength - 2]);
287 |
288 | // Convert the coordinates from polar to rectangular
289 | for (i = filterLength - 2; i >= 2; i -= 2) {
290 | // -k.j
291 | // polar = Mag . e
292 | //
293 | // Where:
294 | // k = (M / 2) * pi * i / (fft length / 2)
295 | // i = index varying from 0 to (fft length / 2)
296 | //
297 | // rectangular:
298 | // real = Mag . cos(-k)
299 | // imag = Mag . sin(-k)
300 | k = M_HALF_PI_FFTLEN2 * (double)(i >> 1);
301 | // **** NOTE:
302 | // When using FFT4g, FFTReal or FFTNR, k MUST BE passed as the argument of sin and cos, due to the
303 | // signal of the imaginary component
304 | // RFFT, intel and other fft's use the opposite signal... therefore, -k MUST BE passed!!
305 | filter[i + 1] = (filter[i] * sin(k));
306 | filter[i] *= cos(k);
307 | }
308 |
309 | ffti(&(editor->fft4g), filter);
310 |
311 | if (repeat) {
312 | // Get the actual filter response, and then, compensate
313 | invMaxMag = applyWindowAndComputeActualMagnitudes(editor, filter);
314 | if (invMaxMag <= 0.0) repeat = 0;
315 | invMaxMag = 1.0 / invMaxMag;
316 | }
317 | } while (repeat);
318 |
319 | // AudioContext uses floats, not doubles...
320 | float* const filterf = (float*)filter;
321 | for (int i = 0; i < filterLength; i++)
322 | filterf[i] = (float)filter[i];
323 | }
324 |
325 | void graphicalFilterEditorUpdateActualChannelCurve(GraphicalFilterEditor* editor, int channelIndex) {
326 | const int filterLength = editor->filterLength;
327 | const int filterLength2 = (filterLength >> 1);
328 | const double bw = (double)editor->sampleRate / (double)filterLength;
329 | // M = filterLength2, so, M_HALF_PI_FFTLEN2 = (filterLength2 * 0.5 * Math.PI) / filterLength2
330 | const double M_HALF_PI_FFTLEN2 = 1.5707963267948966192313216916398;
331 |
332 | double* const filter = editor->filterKernelBuffer;
333 | double* const tmp = editor->tmp;
334 | int* const curve = editor->actualChannelCurve;
335 | const double* const visibleFrequencies = editor->visibleFrequencies;
336 |
337 | int i, ii, avgCount;
338 | double avg, freq;
339 |
340 | // AudioContext uses floats, not doubles...
341 | float* const filterf = (float*)filter;
342 | for (int i = filterLength - 1; i >= 0; i--)
343 | filter[i] = (double)filterf[i];
344 |
345 | applyWindowAndComputeActualMagnitudes(editor, filter);
346 |
347 | // tmp now contains (filterLength2 + 1) magnitudes
348 | i = 0;
349 | ii = 0;
350 | while (ii < (VisibleBinCount - 1) && i < filterLength2 && bw > (visibleFrequencies[ii + 1] - visibleFrequencies[ii])) {
351 | freq = bw * (double)i;
352 | while (i < filterLength2 && (freq + bw) < visibleFrequencies[ii]) {
353 | i++;
354 | freq = bw * (double)i;
355 | }
356 | curve[ii] = magnitudeToY(lerp(freq, tmp[i], freq + bw, tmp[i + 1], visibleFrequencies[ii]));
357 | ii++;
358 | }
359 |
360 | i++;
361 | while (i < filterLength2 && ii < VisibleBinCount) {
362 | avg = 0.0;
363 | avgCount = 0;
364 | do {
365 | avg += tmp[i];
366 | avgCount++;
367 | i++;
368 | freq = bw * (double)i;
369 | } while (freq < visibleFrequencies[ii] && i < filterLength2);
370 | curve[ii] = magnitudeToY(avg / (double)avgCount);
371 | ii++;
372 | }
373 |
374 | // Just to avoid displaying the last few pixels as -Inf. dB on devices with a sample rate of 44100Hz
375 | i = (((editor->sampleRate >> 1) >= 22050) ? curve[ii - 1] : (ValidYRangeHeight + 1));
376 |
377 | for (; ii < VisibleBinCount; ii++)
378 | curve[ii] = i;
379 | }
380 |
381 | void graphicalFilterEditorChangeFilterLength(GraphicalFilterEditor* editor, int newFilterLength) {
382 | editor->filterLength = newFilterLength;
383 | fftChangeN(&(editor->fft4g), newFilterLength);
384 | }
385 |
386 | void graphicalFilterEditorFree(GraphicalFilterEditor* editor) {
387 | if (editor)
388 | free(editor);
389 | }
390 |
--------------------------------------------------------------------------------
/lib/src/plainAnalyzer.c:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | #include
28 | #include
29 | #include
30 | #include
31 | #include "fft4g.h"
32 |
33 | void plainAnalyzer(FFT4gf* fft4gf, const float* window, const unsigned char* data, float* tmp) {
34 | for (int i = 0; i < 1024; i++)
35 | tmp[i] = window[i] * (float)(((int)data[i]) - 128);
36 |
37 | memset(tmp + 1024, 0, 1024 * sizeof(float));
38 |
39 | fftf(fft4gf, tmp);
40 |
41 | // DC and Nyquist bins are being ignored
42 | tmp[0] = 0;
43 | tmp[1] = 0;
44 | for (int i = 2; i < 2048; i += 2) {
45 | // 0.0009765625 = 1 / (2048/2)
46 | const float d = tmp[i] * 0.0009765625f; // re
47 | const float im = tmp[i + 1] * 0.0009765625f; // im
48 | tmp[i >> 1] = logf(sqrtf((d * d) + (im * im)) + 0.2f);
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/lib/src/waveletAnalyzer.c:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | #include
28 | #include
29 | #include
30 | #include
31 |
32 | void haar(float* x, int n, float* tmp) {
33 | // input:
34 | // original time / previous lo pass data
35 | // output:
36 | // scaling = lo pass = 1st half
37 | // wl coeff = hi pass = 2nd half
38 | // 0.70710678118654752440084436210485 = sqrt(2) / 2
39 |
40 | const int n2 = n >> 1;
41 |
42 | for (int i = 0; i < n; i += 2) {
43 | const float xi = x[i];
44 | const float xi1 = x[i + 1];
45 | x[i >> 1] = (xi + xi1) * 0.70710678118654752440084436210485f;
46 | tmp[i >> 1] = (xi - xi1) * 0.70710678118654752440084436210485f;
47 | }
48 |
49 | for (int i = n2; i < n; i++)
50 | x[i] = tmp[i - n2];
51 | }
52 |
53 | void waveletAnalyzer(const unsigned char* dataL, const unsigned char* dataR, float* tmp, float* oL1, float* oR1) {
54 | int i;
55 |
56 | for (int i = 0; i < 128; i++)
57 | oL1[i] = (float)(((int)dataL[i]) - 128) * 4;
58 | i = 128;
59 | while (i >= 4) {
60 | haar(oL1, i, tmp);
61 | i >>= 1;
62 | }
63 |
64 | for (int i = 0; i < 128; i++)
65 | oR1[i] = (float)(((int)dataR[i]) - 128) * 4;
66 | i = 128;
67 | while (i >= 4) {
68 | haar(oR1, i, tmp);
69 | i >>= 1;
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/scripts/analyzer/analyzer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | abstract class Analyzer {
28 | public static readonly controlWidth = GraphicalFilterEditorControl.controlWidth;
29 | public static readonly controlHeight = Analyzer.controlWidth;
30 |
31 | protected static readonly colors = ["#000000", "#0B00B2", "#0C00B1", "#0E00AF", "#0E00AF", "#0F00AE", "#1000AD", "#1200AC", "#1300AB", "#1500AB", "#1600AA", "#1700A9", "#1900A8", "#1A00A6", "#1B00A6", "#1D00A4", "#1F00A3", "#2000A1", "#2200A1", "#2300A0", "#25009E", "#27009D", "#29009C", "#2B009A", "#2D0099", "#2E0098", "#300096", "#320095", "#340094", "#360092", "#380090", "#39008F", "#3C008E", "#3E008C", "#40008B", "#420089", "#440088", "#470086", "#480085", "#4B0083", "#4C0082", "#4F0080", "#51007F", "#54007C", "#56007C", "#57007A", "#5A0078", "#5C0076", "#5F0075", "#610073", "#640071", "#65006F", "#68006E", "#6B006C", "#6D006A", "#6F0069", "#710066", "#740065", "#760063", "#790062", "#7B0060", "#7D005E", "#80005C", "#82005B", "#850059", "#860057", "#890056", "#8C0054", "#8E0052", "#910050", "#93004F", "#96004D", "#97004B", "#9A0049", "#9C0048", "#9F0046", "#A10045", "#A40043", "#A60040", "#A8003F", "#AA003E", "#AD003C", "#AF003A", "#B10039", "#B30037", "#B60035", "#B80034", "#BA0032", "#BC0031", "#BE002E", "#C1002D", "#C3002C", "#C5002A", "#C70028", "#CA0027", "#CB0025", "#CE0024", "#CF0023", "#D10022", "#D30020", "#D6001E", "#D7001D", "#D9001B", "#DB001A", "#DD0019", "#DF0017", "#E10017", "#E20015", "#E40014", "#E60012", "#E70011", "#E90010", "#EA000F", "#EC000D", "#ED000C", "#EF000B", "#F1000B", "#F2000A", "#F40008", "#F50007", "#F60006", "#F70005", "#F90005", "#F90003", "#FB0003", "#FC0002", "#FD0001", "#FE0001", "#FF0000", "#FF0100", "#FF0200", "#FF0300", "#FF0500", "#FF0600", "#FF0600", "#FF0800", "#FF0900", "#FF0B00", "#FF0C00", "#FF0D00", "#FF0F00", "#FF1000", "#FF1200", "#FF1400", "#FF1500", "#FF1700", "#FF1900", "#FF1A00", "#FF1C00", "#FF1D00", "#FF2000", "#FF2200", "#FF2300", "#FF2500", "#FF2700", "#FF2900", "#FF2B00", "#FF2D00", "#FF2F00", "#FF3100", "#FF3400", "#FF3500", "#FF3700", "#FF3900", "#FF3C00", "#FF3E00", "#FF4000", "#FF4200", "#FF4400", "#FF4700", "#FF4900", "#FF4B00", "#FF4E00", "#FF5000", "#FF5200", "#FF5500", "#FF5700", "#FF5900", "#FF5C00", "#FF5E00", "#FF6100", "#FF6300", "#FF6600", "#FF6800", "#FF6A00", "#FF6C00", "#FF6F00", "#FF7200", "#FF7400", "#FF7700", "#FF7900", "#FF7C00", "#FF7E00", "#FF8000", "#FF8300", "#FF8500", "#FF8700", "#FF8A00", "#FF8D00", "#FF8F00", "#FF9200", "#FF9500", "#FF9700", "#FF9900", "#FF9B00", "#FF9E00", "#FFA000", "#FFA300", "#FFA500", "#FFA700", "#FFA900", "#FFAC00", "#FFAE00", "#FFB100", "#FFB200", "#FFB600", "#FFB700", "#FFBA00", "#FFBC00", "#FFBE00", "#FFC100", "#FFC300", "#FFC400", "#FFC700", "#FFC900", "#FFCB00", "#FFCD00", "#FFCF00", "#FFD100", "#FFD300", "#FFD500", "#FFD700", "#FFD900", "#FFDB00", "#FFDD00", "#FFDE00", "#FFE000", "#FFE100", "#FFE400", "#FFE500", "#FFE700", "#FFE900", "#FFEA00", "#FFEB00", "#FFED00", "#FFEF00", "#FFF000", "#FFF100", "#FFF300", "#FFF400", "#FFF500", "#FFF600", "#FFF800", "#FFF900", "#FFFA00", "#FFFB00"];
32 |
33 | protected audioContext: AudioContext;
34 | protected canvas: HTMLCanvasElement;
35 | protected ctx: CanvasRenderingContext2D | null;
36 |
37 | private _alive = false;
38 | private _lastRequest = 0;
39 |
40 | private _boundAnalyze: any = null;
41 |
42 | public constructor(audioContext: AudioContext, parent: HTMLElement, id?: string, ignoreContext?: boolean, controlWidth?: number, controlHeight?: number) {
43 | this.audioContext = audioContext;
44 |
45 | this.canvas = document.createElement("canvas");
46 | this.canvas.width = ((!controlWidth || controlWidth <= 0) ? Analyzer.controlWidth : controlWidth);
47 | this.canvas.height = ((!controlHeight || controlHeight <= 0) ? Analyzer.controlHeight : controlHeight);
48 | this.canvas.style.position = "relative";
49 | this.canvas.style.margin = "0px";
50 | this.canvas.style.padding = "0px";
51 | this.canvas.style.verticalAlign = "top";
52 | this.canvas.style.display = "inline-block";
53 | this.canvas.style.cursor = "default";
54 |
55 | if (id)
56 | this.canvas.id = id;
57 |
58 | this.ctx = (ignoreContext ? null : this.canvas.getContext("2d", { alpha: false }));
59 | parent.appendChild(this.canvas);
60 |
61 | this._boundAnalyze = (time: number) => {
62 | if (!this._alive) {
63 | this._lastRequest = 0;
64 | return;
65 | }
66 |
67 | this._lastRequest = requestAnimationFrame(this._boundAnalyze);
68 |
69 | this.analyze(time);
70 | };
71 | }
72 |
73 | public destroy(): void {
74 | this.stop();
75 |
76 | this.cleanUp();
77 |
78 | if (this.canvas && this.canvas.parentNode)
79 | this.canvas.parentNode.removeChild(this.canvas);
80 |
81 | zeroObject(this);
82 | }
83 |
84 | public start(): boolean {
85 | if (this._alive)
86 | return false;
87 |
88 | this._alive = true;
89 | this._lastRequest = requestAnimationFrame(this._boundAnalyze);
90 |
91 | return true;
92 | }
93 |
94 | public stop(): void {
95 | this._alive = false;
96 |
97 | if (this._lastRequest) {
98 | cancelAnimationFrame(this._lastRequest);
99 | this._lastRequest = 0;
100 | }
101 | }
102 |
103 | protected abstract analyze(time: number): void;
104 |
105 | protected abstract cleanUp(): void;
106 | }
107 |
--------------------------------------------------------------------------------
/scripts/analyzer/plainAnalyzer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class PlainAnalyzer extends Analyzer {
28 | private readonly _sampleRate: number;
29 | private readonly _analyzerL: AnalyserNode;
30 | private readonly _analyzerR: AnalyserNode;
31 |
32 | private readonly _visibleFrequencies: Float64Array;
33 |
34 | private readonly _ptr: number;
35 | private readonly _fft4gfPtr: number;
36 | private readonly _dataPtr: number;
37 | private readonly _data: Uint8Array;
38 | private readonly _tmpPtr: number;
39 | private readonly _tmp: Float32Array;
40 | private readonly _windowPtr: number;
41 | private readonly _window: Float32Array;
42 | private readonly _multiplierPtr: number;
43 | private readonly _multiplier: Float32Array;
44 | private readonly _prevLPtr: number;
45 | private readonly _prevL: Float32Array;
46 | private readonly _prevRPtr: number;
47 | private readonly _prevR: Float32Array;
48 |
49 | public constructor(audioContext: AudioContext, parent: HTMLElement, graphicalFilterEditor: GraphicalFilterEditor, id?: string) {
50 | super(audioContext, parent, id);
51 |
52 | this._sampleRate = graphicalFilterEditor.sampleRate;
53 |
54 | // Only the first 1024 samples are necessary as the last
55 | // 1024 samples would always be zeroed out!
56 | this._analyzerL = audioContext.createAnalyser();
57 | this._analyzerL.fftSize = 1024;
58 | this._analyzerR = audioContext.createAnalyser();
59 | this._analyzerR.fftSize = 1024;
60 |
61 | this._visibleFrequencies = graphicalFilterEditor.visibleFrequencies;
62 |
63 | const buffer = cLib.HEAP8.buffer as ArrayBuffer;
64 |
65 | let ptr = cLib._allocBuffer(1024 + (2048 * 4) + (1024 * 4) + (3 * 512 * 4) + cLib._fftSizeOff(2048));
66 | this._ptr = ptr;
67 |
68 | this._dataPtr = ptr;
69 | this._data = new Uint8Array(buffer, ptr, 1024);
70 | ptr += 1024;
71 |
72 | this._tmpPtr = ptr;
73 | this._tmp = new Float32Array(buffer, ptr, 2048);
74 | ptr += (2048 * 4);
75 |
76 | this._windowPtr = ptr;
77 | this._window = new Float32Array(buffer, ptr, 1024);
78 | ptr += (1024 * 4);
79 |
80 | this._multiplierPtr = ptr;
81 | this._multiplier = new Float32Array(buffer, ptr, 512);
82 | ptr += (512 * 4);
83 |
84 | this._prevLPtr = ptr;
85 | this._prevL = new Float32Array(buffer, ptr, 512);
86 | ptr += (512 * 4);
87 |
88 | this._prevRPtr = ptr;
89 | this._prevR = new Float32Array(buffer, ptr, 512);
90 | ptr += (512 * 4);
91 |
92 | this._fft4gfPtr = ptr;
93 | cLib._fftInitf(this._fft4gfPtr, 2048);
94 |
95 | const window = this._window,
96 | multiplier = this._multiplier,
97 | pi = Math.PI,
98 | exp = Math.exp,
99 | cos = Math.cos,
100 | invln10 = 1 / Math.LN10;
101 |
102 | for (let i = 0; i < 1024; i++) {
103 | window[i] =
104 | // Adjust coefficient (the original C++ code was
105 | // meant to be used with 16 bit samples)
106 | 4 *
107 | // Hamming window
108 | (0.54 - (0.46 * cos(2 * pi * i / 1023)));
109 | }
110 |
111 | for (let i = 0; i < 512; i++) {
112 | // exp is to increase the gain as the frequency increases
113 | // 145 is just a gain to make the analyzer look good! :)
114 | multiplier[i] = invln10 * 145 * exp(2.5 * i / 511);
115 | }
116 | }
117 |
118 | protected analyze(time: number): void {
119 | // All the 0.5's here are because of this explanation:
120 | // http://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element
121 | // "Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line
122 | // over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which
123 | // results in two lines that get drawn on the canvas.
124 |
125 | const multiplier = this._multiplier,
126 | tmp = this._tmp,
127 | ctx = this.ctx as CanvasRenderingContext2D, // ctx is null only with WebGL analyzers
128 | sqrt = Math.sqrt,
129 | ln = Math.log,
130 | valueCount = 512, bw = this._sampleRate / 2048,
131 | filterLength2 = (2048 >>> 1),
132 | cos = Math.cos,
133 | visibleFrequencies = this._visibleFrequencies,
134 | colors = Analyzer.colors;
135 |
136 | let d = 0, im = 0, i = 0, freq = 0, ii = 0, avg = 0, avgCount = 0;
137 |
138 | this._analyzerL.getByteTimeDomainData(this._data);
139 | cLib._plainAnalyzer(this._fft4gfPtr, this._windowPtr, this._dataPtr, this._tmpPtr);
140 |
141 | let dataf = this._prevL;
142 |
143 | ctx.lineWidth = 1;
144 | ctx.fillStyle = "#000000";
145 | ctx.fillRect(0, 0, 512, 512);
146 |
147 | i = 0;
148 | ii = 0;
149 | while (ii < (valueCount - 1) && i < filterLength2 && bw > (visibleFrequencies[ii + 1] - visibleFrequencies[ii])) {
150 | freq = bw * i;
151 | while (i < filterLength2 && (freq + bw) < visibleFrequencies[ii]) {
152 | i++;
153 | freq = bw * i;
154 | }
155 | d = (((dataf[ii] * 4) + (multiplier[ii] * lerp(freq, tmp[i], freq + bw, tmp[i + 1], visibleFrequencies[ii]))) / 2.5) >> 1;
156 | if (d > 255) d = 255;
157 | else if (d < 0) d = 0;
158 | dataf[ii] = d;
159 | ctx.beginPath();
160 | ctx.strokeStyle = colors[d];
161 | ctx.moveTo(ii - 0.5, 256.5 - d);
162 | ctx.lineTo(ii - 0.5, 256.5);
163 | ctx.stroke();
164 | ii++;
165 | }
166 | i++;
167 | while (i < filterLength2 && ii < valueCount) {
168 | avg = 0;
169 | avgCount = 0;
170 | do {
171 | avg += tmp[i];
172 | avgCount++;
173 | i++;
174 | freq = bw * i;
175 | } while (freq < visibleFrequencies[ii] && i < filterLength2);
176 | d = (((dataf[ii] * 4) + (multiplier[ii] * avg / avgCount)) / 2.5) >> 1;
177 | if (d > 255) d = 255;
178 | else if (d < 0) d = 0;
179 | dataf[ii] = d;
180 | ctx.beginPath();
181 | ctx.strokeStyle = colors[d];
182 | ctx.moveTo(ii - 0.5, 256.5 - d);
183 | ctx.lineTo(ii - 0.5, 256.5);
184 | ctx.stroke();
185 | ii++;
186 | }
187 |
188 | // Sorry for the copy/paste :(
189 |
190 | this._analyzerR.getByteTimeDomainData(this._data);
191 | cLib._plainAnalyzer(this._fft4gfPtr, this._windowPtr, this._dataPtr, this._tmpPtr);
192 |
193 | dataf = this._prevR;
194 |
195 | i = 0;
196 | ii = 0;
197 | while (ii < (valueCount - 1) && i < filterLength2 && bw > (visibleFrequencies[ii + 1] - visibleFrequencies[ii])) {
198 | freq = bw * i;
199 | while (i < filterLength2 && (freq + bw) < visibleFrequencies[ii]) {
200 | i++;
201 | freq = bw * i;
202 | }
203 | d = (((dataf[ii] * 4) + (multiplier[ii] * lerp(freq, tmp[i], freq + bw, tmp[i + 1], visibleFrequencies[ii]))) / 2.5) >> 1;
204 | if (d > 255) d = 255;
205 | else if (d < 0) d = 0;
206 | dataf[ii] = d;
207 | ctx.beginPath();
208 | ctx.strokeStyle = colors[d];
209 | ctx.moveTo(ii - 0.5, 256.5);
210 | ctx.lineTo(ii - 0.5, 256.5 + d);
211 | ctx.stroke();
212 | ii++;
213 | }
214 | i++;
215 | while (i < filterLength2 && ii < valueCount) {
216 | avg = 0;
217 | avgCount = 0;
218 | do {
219 | avg += tmp[i];
220 | avgCount++;
221 | i++;
222 | freq = bw * i;
223 | } while (freq < visibleFrequencies[ii] && i < filterLength2);
224 | d = (((dataf[ii] * 4) + (multiplier[ii] * avg / avgCount)) / 2.5) >> 1;
225 | if (d > 255) d = 255;
226 | else if (d < 0) d = 0;
227 | dataf[ii] = d;
228 | ctx.beginPath();
229 | ctx.strokeStyle = colors[d];
230 | ctx.moveTo(ii - 0.5, 256.5);
231 | ctx.lineTo(ii - 0.5, 256.5 + d);
232 | ctx.stroke();
233 | ii++;
234 | }
235 | }
236 |
237 | protected cleanUp(): void {
238 | if (this._ptr)
239 | cLib._freeBuffer(this._ptr);
240 | }
241 | }
242 |
--------------------------------------------------------------------------------
/scripts/analyzer/soundParticlesAnalyzer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | //
28 | // This visualizer is a WebGL port of the visualizer with the same name, created
29 | // for my Android player, FPlay: https://github.com/carlosrafaelgn/FPlayAndroid
30 | //
31 | // https://github.com/carlosrafaelgn/FPlayAndroid/blob/master/jni/Common.h
32 | // https://github.com/carlosrafaelgn/FPlayAndroid/blob/master/jni/OpenGLVisualizerJni.h
33 | // https://github.com/carlosrafaelgn/FPlayAndroid/blob/master/jni/GLSoundParticle.h
34 | //
35 |
36 | class SoundParticlesAnalyzer extends Analyzer {
37 | // Vertex shader (one attribute and one uniform per line)
38 | private static readonly vertexShaderSource = `precision mediump float;
39 | attribute vec4 inPosition;
40 | attribute vec2 inTexCoord;
41 | uniform float amplitude;
42 | uniform float baseX;
43 | uniform vec2 pos;
44 | uniform vec2 aspect;
45 | uniform vec3 color;
46 | uniform float theta;
47 | varying vec2 vTexCoord;
48 | varying vec3 vColor;
49 |
50 | void main() {
51 | float a = mix(0.0625, 0.34375, amplitude);
52 | float bottom = 1.0 - clamp(pos.y, -1.0, 1.0);
53 | bottom = bottom * bottom * bottom * 0.125;
54 | a = (0.75 * a) + (0.25 * bottom);
55 | gl_Position = vec4(baseX + pos.x + (5.0 * (pos.y + 1.0) * pos.x * sin((2.0 * pos.y) + theta)) + (inPosition.x * aspect.x * a), pos.y + (inPosition.y * aspect.y * a), 0.0, 1.0);
56 | vTexCoord = inTexCoord;
57 | vColor = color + bottom + (0.25 * amplitude);
58 | }`;
59 |
60 | // Fragment shader (one uniform per line)
61 | private static readonly fragmentShaderSource = `precision lowp float;
62 | uniform sampler2D texColor;
63 | varying vec2 vTexCoord;
64 | varying vec3 vColor;
65 |
66 | void main() {
67 | float a = texture2D(texColor, vTexCoord).a;
68 | gl_FragColor = vec4(vColor.rgb * a, 1.0);
69 | }`;
70 |
71 | private static rand(): number {
72 | return ((Math.random() * 65536) | 0);
73 | }
74 |
75 | private readonly _analyzerL: AnalyserNode;
76 | private readonly _analyzerR: AnalyserNode;
77 |
78 | private readonly _ptr: number;
79 | private readonly _processedDataPtr: number;
80 | private readonly _processedData: Uint8Array;
81 | private readonly _processedDataRPtr: number;
82 | private readonly _processedDataR: Uint8Array;
83 | private readonly _fftPtr: number;
84 | private readonly _fft: Float32Array;
85 | private readonly _COLORSPtr: number;
86 | private readonly _COLORS: Float32Array;
87 | private readonly _bgPosPtr: number;
88 | private readonly _bgPos: Float32Array;
89 | private readonly _bgSpeedYPtr: number;
90 | private readonly _bgSpeedY: Float32Array;
91 | private readonly _bgThetaPtr: number;
92 | private readonly _bgTheta: Float32Array;
93 | private readonly _bgColorPtr: number;
94 | private readonly _bgColor: Uint8Array;
95 |
96 | private readonly _BG_COLUMNS = 31;
97 | private readonly _BG_PARTICLES_BY_COLUMN = 16;
98 | private readonly _BG_COUNT = (this._BG_COLUMNS * this._BG_PARTICLES_BY_COLUMN);
99 |
100 | private readonly _program: Program;
101 | private _lastTime = 0;
102 |
103 | public constructor(audioContext: AudioContext, parent: HTMLElement, graphicalFilterEditor: GraphicalFilterEditor, id?: string) {
104 | super(audioContext, parent, id, true, Analyzer.controlWidth, 320);
105 |
106 | this._analyzerL = audioContext.createAnalyser();
107 | this._analyzerL.fftSize = 1024;
108 | this._analyzerL.maxDecibels = -12;
109 | this._analyzerL.minDecibels = -45;
110 | this._analyzerL.smoothingTimeConstant = 0;
111 | this._analyzerR = audioContext.createAnalyser();
112 | this._analyzerR.fftSize = 1024;
113 | this._analyzerR.maxDecibels = -12;
114 | this._analyzerR.minDecibels = -45;
115 | this._analyzerR.smoothingTimeConstant = 0;
116 |
117 | const exp = Math.exp,
118 | FULL = 0.75,
119 | HALF = 0.325,
120 | ZERO = 0.0,
121 | COLORS_R = (A: number, B: number) => { this._COLORS[(3 * A)] = B; },
122 | COLORS_G = (A: number, B: number) => { this._COLORS[(3 * A) + 1] = B; },
123 | COLORS_B = (A: number, B: number) => { this._COLORS[(3 * A) + 2] = B; };
124 |
125 | const buffer = cLib.HEAP8.buffer as ArrayBuffer;
126 |
127 | let ptr = cLib._allocBuffer((2 * 512) + (256 * 4) + (16 * 3 * 4) + (this._BG_COUNT * 2 * 4) + (2 * this._BG_COUNT * 4) + this._BG_COUNT);
128 | this._ptr = ptr;
129 |
130 | this._processedDataPtr = ptr;
131 | this._processedData = new Uint8Array(buffer, ptr, 512);
132 | ptr += 512;
133 |
134 | this._processedDataRPtr = ptr;
135 | this._processedDataR = new Uint8Array(buffer, ptr, 512);
136 | ptr += 512;
137 |
138 | this._fftPtr = ptr;
139 | this._fft = new Float32Array(buffer, ptr, 256);
140 | ptr += (256 * 4);
141 |
142 | this._COLORSPtr = ptr;
143 | this._COLORS = new Float32Array(buffer, ptr, 16 * 3);
144 | ptr += (16 * 3 * 4);
145 |
146 | this._bgPosPtr = ptr;
147 | this._bgPos = new Float32Array(buffer, ptr, this._BG_COUNT * 2);
148 | ptr += (this._BG_COUNT * 2 * 4);
149 |
150 | this._bgSpeedYPtr = ptr;
151 | this._bgSpeedY = new Float32Array(buffer, ptr, this._BG_COUNT);
152 | ptr += (this._BG_COUNT * 4);
153 |
154 | this._bgThetaPtr = ptr;
155 | this._bgTheta = new Float32Array(buffer, ptr, this._BG_COUNT);
156 | ptr += (this._BG_COUNT * 4);
157 |
158 | this._bgColorPtr = ptr;
159 | this._bgColor = new Uint8Array(buffer, ptr, this._BG_COUNT);
160 |
161 | COLORS_R(0, FULL); COLORS_G(0, ZERO); COLORS_B(0, ZERO);
162 | COLORS_R(1, ZERO); COLORS_G(1, FULL); COLORS_B(1, ZERO);
163 | COLORS_R(2, ZERO); COLORS_G(2, ZERO); COLORS_B(2, FULL);
164 | COLORS_R(3, FULL); COLORS_G(3, ZERO); COLORS_B(3, FULL);
165 | COLORS_R(4, FULL); COLORS_G(4, FULL); COLORS_B(4, ZERO);
166 | COLORS_R(5, ZERO); COLORS_G(5, FULL); COLORS_B(5, FULL);
167 | COLORS_R(6, FULL); COLORS_G(6, FULL); COLORS_B(6, FULL);
168 | COLORS_R(7, FULL); COLORS_G(7, HALF); COLORS_B(7, ZERO);
169 | COLORS_R(8, FULL); COLORS_G(8, ZERO); COLORS_B(8, HALF);
170 | COLORS_R(9, HALF); COLORS_G(9, FULL); COLORS_B(9, ZERO);
171 | COLORS_R(10, ZERO); COLORS_G(10, FULL); COLORS_B(10, HALF);
172 | COLORS_R(11, ZERO); COLORS_G(11, HALF); COLORS_B(11, FULL);
173 | COLORS_R(12, HALF); COLORS_G(12, ZERO); COLORS_B(12, FULL);
174 | // The colors I like most appear twice ;)
175 | COLORS_R(13, ZERO); COLORS_G(13, ZERO); COLORS_B(13, FULL);
176 | COLORS_R(14, FULL); COLORS_G(14, HALF); COLORS_B(14, ZERO);
177 | COLORS_R(15, ZERO); COLORS_G(15, HALF); COLORS_B(15, FULL);
178 |
179 | for (let c = 0, i = 0; c < this._BG_COLUMNS; c++) {
180 | for (let ic = 0; ic < this._BG_PARTICLES_BY_COLUMN; ic++, i++)
181 | this.fillBgParticle(i, -1.2 + (0.01953125 * (SoundParticlesAnalyzer.rand() & 127)));
182 | }
183 |
184 | const program = Program.create(this.canvas, {
185 | alpha: false,
186 | depth: false,
187 | stencil: false,
188 | antialias: false,
189 | premultipliedAlpha: true
190 | }, SoundParticlesAnalyzer.vertexShaderSource, SoundParticlesAnalyzer.fragmentShaderSource);
191 |
192 | if (!program) {
193 | this.err("Apparently your browser does not support WebGL");
194 | // TypeScript does not understand this.err() does not return...
195 | throw new Error();
196 | }
197 |
198 | this._program = program;
199 |
200 | this._program.use();
201 | this._program["texColor"](0);
202 | this._program["aspect"](320.0 / 512.0, 1);
203 |
204 | const gl = this._program.gl,
205 | glVerticesRect = new Float32Array([
206 | -1, -1, 0, 1,
207 | 1, -1, 0, 1,
208 | -1, 1, 0, 1,
209 | 1, 1, 0, 1
210 | ]),
211 | glTexCoordsRect = new Float32Array([
212 | 0, 1,
213 | 1, 1,
214 | 0, 0,
215 | 1, 0
216 | ]),
217 | glBuf0 = gl.createBuffer(),
218 | glBuf1 = gl.createBuffer();
219 |
220 | if (gl.getError() || !glBuf0 || !glBuf1)
221 | this.err(-1);
222 |
223 | // Create a rectangle for the particles
224 | gl.bindBuffer(gl.ARRAY_BUFFER, glBuf0);
225 | gl.bufferData(gl.ARRAY_BUFFER, glVerticesRect, gl.STATIC_DRAW);
226 | gl.bindBuffer(gl.ARRAY_BUFFER, glBuf1);
227 | gl.bufferData(gl.ARRAY_BUFFER, glTexCoordsRect, gl.STATIC_DRAW);
228 |
229 | gl.clearColor(0, 0, 0, 1);
230 |
231 | // According to the docs, glTexImage2D initially expects images to be aligned on 4-byte
232 | // boundaries, but for ANDROID_BITMAP_FORMAT_RGB_565, AndroidBitmap_lockPixels aligns images
233 | // on 2-byte boundaries, making a few images look terrible!
234 | gl.pixelStorei(gl.UNPACK_ALIGNMENT, 2);
235 |
236 | gl.disable(gl.DEPTH_TEST);
237 | gl.disable(gl.CULL_FACE);
238 | gl.disable(gl.DITHER);
239 | gl.disable(gl.SCISSOR_TEST);
240 | gl.disable(gl.STENCIL_TEST);
241 | gl.enable(gl.BLEND);
242 | gl.blendFunc(gl.ONE, gl.ONE);
243 | gl.blendEquation(gl.FUNC_ADD);
244 | //gl.enable(gl.TEXTURE_2D); // WebGL returns an error by default when enabling TEXTURE_2D
245 | gl.getError(); // Clear any eventual error flags
246 |
247 | const glTex = gl.createTexture();
248 | if (gl.getError() || !glTex)
249 | this.err(-2);
250 |
251 | gl.activeTexture(gl.TEXTURE0);
252 | gl.bindTexture(gl.TEXTURE_2D, glTex);
253 | if (gl.getError())
254 | this.err(-3);
255 |
256 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
257 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
258 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
259 | gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
260 |
261 | this.fillTexture();
262 |
263 | if (gl.getError())
264 | this.err(-4);
265 |
266 | gl.enableVertexAttribArray(0);
267 | gl.bindBuffer(gl.ARRAY_BUFFER, glBuf0);
268 | gl.vertexAttribPointer(0, 4, gl.FLOAT, false, 0, 0);
269 |
270 | gl.enableVertexAttribArray(1);
271 | gl.bindBuffer(gl.ARRAY_BUFFER, glBuf1);
272 | gl.vertexAttribPointer(1, 2, gl.FLOAT, false, 0, 0);
273 | }
274 |
275 | private err(errId: number | string): void {
276 | this.destroy();
277 | alert("Sorry! WebGL error :(\n" + errId);
278 | throw errId;
279 | }
280 |
281 | private fillBgParticle(index: number, y: number): void {
282 | this._bgPos[(index << 1)] = 0.0078125 * ((SoundParticlesAnalyzer.rand() & 7) - 4);
283 | this._bgPos[(index << 1) + 1] = y;
284 | this._bgTheta[index] = 0.03125 * (SoundParticlesAnalyzer.rand() & 63);
285 | this._bgSpeedY[index] = 0.125 + (0.00390625 * (SoundParticlesAnalyzer.rand() & 15));
286 | this._bgColor[index] = SoundParticlesAnalyzer.rand() & 15;
287 | }
288 |
289 | private fillTexture(): void {
290 | const gl = this._program.gl,
291 | sqrtf = Math.sqrt,
292 | TEXTURE_SIZE = 64,
293 | tex = new Uint8Array(TEXTURE_SIZE * TEXTURE_SIZE);
294 |
295 | for (let y = 0; y < TEXTURE_SIZE; y++) {
296 | let yf = (y - (TEXTURE_SIZE >> 1));
297 | yf *= yf;
298 | for (let x = 0; x < TEXTURE_SIZE; x++) {
299 | let xf = (x - (TEXTURE_SIZE >> 1));
300 |
301 | let d = sqrtf((xf * xf) + yf) / ((TEXTURE_SIZE / 2) - 2.0);
302 | if (d > 1.0) d = 1.0;
303 |
304 | let d2 = d;
305 | d = 1.0 - d;
306 | d = d * d;
307 | d = d + (0.5 * d);
308 | if (d < 0.55)
309 | d = 0.0;
310 | else if (d < 1.0)
311 | d = smoothStep(0.55, 1.0, d);
312 |
313 | d2 = 1.0 - d2;
314 | d2 = smoothStep(0.0, 1.0, d2);
315 | d2 = d2 * d2;
316 | d2 = d2 + d2;
317 | if (d2 > 1.0) d2 = 1.0;
318 |
319 | d = (d + 0.5 * d2);
320 |
321 | let v = ((255.0 * d) | 0);
322 | tex[(y * TEXTURE_SIZE) + x] = ((v >= 255) ? 255 : v);
323 | }
324 | }
325 |
326 | gl.texImage2D(gl.TEXTURE_2D, 0, gl.ALPHA, TEXTURE_SIZE, TEXTURE_SIZE, 0, gl.ALPHA, gl.UNSIGNED_BYTE, tex);
327 | }
328 |
329 | protected analyze(time: number): void {
330 | // For the future: rethink this, create a large vertex buffer,
331 | // process everything in C and call drawArrays() only once,
332 | // like what is done in https://github.com/carlosrafaelgn/pixel :)
333 | let delta = (time - this._lastTime);
334 | if (delta > 33)
335 | delta = 33;
336 | this._lastTime = time;
337 |
338 | const gl = this._program.gl,
339 | coefNew = (0.0625 / 16.0) * delta, coefOld = 1.0 - coefNew,
340 | processedData = this._processedData, processedDataR = this._processedDataR, fft = this._fft,
341 | BG_COLUMNS = this._BG_COLUMNS, BG_PARTICLES_BY_COLUMN = this._BG_PARTICLES_BY_COLUMN, COLORS = this._COLORS,
342 | program = this._program, bgPos = this._bgPos, bgSpeedY = this._bgSpeedY, bgColor = this._bgColor,
343 | bgTheta = this._bgTheta, MAX = Math.max;
344 |
345 | let i = 0, last = 44, last2 = 116;
346 |
347 | delta *= 0.001;
348 |
349 | // http://www.w3.org/TR/webaudio/
350 | // http://webaudio.github.io/web-audio-api/#widl-AnalyserNode-getByteTimeDomainData-void-Uint8Array-array
351 | this._analyzerL.getByteFrequencyData(processedData);
352 | this._analyzerR.getByteFrequencyData(processedDataR);
353 |
354 | // Use only the first 256 amplitudes (which convers DC to 11025Hz, considering a sample rate of 44100Hz)
355 | for (i = 0; i < 256; i++) {
356 | let d = MAX(processedData[i], processedDataR[i]);
357 | if (d < 8)
358 | d = 0.0;
359 | const oldD = fft[i];
360 | if (d < oldD)
361 | d = (coefNew * d) + (coefOld * oldD);
362 | fft[i] = d;
363 | processedData[i] = ((d >= 255) ? 255 : d);
364 | }
365 |
366 | gl.clear(gl.COLOR_BUFFER_BIT);
367 |
368 | i = 2;
369 | for (let c = 0, p = 0; c < BG_COLUMNS; c++) {
370 | // Instead of dividing by 255, we are dividing by 256 (* 0.00390625f)
371 | // since the difference is visually unnoticeable
372 | let a: number;
373 |
374 | // Increase the amplitudes as the frequency increases, in order to improve the effect
375 | if (i < 6) {
376 | a = processedData[i] * 0.00390625;
377 | i++;
378 | } else if (i < 20) {
379 | a = MAX(processedData[i], processedData[i + 1]) * (1.5 * 0.00390625);
380 | i += 2;
381 | } else if (i < 36) {
382 | a = MAX(processedData[i], processedData[i + 1], processedData[i + 2], processedData[i + 3]) * (1.5 * 0.00390625);
383 | i += 4;
384 | } else if (i < 100) {
385 | let avg = 0;
386 | for (; i < last; i++)
387 | avg = MAX(avg, processedData[i]);
388 | a = avg * (2.0 * 0.00390625);
389 | last += 8;
390 | } else {
391 | let avg = 0;
392 | for (; i < last2; i++)
393 | avg = MAX(avg, processedData[i]);
394 | a = avg * (2.5 * 0.00390625);
395 | last2 += 16;
396 | }
397 |
398 | program["amplitude"]((a >= 1.0) ? 1.0 : a);
399 | // The 31 columns spread from -0.9 to 0.9, and they are evenly spaced
400 | program["baseX"](-0.9 + (0.06206897 * c));
401 |
402 | for (let ic = 0; ic < BG_PARTICLES_BY_COLUMN; ic++, p++) {
403 | if (bgPos[(p << 1) + 1] > 1.2)
404 | this.fillBgParticle(p, -1.2);
405 | else
406 | bgPos[(p << 1) + 1] += bgSpeedY[p] * delta;
407 | let idx = bgColor[p] * 3;
408 | program["color"](COLORS[idx], COLORS[idx + 1], COLORS[idx + 2]);
409 | idx = (p << 1);
410 | program["pos"](bgPos[idx], bgPos[idx + 1]);
411 | program["theta"](bgTheta[p]);
412 | gl.drawArrays(gl.TRIANGLE_STRIP, 0, 4);
413 | }
414 | }
415 | }
416 |
417 | protected cleanUp(): void {
418 | if (this._ptr)
419 | cLib._freeBuffer(this._ptr);
420 | if (this._program)
421 | this._program.destroy();
422 | }
423 | }
424 |
--------------------------------------------------------------------------------
/scripts/analyzer/waveletAnalyzer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class WaveletAnalyzer extends Analyzer {
28 | private readonly _analyzerL: AnalyserNode;
29 | private readonly _analyzerR: AnalyserNode;
30 |
31 | private readonly _ptr: number;
32 | private readonly _dataLPtr: number;
33 | private readonly _dataL: Uint8Array;
34 | private readonly _dataRPtr: number;
35 | private readonly _dataR: Uint8Array;
36 | private readonly _tmpPtr: number;
37 | private readonly _tmp: Float32Array;
38 | private readonly _oL1Ptr: number;
39 | private readonly _oL1: Float32Array;
40 | private readonly _oR1Ptr: number;
41 | private readonly _oR1: Float32Array;
42 |
43 | public constructor(audioContext: AudioContext, parent: HTMLElement, graphicalFilterEditor: GraphicalFilterEditor, id?: string) {
44 | super(audioContext, parent, id);
45 |
46 | this._analyzerL = audioContext.createAnalyser();
47 | this._analyzerL.fftSize = 128;
48 | this._analyzerR = audioContext.createAnalyser();
49 | this._analyzerR.fftSize = 128;
50 |
51 | const buffer = cLib.HEAP8.buffer as ArrayBuffer;
52 |
53 | let ptr = cLib._allocBuffer((2 * 128) + (64 * 4) + (2 * 128 * 4));
54 | this._ptr = ptr;
55 |
56 | this._dataLPtr = ptr;
57 | this._dataL = new Uint8Array(buffer, ptr, 128);
58 | ptr += 128;
59 |
60 | this._dataRPtr = ptr;
61 | this._dataR = new Uint8Array(buffer, ptr, 128);
62 | ptr += 128;
63 |
64 | this._tmpPtr = ptr;
65 | this._tmp = new Float32Array(buffer, ptr, 64);
66 | ptr += 64 * 4;
67 |
68 | this._oL1Ptr = ptr;
69 | this._oL1 = new Float32Array(buffer, ptr, 128);
70 | ptr += (128 * 4);
71 |
72 | this._oR1Ptr = ptr;
73 | this._oR1 = new Float32Array(buffer, ptr, 128);
74 | }
75 |
76 | protected analyze(time: number): void {
77 | // All the 0.5's here are because of this explanation:
78 | // http://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element
79 | // "Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line
80 | // over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which
81 | // results in two lines that get drawn on the canvas.
82 | const ctx = this.ctx as CanvasRenderingContext2D, // ctx is null only with WebGL analyzers
83 | colors = Analyzer.colors,
84 | oL1 = this._oL1,
85 | oR1 = this._oR1;
86 |
87 | this._analyzerL.getByteTimeDomainData(this._dataL);
88 | this._analyzerR.getByteTimeDomainData(this._dataR);
89 |
90 | cLib._waveletAnalyzer(this._dataLPtr, this._dataRPtr, this._tmpPtr, this._oL1Ptr, this._oR1Ptr);
91 |
92 | let i = 0, t = 0, tot = 64, w = Analyzer.controlWidth / 64, x = 0, y = 0, y2 = Analyzer.controlHeight - 32;
93 |
94 | // 128
95 | // 1 (1)
96 | // 2 .. 3 (2)
97 | // 4 .. 7 (4)
98 | // 8 .. 15 (8)
99 | // 16 .. 31 (16)
100 | // 32 .. 63 (32)
101 | // 64 .. 127 (64)
102 | for (; ; ) {
103 | i = tot;
104 | x = 0;
105 | while (x < 512) {
106 | t = oL1[i]; // (oL1[i] * 0.125) + (oL2[i] * 0.875);
107 | //oL2[i] = t;
108 | if (t < 0)
109 | t = -t;
110 | if (t >= 127)
111 | t = 508;
112 | else
113 | t <<= 2;
114 | ctx.fillStyle = colors[t >>> 1];
115 | ctx.fillRect(x, y, w, 32);
116 | t = oR1[i]; // (oR1[i] * 0.125) + (oR2[i] * 0.875);
117 | //oR2[i] = t;
118 | if (t < 0)
119 | t = -t;
120 | if (t >= 127)
121 | t = 508;
122 | else
123 | t <<= 2;
124 | ctx.fillStyle = colors[t >>> 1];
125 | ctx.fillRect(x, y2, w, 32);
126 | i++;
127 | x += w;
128 | }
129 | w <<= 1;
130 | y += 32;
131 | y2 -= 32;
132 | if (!tot)
133 | break;
134 | tot >>>= 1;
135 | if (!tot)
136 | w = 512;
137 | }
138 | }
139 |
140 | protected cleanUp(): void {
141 | if (this._ptr)
142 | cLib._freeBuffer(this._ptr);
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/scripts/gl/program.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class Program {
28 | public readonly gl: WebGLRenderingContext;
29 | private readonly _program: WebGLProgram;
30 | private readonly _vs: WebGLShader;
31 | private readonly _fs: WebGLShader;
32 |
33 | [uniformName: string]: any;
34 |
35 | public static create(canvas: HTMLCanvasElement, options: any, vertexShaderSource: string, fragmentShaderSource: string): Program | null {
36 | const ctxName = ["webkit-3d", "moz-webgl", "webgl", "experimental-webgl"];
37 |
38 | for (let i = 0; i < ctxName.length; i++) {
39 | try {
40 | const gl = canvas.getContext(ctxName[i], options) as WebGLRenderingContext;
41 | return new Program(gl, vertexShaderSource, fragmentShaderSource);
42 | } catch (ex) {
43 | }
44 | }
45 |
46 | return null;
47 | }
48 |
49 | private constructor(gl: WebGLRenderingContext, vertexShaderSource: string, fragmentShaderSource: string) {
50 | const attribs: string[] = [],
51 | uniforms: string[] = [],
52 | utypes: string[] = [];
53 |
54 | this.gl = gl;
55 | const program = gl.createProgram();
56 | if (!program)
57 | throw new Error("Null program");
58 | this._program = program;
59 |
60 | const vs = gl.createShader(gl.VERTEX_SHADER);
61 | if (!vs)
62 | throw new Error("Null vertex shader");
63 | this._vs = vs;
64 |
65 | const fs = gl.createShader(gl.FRAGMENT_SHADER);
66 | if (!fs)
67 | throw new Error("Null fragment shader");
68 | this._fs = fs;
69 |
70 | gl.shaderSource(this._vs, vertexShaderSource);
71 | gl.compileShader(this._vs);
72 | if (!gl.getShaderParameter(this._vs, gl.COMPILE_STATUS)) {
73 | const msg = "Vertex shader: " + gl.getShaderInfoLog(this._vs);
74 | this.destroy();
75 | alert(msg);
76 | throw msg;
77 | }
78 | this.getAttribsAndUniforms(vertexShaderSource, attribs, uniforms, utypes);
79 |
80 | gl.shaderSource(this._fs, fragmentShaderSource);
81 | gl.compileShader(this._fs);
82 | if (!gl.getShaderParameter(this._fs, gl.COMPILE_STATUS)) {
83 | const msg = "Fragment shader: " + gl.getShaderInfoLog(this._fs);
84 | this.destroy();
85 | alert(msg);
86 | throw msg;
87 | }
88 | this.getAttribsAndUniforms(fragmentShaderSource, attribs, uniforms, utypes);
89 |
90 | gl.attachShader(this._program, this._vs);
91 | gl.attachShader(this._program, this._fs);
92 | // This way all attributes are numbered starting at 0, beginning with
93 | // the first attribute found in the file
94 | for (let i = 0; i < attribs.length; i++)
95 | gl.bindAttribLocation(this._program, i, attribs[i]);
96 | gl.linkProgram(this._program);
97 |
98 | for (let i = 0; i < uniforms.length; i++)
99 | this.prepareUniform(uniforms[i], utypes[i]);
100 | }
101 |
102 | private getAttribsAndUniforms(src: string, attribs: string[], uniforms: string[], utypes: string[]): void {
103 | // This is a very simple parser, which handles only lines starting with "uniform",
104 | // with "attribute or lines starting with a // comment
105 | const lines = src.split("\n");
106 |
107 | for (let i = 0; i < lines.length; i++) {
108 | // Remove extra spaces from the beginning and from the end of the line
109 | const line = lines[i].trim();
110 | if (line.substring(0, 7) === "uniform") {
111 | // We do not consider the possibility of "\t" separating tokens (for now)
112 | const tokens = line.split(" ");
113 | // Skip the "uniform" token and store the current type to be used aftwerwards
114 | const currentType = tokens[1];
115 | for (let ii = 2; ii < tokens.length; ii++) {
116 | let token = tokens[ii];
117 | if (token === ";")
118 | break;
119 | const lastChar = token.charAt(token.length - 1);
120 | const breakNow = (lastChar === ";");
121 | if (lastChar === "," || breakNow)
122 | token = token.substring(0, token.length - 1);
123 | if (token !== "," && token.length) {
124 | uniforms.push(token);
125 | utypes.push(currentType);
126 | }
127 | if (breakNow)
128 | break;
129 | }
130 | } else if (line.substring(0, 9) === "attribute") {
131 | // We do not consider the possibility of "\t" separating tokens (for now)
132 | const tokens = line.split(" ");
133 | // Skip the "attribute" token and store the current type to be used aftwerwards
134 | for (let ii = 2; ii < tokens.length; ii++) {
135 | let token = tokens[ii];
136 | if (token === ";")
137 | break;
138 | const lastChar = token.charAt(token.length - 1);
139 | const breakNow = (lastChar === ";");
140 | if (lastChar === "," || breakNow)
141 | token = token.substring(0, token.length - 1);
142 | if (token !== "," && token.length)
143 | attribs.push(token);
144 | if (breakNow)
145 | break;
146 | }
147 | }
148 | }
149 | }
150 |
151 | private prepareUniform(u: string, t: string): boolean {
152 | const gl = this.gl, l = gl.getUniformLocation(this._program, u);
153 | if (!this[u]) {
154 | if (t === "bool" || t === "int" || t === "sampler2D") {
155 | this[u] = function (i: number): void { gl.uniform1i(l, i); }
156 | } else if (t === "float") {
157 | this[u] = function (f: number): void { gl.uniform1f(l, f); }
158 | } else if (t === "vec2") {
159 | this[u] = function (x: number, y: number) : void{ gl.uniform2f(l, x, y); }
160 | this[u + "v"] = function (v: Float32List): void { gl.uniform2fv(l, v); }
161 | } else if (t === "vec3") {
162 | this[u] = function (x: number, y: number, z: number): void { gl.uniform3f(l, x, y, z); }
163 | this[u + "v"] = function (v: Float32List): void { gl.uniform3fv(l, v); }
164 | } else if (t === "vec4") {
165 | this[u] = function (x: number, y: number, z: number, w: number): void { gl.uniform4f(l, x, y, z, w); }
166 | this[u + "v"] = function (v: Float32List): void { gl.uniform4fv(l, v); }
167 | } else if (t === "mat4") {
168 | this[u] = function (mat: Float32List): void { gl.uniformMatrix4fv(l, false, mat); }
169 | } else {
170 | return false;
171 | }
172 | }
173 | return true;
174 | }
175 |
176 | public use(): void {
177 | this.gl.useProgram(this._program);
178 | }
179 |
180 | public destroy(): void {
181 | if (this.gl) {
182 | this.gl.useProgram(null);
183 | if (this._vs) {
184 | this.gl.detachShader(this._program, this._vs);
185 | this.gl.deleteShader(this._vs);
186 | }
187 | if (this._fs) {
188 | this.gl.detachShader(this._program, this._fs);
189 | this.gl.deleteShader(this._fs);
190 | }
191 | if (this._program)
192 | this.gl.deleteProgram(this._program);
193 | zeroObject(this);
194 | }
195 | }
196 | }
197 |
--------------------------------------------------------------------------------
/scripts/graphicalFilterEditor/graphicalFilterEditorCanvasRenderer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class GraphicalFilterEditorCanvasRenderer extends GraphicalFilterEditorRenderer {
28 | private _pixelRatio: number;
29 | private _ctx: CanvasRenderingContext2D;
30 | private _rangeImage: CanvasGradient;
31 |
32 | public constructor(editor: GraphicalFilterEditorControl) {
33 | super(document.createElement("canvas"), Math.abs(GraphicalFilterEditorControl.controlWidth - GraphicalFilterEditor.visibleBinCount) >> 1, editor);
34 |
35 | this.element.className = "GECV";
36 |
37 | this._pixelRatio = 1;
38 | this._ctx = null as any;
39 | this._rangeImage = null as any;
40 |
41 | this.scaleChanged();
42 | }
43 |
44 | public scaleChanged(): void {
45 | const element = this.element,
46 | editor = this.editor;
47 |
48 | if (!element || !editor)
49 | return;
50 |
51 | const controlWidth = GraphicalFilterEditorControl.controlWidth,
52 | controlHeight = GraphicalFilterEditorControl.controlHeight,
53 | pixelRatio = (devicePixelRatio > 1 ? devicePixelRatio : 1),
54 | editorScale = editor.scale,
55 | scale = editorScale * pixelRatio;
56 |
57 | element.width = (controlWidth * scale) | 0;
58 | element.height = (controlHeight * scale) | 0;
59 | editor.element.style.width = ((controlWidth * editorScale) | 0) + "px";
60 | element.style.width = ((controlWidth * editorScale) | 0) + "px";
61 | element.style.height = ((controlHeight * editorScale) | 0) + "px";
62 |
63 | const ctx = this.element.getContext("2d", { alpha: false });
64 | if (!ctx)
65 | throw new Error("Null canvas context");
66 |
67 | const rangeImage = ctx.createLinearGradient(0, 0, 1, element.height);
68 | rangeImage.addColorStop(0, "#ff0000");
69 | rangeImage.addColorStop(0.1875, "#ffff00");
70 | rangeImage.addColorStop(0.39453125, "#00ff00");
71 | rangeImage.addColorStop(0.60546875, "#00ffff");
72 | rangeImage.addColorStop(0.796875, "#0000ff");
73 | rangeImage.addColorStop(1, "#ff00ff");
74 |
75 | this._pixelRatio = pixelRatio;
76 | this._ctx = ctx;
77 | this._rangeImage = rangeImage;
78 | }
79 |
80 | public drawCurve(showZones: boolean, isActualChannelCurveNeeded: boolean, currentChannelIndex: number): void {
81 | // All the halfLineWidth's here are because of this explanation:
82 | // http://stackoverflow.com/questions/195262/can-i-turn-off-antialiasing-on-an-html-canvas-element
83 | // "Draw your 1-pixel lines on coordinates like ctx.lineTo(10.5, 10.5). Drawing a one-pixel line
84 | // over the point (10, 10) means, that this 1 pixel at that position reaches from 9.5 to 10.5 which
85 | // results in two lines that get drawn on the canvas.
86 |
87 | const ctx = this._ctx;
88 |
89 | if (!ctx)
90 | return;
91 |
92 | let pixelRatio = (devicePixelRatio > 1 ? devicePixelRatio : 1);
93 | if (pixelRatio !== this._pixelRatio) {
94 | this.scaleChanged();
95 | pixelRatio = this._pixelRatio;
96 | }
97 |
98 | const editor = this.editor,
99 | filter = editor.filter,
100 | scale = editor.scale * pixelRatio,
101 | canvas = this.element,
102 | canvasLeftMargin = this.leftMargin,
103 | canvasWidth = canvas.width,
104 | canvasHeight = canvas.height,
105 | widthPlusMarginMinus1 = canvasLeftMargin + GraphicalFilterEditor.visibleBinCount - 1,
106 | dashGap = Math.round(8 * scale),
107 | dashLength = Math.round(4 * scale),
108 | dashCount = ((canvasWidth / dashGap) | 0) + 1,
109 | maximumChannelValueY = (GraphicalFilterEditor.maximumChannelValueY * scale) | 0;
110 |
111 | let lineWidth = (scale < 1 ? scale : (scale | 0)),
112 | halfLineWidth = lineWidth * 0.5;
113 |
114 | ctx.fillStyle = "#303030";
115 | ctx.fillRect(0, 0, canvasWidth, canvasHeight);
116 | ctx.lineWidth = lineWidth;
117 | ctx.strokeStyle = "#5a5a5a";
118 | ctx.beginPath();
119 |
120 | let x = canvasWidth + (dashLength >> 1),
121 | y = ((GraphicalFilterEditor.zeroChannelValueY * scale) | 0) + halfLineWidth;
122 | ctx.moveTo(x, y);
123 | for (let i = dashCount - 1; i >= 0; i--) {
124 | ctx.lineTo(x - dashLength, y);
125 | x -= dashGap;
126 | ctx.moveTo(x, y);
127 | }
128 | ctx.stroke();
129 |
130 | ctx.beginPath();
131 | x = canvasWidth + (dashLength >> 1),
132 | y = ((GraphicalFilterEditor.validYRangeHeight * scale) | 0) + halfLineWidth;
133 | ctx.moveTo(x, y);
134 | for (let i = dashCount - 1; i >= 0; i--) {
135 | ctx.lineTo(x - dashLength, y);
136 | x -= dashGap;
137 | ctx.moveTo(x, y);
138 | }
139 | ctx.stroke();
140 |
141 | if (showZones) {
142 | for (let i = filter.equivalentZonesFrequencyCount.length - 2; i > 0; i--) {
143 | x = (((filter.equivalentZonesFrequencyCount[i] + canvasLeftMargin) * scale) | 0) + halfLineWidth;
144 | y = maximumChannelValueY;
145 |
146 | ctx.beginPath();
147 | ctx.moveTo(x, y);
148 |
149 | while (y < canvasHeight) {
150 | ctx.lineTo(x, y + dashLength);
151 | y += dashGap;
152 | ctx.moveTo(x, y);
153 | }
154 |
155 | ctx.stroke();
156 | }
157 | }
158 |
159 | lineWidth = (scale < 1 ? (scale * 2) : ((scale * 2) | 0));
160 | halfLineWidth = lineWidth * 0.5;
161 |
162 | ctx.lineWidth = lineWidth;
163 |
164 | const visibleBinCountMinus1 = GraphicalFilterEditor.visibleBinCount - 1;
165 |
166 | for (let turn = (isActualChannelCurveNeeded ? 1 : 0); turn >= 0; turn--) {
167 | const curve = ((turn || !isActualChannelCurveNeeded) ? filter.channelCurves[currentChannelIndex] : filter.actualChannelCurve);
168 |
169 | ctx.strokeStyle = (turn ? "#707070" : this._rangeImage);
170 | ctx.beginPath();
171 | ctx.moveTo((canvasLeftMargin * scale) | 0, (curve[0] * scale) + halfLineWidth);
172 |
173 | for (x = 1; x < visibleBinCountMinus1; x++)
174 | ctx.lineTo(((canvasLeftMargin + x) * scale) | 0, (curve[x] * scale) + halfLineWidth);
175 |
176 | // Just to fill up the last pixel!
177 | ctx.lineTo(((canvasLeftMargin + x + 1) * scale) | 0, (curve[x] * scale) + halfLineWidth);
178 | ctx.stroke();
179 | }
180 | }
181 | }
182 |
--------------------------------------------------------------------------------
/scripts/graphicalFilterEditor/graphicalFilterEditorControl.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | interface GraphicalFilterEditorSettings {
28 | showZones?: boolean;
29 | editMode?: number;
30 | isActualChannelCurveNeeded?: boolean;
31 | currentChannelIndex?: number;
32 | isSameFilterLR?: boolean;
33 | isNormalized?: boolean;
34 | leftCurve?: string;
35 | rightCurve?: string;
36 | }
37 |
38 | interface GraphicalFilterEditorUISettings {
39 | svgRenderer?: boolean;
40 | scale?: number;
41 | fontSize?: string;
42 | lineHeight?: string;
43 | hideEditModePeakingEq?: boolean;
44 | hideEditModeShelfEq?: boolean;
45 |
46 | checkFontFamily?: string;
47 | checkFontSize?: string;
48 | radioHTML?: string;
49 | radioCharacter?: string;
50 | radioMargin?: string;
51 | checkHTML?: string;
52 | checkCharacter?: string;
53 | checkMargin?: string;
54 |
55 | menuFontFamily?: string;
56 | menuFontSize?: string;
57 | menuWidth?: string;
58 | menuPadding?: string;
59 | openMenuHTML?: string;
60 | openMenuCharacter?: string;
61 | closeMenuHTML?: string;
62 | closeMenuCharacter?: string;
63 | }
64 |
65 | class GraphicalFilterEditorControl {
66 | public static readonly controlWidth = Math.max(512, GraphicalFilterEditor.visibleBinCount);
67 | public static readonly controlHeight = GraphicalFilterEditor.validYRangeHeight + 3;
68 |
69 | public static readonly editModeRegular = 0;
70 | public static readonly editModeZones = 1;
71 | public static readonly editModeSmoothNarrow = 2;
72 | public static readonly editModeSmoothWide = 3;
73 | public static readonly editModePeakingEq = 4;
74 | public static readonly editModeShelfEq = 5;
75 | public static readonly editModeFirst = 0;
76 | public static readonly editModeLast = 5;
77 |
78 | public readonly filter: GraphicalFilterEditor;
79 | public readonly element: HTMLDivElement;
80 |
81 | private readonly pointerHandler: PointerHandler;
82 | private readonly renderer: GraphicalFilterEditorRenderer;
83 | private readonly btnMnu: HTMLDivElement;
84 | private readonly mnu: HTMLDivElement;
85 | private readonly mnuChBL: HTMLDivElement;
86 | private readonly mnuChL: HTMLDivElement;
87 | private readonly mnuChBR: HTMLDivElement;
88 | private readonly mnuChR: HTMLDivElement;
89 | private readonly mnuShowZones: HTMLDivElement;
90 | private readonly mnuEditRegular: HTMLDivElement;
91 | private readonly mnuEditZones: HTMLDivElement;
92 | private readonly mnuEditSmoothNarrow: HTMLDivElement;
93 | private readonly mnuEditSmoothWide: HTMLDivElement;
94 | private readonly mnuEditPeakingEq: HTMLDivElement;
95 | private readonly mnuEditShelfEq: HTMLDivElement;
96 | private readonly mnuNormalizeCurves: HTMLDivElement;
97 | private readonly mnuShowActual: HTMLDivElement;
98 | private readonly lblCursor: HTMLSpanElement;
99 | private readonly lblCurve: HTMLSpanElement;
100 | private readonly lblFrequency: HTMLSpanElement;
101 |
102 | private readonly openMenuElement: HTMLElement | null;
103 | private readonly openMenuCharacter: string;
104 | private readonly closeMenuElement: HTMLElement | null;
105 | private readonly closeMenuCharacter: string;
106 |
107 | private _scale: number;
108 | private _fontSize: string | null;
109 | private _lineHeight: string | null;
110 | private _showZones = false;
111 | private _editMode = GraphicalFilterEditorControl.editModeRegular;
112 | private _isActualChannelCurveNeeded = true;
113 | private _currentChannelIndex = 0;
114 | private isSameFilterLR = true;
115 | private drawingMode = 0;
116 | private lastDrawX = 0;
117 | private lastDrawY = 0;
118 | private drawOffsetX = 0;
119 | private drawOffsetY = 0;
120 |
121 | private boundMouseMove: any;
122 |
123 | public constructor(element: HTMLDivElement, filterLength: number, audioContext: AudioContext, filterChangedCallback: FilterChangedCallback, settings?: GraphicalFilterEditorSettings | null, uiSettings?: GraphicalFilterEditorUISettings | null) {
124 | if (filterLength < 8 || (filterLength & (filterLength - 1)))
125 | throw "Sorry, class available only for fft sizes that are a power of 2 >= 8! :(";
126 |
127 | this.filter = new GraphicalFilterEditor(filterLength, audioContext, filterChangedCallback);
128 |
129 | const createMenuSep = function () {
130 | const s = document.createElement("div");
131 | s.className = "GEMNUSEP";
132 | return s;
133 | },
134 | createMenuLabel = function (text: string) {
135 | const l = document.createElement("div");
136 | l.className = "GEMNULBL";
137 | l.appendChild(document.createTextNode(text));
138 | return l;
139 | },
140 | createMenuItem = function (text: string, checkable: boolean, checked: boolean, radio: boolean, clickHandler: (e: MouseEvent) => any, className?: string | null) {
141 | const i = document.createElement("div");
142 | i.className = (className ? ("GEMNUIT GECLK " + className) : "GEMNUIT GECLK");
143 | if (checkable) {
144 | if (uiSettings && ((radio && uiSettings.radioHTML) || (!radio && uiSettings.checkHTML))) {
145 | i.innerHTML = (radio ? uiSettings.radioHTML : uiSettings.checkHTML) as string;
146 | const s = i.firstChild as HTMLElement;
147 | if (radio)
148 | s.style.marginRight = (uiSettings.radioMargin || "2px");
149 | else
150 | s.style.marginRight = (uiSettings.checkMargin || "2px");
151 | if (!checked)
152 | s.style.visibility = "hidden";
153 | } else {
154 | const s = document.createElement("span");
155 | let checkCharacter = (radio ? "\u25CF " : "\u25A0 "),
156 | margin: string | null = null;
157 | if (uiSettings) {
158 | let checkCharacterOK = false;
159 | if (radio) {
160 | if (uiSettings.radioCharacter) {
161 | checkCharacterOK = true;
162 | checkCharacter = uiSettings.radioCharacter;
163 | margin = (uiSettings.radioMargin || "2px");
164 | }
165 | } else if (uiSettings.checkCharacter) {
166 | checkCharacterOK = true;
167 | checkCharacter = uiSettings.checkCharacter;
168 | margin = (uiSettings.checkMargin || "2px");
169 | }
170 | if (checkCharacterOK) {
171 | if (uiSettings.checkFontFamily)
172 | s.style.fontFamily = uiSettings.checkFontFamily;
173 | if (uiSettings.checkFontSize)
174 | s.style.fontSize = uiSettings.checkFontSize;
175 | }
176 | }
177 | if (margin)
178 | s.style.marginRight = margin;
179 | s.appendChild(document.createTextNode(checkCharacter));
180 | if (!checked)
181 | s.style.visibility = "hidden";
182 | i.appendChild(s);
183 | }
184 | }
185 | i.appendChild(document.createTextNode(text));
186 | if (clickHandler)
187 | i.onclick = clickHandler;
188 | return i;
189 | };
190 |
191 | this.element = element;
192 | element.className = "GE";
193 | element.ariaHidden = "true";
194 |
195 | this.boundMouseMove = this.mouseMove.bind(this);
196 |
197 | this._fontSize = null;
198 | if (uiSettings && uiSettings.fontSize)
199 | this.fontSize = uiSettings.fontSize;
200 |
201 | this._lineHeight = null;
202 | if (uiSettings && uiSettings.lineHeight)
203 | this.lineHeight = uiSettings.lineHeight;
204 |
205 | this._scale = 0;
206 | this.scale = ((uiSettings && uiSettings.scale && uiSettings.scale > 0) ? uiSettings.scale : 1);
207 |
208 | this.renderer = ((uiSettings && uiSettings.svgRenderer) ? new GraphicalFilterEditorSVGRenderer(this) : new GraphicalFilterEditorCanvasRenderer(this));
209 | this.renderer.element.addEventListener("mousemove", this.boundMouseMove);
210 | this.renderer.element.oncontextmenu = cancelEvent;
211 | element.appendChild(this.renderer.element);
212 |
213 | this.pointerHandler = new PointerHandler(this.renderer.element, this.mouseDown.bind(this), this.mouseMove.bind(this), this.mouseUp.bind(this));
214 |
215 | element.oncontextmenu = cancelEvent;
216 |
217 | let lbl = document.createElement("div");
218 | lbl.className = "GELBL";
219 | lbl.style.width = "11em";
220 | lbl.appendChild(document.createTextNode(GraphicalFilterEditorStrings.Cursor));
221 | lbl.appendChild(this.lblCursor = document.createElement("span"));
222 | lbl.appendChild(document.createTextNode(" dB"));
223 | this.lblCursor.appendChild(document.createTextNode(GraphicalFilterEditorStrings.Minus0));
224 | element.appendChild(lbl);
225 |
226 | lbl = document.createElement("div");
227 | lbl.className = "GELBL";
228 | lbl.style.width = "11em";
229 | lbl.appendChild(document.createTextNode(GraphicalFilterEditorStrings.Curve));
230 | lbl.appendChild(this.lblCurve = document.createElement("span"));
231 | lbl.appendChild(document.createTextNode(" dB"));
232 | this.lblCurve.appendChild(document.createTextNode(GraphicalFilterEditorStrings.Minus0));
233 | element.appendChild(lbl);
234 |
235 | lbl = document.createElement("div");
236 | lbl.className = "GELBL";
237 | //lbl.appendChild(document.createTextNode(GraphicalFilterEditorStrings.Frequency));
238 | lbl.appendChild(this.lblFrequency = document.createElement("span"));
239 | this.lblFrequency.appendChild(document.createTextNode("0 Hz (31 Hz)"));
240 | element.appendChild(lbl);
241 |
242 | this.btnMnu = document.createElement("div");
243 | this.btnMnu.className = "GEBTN GECLK";
244 | this.openMenuElement = null;
245 | this.openMenuCharacter = "\u25B2";
246 | this.closeMenuElement = null;
247 | this.closeMenuCharacter = "\u25BC";
248 | if (uiSettings) {
249 | let menuCharacterOK = false;
250 | if (uiSettings.openMenuHTML && uiSettings.closeMenuHTML) {
251 | menuCharacterOK = true;
252 | this.btnMnu.innerHTML = uiSettings.openMenuHTML + uiSettings.closeMenuHTML;
253 | this.openMenuElement = this.btnMnu.childNodes[0] as HTMLElement;
254 | this.closeMenuElement = this.btnMnu.childNodes[1] as HTMLElement;
255 | this.closeMenuElement.style.display = "none";
256 | } else if (uiSettings.openMenuCharacter) {
257 | menuCharacterOK = true;
258 | this.openMenuCharacter = uiSettings.openMenuCharacter;
259 | this.closeMenuCharacter = (uiSettings.closeMenuCharacter || this.openMenuCharacter);
260 | } else if (uiSettings.closeMenuCharacter) {
261 | menuCharacterOK = true;
262 | this.openMenuCharacter = uiSettings.closeMenuCharacter;
263 | this.closeMenuCharacter = this.openMenuCharacter;
264 | }
265 | if (menuCharacterOK) {
266 | if (uiSettings.menuFontFamily)
267 | this.btnMnu.style.fontFamily = uiSettings.menuFontFamily;
268 | if (uiSettings.menuFontSize)
269 | this.btnMnu.style.fontSize = uiSettings.menuFontSize;
270 | if (uiSettings.menuWidth)
271 | this.btnMnu.style.width = uiSettings.menuWidth;
272 | if (uiSettings.menuPadding)
273 | this.btnMnu.style.padding = uiSettings.menuPadding;
274 | }
275 | }
276 | if (!this.openMenuElement)
277 | this.btnMnu.appendChild(document.createTextNode(this.openMenuCharacter));
278 | this.btnMnu.onclick = this.btnMnu_Click.bind(this);
279 | element.appendChild(this.btnMnu);
280 |
281 | this.mnu = document.createElement("div");
282 | this.mnu.className = "GEMNU";
283 | this.mnu.style.display = "none";
284 |
285 | let mnuh = document.createElement("div");
286 | mnuh.className = "GEMNUH GEFILTER";
287 | mnuh.appendChild(createMenuLabel(GraphicalFilterEditorStrings.SameCurve));
288 | mnuh.appendChild(this.mnuChBL = createMenuItem(GraphicalFilterEditorStrings.UseLeftCurve, true, true, true, this.mnuChB_Click.bind(this, 0)));
289 | mnuh.appendChild(this.mnuChBR = createMenuItem(GraphicalFilterEditorStrings.UseRightCurve, true, false, true, this.mnuChB_Click.bind(this, 1)));
290 | mnuh.appendChild(createMenuSep());
291 | mnuh.appendChild(createMenuLabel(GraphicalFilterEditorStrings.OneForEach));
292 | mnuh.appendChild(this.mnuChL = createMenuItem(GraphicalFilterEditorStrings.ShowLeftCurve, true, false, true, this.mnuChLR_Click.bind(this, 0)));
293 | mnuh.appendChild(this.mnuChR = createMenuItem(GraphicalFilterEditorStrings.ShowRightCurve, true, false, true, this.mnuChLR_Click.bind(this, 1)));
294 | this.mnu.appendChild(mnuh);
295 |
296 | mnuh = document.createElement("div");
297 | mnuh.className = "GEMNUH GEMNUSEPH";
298 | mnuh.appendChild(createMenuItem(GraphicalFilterEditorStrings.ResetCurve, false, false, false, this.mnuResetCurve_Click.bind(this)));
299 | mnuh.appendChild(createMenuSep());
300 | mnuh.appendChild(createMenuLabel(GraphicalFilterEditorStrings.EditMode));
301 | mnuh.appendChild(this.mnuEditRegular = createMenuItem(GraphicalFilterEditorStrings.Regular, true, true, true, this.mnuEditRegular_Click.bind(this)));
302 | mnuh.appendChild(this.mnuEditZones = createMenuItem(GraphicalFilterEditorStrings.Zones, true, false, true, this.mnuEditZones_Click.bind(this)));
303 | mnuh.appendChild(this.mnuEditSmoothNarrow = createMenuItem(GraphicalFilterEditorStrings.SmoothNarrow, true, false, true, this.mnuEditSmoothNarrow_Click.bind(this)));
304 | mnuh.appendChild(this.mnuEditSmoothWide = createMenuItem(GraphicalFilterEditorStrings.SmoothWide, true, false, true, this.mnuEditSmoothWide_Click.bind(this)));
305 | mnuh.appendChild(this.mnuEditPeakingEq = createMenuItem(GraphicalFilterEditorStrings.PeakingEq, true, false, true, this.mnuEditPeakingEq_Click.bind(this)));
306 | if ((uiSettings && uiSettings.hideEditModePeakingEq) || !this.filter.iirSupported)
307 | this.mnuEditPeakingEq.style.display = "none";
308 | mnuh.appendChild(this.mnuEditShelfEq = createMenuItem(GraphicalFilterEditorStrings.ShelfEq, true, false, true, this.mnuEditShelfEq_Click.bind(this)));
309 | if ((uiSettings && uiSettings.hideEditModeShelfEq) || !this.filter.iirSupported)
310 | this.mnuEditShelfEq.style.display = "none";
311 | mnuh.appendChild(createMenuSep());
312 | mnuh.appendChild(this.mnuNormalizeCurves = createMenuItem(GraphicalFilterEditorStrings.NormalizeCurves, true, false, false, this.mnuNormalizeCurves_Click.bind(this), "GEFILTER"));
313 | mnuh.appendChild(this.mnuShowZones = createMenuItem(GraphicalFilterEditorStrings.ShowZones, true, false, false, this.mnuShowZones_Click.bind(this)));
314 | mnuh.appendChild(this.mnuShowActual = createMenuItem(GraphicalFilterEditorStrings.ShowActualResponse, true, true, false, this.mnuShowActual_Click.bind(this)));
315 | this.mnu.appendChild(mnuh);
316 |
317 | element.appendChild(this.mnu);
318 |
319 | if (settings)
320 | this.loadSettings(settings);
321 |
322 | this.drawCurve();
323 | }
324 |
325 | public destroy() : void {
326 | if (this.filter)
327 | this.filter.destroy();
328 |
329 | if (this.pointerHandler)
330 | this.pointerHandler.destroy();
331 |
332 | if (this.renderer)
333 | this.renderer.destroy();
334 |
335 | zeroObject(this);
336 | }
337 |
338 | public loadSettings(settings?: GraphicalFilterEditorSettings | null): void {
339 | if (!settings)
340 | return;
341 |
342 | const filter = this.filter;
343 |
344 | if (settings.showZones === false || settings.showZones === true)
345 | this._showZones = settings.showZones;
346 |
347 | if (settings.editMode &&
348 | settings.editMode >= GraphicalFilterEditorControl.editModeFirst &&
349 | settings.editMode <= GraphicalFilterEditorControl.editModeLast &&
350 | (filter.iirSupported || (settings.editMode !== GraphicalFilterEditorControl.editModePeakingEq && settings.editMode !== GraphicalFilterEditorControl.editModeShelfEq)))
351 | this._editMode = settings.editMode;
352 |
353 | if (settings.isActualChannelCurveNeeded === false || settings.isActualChannelCurveNeeded === true)
354 | this._isActualChannelCurveNeeded = settings.isActualChannelCurveNeeded;
355 |
356 | if (settings.currentChannelIndex === 0 || settings.currentChannelIndex === 1)
357 | this._currentChannelIndex = settings.currentChannelIndex;
358 |
359 | if (settings.isSameFilterLR === false || settings.isSameFilterLR === true)
360 | this.isSameFilterLR = settings.isSameFilterLR;
361 |
362 | let leftCurve = GraphicalFilterEditor.decodeCurve(settings.leftCurve),
363 | rightCurve = GraphicalFilterEditor.decodeCurve(settings.rightCurve);
364 |
365 | if (leftCurve && !rightCurve)
366 | rightCurve = leftCurve;
367 | else if (rightCurve && !leftCurve)
368 | leftCurve = rightCurve;
369 |
370 | if (leftCurve) {
371 | const curve = filter.channelCurves[0];
372 | for (let i = GraphicalFilterEditor.visibleBinCount - 1; i >= 0; i--)
373 | curve[i] = filter.clampY(leftCurve[i]);
374 | }
375 |
376 | if (rightCurve) {
377 | const curve = filter.channelCurves[1];
378 | for (let i = GraphicalFilterEditor.visibleBinCount - 1; i >= 0; i--)
379 | curve[i] = filter.clampY(rightCurve[i]);
380 | }
381 |
382 | if (this.isSameFilterLR) {
383 | this.checkMenu(this.mnuChBL, (this._currentChannelIndex === 0));
384 | this.checkMenu(this.mnuChL, false);
385 | this.checkMenu(this.mnuChBR, (this._currentChannelIndex === 1));
386 | this.checkMenu(this.mnuChR, false);
387 | } else {
388 | this.checkMenu(this.mnuChBL, false);
389 | this.checkMenu(this.mnuChL, (this._currentChannelIndex === 0));
390 | this.checkMenu(this.mnuChBR, false);
391 | this.checkMenu(this.mnuChR, (this._currentChannelIndex === 1));
392 | }
393 |
394 | const isNormalized = ((settings.isNormalized === false || settings.isNormalized === true) ? settings.isNormalized : filter.isNormalized);
395 |
396 | if (isNormalized === filter.isNormalized)
397 | filter.updateFilter(this._currentChannelIndex, this.isSameFilterLR, true);
398 | else
399 | filter.changeIsNormalized(isNormalized, this._currentChannelIndex, this.isSameFilterLR);
400 |
401 | if (this._isActualChannelCurveNeeded)
402 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
403 |
404 | this.checkMenu(this.mnuShowZones, this._showZones);
405 | this.editMode = this._editMode;
406 | this.checkMenu(this.mnuNormalizeCurves, this.filter.isNormalized);
407 | this.checkMenu(this.mnuShowActual, this._isActualChannelCurveNeeded);
408 |
409 | this.drawCurve();
410 | }
411 |
412 | public saveSettings(): GraphicalFilterEditorSettings {
413 | return {
414 | showZones: this._showZones,
415 | editMode: this._editMode,
416 | isActualChannelCurveNeeded: this._isActualChannelCurveNeeded,
417 | currentChannelIndex: this._currentChannelIndex,
418 | isSameFilterLR: this.isSameFilterLR,
419 | isNormalized: this.filter.isNormalized,
420 | leftCurve: GraphicalFilterEditor.encodeCurve(this.filter.channelCurves[0]),
421 | rightCurve: GraphicalFilterEditor.encodeCurve(this.filter.channelCurves[1])
422 | };
423 | }
424 |
425 | public get scale(): number {
426 | return this._scale;
427 | }
428 |
429 | public set scale(scale: number) {
430 | if (scale <= 0 || this._scale === scale || !this.element)
431 | return;
432 |
433 | this._scale = scale;
434 | if (!this._fontSize)
435 | this.element.style.fontSize = (12 * scale) + "px";
436 | if (!this._lineHeight)
437 | this.element.style.lineHeight = (16 * scale) + "px";
438 |
439 | if (this.renderer) {
440 | this.renderer.scaleChanged();
441 | this.drawCurve();
442 | }
443 | }
444 |
445 | public get fontSize(): string | null {
446 | return this._fontSize;
447 | }
448 |
449 | public set fontSize(fontSize: string | null) {
450 | if (this._fontSize === fontSize || !this.element)
451 | return;
452 |
453 | this._fontSize = fontSize;
454 | this.element.style.fontSize = (fontSize || ((12 * this._scale) + "px"));
455 | }
456 |
457 | public get lineHeight(): string | null {
458 | return this._lineHeight;
459 | }
460 |
461 | public set lineHeight(lineHeight: string | null) {
462 | if (this._lineHeight === lineHeight || !this.element)
463 | return;
464 |
465 | this._lineHeight = lineHeight;
466 | this.element.style.lineHeight = (lineHeight || ((16 * this._scale) + "px"));
467 | }
468 |
469 | public get showZones(): boolean {
470 | return this._showZones;
471 | }
472 |
473 | public set showZones(showZones: boolean) {
474 | this._showZones = showZones;
475 | this.checkMenu(this.mnuShowZones, showZones);
476 | this.drawCurve();
477 | }
478 |
479 | public get editMode(): number {
480 | return this._editMode;
481 | }
482 |
483 | public set editMode(editMode: number) {
484 | if (editMode < GraphicalFilterEditorControl.editModeFirst ||
485 | editMode > GraphicalFilterEditorControl.editModeLast ||
486 | (!this.filter.iirSupported && (editMode === GraphicalFilterEditorControl.editModePeakingEq || editMode === GraphicalFilterEditorControl.editModeShelfEq)))
487 | return;
488 |
489 | this._editMode = editMode;
490 | this.checkMenu(this.mnuEditRegular, editMode === GraphicalFilterEditorControl.editModeRegular);
491 | this.checkMenu(this.mnuEditZones, editMode === GraphicalFilterEditorControl.editModeZones);
492 | this.checkMenu(this.mnuEditSmoothNarrow, editMode === GraphicalFilterEditorControl.editModeSmoothNarrow);
493 | this.checkMenu(this.mnuEditSmoothWide, editMode === GraphicalFilterEditorControl.editModeSmoothWide);
494 | this.checkMenu(this.mnuEditPeakingEq, editMode === GraphicalFilterEditorControl.editModePeakingEq);
495 | this.checkMenu(this.mnuEditShelfEq, editMode === GraphicalFilterEditorControl.editModeShelfEq);
496 |
497 | let iirType = GraphicalFilterEditorIIRType.None;
498 | switch (editMode) {
499 | case GraphicalFilterEditorControl.editModePeakingEq:
500 | iirType = GraphicalFilterEditorIIRType.Peaking;
501 | break;
502 | case GraphicalFilterEditorControl.editModeShelfEq:
503 | iirType = GraphicalFilterEditorIIRType.Shelf;
504 | break;
505 | }
506 |
507 | if (this.filter.iirType !== iirType) {
508 | this.mnu.className = (iirType ? "GEMNU GEEQ" : "GEMNU");
509 |
510 | this.filter.changeIIRType(iirType, this._currentChannelIndex, this.isSameFilterLR);
511 | if (this._isActualChannelCurveNeeded) {
512 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
513 | this.drawCurve();
514 | }
515 | }
516 | }
517 |
518 | public get isActualChannelCurveNeeded(): boolean {
519 | return this._isActualChannelCurveNeeded;
520 | }
521 |
522 | public set isActualChannelCurveNeeded(isActualChannelCurveNeeded: boolean) {
523 | this._isActualChannelCurveNeeded = isActualChannelCurveNeeded;
524 | this.checkMenu(this.mnuShowActual, isActualChannelCurveNeeded);
525 | if (isActualChannelCurveNeeded)
526 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
527 | this.drawCurve();
528 | }
529 |
530 | public get currentChannelIndex(): number {
531 | return this._currentChannelIndex;
532 | }
533 |
534 | public get isNormalized(): boolean {
535 | return this.filter.isNormalized;
536 | }
537 |
538 | public set isNormalized(isNormalized: boolean) {
539 | this.filter.changeIsNormalized(isNormalized, this._currentChannelIndex, this.isSameFilterLR);
540 | this.checkMenu(this.mnuNormalizeCurves, isNormalized);
541 | if (this._isActualChannelCurveNeeded) {
542 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
543 | this.drawCurve();
544 | }
545 | }
546 |
547 | private static formatDB(dB: number): string {
548 | if (dB < -40) return GraphicalFilterEditorStrings.MinusInfinity;
549 | return ((dB < 0) ? GraphicalFilterEditorStrings.toFixed(dB, 2) : ((dB === 0) ? GraphicalFilterEditorStrings.Minus0 : "+" + GraphicalFilterEditorStrings.toFixed(dB, 2)));
550 | }
551 |
552 | private static formatFrequency(frequencyAndEquivalent: number[]): string {
553 | return frequencyAndEquivalent[0].toFixed(0) + " Hz (" + ((frequencyAndEquivalent[1] < 1000) ? (frequencyAndEquivalent[1] + " Hz") : ((frequencyAndEquivalent[1] / 1000) + " kHz")) + ")";
554 | }
555 |
556 | private static setFirstNodeText(element: HTMLElement, text: string): void {
557 | if (element.firstChild)
558 | element.firstChild.nodeValue = text;
559 | }
560 |
561 | private btnMnu_Click(e: MouseEvent): boolean {
562 | if (!e.button) {
563 | if (this.mnu.style.display === "none") {
564 | this.mnu.style.bottom = (this.btnMnu.clientHeight) + "px";
565 | this.mnu.style.display = "inline-block";
566 | if (this.openMenuElement && this.closeMenuElement) {
567 | this.openMenuElement.style.display = "none";
568 | this.closeMenuElement.style.display = "";
569 | } else {
570 | GraphicalFilterEditorControl.setFirstNodeText(this.btnMnu, this.closeMenuCharacter);
571 | }
572 | } else {
573 | this.mnu.style.display = "none";
574 | if (this.openMenuElement && this.closeMenuElement) {
575 | this.closeMenuElement.style.display = "none";
576 | this.openMenuElement.style.display = "";
577 | } else {
578 | GraphicalFilterEditorControl.setFirstNodeText(this.btnMnu, this.openMenuCharacter);
579 | }
580 | }
581 | }
582 | return true;
583 | }
584 |
585 | private checkMenu(mnu: HTMLDivElement, chk: boolean): boolean {
586 | (mnu.firstChild as HTMLElement).style.visibility = (chk ? "visible" : "hidden");
587 | return chk;
588 | }
589 |
590 | private mnuChB_Click(channelIndex: number, e: MouseEvent): boolean {
591 | if (!e.button) {
592 | if (!this.isSameFilterLR || this._currentChannelIndex !== channelIndex) {
593 | if (this.isSameFilterLR) {
594 | this._currentChannelIndex = channelIndex;
595 | this.filter.updateFilter(channelIndex, true, true);
596 | if (this._isActualChannelCurveNeeded)
597 | this.filter.updateActualChannelCurve(channelIndex);
598 | this.drawCurve();
599 | } else {
600 | this.isSameFilterLR = true;
601 | this.filter.copyFilter(channelIndex, 1 - channelIndex);
602 | if (this._currentChannelIndex !== channelIndex) {
603 | this._currentChannelIndex = channelIndex;
604 | if (this._isActualChannelCurveNeeded)
605 | this.filter.updateActualChannelCurve(channelIndex);
606 | this.drawCurve();
607 | }
608 | }
609 | this.checkMenu(this.mnuChBL, (channelIndex === 0));
610 | this.checkMenu(this.mnuChL, false);
611 | this.checkMenu(this.mnuChBR, (channelIndex === 1));
612 | this.checkMenu(this.mnuChR, false);
613 | }
614 | }
615 | return this.btnMnu_Click(e);
616 | }
617 |
618 | private mnuChLR_Click(channelIndex: number, e: MouseEvent): boolean {
619 | if (!e.button) {
620 | if (this.isSameFilterLR || this._currentChannelIndex !== channelIndex) {
621 | if (this.isSameFilterLR) {
622 | this.isSameFilterLR = false;
623 | this.filter.updateFilter(1 - this._currentChannelIndex, false, false);
624 | }
625 | if (this._currentChannelIndex !== channelIndex) {
626 | this._currentChannelIndex = channelIndex;
627 | if (this._isActualChannelCurveNeeded)
628 | this.filter.updateActualChannelCurve(channelIndex);
629 | this.drawCurve();
630 | }
631 | this.checkMenu(this.mnuChBL, false);
632 | this.checkMenu(this.mnuChL, (channelIndex === 0));
633 | this.checkMenu(this.mnuChBR, false);
634 | this.checkMenu(this.mnuChR, (channelIndex === 1));
635 | }
636 | }
637 | return this.btnMnu_Click(e);
638 | }
639 |
640 | private mnuResetCurve_Click(e: MouseEvent): boolean {
641 | if (!e.button)
642 | this.resetCurve();
643 | return this.btnMnu_Click(e);
644 | }
645 |
646 | private mnuShowZones_Click(e: MouseEvent): boolean {
647 | if (!e.button)
648 | this.showZones = !this._showZones;
649 | return this.btnMnu_Click(e);
650 | }
651 |
652 | private mnuEditRegular_Click(e: MouseEvent): boolean {
653 | if (!e.button)
654 | this.editMode = GraphicalFilterEditorControl.editModeRegular;
655 | return this.btnMnu_Click(e);
656 | }
657 |
658 | private mnuEditZones_Click(e: MouseEvent): boolean {
659 | if (!e.button)
660 | this.editMode = GraphicalFilterEditorControl.editModeZones;
661 | return this.btnMnu_Click(e);
662 | }
663 |
664 | private mnuEditSmoothNarrow_Click(e: MouseEvent): boolean {
665 | if (!e.button)
666 | this.editMode = GraphicalFilterEditorControl.editModeSmoothNarrow;
667 | return this.btnMnu_Click(e);
668 | }
669 |
670 | private mnuEditSmoothWide_Click(e: MouseEvent): boolean {
671 | if (!e.button)
672 | this.editMode = GraphicalFilterEditorControl.editModeSmoothWide;
673 | return this.btnMnu_Click(e);
674 | }
675 |
676 | private mnuEditPeakingEq_Click(e: MouseEvent): boolean {
677 | if (!e.button)
678 | this.editMode = GraphicalFilterEditorControl.editModePeakingEq;
679 | return this.btnMnu_Click(e);
680 | }
681 |
682 | private mnuEditShelfEq_Click(e: MouseEvent): boolean {
683 | if (!e.button)
684 | this.editMode = GraphicalFilterEditorControl.editModeShelfEq;
685 | return this.btnMnu_Click(e);
686 | }
687 |
688 | private mnuNormalizeCurves_Click(e: MouseEvent): boolean {
689 | if (!e.button)
690 | this.isNormalized = !this.filter.isNormalized;
691 | return this.btnMnu_Click(e);
692 | }
693 |
694 | private mnuShowActual_Click(e: MouseEvent): boolean {
695 | if (!e.button)
696 | this.isActualChannelCurveNeeded = !this._isActualChannelCurveNeeded;
697 | return this.btnMnu_Click(e);
698 | }
699 |
700 | private mouseDown(e: MouseEvent): boolean {
701 | if (!e.button && !this.drawingMode) {
702 | const rect = this.renderer.element.getBoundingClientRect(),
703 | x = (((e.clientX - rect.left) / this._scale) | 0) - this.renderer.leftMargin,
704 | y = (((e.clientY - rect.top) / this._scale) | 0);
705 |
706 | this.renderer.element.removeEventListener("mousemove", this.boundMouseMove);
707 |
708 | this.drawingMode = 1;
709 |
710 | switch (this._editMode) {
711 | case GraphicalFilterEditorControl.editModeZones:
712 | case GraphicalFilterEditorControl.editModePeakingEq:
713 | this.filter.changeZoneY(this._currentChannelIndex, x, y);
714 | break;
715 | case GraphicalFilterEditorControl.editModeShelfEq:
716 | this.filter.changeShelfZoneY(this._currentChannelIndex, x, y);
717 | break;
718 | case GraphicalFilterEditorControl.editModeSmoothNarrow:
719 | this.filter.startSmoothEdition(this._currentChannelIndex);
720 | this.filter.changeSmoothY(this._currentChannelIndex, x, y, GraphicalFilterEditor.visibleBinCount >> 3);
721 | break;
722 | case GraphicalFilterEditorControl.editModeSmoothWide:
723 | this.filter.startSmoothEdition(this._currentChannelIndex);
724 | this.filter.changeSmoothY(this._currentChannelIndex, x, y, GraphicalFilterEditor.visibleBinCount >> 1);
725 | break;
726 | default:
727 | this.filter.channelCurves[this._currentChannelIndex][this.filter.clampX(x)] = this.filter.clampY(y);
728 | this.lastDrawX = x;
729 | this.lastDrawY = y;
730 | break;
731 | }
732 |
733 | this.drawCurve();
734 |
735 | return true;
736 | }
737 | return false;
738 | }
739 |
740 | private mouseMove(e: MouseEvent): void {
741 | const rect = this.renderer.element.getBoundingClientRect();
742 | let x = (((e.clientX - rect.left) / this._scale) | 0) - this.renderer.leftMargin,
743 | y = (((e.clientY - rect.top) / this._scale) | 0);
744 |
745 | let curve = this.filter.channelCurves[this._currentChannelIndex];
746 |
747 | if (this.drawingMode) {
748 | switch (this._editMode) {
749 | case GraphicalFilterEditorControl.editModeZones:
750 | case GraphicalFilterEditorControl.editModePeakingEq:
751 | this.filter.changeZoneY(this._currentChannelIndex, x, y);
752 | break;
753 | case GraphicalFilterEditorControl.editModeShelfEq:
754 | this.filter.changeShelfZoneY(this._currentChannelIndex, x, y);
755 | break;
756 | case GraphicalFilterEditorControl.editModeSmoothNarrow:
757 | this.filter.changeSmoothY(this._currentChannelIndex, x, y, GraphicalFilterEditor.visibleBinCount >> 3);
758 | break;
759 | case GraphicalFilterEditorControl.editModeSmoothWide:
760 | this.filter.changeSmoothY(this._currentChannelIndex, x, y, GraphicalFilterEditor.visibleBinCount >> 1);
761 | break;
762 | default:
763 | if (Math.abs(x - this.lastDrawX) > 1) {
764 | const delta = (y - this.lastDrawY) / Math.abs(x - this.lastDrawX),
765 | inc = ((x < this.lastDrawX) ? -1 : 1);
766 | let count = Math.abs(x - this.lastDrawX) - 1;
767 | y = this.lastDrawY + delta;
768 | for (x = this.lastDrawX + inc; count > 0; x += inc, count--) {
769 | curve[this.filter.clampX(x)] = this.filter.clampY(y);
770 | y += delta;
771 | }
772 | }
773 | curve[this.filter.clampX(x)] = this.filter.clampY(y);
774 | this.lastDrawX = x;
775 | this.lastDrawY = y;
776 | break;
777 | }
778 | this.drawCurve();
779 | } else if (this._isActualChannelCurveNeeded) {
780 | curve = this.filter.actualChannelCurve;
781 | }
782 |
783 | x = this.filter.clampX(x);
784 | GraphicalFilterEditorControl.setFirstNodeText(this.lblCursor, GraphicalFilterEditorControl.formatDB(this.filter.yToDB(y)));
785 | GraphicalFilterEditorControl.setFirstNodeText(this.lblCurve, GraphicalFilterEditorControl.formatDB(this.filter.yToDB(curve[x])));
786 | GraphicalFilterEditorControl.setFirstNodeText(this.lblFrequency, GraphicalFilterEditorControl.formatFrequency(this.filter.visibleBinToFrequency(x, true) as number[]));
787 | }
788 |
789 | private mouseUp(e: MouseEvent): void {
790 | if (this.drawingMode) {
791 | this.renderer.element.addEventListener("mousemove", this.boundMouseMove);
792 | this.drawingMode = 0;
793 | this.commitChanges();
794 | }
795 | }
796 |
797 | public resetCurve() {
798 | const curve = this.filter.channelCurves[this._currentChannelIndex];
799 | for (let i = curve.length - 1; i >= 0; i--)
800 | curve[i] = GraphicalFilterEditor.zeroChannelValueY;
801 |
802 | this.filter.updateFilter(this._currentChannelIndex, this.isSameFilterLR, false);
803 | if (this._isActualChannelCurveNeeded)
804 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
805 | this.drawCurve();
806 | }
807 |
808 | public getZoneY(zoneIndex: number): number {
809 | return this.filter.getZoneY(this._currentChannelIndex, zoneIndex);
810 | }
811 |
812 | public changeZoneY(zoneIndex: number, y: number, removeActualChannelCurve?: boolean): void {
813 | this.filter.changeZoneYByIndex(this._currentChannelIndex, zoneIndex, y);
814 | this.drawCurve(removeActualChannelCurve);
815 | }
816 |
817 | public getShelfZoneY(shelfZoneIndex: number): number {
818 | return this.filter.getShelfZoneY(this._currentChannelIndex, shelfZoneIndex);
819 | }
820 |
821 | public changeShelfZoneY(shelfZoneIndex: number, y: number, removeActualChannelCurve?: boolean): void {
822 | this.filter.changeShelfZoneYByIndex(this._currentChannelIndex, shelfZoneIndex, y);
823 | this.drawCurve(removeActualChannelCurve);
824 | }
825 |
826 | public changeFilterY(x: number, y: number, removeActualChannelCurve?: boolean): void {
827 | this.filter.channelCurves[this._currentChannelIndex][this.filter.clampX(x)] = this.filter.clampY(y);
828 | this.drawCurve(removeActualChannelCurve);
829 | }
830 |
831 | public commitChanges(): void {
832 | this.filter.updateFilter(this._currentChannelIndex, this.isSameFilterLR, false);
833 | if (this._isActualChannelCurveNeeded)
834 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
835 | this.drawCurve();
836 | }
837 |
838 | public changeFilterLength(newFilterLength: number): boolean {
839 | if (this.filter.changeFilterLength(newFilterLength, this._currentChannelIndex, this.isSameFilterLR)) {
840 | if (this._isActualChannelCurveNeeded)
841 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
842 | this.drawCurve();
843 | return true;
844 | }
845 | return false;
846 | }
847 |
848 | public changeSampleRate(newSampleRate: number): boolean {
849 | if (this.filter.changeSampleRate(newSampleRate, this._currentChannelIndex, this.isSameFilterLR)) {
850 | if (this._isActualChannelCurveNeeded)
851 | this.filter.updateActualChannelCurve(this._currentChannelIndex);
852 | this.drawCurve();
853 | return true;
854 | }
855 | return false;
856 | }
857 |
858 | public changeAudioContext(newAudioContext: AudioContext): boolean {
859 | return this.filter.changeAudioContext(newAudioContext, this._currentChannelIndex, this.isSameFilterLR);
860 | }
861 |
862 | public drawCurve(removeActualChannelCurve?: boolean): void {
863 | if (this.renderer)
864 | this.renderer.drawCurve(this._showZones, !removeActualChannelCurve && this._isActualChannelCurveNeeded && !this.drawingMode, this._currentChannelIndex);
865 | }
866 | }
867 |
--------------------------------------------------------------------------------
/scripts/graphicalFilterEditor/graphicalFilterEditorRenderer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | abstract class GraphicalFilterEditorRenderer {
28 | public readonly element: T;
29 | public readonly leftMargin: number;
30 | public readonly editor: GraphicalFilterEditorControl;
31 |
32 | public constructor(element: T, leftMargin: number, editor: GraphicalFilterEditorControl) {
33 | this.element = element;
34 | this.leftMargin = leftMargin;
35 | this.editor = editor;
36 | }
37 |
38 | public destroy(): void {
39 | zeroObject(this);
40 | }
41 |
42 | public abstract scaleChanged(): void;
43 |
44 | public abstract drawCurve(showZones: boolean, isActualChannelCurveNeeded: boolean, currentChannelIndex: number): void;
45 | }
46 |
--------------------------------------------------------------------------------
/scripts/graphicalFilterEditor/graphicalFilterEditorSVGRenderer.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class GraphicalFilterEditorSVGRenderer extends GraphicalFilterEditorRenderer {
28 | private readonly _svg: SVGElement;
29 | private readonly _svgGradient: SVGLinearGradientElement;
30 | private readonly _svgZones: SVGElement;
31 | private readonly _svgGrayCurve: SVGElement;
32 | private readonly _svgCurve: SVGElement;
33 | private readonly _svgLines: SVGLineElement[];
34 |
35 | private _pixelRatio: number;
36 | private _showZones: boolean;
37 | private _isActualChannelCurveNeeded: boolean;
38 |
39 | public constructor(editor: GraphicalFilterEditorControl) {
40 | super(document.createElement("div"), Math.abs(GraphicalFilterEditorControl.controlWidth - GraphicalFilterEditor.visibleBinCount) >> 1, editor);
41 |
42 | this.element.className = "GECV";
43 | this.element.style.overflow = "hidden";
44 | this.element.style.backgroundColor = "#303030";
45 |
46 | this.element.innerHTML = ``;
73 |
74 | this._pixelRatio = 1;
75 | this._showZones = false;
76 | this._isActualChannelCurveNeeded = true;
77 |
78 | const svg = this.element.firstChild as SVGElement;
79 | this._svg = svg;
80 | this._svgGradient = svg.getElementsByTagName("linearGradient")[0];
81 | this._svgZones = svg.getElementsByTagName("g")[0];
82 | const paths = svg.getElementsByTagName("path");
83 | this._svgGrayCurve = paths[0];
84 | this._svgCurve = paths[1];
85 | const svgLines = svg.getElementsByTagName("line");
86 | this._svgLines = new Array(svgLines.length);
87 | for (let i = svgLines.length - 1; i >= 0; i--)
88 | this._svgLines[i] = svgLines[i];
89 |
90 | this.scaleChanged();
91 | }
92 |
93 | public scaleChanged(): void {
94 | const element = this.element,
95 | editor = this.editor,
96 | filter = editor.filter,
97 | svg = this._svg;
98 |
99 | if (!element || !editor || !filter || !svg)
100 | return;
101 |
102 | const visibleBinCount = GraphicalFilterEditor.visibleBinCount,
103 | controlWidth = GraphicalFilterEditorControl.controlWidth,
104 | controlHeight = GraphicalFilterEditorControl.controlHeight,
105 | pixelRatio = (devicePixelRatio > 1 ? devicePixelRatio : 1),
106 | editorScale = editor.scale,
107 | scale = editorScale * pixelRatio,
108 | canvasLeftMargin = this.leftMargin,
109 | canvasWidth = (controlWidth * scale) | 0,
110 | canvasHeight = (controlHeight * scale) | 0,
111 | svgLines = this._svgLines;
112 |
113 | let lineWidth = (scale < 1 ? scale : (scale | 0)),
114 | halfLineWidth = lineWidth * 0.5,
115 | x = canvasWidth.toString(),
116 | y = canvasHeight.toString();
117 |
118 | svg.setAttribute("width", x);
119 | svg.setAttribute("height", y);
120 | svg.setAttribute("viewBox", `0 0 ${x} ${y}`);
121 | svg.style.transform = ((pixelRatio === 1) ? "" : ("scale(" + (1 / pixelRatio) + ")"));
122 | this._svgGradient.setAttribute("y2", y);
123 | editor.element.style.width = ((controlWidth * editorScale) | 0) + "px";
124 | element.style.width = ((controlWidth * editorScale) | 0) + "px";
125 | element.style.height = ((controlHeight * editorScale) | 0) + "px";
126 |
127 | svgLines[0].setAttribute("x2", x);
128 | svgLines[1].setAttribute("x2", x);
129 | for (let i = svgLines.length - 1; i >= 2; i--)
130 | svgLines[i].setAttribute("y2", y);
131 |
132 | y = (((GraphicalFilterEditor.zeroChannelValueY * scale) | 0) + halfLineWidth).toString();
133 | svgLines[0].setAttribute("y1", y);
134 | svgLines[0].setAttribute("y2", y);
135 |
136 | y = (((GraphicalFilterEditor.validYRangeHeight * scale) | 0) + halfLineWidth).toString();
137 | svgLines[1].setAttribute("y1", y);
138 | svgLines[1].setAttribute("y2", y);
139 |
140 | for (let i = filter.equivalentZonesFrequencyCount.length - 2; i > 0; i--) {
141 | x = ((((filter.equivalentZonesFrequencyCount[i] + canvasLeftMargin) * scale) | 0) + halfLineWidth).toString();
142 | svgLines[i + 1].setAttribute("x1", x);
143 | svgLines[i + 1].setAttribute("x2", x);
144 | }
145 |
146 | x = lineWidth.toString() + "px";
147 | y = ((4 * scale) | 0).toString();
148 | for (let i = svgLines.length - 1; i >= 0; i--) {
149 | svgLines[i].setAttribute("stroke-width", x);
150 | svgLines[i].setAttribute("stroke-dasharray", y);
151 | }
152 |
153 | lineWidth = (scale < 1 ? (scale * 2) : ((scale * 2) | 0));
154 | x = lineWidth.toString() + "px";
155 |
156 | this._svgGrayCurve.style.strokeWidth = x;
157 | this._svgCurve.style.strokeWidth = x;
158 |
159 | this._pixelRatio = pixelRatio;
160 | }
161 |
162 | public drawCurve(showZones: boolean, isActualChannelCurveNeeded: boolean, currentChannelIndex: number): void {
163 | let pixelRatio = (devicePixelRatio > 1 ? devicePixelRatio : 1);
164 | if (pixelRatio !== this._pixelRatio) {
165 | this.scaleChanged();
166 | pixelRatio = this._pixelRatio;
167 | }
168 |
169 | if (this._showZones !== showZones) {
170 | this._showZones = showZones;
171 | this._svgZones.style.display = (showZones ? "" : "none");
172 | }
173 |
174 | const editor = this.editor,
175 | filter = editor.filter,
176 | scale = editor.scale * pixelRatio,
177 | lineWidth = (scale < 1 ? (scale * 2) : ((scale * 2) | 0)),
178 | halfLineWidth = lineWidth * 0.5,
179 | canvasLeftMargin = this.leftMargin,
180 | visibleBinCount = GraphicalFilterEditor.visibleBinCount;
181 |
182 | for (let turn = (isActualChannelCurveNeeded ? 1 : 0); turn >= 0; turn--) {
183 | const curve = ((turn || !isActualChannelCurveNeeded) ? filter.channelCurves[currentChannelIndex] : filter.actualChannelCurve),
184 | svgCurve = (turn ? this._svgGrayCurve : this._svgCurve);
185 |
186 | let x = 1,
187 | lastX = 0,
188 | lastY = curve[0],
189 | str = "M " + ((canvasLeftMargin * scale) | 0) + "," + ((lastY * scale) + halfLineWidth);
190 |
191 | for (; x < visibleBinCount; x++) {
192 | const y = curve[x];
193 | if (y === lastY)
194 | continue;
195 |
196 | if (x > (lastX + 1) && x > 1)
197 | str += " " + (((canvasLeftMargin + x - 1) * scale) | 0) + "," + ((curve[x - 1] * scale) + halfLineWidth);
198 |
199 | lastX = x;
200 | lastY = curve[x];
201 | str += " " + (((canvasLeftMargin + lastX) * scale) | 0) + "," + ((lastY * scale) + halfLineWidth);
202 | }
203 |
204 | // Just to fill up the last pixel!
205 | str += " " + (((canvasLeftMargin + x) * scale) | 0) + "," + ((curve[x - 1] * scale) + halfLineWidth);
206 | svgCurve.setAttribute("d", str);
207 | }
208 |
209 | if (this._isActualChannelCurveNeeded !== isActualChannelCurveNeeded) {
210 | this._isActualChannelCurveNeeded = isActualChannelCurveNeeded;
211 | this._svgGrayCurve.style.display = (isActualChannelCurveNeeded ? "" : "none");
212 | }
213 | }
214 | }
215 |
--------------------------------------------------------------------------------
/scripts/graphicalFilterEditor/graphicalFilterEditorStrings.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | class GraphicalFilterEditorStrings {
28 | public static Minus0 = "-0.00";
29 |
30 | public static Cursor = "Cursor: ";
31 | public static Curve = "Curve: ";
32 | public static Frequency = "Frequency: ";
33 | public static SameCurve = "Same curve for both channels";
34 | public static UseLeftCurve = "Use left curve";
35 | public static UseRightCurve = "Use right curve";
36 | public static OneForEach = "One curve for each channel";
37 | public static ShowLeftCurve = "Show left curve";
38 | public static ShowRightCurve = "Show right curve";
39 | public static ResetCurve = "Reset curve";
40 | public static EditMode = "Edit mode";
41 | public static Regular = "Regular";
42 | public static Zones = "Zones";
43 | public static SmoothNarrow = "Smooth (narrow)";
44 | public static SmoothWide = "Smooth (wide)";
45 | public static PeakingEq = "10-band Peaking Filter";
46 | public static ShelfEq = "7-band Low Shelf Filter";
47 | public static NormalizeCurves = "Normalize curves";
48 | public static ShowZones = "Show zones";
49 | public static ShowActualResponse = "Show actual response";
50 | public static MinusInfinity = "-Inf.";
51 |
52 | public static toFixed(x: number, fractionDigits: number): string { return x.toFixed(fractionDigits); }
53 |
54 | public static init(language: string): void {
55 | if (language && language.toLowerCase().indexOf("pt") === 0) {
56 | GraphicalFilterEditorStrings.Minus0 = "-0,00";
57 |
58 | //GraphicalFilterEditorStrings.Cursor = "Cursor: ";
59 | GraphicalFilterEditorStrings.Curve = "Curva: ";
60 | GraphicalFilterEditorStrings.Frequency = "Frequência: ";
61 | GraphicalFilterEditorStrings.SameCurve = "Mesma curva para ambos canais";
62 | GraphicalFilterEditorStrings.UseLeftCurve = "Usar curva da esquerda";
63 | GraphicalFilterEditorStrings.UseRightCurve = "Usar curva da direita";
64 | GraphicalFilterEditorStrings.OneForEach = "Uma curva por canal";
65 | GraphicalFilterEditorStrings.ShowLeftCurve = "Exibir curva da esquerda";
66 | GraphicalFilterEditorStrings.ShowRightCurve = "Exibir curva da direita";
67 | GraphicalFilterEditorStrings.ResetCurve = "Zerar curva";
68 | GraphicalFilterEditorStrings.EditMode = "Modo de edição";
69 | GraphicalFilterEditorStrings.Regular = "Normal";
70 | GraphicalFilterEditorStrings.Zones = "Zonas";
71 | GraphicalFilterEditorStrings.SmoothNarrow = "Suave (estreito)";
72 | GraphicalFilterEditorStrings.SmoothWide = "Suave (largo)";
73 | GraphicalFilterEditorStrings.PeakingEq = "Filtro \"Peaking\" de 10 Bandas";
74 | GraphicalFilterEditorStrings.ShelfEq = "Filtro \"Low Shelf\" de 7 Bandas";
75 | GraphicalFilterEditorStrings.NormalizeCurves = "Normalizar curvas";
76 | GraphicalFilterEditorStrings.ShowZones = "Exibir zonas";
77 | GraphicalFilterEditorStrings.ShowActualResponse = "Exibir resposta real";
78 | //GraphicalFilterEditorStrings.MinusInfinity = "-Inf.";
79 |
80 | GraphicalFilterEditorStrings.toFixed = function (x: number, fractionDigits: number): string { return x.toFixed(fractionDigits).replace(".", ","); };
81 | }
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/scripts/main.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | let cLib: CLib;
28 |
29 | function cancelEvent(e: Event): boolean {
30 | if (e) {
31 | if ("isCancelled" in e)
32 | (e as any).isCancelled = true;
33 | if (("preventDefault" in e) && (!("cancelable" in e) || e.cancelable))
34 | e.preventDefault();
35 | if ("stopPropagation" in e)
36 | e.stopPropagation();
37 | }
38 | return false;
39 | }
40 |
41 | function lerp(x0: number, y0: number, x1: number, y1: number, x: number): number {
42 | return ((x - x0) * (y1 - y0) / (x1 - x0)) + y0;
43 | }
44 |
45 | function smoothStep(edge0: number, edge1: number, x: number): number {
46 | const t = (x - edge0) / (edge1 - edge0);
47 | return ((t <= 0.0) ? 0.0 :
48 | ((t >= 1.0) ? 1.0 :
49 | (t * t * (3.0 - (2.0 * t)))
50 | )
51 | );
52 | }
53 |
54 | function zeroObject(o: any, includeFunctions?: boolean): void {
55 | for (let p in o) {
56 | switch (typeof o[p]) {
57 | case "function":
58 | if (includeFunctions)
59 | o[p] = null;
60 | break;
61 | case "boolean":
62 | o[p] = false;
63 | break;
64 | case "number":
65 | o[p] = 0;
66 | break;
67 | default:
68 | const v = o[p];
69 | if (Array.isArray(v))
70 | v.fill(null);
71 | o[p] = null;
72 | break;
73 | }
74 | }
75 | }
76 |
77 | function setup(): void {
78 | if (!Array.prototype.fill) {
79 | // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill
80 | Array.prototype.fill = function (value: any, start?: number, end?: number): any {
81 | var i, length = this.length | 0;
82 | start = (start as number) | 0;
83 | end = ((end === undefined) ? length : (end | 0));
84 | end = ((end < 0) ? Math.max(length + end, 0) : Math.min(end, length));
85 | i = ((start < 0) ? Math.max(length + start, 0) : Math.min(start, length));
86 | while (i < end)
87 | this[i++] = value;
88 | return this;
89 | };
90 | }
91 |
92 | function finishLoading(options?: any): void {
93 | ((window as any)["CLib"](options) as Promise).then(function (value) {
94 | cLib = value;
95 | if ((window as any)["main"])
96 | (window as any)["main"]();
97 | }, function (reason) {
98 | alert(reason);
99 | throw reason;
100 | });
101 | }
102 |
103 | const wasmBinary: ArrayBuffer | PromiseLike | undefined = (window as any)["CLibWasmBinary"];
104 | if (wasmBinary) {
105 | delete (window as any)["CLibWasmBinary"];
106 | Promise.resolve(wasmBinary).then(function (wasmBinary) {
107 | finishLoading({ wasmBinary });
108 | }, function () {
109 | finishLoading();
110 | });
111 | } else {
112 | const memoryArrayBuffer: ArrayBuffer | PromiseLike | undefined = (window as any)["CLibMemoryArrayBuffer"];
113 | if (memoryArrayBuffer) {
114 | delete (window as any)["CLibMemoryArrayBuffer"];
115 | Promise.resolve(memoryArrayBuffer).then(function (memoryArrayBuffer) {
116 | finishLoading({
117 | memoryInitializerRequest: {
118 | status: 200,
119 | response: memoryArrayBuffer
120 | }
121 | });
122 | }, function () {
123 | finishLoading();
124 | });
125 | } else {
126 | finishLoading();
127 | }
128 | }
129 | }
130 |
131 | setup();
132 |
--------------------------------------------------------------------------------
/scripts/ui/pointerHandler.ts:
--------------------------------------------------------------------------------
1 | //
2 | // MIT License
3 | //
4 | // Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves
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 all
14 | // 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 THE
22 | // SOFTWARE.
23 | //
24 | // https://github.com/carlosrafaelgn/GraphicalFilterEditor
25 | //
26 |
27 | //
28 | // This file came from my other project: https://github.com/carlosrafaelgn/pixel
29 | //
30 | interface OutsidePointerHandler {
31 | (e: Event): boolean;
32 | }
33 |
34 | interface BooleanPointerHandler {
35 | (e: MouseEvent): boolean;
36 | }
37 |
38 | interface VoidPointerHandler {
39 | (e: MouseEvent): void;
40 | }
41 |
42 | class PointerHandler {
43 | private readonly _documentTarget: HTMLElement;
44 | private readonly _element: HTMLElement;
45 |
46 | private readonly _downCallback: BooleanPointerHandler | null;
47 | private readonly _moveCallback: VoidPointerHandler | null;
48 | private readonly _upCallback: VoidPointerHandler | null;
49 |
50 | private readonly _documentDownEvent: string;
51 | private readonly _documentMoveEvent: string;
52 | private readonly _documentUpEvent: string;
53 | private readonly _documentCancelEvent: string | null;
54 |
55 | private readonly _boundDocumentDown: any;
56 | private readonly _boundDocumentMove: any;
57 | private readonly _boundDocumentUp: any;
58 | private readonly _boundExtraTouchStart: any;
59 |
60 | private readonly _lazy: boolean;
61 |
62 | private readonly _outsidePointerHandler: OutsidePointerHandler | null;
63 |
64 | private _captured: boolean;
65 | private _pointerId: number;
66 |
67 | public constructor(element: HTMLElement, downCallback: BooleanPointerHandler | null = null, moveCallback: VoidPointerHandler | null = null, upCallback: VoidPointerHandler | null = null, lazy: boolean = true, outsidePointerHandler: OutsidePointerHandler | null = null) {
68 | this._documentTarget = (document.documentElement || document.body);
69 | this._element = element;
70 |
71 | this._downCallback = downCallback;
72 | this._moveCallback = moveCallback;
73 | this._upCallback = upCallback;
74 |
75 | this._boundExtraTouchStart = null;
76 |
77 | // If the element using the PointerHandler wants to track pointer/touch position,
78 | // it is important not to forget to add the touch-action CSS property, with a value
79 | // like none (or similiar, depending on the case) to prevent pointercancel/touchcancel
80 | // events from happening when the touch actually starts outside the element, and
81 | // sometimes, even when the touch starts inside it.
82 | // https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/pointercancel_event
83 | if ("onpointerdown" in element) {
84 | this._documentDownEvent = "pointerdown";
85 | this._documentMoveEvent = "pointermove";
86 | this._documentUpEvent = "pointerup";
87 | this._documentCancelEvent = "pointercancel";
88 |
89 | this._boundDocumentDown = this.pointerDown.bind(this);
90 | this._boundDocumentUp = this.pointerUp.bind(this);
91 | this._boundDocumentMove = this.pointerMove.bind(this);
92 |
93 | // Firefox mobile and a few iOS devices cause a buggy behavior if trying to handle
94 | // pointerdown/move/up but not touchstart/end/cancel...
95 | if ("ontouchstart" in element)
96 | this._boundExtraTouchStart = this.extraTouchStart.bind(this);
97 | } else if ("ontouchstart" in element) {
98 | this._documentDownEvent = "touchstart";
99 | this._documentMoveEvent = "touchmove";
100 | this._documentUpEvent = "touchend";
101 | this._documentCancelEvent = "touchcancel";
102 |
103 | this._boundDocumentDown = this.touchStart.bind(this);
104 | this._boundDocumentUp = this.touchEnd.bind(this);
105 | this._boundDocumentMove = this.touchMove.bind(this);
106 | } else {
107 | this._documentDownEvent = "mousedown";
108 | this._documentMoveEvent = "mousemove";
109 | this._documentUpEvent = "mouseup";
110 | this._documentCancelEvent = null;
111 |
112 | this._boundDocumentDown = this.mouseDown.bind(this);
113 | this._boundDocumentUp = this.mouseUp.bind(this);
114 | this._boundDocumentMove = this.mouseMove.bind(this);
115 | }
116 |
117 | this._lazy = lazy;
118 |
119 | this._outsidePointerHandler = outsidePointerHandler;
120 |
121 | if (this._boundExtraTouchStart)
122 | element.addEventListener("touchstart", this._boundExtraTouchStart);
123 | element.addEventListener(this._documentDownEvent, this._boundDocumentDown);
124 |
125 | if (!lazy)
126 | this.addSecondaryHandlers();
127 |
128 | this._captured = false;
129 | this._pointerId = -1;
130 | }
131 |
132 | public destroy(): void {
133 | if (this._element) {
134 | if (this._boundExtraTouchStart)
135 | this._element.removeEventListener("touchstart", this._boundExtraTouchStart);
136 | if (this._boundDocumentDown)
137 | this._element.removeEventListener(this._documentDownEvent, this._boundDocumentDown);
138 | }
139 |
140 | this.removeSecondaryHandlers();
141 |
142 | this.mouseUp({} as MouseEvent);
143 |
144 | zeroObject(this);
145 | }
146 |
147 | public get captured(): boolean {
148 | return this._captured;
149 | }
150 |
151 | private addSecondaryHandlers(): void {
152 | if (!this._documentTarget)
153 | return;
154 |
155 | // Firefox mobile and a few iOS devices treat a few events on the root element as passive by default
156 | // https://stackoverflow.com/a/49853392/3569421
157 | // https://stackoverflow.com/a/57076149/3569421
158 | this._documentTarget.addEventListener(this._documentMoveEvent, this._boundDocumentMove, { capture: true, passive: false });
159 | this._documentTarget.addEventListener(this._documentUpEvent, this._boundDocumentUp, true);
160 | if (this._documentCancelEvent)
161 | this._documentTarget.addEventListener(this._documentCancelEvent, this._boundDocumentUp, true);
162 | }
163 |
164 | private removeSecondaryHandlers(): void {
165 | if (!this._documentTarget)
166 | return;
167 |
168 | if (this._boundDocumentUp) {
169 | this._documentTarget.removeEventListener(this._documentUpEvent, this._boundDocumentUp, true);
170 | if (this._documentCancelEvent)
171 | this._documentTarget.removeEventListener(this._documentCancelEvent, this._boundDocumentUp, true);
172 | }
173 |
174 | if (this._boundDocumentMove)
175 | this._documentTarget.removeEventListener(this._documentMoveEvent, this._boundDocumentMove, true);
176 | }
177 |
178 | private extraTouchStart(e: TouchEvent): boolean | undefined {
179 | if (e.target === this._element || (this._outsidePointerHandler && this._outsidePointerHandler(e)))
180 | return cancelEvent(e);
181 | }
182 |
183 | private pointerDown(e: PointerEvent): boolean | undefined {
184 | if (this._pointerId >= 0 && e.pointerType !== "mouse")
185 | return cancelEvent(e);
186 |
187 | const ret = this.mouseDown(e);
188 |
189 | if (this._captured)
190 | this._pointerId = e.pointerId;
191 |
192 | return ret;
193 | }
194 |
195 | private pointerMove(e: PointerEvent): boolean | undefined {
196 | if (!this._captured || e.pointerId !== this._pointerId)
197 | return;
198 |
199 | return this.mouseMove(e);
200 | }
201 |
202 | private pointerUp(e: PointerEvent): boolean | undefined {
203 | if (!this._captured || e.pointerId !== this._pointerId)
204 | return;
205 |
206 | this._pointerId = -1;
207 |
208 | return this.mouseUp(e);
209 | }
210 |
211 | private touchStart(e: TouchEvent): boolean | undefined {
212 | if (e.touches.length > 1)
213 | return;
214 |
215 | if (this._pointerId >= 0)
216 | this.touchEnd(e);
217 |
218 | this._pointerId = 1;
219 |
220 | (e as any).clientX = e.touches[0].clientX;
221 | (e as any).clientY = e.touches[0].clientY;
222 |
223 | let ret = this.mouseDown(e as any);
224 | if (ret === undefined)
225 | this._pointerId = -1;
226 |
227 | return ret;
228 | }
229 |
230 | private touchMove(e: TouchEvent): boolean | undefined {
231 | if (!this._captured || e.touches.length > 1)
232 | return;
233 |
234 | (e as any).clientX = e.touches[0].clientX;
235 | (e as any).clientY = e.touches[0].clientY;
236 |
237 | return this.mouseMove(e as any);
238 | }
239 |
240 | private touchEnd(e: TouchEvent): boolean | undefined {
241 | if (!this._captured || this._pointerId < 0)
242 | return;
243 |
244 | this._pointerId = -1;
245 |
246 | return this.mouseUp(e as any);
247 | }
248 |
249 | private mouseDown(e: MouseEvent): boolean | undefined {
250 | this.mouseUp(e);
251 |
252 | if (e.button || (e.target && e.target !== this._element && (!this._outsidePointerHandler || !this._outsidePointerHandler(e))))
253 | return;
254 |
255 | if (this._downCallback && !this._downCallback(e))
256 | return cancelEvent(e);
257 |
258 | this._captured = true;
259 |
260 | if (("setPointerCapture" in this._element) && (e as any).pointerId >= 0)
261 | this._element.setPointerCapture((e as any).pointerId);
262 |
263 | if (this._lazy)
264 | this.addSecondaryHandlers();
265 |
266 | return cancelEvent(e);
267 | }
268 |
269 | private mouseMove(e: MouseEvent): boolean | undefined {
270 | if (!this._captured)
271 | return;
272 |
273 | if (this._moveCallback)
274 | this._moveCallback(e);
275 |
276 | return cancelEvent(e);
277 | }
278 |
279 | private mouseUp(e: MouseEvent): boolean | undefined {
280 | if (!this._captured)
281 | return;
282 |
283 | this._captured = false;
284 | if (this._lazy)
285 | this.removeSecondaryHandlers();
286 |
287 | if (this._upCallback)
288 | this._upCallback(e);
289 |
290 | return cancelEvent(e);
291 | }
292 | }
293 |
--------------------------------------------------------------------------------
/tscdbg.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | CALL tsc
4 |
5 | MOVE assets\js\graphicalFilterEditor.js assets\js\graphicalFilterEditor.min.js
6 |
--------------------------------------------------------------------------------
/tscdbg.sh:
--------------------------------------------------------------------------------
1 | tsc
2 |
3 | mv assets/js/graphicalFilterEditor.js assets/js/graphicalFilterEditor.min.js
4 |
--------------------------------------------------------------------------------
/tscmin.bat:
--------------------------------------------------------------------------------
1 | @ECHO OFF
2 |
3 | CALL tsc
4 |
5 | REM ECMASCRIPT_2015 and ES6 are the same thing...
6 | REM https://www.typescriptlang.org/docs/handbook/compiler-options.html (--target section)
7 | REM https://github.com/google/closure-compiler/wiki/Flags-and-Options
8 |
9 | REM We are using ECMASCRIPT_2015 (without async/await support) in favor of a few old Android devices... (here and at tsconfig.json)
10 | java -jar D:\Tools\closure-compiler.jar --js assets\js\graphicalFilterEditor.js --js_output_file assets\js\graphicalFilterEditor.min.js --language_in ECMASCRIPT_2015 --language_out ECMASCRIPT_2015 --strict_mode_input --compilation_level SIMPLE
11 | REM java -jar D:\Tools\closure-compiler.jar --js assets\js\graphicalFilterEditor.js --js_output_file assets\js\graphicalFilterEditor.min.js --language_in ECMASCRIPT_2017 --language_out ECMASCRIPT_2017 --strict_mode_input --compilation_level SIMPLE
12 |
13 | DEL assets\js\graphicalFilterEditor.js
14 |
--------------------------------------------------------------------------------
/tscmin.sh:
--------------------------------------------------------------------------------
1 | tsc
2 |
3 | # ECMASCRIPT_2015 and ES6 are the same thing...
4 | # https://www.typescriptlang.org/docs/handbook/compiler-options.html (--target section)
5 | # https://github.com/google/closure-compiler/wiki/Flags-and-Options
6 |
7 | # We are using ECMASCRIPT_2015 (without async/await support) in favor of a few old Android devices... (here and at tsconfig.json)
8 | java -jar /d/Tools/closure-compiler.jar --js assets/js/graphicalFilterEditor.js --js_output_file assets/js/graphicalFilterEditor.min.js --language_in ECMASCRIPT_2015 --language_out ECMASCRIPT_2015 --strict_mode_input --compilation_level SIMPLE
9 | # java -jar /d/Tools/closure-compiler.jar --js assets/js/graphicalFilterEditor.js --js_output_file assets/js/graphicalFilterEditor.min.js --language_in ECMASCRIPT_2017 --language_out ECMASCRIPT_2017 --strict_mode_input --compilation_level SIMPLE
10 |
11 | rm assets/js/graphicalFilterEditor.js
12 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "system",
4 | "target": "ES2015",
5 | "lib": [ "ES2017", "DOM" ],
6 | "sourceMap": false,
7 | "outFile": "assets/js/graphicalFilterEditor.js",
8 | "emitBOM": true,
9 | "strict": true,
10 | "removeComments": true
11 | },
12 | "files": [
13 | "lib/lib.ts",
14 | "scripts/main.ts",
15 | "scripts/ui/pointerHandler.ts",
16 | "scripts/graphicalFilterEditor/graphicalFilterEditorStrings.ts",
17 | "scripts/graphicalFilterEditor/graphicalFilterEditor.ts",
18 | "scripts/graphicalFilterEditor/graphicalFilterEditorRenderer.ts",
19 | "scripts/graphicalFilterEditor/graphicalFilterEditorCanvasRenderer.ts",
20 | "scripts/graphicalFilterEditor/graphicalFilterEditorSVGRenderer.ts",
21 | "scripts/graphicalFilterEditor/graphicalFilterEditorControl.ts",
22 | "scripts/gl/program.ts",
23 | "scripts/analyzer/analyzer.ts",
24 | "scripts/analyzer/plainAnalyzer.ts",
25 | "scripts/analyzer/soundParticlesAnalyzer.ts",
26 | "scripts/analyzer/waveletAnalyzer.ts"
27 | ]
28 | }
29 |
--------------------------------------------------------------------------------