Signal Viewer
EEG signal visualization in the browser.
A WebGL-powered viewer for EEG signal data. Renders multi-channel neural signals as real-time waveforms using instanced geometry. Built as a research prototype while studying BCI fundamentals.
// project.spec
// stack
Signal Viewer is a browser-based tool for visualizing EEG signal data in real time. It renders multi-channel neural signals as waveforms using WebGL instanced geometry, with the goal of making BCI research more accessible by lowering the barrier to signal inspection.
Motivation
Reading research papers about BCI often means staring at static plots of EEG data — a single epoch, a single electrode, a single averaged waveform. This hides the temporal dynamics that make EEG interesting. You can't see the burst. You can't see the coherence between channels. You can't see the moment when a subject's attention shifts and the entire signal field reorganizes.
Existing EEG viewers are either proprietary, tied to specific hardware, or too slow to handle real-time data at the channel counts that modern research uses. Signal Viewer is my attempt to fix this — an open-source, browser-based viewer that renders EEG signals the way they should be rendered: as a live, multi-channel, interactive visualization.
Technical architecture
The core insight is that EEG signal rendering is a graphics problem, not a data visualization problem. Each electrode channel produces a time series of voltage values. To render this as a waveform, you need to draw a line through the sample points — a line strip in GPU terms. With 32 channels and a 10-second buffer at 256 Hz, that's 81,920 vertices that need to be updated and rendered at display refresh rate.
The architecture:
-
Data ingestion — EEG data arrives via WebSocket as a stream of sample arrays (one value per electrode per time step). The CPU validates, timestamps, and writes samples into a GPU buffer.
-
Vertex shader — Each electrode channel is a line strip. The vertex shader maps (time, amplitude) → (x, y) screen coordinates. Channel separation is handled by adding a per-channel y-offset based on electrode index.
-
Fragment shader — Color is mapped to amplitude: near-zero values are the base color, positive deflections shift toward gold, negative deflections shift toward a cooler tone. This makes event-related potentials visually salient without manual thresholding.
-
Interaction — Zoom and pan are handled by adjusting the view matrix in the vertex shader uniform buffer. No geometry is regenerated on interaction — it's all done in the shader.
Performance characteristics
At 32 channels × 256 Hz × 10 seconds, the vertex buffer is ~82k vertices. The vertex shader does a straightforward linear mapping, and the fragment shader is lightweight. On integrated graphics (Apple M1), the render loop runs at a consistent 60fps with the GPU at approximately 12% utilization. The CPU sits at around 3% — it's only doing WebSocket deserialization and buffer updates.
The bottleneck is the WebSocket data rate, not the rendering. At 32 channels × 256 Hz × 4 bytes per float, the incoming data rate is ~32 KB/s. This is trivial for any modern network connection but requires careful buffer management on the receiving end to avoid accumulating latency.
What's next
The next features are:
- Event markers — vertical lines indicating stimulus onset, with configurable labels and colors
- Coherence view — a second rendering mode that shows phase coherence between electrode pairs as a connectivity matrix
- Export — snapshot-to-PNG and record-to-EDF for sharing findings with collaborators
The long-term goal is integration with real-time BCI pipelines — feeding the rendered signal into a classification model that runs in the same WebGPU context and outputs predictions without ever leaving the browser.
more projects