-
Notifications
You must be signed in to change notification settings - Fork 13
Expand file tree
/
Copy pathphase_scope.jsfx
More file actions
437 lines (382 loc) · 13.7 KB
/
phase_scope.jsfx
File metadata and controls
437 lines (382 loc) · 13.7 KB
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
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
desc: Phase Scope
version: 1.8.3
author: chokehold
tags: utility metering visualization panorama stereo phase vector scope goniometer lissajous
link: https://github.com/chkhld/jsfx/
screenshot: https://github.com/chkhld/jsfx/blob/main/assets/screenshots/phase_scope.png
about:
# Phase Scope
This is also commonly known as Goniometer, Vector Scope or
Lissajous display. It visualises the relation of amplitude
and phase in a signal, i.e. "the stereo field".
// ----------------------------------------------------------------------------
in_pin:left input
in_pin:right input
out_pin:none
options:no_meter
slider1:view=0<0,2,1{Dots,Lines,Rays}>Visualisation
slider2:tint=85<0,255,0.01>Colour
slider3:glow=1<0,1,1{Off,On}>Glow
slider4:axes=1<0,3,1{None,Left/Right,Left/Right + Decibels,Mid/Side,Mid/Side + Decibels,Left/Right + Mid/Side + Decibels}>Indicators
slider5:labels=1<0,1,1{Off,On}>Labels
slider6:freeze=2<0,2,1{Off,On,Stop/Pause}>Freeze
@init
// Simple hard clipping
//
function clamp (signal) (max(-1, min(1, signal)));
// Some radian constants to minimise per-sample calculations
quarterPi = $PI * 0.25; // 45°
pi = $PI; // 180°
oneDot5Pi = $PI * 1.5; // 270°
twoPi = $PI * 2.0; // 360°
// At 786 kHz sample rate, 20 ms = 15270 samples,
// so 20000 samples buffer should be sufficient.
//
historyX = 10000; // Stores X coordinates
historyY = 30000; // Stores Y coordinates
numCoords = ceil(srate * 0.05); // # of samples ~20 ms window
lastCoord = numCoords - 1; // To save calculations later
// Things related to the display drawing colours
//
colourRange = 85; // Fader 0-255 --> 255/3 = 85
colourR = 1; // Red colour default amount [0,1]
colourG = 0; // Green colour default amount [0,1]
colourB = 0; // Blue colour default amount [0,1]
colourA = 0.75; // Alpha/transparency amount fixed
//
function setAxisHardColour () (gfx_set(1,1,1,0.4));
function setAxisSoftColour () (gfx_set(1,1,1,0.2));
function setDecibelColour () (gfx_set(1,1,1,0.1));
function setLabelColour () (gfx_set(1,1,1,0.25));
//
// This sets the drawing colour of the display
//
// The UI fader ranges between [0,255] and its range is split into
// three sections of an equal third (=85) each. In every section,
// one colour is totally out and the two remaining colours fade in
// opposite directions, i.e. one gets stronger and one drops off.
//
// Colour amount calculations implemented after FastLED "Spectrum"
// hue chart rather than "Rainbow", merely for its simplicity.
// https://github.com/FastLED/FastLED/wiki/FastLED-HSV-Colors
// https://raw.githubusercontent.com/FastLED/FastLED/gh-pages/images/HSV-spectrum-with-desc.jpg
//
function tintColour () local (fraction)
(
// Positions 0 and 255 are always Red,
// so no calculations necessary here.
tint % 255 == 0 ?
(
colourR = 1; // full
colourG = 0; // out
colourB = 0; // out
);
// First third of the colour range:
// Red falls, Green rises, no blue
(tint > 0) && (tint < 85) ?
(
// How far into this third of 85 is
// the fader, result in range [0,1]
fraction = tint / 85;
colourR = 1 - fraction; // fall
colourG = fraction; // rise
colourB = 0; // out
);
// Second third of the colour range:
// No red, Green falls, Blue rises
(tint >= 85) && (tint < 171) ?
(
// How far into this third of 85 is
// the fader, result in range [0,1]
fraction = (tint - 84) / 85;
colourR = 0; // out
colourG = 1 - fraction; // fall
colourB = fraction; // rise
);
// Last third of the colour range:
// Red rises, no Green, Blue falls
(tint >= 171) && (tint < 255) ?
(
// How far into this third of 85 is
// the fader, result in range [0,1]
fraction = (tint - 170) / 85;
colourR = fraction; // rise
colourG = 0; // out
colourB = 1 - fraction; // fall
);
);
// This will take a regular set of L/R samples and turn them
// into a set of X/Y coordinates for placement on the scope.
//
// The samples are turned into polar coordinates, this gives
// a rotational angle and the distance from the center, from
// which the carthesian X/Y coordinates are then calculated.
//
// Since this effect has no output pins, it's not necessary
// to copy the input samples to variable containers, instead
// the processing will be performed directly on the samples.
//
function calculateCoordinates () local (radius, angle)
(
// Calculate polar coordinate from input samples
radius = sqrt(sqr(spl0) + sqr(spl1));
angle = atan(spl1 / spl0);
// Arctan doesn't like it if the samples are zero or lower,
// so compensate the angle if that happens to be the case.
angle += ((spl0 < 0 && spl1 != 0) * pi) + ((spl0 > 0 && spl1 < 0) * twoPi);
spl0 == 0 ? angle = oneDot5Pi - (spl1 > 0) * pi;
spl1 == 0 ? angle = (spl0 <= 0) * pi;
// To make the scope display upright, add 45° to the angle
angle += quarterPi;
// Convert polar coordinate to cartesian X/Y coordinate
spl0 = (radius * cos(angle)); // X value
spl1 = (radius * sin(angle)); // Y value
);
// Move everything in the history back one step and add the
// latest samples at the end. At this point, the values are
// not the input samples anymore, but their X/Y coordinates.
//
function addToHistory ()
(
// Shift buffers -1 i.e. to the left, loses the last value
memcpy(historyX, historyX+1, numCoords);
memcpy(historyY, historyY+1, numCoords);
// Insert the latest values at the back of the buffers
historyX[lastCoord] = spl0;
historyY[lastCoord] = spl1;
);
// Paints the L/R, M/S, dB guides and labels onto the canvas
//
function drawIndicators () local (top, left, right, bottom, middleH, middleV)
(
// Switch to the main screen buffer
gfx_dest = -1;
// Draw L/R indicator axes
//
(axes == 1 || axes == 2 || axes == 5) ?
(
// Convenience variables for code readability
top = centerY - viewScale + 1;
left = centerX - viewScale + 1;
bottom = centerY + viewScale;
right = centerX + viewScale;
// Draw L/R axis guides
setAxisHardColour();
gfx_line(left, top, right, bottom, 1);
gfx_line(left, bottom, right, top, 1);
// Draw L/R labels
labels == 1 ?
(
setLabelColour();
gfx_y = top - 11;
gfx_x = left - 12;
gfx_drawstr("L");
gfx_x = right + 6;
gfx_drawstr("R");
);
);
// Draw Mid/Side indicator axes
//
(axes == 3 || axes == 4 || axes == 5) ?
(
// Convenience variables for code readability
top = centerY - viewSize;
left = centerX - viewSize;
bottom = centerY + viewSize;
right = centerX + viewSize;
middleH = centerX; // Horizontal = left/right
middleV = centerY; // Vertical = up/down
// Draw M/S axis guides (softer if mixed with L/R axes)
axes == 5 ? setAxisSoftColour() : setAxisHardColour();
gfx_line(left, middleV, right, middleV, 1);
gfx_line(middleH, top, middleH, bottom, 1);
// Draw M/+S/S- labels
labels == 1 ?
(
setLabelColour();
gfx_x = centerX + 8;
gfx_y = centerY + viewSize - 15;
gfx_drawstr("M");
gfx_y = centerY + 7;
gfx_x = centerX - viewSize + 6;
gfx_drawstr("+S");
gfx_x = centerX + viewSize - 22;
gfx_drawstr("S-");
);
);
// Draw dB indicator rings and labels
//
(axes == 2 || axes == 4 || axes == 5) ?
(
// 0 dBfs = 1.0 * viewSize = as far out as it will go
//
gfx_set(1, 1, 1, 0.4);
gfx_circle(centerX, centerY, viewSize, 0, 1);
labels == 1 ?
(
setLabelColour();
gfx_y = centerY + viewScale - 4;
gfx_x = max(centerX - viewScale - 19, 6);
gfx_drawstr("0");
);
//
// -3 dBfs = 0.7079457844
//
gfx_set(1,1,1,0.175);
gfx_circle(centerX, centerY, viewSize * 0.7079457844, 0, 1);
labels == 1 ?
(
//setLabelColour();
gfx_y = centerY + viewScale * 0.7079457844 - 6;
gfx_x = max(centerX - viewScale * 0.7079457844 - 28, 6);
gfx_drawstr("-3");
);
//
// -6 dBfs = 0.5011872336
//
gfx_set(1,1,1,0.15);
gfx_circle(centerX, centerY, viewSize * 0.5011872336, 0, 1);
labels == 1 ?
(
//setLabelColour();
gfx_y = centerY + viewScale * 0.5011872336 - 6;
gfx_x = max(centerX - viewScale * 0.5011872336 - 28, 6);
gfx_drawstr("-6");
);
//
// -12 dBfs = 0.2511886432
//
gfx_set(1,1,1,0.1);
gfx_circle(centerX, centerY, viewSize * 0.2511886432, 0, 1);
labels == 1 ?
(
//setLabelColour();
gfx_y = centerY + viewScale * 0.2511886432 - 5;
gfx_x = max(centerX - viewScale * 0.2511886432 - 35, 6);
gfx_drawstr("-12");
);
//
// -24 dBfs = 0.06309573445
//
gfx_set(1,1,1,0.075);
gfx_circle(centerX, centerY, viewSize * 0.06309573445, 0, 1);
//
// Leaving out -24 dBfs label because too compact
);
);
// Draws the sample history visualisations
//
// Blurring the main screen buffer would not just blur the history
// display, but also the indicators and labels. To avoid this, the
// history display will be drawn onto a separate off-screen buffer
// first, so it doesn't mix with the axes and labels. If no "glow"
// is required, the off-screen buffer can be blitted directly onto
// the main screen buffer. If "glow" activated, blit the unblurred
// history display to yet another off-screen buffer first, blur it
// there, and finally blit it back to the unblurred buffer and mix
// them in alpha, i.e. make both partially transparent so they add
// up to an evenly lit image.
//
function drawHistory () local (dim, coord, offsetX, offsetY, posX, posY, lastX, lastY, thisR, thisG)
(
// Switch to off-screen frame buffer. Don't clear it, so
// the last calculated history is still contained there.
gfx_dest = 127;
gfx_setimgdim(127, gfx_w, gfx_h);
// Dim the existing history that's still in the buffer.
// If "glow" activated, dimming can/should be faster so
// the mix of the two doesn't accumulate too much after
// a few seconds. The image should not dim/decay while
// the display is "frozen".
(freeze != 1 && stopped != 1) ?
(
dim = (glow == 1 ? 0.925 : 0.98);
gfx_muladdrect(0,0,gfx_w,gfx_h, dim, dim, dim);
);
// Iterate through all buffered coordinates in the history
coord = lastCoord;
while
(
// Calculate this point's position in relation to X/Y center
offsetX = (historyX[coord] * viewScale);
offsetY = (historyY[coord] * viewScale);
// Set the drawing position
gfx_x = centerX - offsetX;
gfx_y = centerY - offsetY;
// History colour
gfx_set(colourR, colourG, colourB, colourA, 127);
// Draw point onto graph
view == 0 ? (gfx_setpixel(colourR, colourG, colourB));
view == 1 ? (gfx_line(lastX, lastY, gfx_x, gfx_y, 1));
view == 2 ? (gfx_lineto(centerX, centerY, 1));
// Save current coordinates so can be referred
// to in next iterations when drawing lines.
lastX = gfx_x;
lastY = gfx_y;
// Decrement iterator
(coord -= 1) > -1;
);
// Blit the image from the off-screen buffer back to the
// main buffer, i.e. plugin GUI. If "glow" active, lower
// the blitting opacity as the intensity will accumulate
// when mixed with the later blitted "glow" image buffer.
gfx_dest = -1;
gfx_x = gfx_y = 0;
gfx_a = (glow == 1 ? 0.7 : 1.0);
gfx_blit(127,1,0);
// If the history display should "glow"
glow == 1 ?
(
// Blit currently unblurred image to off-screen buffer
gfx_dest = 100;
gfx_setimgdim(100, gfx_w, gfx_h);
gfx_a = 1.0; // full opacity before blurring
gfx_x = gfx_y = 0;
gfx_blit(127,1,0);
// Blur it a few times, more blur runs = faster decay.
gfx_x = gfx_y = 0;
gfx_blurto(gfx_w, gfx_h);
gfx_x = gfx_y = 0;
gfx_blurto(gfx_w, gfx_h);
gfx_x = gfx_y = 0;
gfx_blurto(gfx_w, gfx_h);
// Switch to the main buffer and blit the now blurred
// visualisation on top of what's already there, i.e.
// indicators and un-blurred visualisation.
gfx_dest = -1;
gfx_a = 0.3;
gfx_x = gfx_y = 0;
gfx_blit(100,1,0);
);
);
@slider
// Recalculate the RGB colours to tint the display
tintColour();
@block
// Triggers a display freeze when project is stopped
// or paused in "Play/Pause" freeze mode. Responsive
// enough if this is switched in the @block section.
stopped = (freeze == 2 && play_state < 1) ? 1 : 0;
@sample
// If the display is frozen, don't calculate any new
// samples and don't mess with the history buffers.
(freeze != 1 && stopped != 1) ?
(
calculateCoordinates();
addToHistory();
);
@gfx 480 480
// Switch to main drawing context
gfx_dest = -1;
gfx_mode = 1; // additive
gfx_clear = 0; // black
// Calculate the side length of a square centred in the window
viewSize = min(gfx_w, gfx_h) / 2;
// The distance a dot may be from center in any
// direction to still appear inside a circle.
viewScale = (viewSize * 0.70710681);
// The absolute center point in the current window
centerX = gfx_w * 0.5;
centerY = gfx_h * 0.5;
drawIndicators();
drawHistory();