Skip to content
Permalink
Branch: master
Find file Copy path
Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
359 lines (314 sloc) 11.1 KB
// 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}> Display
slider2:glow=1<0,1,1{Off,On}> Glow
slider3:axes=1<0,3,1{None,Left/Right,Left/Right + Decibels,Mid/Side,Mid/Side + Decibels,Left/Right + Mid/Side + Decibels}> Indicators
slider4:labels=1<0,1,1{Off,On}> Labels
slider5:freeze=0<0,1,1{Off,On}> 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
// Colour handling for scope display
//
rgba = 50000; // stores R,G,B,A for history points
rgba[0] = 0.0;
rgba[1] = 1.0;
rgba[2] = 0.0;
rgba[3] = 0.75;
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 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 == 0 ?
(
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(rgba[0], rgba[1], rgba[2], rgba[3], 127);
// Draw point onto graph
view == 0 ? (gfx_setpixel(rgba[0], rgba[1], rgba[2]));
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);
);
);
@sample
// If the display is frozen, don't calculate any new
// samples and don't mess with the history buffers.
freeze == 0 ?
(
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();
You can’t perform that action at this time.