Setting Up the EEG Dev Environment
I needed a clean C++ development environment to start building the real-time EEG visualizer. The goal: acquire brain signals from the OpenBCI Cyton board, process them, and render scrolling waveforms on screen — all with minimal dependencies and no package manager bloat.
Here’s what I landed on and how it all fits together.
The stack
After evaluating several options, I settled on four libraries that cover acquisition, rendering, audio, and signal processing — all MIT/BSD/public-domain licensed with zero runtime dependencies beyond macOS system frameworks.
| Library | Purpose | Size | Notes |
|---|---|---|---|
| BrainFlow | EEG acquisition + DSP | ~5MB dylibs | Also handles filtering, FFT, and board abstraction |
| Sokol | GPU rendering (Metal) | 6 headers | Immediate-mode, header-only, perfect for waveforms |
| miniaudio | Audio I/O (CoreAudio) | 1 header | For future sonification work |
| KissFFT | FFT (real-to-complex) | ~20KB | Lightweight alternative to BrainFlow’s built-in FFT |
Currently, the environment runs on macOS ARM64 with Apple Clang 16 and CMake 4.2. I’m using Ninja for faster builds. I will soon test the same environment on Linux.
BrainFlow — built from source
Pre-built binaries exist on the BrainFlow GitHub releases, but building from source gives you the CMake config files that find_package() needs. It takes about two minutes.
git clone https://github.com/brainflow-dev/brainflow.git
cd brainflow && mkdir build && cd build
cmake -DCMAKE_INSTALL_PREFIX=~/dev/brainflow/installed \
-DCMAKE_BUILD_TYPE=Release -G Ninja ..
ninja && ninja install
The install produces libBoardController.dylib, libDataHandler.dylib, and the C++ headers (board_shim.h, data_filter.h, etc.) — everything needed to stream from the Cyton or run a synthetic board for testing.
Sokol — header-only rendering
Sokol is a set of single-file headers for cross-platform graphics. On macOS it uses Metal as the backend. No build step — just copy the headers and create one implementation file compiled as Objective-C:
// sokol_impl.c — compiled once with -x objective-c
#define SOKOL_IMPL
#define SOKOL_METAL
#include "sokol_app.h"
#include "sokol_gfx.h"
#include "sokol_glue.h"
#include "sokol_log.h"
#include "sokol_time.h"
#include "sokol_gl.h"
This gets linked against Metal, MetalKit, Cocoa, and QuartzCore frameworks.
miniaudio and KissFFT
Both follow the same pattern: single-header libraries with a small implementation file.
miniaudio auto-detects CoreAudio on macOS, so no extra linking is needed. KissFFT provides a clean real-to-complex FFT in about 500 lines of C — useful for custom spectral analysis beyond what BrainFlow’s DataFilter offers.
The build system
Everything ties together in a single CMakeLists.txt. Each library becomes a small static lib, and the main app links against all of them plus BrainFlow’s dynamic libraries. A single ninja command builds everything from scratch.
The project tree looks like this:
eeg-viz/
├── CMakeLists.txt
├── lib/
│ ├── sokol/ ← 6 headers + sokol_impl.c
│ ├── miniaudio/ ← miniaudio.h + miniaudio_impl.c
│ └── kissfft/ ← kiss_fft.h/.c + kiss_fftr.h/.c
├── src/
│ └── main.cpp
└── build/
First smoke test
For the initial test, I wrote a minimal app that streams from BrainFlow’s synthetic board (no hardware needed), buffers the last 1000 samples per channel into circular buffers, and draws 8 colored scrolling waveforms using Sokol’s immediate-mode GL layer.
It works. Eight channels of synthetic EEG data scrolling across a dark window at the board’s native sample rate. Switching to the real Cyton hardware later is a two-line change: set the board ID to CYTON_BOARD and point it at the serial port.
What’s next
The immediate next steps are adding Dear ImGui for UI controls (there’s a sokol_imgui.h bridge that makes this trivial), ImPlot for proper scrolling plots and heatmaps, and eventually ONNX Runtime for deploying trained models. But for now, the foundation is solid — clean C++17, no package managers, single build command.