;
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 |
--------------------------------------------------------------------------------
/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 | Load your own file:
103 |
104 |
105 | or
106 |
107 |
108 | Enter the address of a file: * Not all URL's may work, and may fail without warning :(
109 |
110 |
111 | or
112 |
113 |
116 |
117 |
Play
118 |
Stop
119 | |
120 |
121 | Play file via streaming *
122 | Load entire file into memory before playing **
123 |
124 |
125 |
Process file offline and download the filtered version (WAVE)! **
126 |
127 |
Filter Length:
128 | 64
129 | 128
130 | 256
131 | 512
132 | 1024
133 | 2048
134 | 4096
135 | 8192
136 | Analyzer:
137 | None
138 | Sound Particles
139 | Frequency analyzer
140 | Haar wavelet analyzer
141 |
142 |
143 | This work was presented at CICTEM 2013 , in Argentina! :D
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------