Oscilloscope

by MusicBar, visualizer

A CRT oscilloscope draws the live sound wave as a glowing phosphor trace over a faint graticule, with afterglow and scanlines.

20 downloads Download file, 4.4 KB
How it installs, its SHA-256 and its code

Click Apply: MusicBar downloads it, checks its SHA-256 and puts it on the taskbar, or just puts it on if it already has it. Needs MusicBar 1.8 or newer.

SHA-256 6efcdbc47da34d3d3a07a9a85b1a10f11fa00c2f7c6cac6a8524c67f50b26186

//! name: Oscilloscope
//! author: MusicBar
//! kind: visualizer
//! description: A CRT oscilloscope draws the live sound wave as a glowing phosphor trace over a faint graticule, with bloom, afterglow and scanlines.

void layer(inout float3 acc, inout float cover, float3 rgb, float a)
{
    a = saturate(a);
    acc = rgb * a + acc * (1.0 - a);
    cover = a + cover * (1.0 - a);
}

// Distance from p to the segment from a to b.
float seg(float2 p, float2 a, float2 b)
{
    float2 pa = p - a;
    float2 ba = b - a;
    return length(pa - ba * saturate(dot(pa, ba) / dot(ba, ba)));
}

float4 main(float2 uv : TEXCOORD) : COLOR
{
    float2 px = float2(uv.x * size.x, rise(uv) * size.y);   // pixels: along the bar, up from the screen edge
    float H = size.y;
    float mid = 0.5 * H;
    float amp = 0.36 * H * (0.85 + 0.2 * level);

    // The wave a few pixels either side; the trace is the polyline through those points, so steep
    // edges stay as thick as flat ones.
    float st = max(2.0, 0.06 * H);
    float w0 = wave((px.x - 2.0 * st) / size.x);
    float w1 = wave((px.x - st) / size.x);
    float w2 = wave(px.x / size.x);
    float w3 = wave((px.x + st) / size.x);
    float w4 = wave((px.x + 2.0 * st) / size.x);
    float2 p0 = float2(px.x - 2.0 * st, mid + amp * w0);
    float2 p1 = float2(px.x - st, mid + amp * w1);
    float2 p2 = float2(px.x, mid + amp * w2);
    float2 p3 = float2(px.x + st, mid + amp * w3);
    float2 p4 = float2(px.x + 2.0 * st, mid + amp * w4);
    float d = min(min(seg(px, p0, p1), seg(px, p1, p2)), min(seg(px, p2, p3), seg(px, p3, p4)));

    // The beam moves slower where the trace is flat, so it burns brighter there.
    float slope = (w3 - w1) * amp / (2.0 * st);
    float flat = lerp(0.45, 1.0, 1.0 / sqrt(1.0 + slope * slope));

    // Afterglow: the wave a moment ago (the same wave a little further along) and a soft blurred copy.
    float wg = wave((px.x + 0.035 * size.x) / size.x);
    float dGhost = abs(px.y - (mid + amp * wg * 0.92)) / sqrt(1.0 + slope * slope);
    float wSoft = (w0 + w1 + w2 + w3 + w4) * 0.2;
    float dSoft = abs(px.y - (mid + amp * wSoft));

    // CRT: every other line a touch darker, a faint flicker, and the phosphor still glowing hardest
    // just behind the sweeping beam.
    float scan = 0.78 + 0.22 * step(0.5, frac(floor(px.y) * 0.5));
    float flicker = 0.97 + 0.03 * sin(time * 47.0);
    float sx = frac(time * 0.45) * size.x;
    float behind = sx - px.x;
    behind = behind < 0.0 ? behind + size.x : behind;
    float crt = scan * flicker * (0.78 + 0.3 * exp(-behind / (0.18 * size.x)));

    float3 acc = float3(0, 0, 0);
    float cover = 0;

    // The graticule: square divisions, a brighter center axis with fine ticks, fading toward the ends.
    float div = 0.25 * H;
    float gx = abs(frac(px.x / div + 0.5) - 0.5) * div;
    float gy = abs(frac((px.y - mid) / div + 0.5) - 0.5) * div;
    float grid = smoothstep(0.9, 0.2, min(gx, gy)) * step(abs(px.y - mid), 0.5 * H - 1.0);
    float tickP = max(3.0, div * 0.2);
    float tx = abs(frac(px.x / tickP + 0.5) - 0.5) * tickP;
    float axis = smoothstep(0.9, 0.2, abs(px.y - mid)) + smoothstep(0.7, 0.1, tx) * step(abs(px.y - mid), 0.06 * H);
    float ends = smoothstep(0.0, 0.08, uv.x) * smoothstep(1.0, 0.92, uv.x);
    layer(acc, cover, colorB.rgb, (grid * 0.16 + saturate(axis) * 0.26) * crt * (0.4 + 0.6 * ends));

    // Afterglow and bloom under the trace.
    layer(acc, cover, lerp(colorB.rgb, colorA.rgb, 0.4), exp(-dSoft / (0.12 * H)) * 0.22 * crt);
    layer(acc, cover, colorA.rgb, exp(-dGhost / (0.04 * H + 0.6)) * 0.28 * crt);
    float bloomR = 0.08 * H * (1.0 + 0.6 * beat);
    layer(acc, cover, colorA.rgb, (exp(-d / bloomR) * 0.55 + exp(-d / (0.25 * H)) * 0.18) * flat * crt * (0.8 + 0.3 * level));

    // The trace: phosphor color, white-hot in the core where the beam lingers.
    float core = smoothstep(0.05 * H + 0.9, 0.3, d);
    layer(acc, cover, lerp(colorA.rgb, colorC.rgb, 0.3 * flat), core * crt);
    layer(acc, cover, colorC.rgb, smoothstep(0.8, 0.1, d) * flat * flat * crt);

    // The electron beam itself, a bright spot sweeping along the trace.
    float2 spot = float2(sx, mid + amp * wave(sx / size.x));
    float ds = length(px - spot);
    layer(acc, cover, colorA.rgb, exp(-ds / (0.1 * H)) * 0.45);
    layer(acc, cover, lerp(colorC.rgb, float3(1, 1, 1), 0.5), exp(-ds / (0.03 * H + 0.6)) * 0.9);

    return paint(acc / max(cover, 0.001), cover);
}