├── .gitignore ├── .vscode ├── c_cpp_properties.json └── settings.json ├── LICENSE.txt ├── Makefile ├── README.md ├── assets ├── audio │ └── midnightride.mp3 ├── css │ └── graphicalFilterEditor.css ├── doc │ ├── paper45_ptbr.pdf │ └── presentation.pdf ├── favicons │ ├── apple-icon-114x114.png │ ├── apple-icon-120x120.png │ ├── apple-icon-144x144.png │ ├── apple-icon-152x152.png │ ├── apple-icon-180x180.png │ ├── apple-icon-57x57.png │ ├── apple-icon-60x60.png │ ├── apple-icon-72x72.png │ ├── apple-icon-76x76.png │ ├── apple-icon-precomposed.png │ ├── apple-icon.png │ ├── browserconfig.xml │ ├── favicon-144x144.png │ ├── favicon-150x150.png │ ├── favicon-152x152.png │ ├── favicon-16x16.png │ ├── favicon-192x192.png │ ├── favicon-256x256.png │ ├── favicon-310x310.png │ ├── favicon-32x32.png │ ├── favicon-36x36.png │ ├── favicon-48x48.png │ ├── favicon-512x512.png │ ├── favicon-70x70.png │ ├── favicon-72x72.png │ ├── favicon-96x96.png │ ├── favicon.ico │ ├── favicon.png │ └── manifest.webmanifest ├── images │ ├── cictem.png │ └── loader.gif └── js │ ├── graphicalFilterEditor.min.js │ └── waveExporterWorker.js ├── buildwasm.bat ├── index.html ├── lib ├── lib-nowasm.js ├── lib.js ├── lib.js.mem ├── lib.ts ├── lib.wasm └── src │ ├── common.h │ ├── fft4g.c │ ├── fft4g.h │ ├── fft4gf.c │ ├── graphicalFilterEditor.c │ ├── plainAnalyzer.c │ └── waveletAnalyzer.c ├── scripts ├── analyzer │ ├── analyzer.ts │ ├── plainAnalyzer.ts │ ├── soundParticlesAnalyzer.ts │ └── waveletAnalyzer.ts ├── gl │ └── program.ts ├── graphicalFilterEditor │ ├── graphicalFilterEditor.ts │ ├── graphicalFilterEditorCanvasRenderer.ts │ ├── graphicalFilterEditorControl.ts │ ├── graphicalFilterEditorRenderer.ts │ ├── graphicalFilterEditorSVGRenderer.ts │ └── graphicalFilterEditorStrings.ts ├── main.ts └── ui │ └── pointerHandler.ts ├── tscdbg.bat ├── tscdbg.sh ├── tscmin.bat ├── tscmin.sh └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | Misc/ 2 | rebuild.bat 3 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Win32", 5 | "includePath": [ 6 | "${workspaceFolder}/c", 7 | "C:\\emsdk\\upstream\\emscripten\\system\\include" 8 | ], 9 | "defines": [ 10 | "_DEBUG", 11 | "UNICODE", 12 | "_UNICODE" 13 | ], 14 | "windowsSdkVersion": "10.0.17134.0", 15 | "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2017/Community/VC/Tools/MSVC/14.15.26726/bin/Hostx64/x64/cl.exe", 16 | "cStandard": "c11", 17 | "cppStandard": "c++17", 18 | "intelliSenseMode": "msvc-x64" 19 | } 20 | ], 21 | "version": 4 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.associations": { 3 | "*.ejs": "html", 4 | "cmath": "c", 5 | "cstdlib": "c" 6 | } 7 | } -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2012-2020 Carlos Rafael Gimenes das Neves 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | LIB_DIR=lib 2 | SRC_DIR=$(LIB_DIR)/src 3 | 4 | SRCS=\ 5 | $(SRC_DIR)/fft4g.c \ 6 | $(SRC_DIR)/fft4gf.c \ 7 | $(SRC_DIR)/graphicalFilterEditor.c \ 8 | $(SRC_DIR)/plainAnalyzer.c \ 9 | $(SRC_DIR)/waveletAnalyzer.c 10 | 11 | all: $(LIB_DIR)/lib.js 12 | 13 | # General options: https://emscripten.org/docs/tools_reference/emcc.html 14 | # -s flags: https://github.com/emscripten-core/emscripten/blob/master/src/settings.js 15 | # 16 | # Extra: 17 | # https://emscripten.org/docs/porting/Debugging.html 18 | # https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap 19 | # -s EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap'] 20 | # 21 | # Debugging: 22 | # https://emscripten.org/docs/porting/Debugging.html#debugging-debug-information-g 23 | # https://emscripten.org/docs/tools_reference/emcc.html 24 | # -s ASSERTIONS=2 25 | # -s STACK_OVERFLOW_CHECK=2 26 | # -g4 27 | # --source-map-base '/GraphicalFilterEditor/' 28 | # 29 | # As of August 2020, WASM=2 does not work properly, even if loading the correct file 30 | # manually during runtime... That's why I'm compiling it twice... 31 | # 32 | # 8388608 bytes (2097152 stack + 6291456 heap) is enough to hold even the largest 33 | # structure, ImageInfo, which has a total of 4719244 bytes. 34 | 35 | $(LIB_DIR)/lib.js: $(SRCS) 36 | emcc \ 37 | -I$(SRC_DIR) \ 38 | -s WASM=0 \ 39 | -s PRECISE_F32=0 \ 40 | -s DYNAMIC_EXECUTION=0 \ 41 | -s EXPORTED_FUNCTIONS='["_allocBuffer", "_freeBuffer", "_fftSizeOf", "_fftInit", "_fftAlloc", "_fftFree", "_fftChangeN", "_fftSizeOff", "_fftInitf", "_fftAllocf", "_fftFreef", "_fftChangeNf", "_fft", "_ffti", "_fftf", "_fftif", "_graphicalFilterEditorAlloc", "_graphicalFilterEditorGetFilterKernelBuffer", "_graphicalFilterEditorGetChannelCurve", "_graphicalFilterEditorGetActualChannelCurve", "_graphicalFilterEditorGetVisibleFrequencies", "_graphicalFilterEditorGetEquivalentZones", "_graphicalFilterEditorGetEquivalentZonesFrequencyCount", "_graphicalFilterEditorUpdateFilter", "_graphicalFilterEditorUpdateActualChannelCurve", "_graphicalFilterEditorChangeFilterLength", "_graphicalFilterEditorFree", "_plainAnalyzer", "_waveletAnalyzer"]' \ 42 | -s EXTRA_EXPORTED_RUNTIME_METHODS='["stackSave", "stackAlloc", "stackRestore"]' \ 43 | -s ALLOW_MEMORY_GROWTH=0 \ 44 | -s INITIAL_MEMORY=3145728 \ 45 | -s MAXIMUM_MEMORY=3145728 \ 46 | -s TOTAL_STACK=1048576 \ 47 | -s SUPPORT_LONGJMP=0 \ 48 | -s MINIMAL_RUNTIME=0 \ 49 | -s ASSERTIONS=0 \ 50 | -s STACK_OVERFLOW_CHECK=0 \ 51 | -s EXPORT_NAME=CLib \ 52 | -s MODULARIZE=1 \ 53 | -s ENVIRONMENT='web,webview' \ 54 | -Os \ 55 | -DNDEBUG \ 56 | -o $@ \ 57 | $(SRCS) 58 | 59 | move $(LIB_DIR)\lib.js $(LIB_DIR)\lib-nowasm.js 60 | 61 | emcc \ 62 | -I$(SRC_DIR) \ 63 | -s WASM=1 \ 64 | -s DYNAMIC_EXECUTION=0 \ 65 | -s EXPORTED_FUNCTIONS='["_allocBuffer", "_freeBuffer", "_fftSizeOf", "_fftInit", "_fftAlloc", "_fftFree", "_fftChangeN", "_fftSizeOff", "_fftInitf", "_fftAllocf", "_fftFreef", "_fftChangeNf", "_fft", "_ffti", "_fftf", "_fftif", "_graphicalFilterEditorAlloc", "_graphicalFilterEditorGetFilterKernelBuffer", "_graphicalFilterEditorGetChannelCurve", "_graphicalFilterEditorGetActualChannelCurve", "_graphicalFilterEditorGetVisibleFrequencies", "_graphicalFilterEditorGetEquivalentZones", "_graphicalFilterEditorGetEquivalentZonesFrequencyCount", "_graphicalFilterEditorUpdateFilter", "_graphicalFilterEditorUpdateActualChannelCurve", "_graphicalFilterEditorChangeFilterLength", "_graphicalFilterEditorFree", "_plainAnalyzer", "_waveletAnalyzer"]' \ 66 | -s EXTRA_EXPORTED_RUNTIME_METHODS='["stackSave", "stackAlloc", "stackRestore"]' \ 67 | -s ALLOW_MEMORY_GROWTH=0 \ 68 | -s INITIAL_MEMORY=3145728 \ 69 | -s MAXIMUM_MEMORY=3145728 \ 70 | -s TOTAL_STACK=1048576 \ 71 | -s SUPPORT_LONGJMP=0 \ 72 | -s MINIMAL_RUNTIME=0 \ 73 | -s ASSERTIONS=0 \ 74 | -s STACK_OVERFLOW_CHECK=0 \ 75 | -s EXPORT_NAME=CLib \ 76 | -s MODULARIZE=1 \ 77 | -s ENVIRONMENT='web,webview' \ 78 | -Os \ 79 | -DNDEBUG \ 80 | -o $@ \ 81 | $(SRCS) 82 | 83 | cacls $(LIB_DIR)\lib.js /E /P Todos:R 84 | cacls $(LIB_DIR)\lib.js.mem /E /P Todos:R 85 | cacls $(LIB_DIR)\lib.wasm /E /P Todos:R 86 | cacls $(LIB_DIR)\lib-nowasm.js /E /P Todos:R 87 | 88 | # Windows 89 | clean: 90 | del $(LIB_DIR)\lib.js 91 | del $(LIB_DIR)\lib.js.mem 92 | del $(LIB_DIR)\lib.wasm 93 | del $(LIB_DIR)\lib-nowasm.js 94 | 95 | rebuild: 96 | $(MAKE) clean 97 | $(MAKE) all 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GraphicalFilterEditor 2 | ===================== 3 | 4 | This is a test for a JavaScript graphical filter editor, based on my old C++ graphic equalizer. 5 | 6 | With this editor you can graphically edit an equalizer filter and apply it to songs in real time. You can also apply the filter to an entire song and download a WAVE file with the filtered song. Check out the live sample: 7 | 8 | https://carlosrafaelgn.github.io/GraphicalFilterEditor/ 9 | 10 | The code in index.html can be used as a demo on how to load and generate files during runtime in client-side JavaScript. 11 | 12 | This project uses [Web Audio API](http://www.w3.org/TR/webaudio/), [File API](http://www.w3.org/TR/FileAPI/) and [Web Worker API](http://www.w3.org/TR/workers/) and requires a [compliant browser](http://caniuse.com/audio-api) to run properly. In [Firefox 23 and 24](https://wiki.mozilla.org/WebAudio_API_Rollout_Status), Web Audio API must be enabled using about:config. 13 | 14 | If running this sample locally, Chrome must be started with the command-line option --allow-file-access-from-files otherwise you will not be able to load any files! 15 | 16 | Run `tscdbg` or `tscmin` to compile the TypeScript files (requires tsc and closure-compiler), or run `make rebuild` to compile the C code (requires make and Emscripten). All build scripts target the Windows platform, and a few changes should be made to make them run under Linux or Mac. 17 | 18 | This project is licensed under the [MIT License](https://github.com/carlosrafaelgn/GraphicalFilterEditor/blob/master/LICENSE.txt). 19 | 20 | --- 21 | 22 | Notice for the FFT library 23 | 24 | Reference: 25 | 26 | * Masatake MORI, Makoto NATORI, Tatuo TORII: Suchikeisan, Iwanamikouzajyouhoukagaku18, Iwanami, 1982 (Japanese) 27 | 28 | * Henri J. Nussbaumer: Fast Fourier Transform and Convolution Algorithms, Springer Verlag, 1982 29 | 30 | * C. S. Burrus, Notes on the FFT (with large FFT paper list) http://www-dsp.rice.edu/research/fft/fftnote.asc 31 | 32 | Copyright(C) 1996-2001 Takuya OOURA 33 | 34 | email: ooura@mmm.t.u-tokyo.ac.jp 35 | 36 | download: http://momonga.t.u-tokyo.ac.jp/~ooura/fft.html 37 | 38 | You may use, copy, modify this code for any purpose and without fee. You may distribute this ORIGINAL package. 39 | 40 | http://www.kurims.kyoto-u.ac.jp/~ooura/fft.html 41 | 42 | https://www.jjj.de/fft/fftpage.html 43 | -------------------------------------------------------------------------------- /assets/audio/midnightride.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/audio/midnightride.mp3 -------------------------------------------------------------------------------- /assets/css/graphicalFilterEditor.css: -------------------------------------------------------------------------------- 1 | .GE, .GECV, .GELBL, .GEBTN, .GEMNU, .GEMNUIT, .GEMNUSEP, .GEMNULBL { 2 | -webkit-user-select: none; 3 | -moz-user-select: none; 4 | -ms-user-select: none; 5 | user-select: none; 6 | /* Let's prevent the creation of extra render layers by removing these */ 7 | /*-moz-backface-visibility: hidden; 8 | -webkit-backface-visibility: hidden; 9 | backface-visibility: hidden;*/ 10 | -webkit-tap-highlight-color: rgba(0,0,0,0); 11 | } 12 | 13 | .GECV { 14 | touch-action: none; 15 | } 16 | 17 | .GE { 18 | vertical-align: top; 19 | font-style: normal; 20 | font-family: inherit; 21 | display: inline-block; 22 | background-color: #1c1c1c; 23 | color: #fff; 24 | position: relative; 25 | cursor: default; 26 | text-align: left; 27 | } 28 | 29 | .GECV { 30 | display: block; 31 | margin: 0; 32 | cursor: crosshair; 33 | } 34 | 35 | .GELBL { 36 | display: inline-block; 37 | padding: 0.33333333333333333333333333333333em; 38 | } 39 | 40 | .GEBTN { 41 | padding: 0.33333333333333333333333333333333em; 42 | float: right; 43 | width: 1.5em; 44 | text-align: center; 45 | box-sizing: content-box; 46 | } 47 | 48 | .GEMNU { 49 | background-color: #1c1c1c; 50 | color: #fff; 51 | position: absolute; 52 | right: 0; 53 | padding: 0.33333333333333333333333333333333em; 54 | } 55 | 56 | .GEMNUH { 57 | display: inline-block; 58 | vertical-align: top; 59 | min-width: 180px; 60 | max-width: 225px; 61 | box-sizing: content-box; 62 | } 63 | 64 | .GEMNUIT, .GEMNULBL { 65 | padding: 0 0.33333333333333333333333333333333em; 66 | line-height: 2em; 67 | } 68 | 69 | .GEMNUSEP { 70 | margin: 0.33333333333333333333333333333333em 0; 71 | border-top: 0.08333333333333333333333333333333em solid #fff; 72 | height: 0; 73 | box-sizing: content-box; 74 | } 75 | 76 | .GEMNUSEPH { 77 | margin-left: 0.33333333333333333333333333333333em; 78 | padding-left: 0.33333333333333333333333333333333em; 79 | border-left: 0.08333333333333333333333333333333em solid #fff; 80 | } 81 | 82 | .GEMNULBL { 83 | color: #0f2; 84 | } 85 | 86 | .GECLK { 87 | cursor: pointer; 88 | } 89 | 90 | .GECLK:hover { 91 | color: #1c1c1c; 92 | background-color: #fff; 93 | } 94 | 95 | .GEMNU.GEEQ .GEFILTER { 96 | display: none; 97 | } 98 | 99 | .GEMNU.GEEQ > .GEMNUSEPH { 100 | margin-left: 0; 101 | padding-left: 0; 102 | border-left: 0 none transparent; 103 | } 104 | -------------------------------------------------------------------------------- /assets/doc/paper45_ptbr.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/doc/paper45_ptbr.pdf -------------------------------------------------------------------------------- /assets/doc/presentation.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/doc/presentation.pdf -------------------------------------------------------------------------------- /assets/favicons/apple-icon-114x114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-114x114.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-120x120.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-144x144.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-152x152.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-180x180.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-57x57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-57x57.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-60x60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-60x60.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-72x72.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-76x76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-76x76.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon-precomposed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon-precomposed.png -------------------------------------------------------------------------------- /assets/favicons/apple-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/apple-icon.png -------------------------------------------------------------------------------- /assets/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | #222222 -------------------------------------------------------------------------------- /assets/favicons/favicon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-144x144.png -------------------------------------------------------------------------------- /assets/favicons/favicon-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-150x150.png -------------------------------------------------------------------------------- /assets/favicons/favicon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-152x152.png -------------------------------------------------------------------------------- /assets/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /assets/favicons/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-192x192.png -------------------------------------------------------------------------------- /assets/favicons/favicon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-256x256.png -------------------------------------------------------------------------------- /assets/favicons/favicon-310x310.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-310x310.png -------------------------------------------------------------------------------- /assets/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /assets/favicons/favicon-36x36.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-36x36.png -------------------------------------------------------------------------------- /assets/favicons/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-48x48.png -------------------------------------------------------------------------------- /assets/favicons/favicon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-512x512.png -------------------------------------------------------------------------------- /assets/favicons/favicon-70x70.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-70x70.png -------------------------------------------------------------------------------- /assets/favicons/favicon-72x72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-72x72.png -------------------------------------------------------------------------------- /assets/favicons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon-96x96.png -------------------------------------------------------------------------------- /assets/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon.ico -------------------------------------------------------------------------------- /assets/favicons/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/favicons/favicon.png -------------------------------------------------------------------------------- /assets/favicons/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Graphical Filter Editor", 3 | "short_name": "Filter", 4 | "start_url": "/GraphicalFilterEditor/", 5 | "dir": "ltr", 6 | "lang": "en-US", 7 | "orientation": "portrait-primary", 8 | "display": "standalone", 9 | "background_color": "#222222", 10 | "theme_color": "#222222", 11 | "description": "A test for porting an old C++ graphic equalizer to JavaScript + HTML5 using Web Audio API", 12 | "icons": [ 13 | { 14 | "src": "favicon-36x36.png", 15 | "sizes": "36x36", 16 | "type": "image/png", 17 | "purpose": "any maskable" 18 | }, 19 | { 20 | "src": "favicon-48x48.png", 21 | "sizes": "48x48", 22 | "type": "image/png", 23 | "purpose": "any maskable" 24 | }, 25 | { 26 | "src": "favicon-72x72.png", 27 | "sizes": "72x72", 28 | "type": "image/png", 29 | "purpose": "any maskable" 30 | }, 31 | { 32 | "src": "favicon-96x96.png", 33 | "sizes": "96x96", 34 | "type": "image/png", 35 | "purpose": "any maskable" 36 | }, 37 | { 38 | "src": "favicon-144x144.png", 39 | "sizes": "144x144", 40 | "type": "image/png", 41 | "purpose": "any maskable" 42 | }, 43 | { 44 | "src": "favicon-152x152.png", 45 | "sizes": "152x152", 46 | "type": "image/png", 47 | "purpose": "any maskable" 48 | }, 49 | { 50 | "src": "favicon-192x192.png", 51 | "sizes": "192x192", 52 | "type": "image/png", 53 | "purpose": "any maskable" 54 | }, 55 | { 56 | "src": "favicon-256x256.png", 57 | "sizes": "256x256", 58 | "type": "image/png", 59 | "purpose": "any maskable" 60 | }, 61 | { 62 | "src": "favicon-512x512.png", 63 | "sizes": "512x512", 64 | "type": "image/png", 65 | "purpose": "any maskable" 66 | } 67 | ] 68 | } 69 | -------------------------------------------------------------------------------- /assets/images/cictem.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/images/cictem.png -------------------------------------------------------------------------------- /assets/images/loader.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosrafaelgn/GraphicalFilterEditor/cc2b5052c771a59258c891992b2f5275948bdc3d/assets/images/loader.gif -------------------------------------------------------------------------------- /assets/js/waveExporterWorker.js: -------------------------------------------------------------------------------- 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 | "use strict"; 28 | 29 | function uint32ToArray(array, startIndex, value) { 30 | array[startIndex] = value; 31 | array[startIndex + 1] = (value >>> 8); 32 | array[startIndex + 2] = (value >>> 16); 33 | array[startIndex + 3] = (value >>> 24); 34 | } 35 | 36 | function uint16ToArray(array, startIndex, value) { 37 | array[startIndex] = value; 38 | array[startIndex + 1] = (value >>> 8); 39 | } 40 | 41 | onmessage = function (e) { 42 | let i, d, s, hdrData, dstData, blockAlign, byteLength, 43 | left = new Float32Array(e.data.left), 44 | right = e.data.right, 45 | length = e.data.length, channelCount; 46 | if (right) { 47 | channelCount = 2; 48 | right = new Float32Array(right); 49 | dstData = new Uint8Array(length << 2); 50 | for (i = length - 1, d = i << 2; i >= 0; i--, d -= 4) { 51 | //Interleave left and right channels before saving the WAVE file 52 | //convert the output into an array of 16 bit samples (little endian) 53 | s = (left[i] * 0x7FFF) | 0; 54 | if (s > 0x7FFF) s = 0x7FFF; 55 | else if (s < -0x8000) s = -0x8000; 56 | uint16ToArray(dstData, d, s); 57 | 58 | s = (right[i] * 0x7FFF) | 0; 59 | if (s > 0x7FFF) s = 0x7FFF; 60 | else if (s < -0x8000) s = -0x8000; 61 | uint16ToArray(dstData, d + 2, s); 62 | } 63 | } else { 64 | channelCount = 1; 65 | dstData = new Uint8Array(length << 1); 66 | for (i = length - 1, d = i << 1; i >= 0; i--, d -= 2) { 67 | //Convert the output into an array of 16 bit samples (little endian) 68 | s = (left[i] * 0x7FFF) | 0; 69 | if (s > 0x7FFF) s = 0x7FFF; 70 | else if (s < -0x8000) s = -0x8000; 71 | uint16ToArray(dstData, d, s); 72 | } 73 | } 74 | //Generate the WAVE file header 75 | blockAlign = channelCount << 1; //16 bit samples (2 bytes per channel) 76 | byteLength = length * blockAlign; 77 | hdrData = new Uint8Array(44); 78 | uint32ToArray(hdrData, 0, 0x46464952); //"RIFF" 79 | uint32ToArray(hdrData, 4, byteLength + 36); //chunk size 80 | uint32ToArray(hdrData, 8, 0x45564157); //"WAVE" 81 | uint32ToArray(hdrData, 12, 0x20746d66); //"fmt " 82 | uint32ToArray(hdrData, 16, 16); //PCM header size 83 | uint16ToArray(hdrData, 20, 1); //audio format (PCM = 1) 84 | uint16ToArray(hdrData, 22, channelCount); 85 | uint32ToArray(hdrData, 24, e.data.sampleRate); 86 | uint32ToArray(hdrData, 28, e.data.sampleRate * blockAlign); 87 | uint16ToArray(hdrData, 32, blockAlign); 88 | uint16ToArray(hdrData, 34, 16); //bits per samples 89 | uint32ToArray(hdrData, 36, 0x61746164); //"data" 90 | uint32ToArray(hdrData, 40, byteLength); 91 | postMessage([hdrData.buffer, dstData.buffer], [hdrData.buffer, dstData.buffer]); 92 | return true; 93 | }; 94 | -------------------------------------------------------------------------------- /buildwasm.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | SET LIB_DIR=lib 4 | SET SRC_DIR=%LIB_DIR%\src 5 | 6 | SET SRCS=^ 7 | %SRC_DIR%\fft4g.c ^ 8 | %SRC_DIR%\fft4gf.c ^ 9 | %SRC_DIR%\graphicalFilterEditor.c ^ 10 | %SRC_DIR%\plainAnalyzer.c ^ 11 | %SRC_DIR%\waveletAnalyzer.c 12 | 13 | REM General options: https://emscripten.org/docs/tools_reference/emcc.html 14 | REM -s flags: https://github.com/emscripten-core/emscripten/blob/master/src/settings.js 15 | REM 16 | REM Extra: 17 | REM https://emscripten.org/docs/porting/Debugging.html 18 | REM https://emscripten.org/docs/porting/connecting_cpp_and_javascript/Interacting-with-code.html#interacting-with-code-ccall-cwrap 19 | REM -s EXTRA_EXPORTED_RUNTIME_METHODS=['cwrap'] 20 | REM 21 | REM Debugging: 22 | REM https://emscripten.org/docs/porting/Debugging.html#debugging-debug-information-g 23 | REM https://emscripten.org/docs/tools_reference/emcc.html 24 | REM -s ASSERTIONS=2 25 | REM -s STACK_OVERFLOW_CHECK=2 26 | REM -g4 27 | REM --source-map-base '/GraphicalFilterEditor/' 28 | REM 29 | REM As of August 2020, WASM=2 does not work properly, even if loading the correct file 30 | REM manually during runtime... That's why I'm compiling it twice... 31 | 32 | FOR /l %%X IN (0,1,1) DO ( 33 | MOVE %LIB_DIR%\lib.js %LIB_DIR%\lib-nowasm.js 34 | 35 | CALL emcc ^ 36 | -I%SRC_DIR% ^ 37 | -s WASM=%%X ^ 38 | -s PRECISE_F32=0 ^ 39 | -s DYNAMIC_EXECUTION=0 ^ 40 | -s EXPORTED_FUNCTIONS="['_allocBuffer', '_freeBuffer', '_fftSizeOf', '_fftInit', '_fftAlloc', '_fftFree', '_fftChangeN', '_fftSizeOff', '_fftInitf', '_fftAllocf', '_fftFreef', '_fftChangeNf', '_fft', '_ffti', '_fftf', '_fftif', '_graphicalFilterEditorAlloc', '_graphicalFilterEditorGetFilterKernelBuffer', '_graphicalFilterEditorGetChannelCurve', '_graphicalFilterEditorGetActualChannelCurve', '_graphicalFilterEditorGetVisibleFrequencies', '_graphicalFilterEditorGetEquivalentZones', '_graphicalFilterEditorGetEquivalentZonesFrequencyCount', '_graphicalFilterEditorUpdateFilter', '_graphicalFilterEditorUpdateActualChannelCurve', '_graphicalFilterEditorChangeFilterLength', '_graphicalFilterEditorFree', '_plainAnalyzer', '_waveletAnalyzer']" ^ 41 | -s ALLOW_MEMORY_GROWTH=0 ^ 42 | -s INITIAL_MEMORY=327680 ^ 43 | -s MAXIMUM_MEMORY=327680 ^ 44 | -s TOTAL_STACK=65536 ^ 45 | -s SUPPORT_LONGJMP=0 ^ 46 | -s MINIMAL_RUNTIME=0 ^ 47 | -s ASSERTIONS=0 ^ 48 | -s STACK_OVERFLOW_CHECK=0 ^ 49 | -s EXPORT_NAME=CLib ^ 50 | -s MODULARIZE=1 ^ 51 | -s ENVIRONMENT='web,webview' ^ 52 | -Os ^ 53 | -DNDEBUG ^ 54 | -o %LIB_DIR%\lib.js ^ 55 | %SRCS% 56 | ) 57 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | Graphical Filter Editor 49 | 50 | 51 | 52 | 98 | 99 | 100 |
101 |
102 | 103 |
104 |
105 | or 106 |
107 |
108 | * Not all URL's may work, and may fail without warning :( 109 |
110 |
111 | or 112 |
113 |
114 | 115 |
116 |
117 | 118 | 119 | | 120 | 124 |
125 | Processing... 126 |
127 |
142 |
143 |

This work was presented at CICTEM 2013, in Argentina! :D CICTEM 2013

144 |

Download links for the documentation:
145 | Presentation
146 | Portuguese paper 147 |

148 |
149 |

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.

151 |

Check out the source for more information! This test uses Web Audio API, File API and Web Worker API and requires a compliant browser to run properly. In Firefox 23 and 24, Web Audio API must be enabled using about:config.

152 |

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 = ` 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | `; 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 | --------------------------------------------------------------------------------