File size: 3,168 Bytes
2b18d49
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
/**
 * WaveformVisualizer - handles canvas waveform animation for user and agent audio
 */
export class WaveformVisualizer {
    constructor(userCanvas, agentCanvas) {
        this.userCanvas = userCanvas;
        this.agentCanvas = agentCanvas;
        this.userCtx = userCanvas.getContext('2d');
        this.agentCtx = agentCanvas.getContext('2d');

        this.animationId = null;
        this.isUserSpeaking = false;
        this.isAgentSpeaking = false;

        this._initCanvases();
    }

    _initCanvases() {
        this.userCanvas.width = 350;
        this.userCanvas.height = 60;
        this.agentCanvas.width = 350;
        this.agentCanvas.height = 60;

        this._clear(this.userCtx, this.userCanvas);
        this._clear(this.agentCtx, this.agentCanvas);
    }

    _clear(ctx, canvas) {
        ctx.fillStyle = '#000';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
    }

    _draw(ctx, canvas, isActive, color, waveformData) {
        this._clear(ctx, canvas);
        if (!isActive) return;

        ctx.strokeStyle = color;
        ctx.lineWidth = 2;
        ctx.beginPath();

        const centerY = canvas.height / 2;

        if (waveformData && waveformData.length > 0) {
            const sliceWidth = canvas.width / waveformData.length;
            let x = 0;
            for (let i = 0; i < waveformData.length; i++) {
                const v = waveformData[i] / 128.0;
                const y = (v * canvas.height) / 2;
                if (i === 0) ctx.moveTo(x, y);
                else ctx.lineTo(x, y);
                x += sliceWidth;
            }
        } else {
            const frequency = Date.now() * 0.01;
            for (let x = 0; x < canvas.width; x++) {
                const y = centerY
                    + Math.sin((x * 0.02) + frequency) * 12
                    + Math.sin((x * 0.05) + frequency * 0.7) * 6
                    + Math.sin((x * 0.03) + frequency * 1.3) * 4
                    + (Math.random() - 0.5) * 3;
                if (x === 0) ctx.moveTo(x, y);
                else ctx.lineTo(x, y);
            }
        }

        ctx.stroke();
    }

    /**
     * Start animation loop.
     * @param {Function} getUserWaveformData - returns Uint8Array or null
     * @param {Function} getAgentWaveformData - returns Uint8Array or null
     */
    start(getUserWaveformData, getAgentWaveformData) {
        const animate = () => {
            this._draw(this.userCtx, this.userCanvas, this.isUserSpeaking, '#2196f3', getUserWaveformData());
            this._draw(this.agentCtx, this.agentCanvas, this.isAgentSpeaking, '#9c27b0', getAgentWaveformData());
            this.animationId = requestAnimationFrame(animate);
        };
        animate();
    }

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

    startUserSpeaking() {
        this.isUserSpeaking = true;
    }

    stopUserSpeaking() {
        this.isUserSpeaking = false;
    }

    startAgentSpeaking() {
        this.isAgentSpeaking = true;
    }

    stopAgentSpeaking() {
        this.isAgentSpeaking = false;
    }
}