File size: 2,977 Bytes
843a4b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * Canvas visualizer: a scrolling level meter with vertical markers at each
 * detected onset, plus a recent-waveform trace. Purely presentational; it
 * reads values pushed from the Mic level callback and onset events.
 */
export class Scope {
    constructor(canvas) {
        this.canvas = canvas;
        this.ctx = canvas.getContext("2d");
        this.level = new Float32Array(600); // ring buffer of recent levels
        this.head = 0;
        this.onsets = []; // {age} markers scrolling left
        this._raf = null;
        this._peak = 0.02;
    }

    pushLevel(level) {
        this.level[this.head] = level;
        this.head = (this.head + 1) % this.level.length;
        if (level > this._peak) this._peak = level;
        else this._peak = this._peak * 0.999 + level * 0.001;
    }

    markOnset() {
        // a marker positioned at the current write head
        this.onsets.push({ pos: this.head });
        if (this.onsets.length > 64) this.onsets.shift();
    }

    start() {
        const draw = () => {
            this._draw();
            this._raf = requestAnimationFrame(draw);
        };
        draw();
    }

    stop() {
        if (this._raf) cancelAnimationFrame(this._raf);
        this._raf = null;
    }

    _draw() {
        const { ctx, canvas } = this;
        const dpr = window.devicePixelRatio || 1;
        const w = canvas.clientWidth, h = canvas.clientHeight;
        if (canvas.width !== w * dpr || canvas.height !== h * dpr) {
            canvas.width = w * dpr; canvas.height = h * dpr;
        }
        ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
        const css = getComputedStyle(canvas);
        ctx.clearRect(0, 0, w, h);

        const n = this.level.length;
        const scale = 1 / (this._peak * 1.4 + 1e-6);
        const mid = h / 2;

        // level fill
        ctx.beginPath();
        ctx.moveTo(0, mid);
        for (let i = 0; i < n; i++) {
            const idx = (this.head + i) % n;
            const x = (i / n) * w;
            const v = Math.min(1, this.level[idx] * scale);
            ctx.lineTo(x, mid - v * (h * 0.45));
        }
        for (let i = n - 1; i >= 0; i--) {
            const idx = (this.head + i) % n;
            const x = (i / n) * w;
            const v = Math.min(1, this.level[idx] * scale);
            ctx.lineTo(x, mid + v * (h * 0.45));
        }
        ctx.closePath();
        ctx.fillStyle = css.getPropertyValue("--scope-fill") || "rgba(120,160,255,0.5)";
        ctx.fill();

        // onset markers (their stored head pos maps to a position in the ring)
        ctx.strokeStyle = css.getPropertyValue("--scope-mark") || "rgba(255,90,120,0.9)";
        ctx.lineWidth = 2;
        for (const o of this.onsets) {
            let rel = (o.pos - this.head + n) % n; // distance behind head
            const x = (rel / n) * w;
            ctx.beginPath();
            ctx.moveTo(x, 4);
            ctx.lineTo(x, h - 4);
            ctx.stroke();
        }
    }
}