Skip to content
Permalink
master
Switch branches/tags
Go to file
 
 
Cannot retrieve contributors at this time
// Phase Scope
//
// This is also commonly known as Goniometer, Vector Scope or
// Lissajous display. It visualizes the relation of amplitude
// and phase in a signal, i.e. "the stereo field".
//
// author: chokehold
// url: https://github.com/chkhld/jsfx/
// tags: utility metering visualization panorama stereo phase vector scope goniometer lissajous
//
desc: Phase Scope
in_pin:left input
in_pin:right input
out_pin:none
options:no_meter
slider1:view=0<0,2,1{Dots,Lines,Rays}>Visualization
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 minimize 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 visualizations
//
// 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
// visualization on top of what's already there, i.e.
// indicators and un-blurred visualization.
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 centered 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();