Research should be intuitive.

Signal Studio is the visual workspace for neural data — node-based pipelines, real-time waveforms, sharable workflows. The full power of MNE and EEGLAB, with the interface you've always wished they had.

Open source & free forever macOS · Windows · Linux v0.9 — May 2026

Used in labs & classrooms at

UNIV / 01
UNIV / 02
UNIV / 03
LAB / 04
UNIV / 05
LAB / 06
UNIV / 07
INST / 08
UNIV / 09
LAB / 10
UNIV / 11
INST / 12
UNIV / 01
UNIV / 02
UNIV / 03
LAB / 04
UNIV / 05
LAB / 06
UNIV / 07
INST / 08
UNIV / 09
LAB / 10
UNIV / 11
INST / 12
WHY SIGNAL STUDIO

The interface neuroscience deserves.

Traditional EEG tools were built for the 1990s. Signal Studio is built for how you actually think — visually, iteratively, in conversation with the data.

01 / FOR EVERYONE

Intuitive enough for undergrads.

App Mode hides the graph until you need it. Drop in a file, pick a template, hit run. No scripts, no Matlab license, no rage-quitting.

02 / OPEN

Free, forever. Open source.

No tiers. No paywalls. No "enterprise" footnotes. The whole stack lives on GitHub under MIT — fork it, audit it, ship it.

03 / EXTENSIBLE

A real plugin ecosystem.

Write a Python node in under 20 lines. Publish it to the hub. Your lab's preprocessing pipeline becomes everyone's. Reproducibility, by default.

04 / MODERN

UX from this decade.

Built like a pro audio plugin — macro knobs, animated meters, eight visualization tabs, full theming. EEG analysis that doesn't feel like punishment.

THE WORKSPACE

Every signal, every step, visible.

Connect models, filters, and analyses on an infinite canvas. Inspect every intermediate result. Tweak one knob, see the impact downstream — live.

1Node graph editor
28 visualization tabs
3Macro knobs & meters
4Live waveform inspect
signal studio — preprocessing_pipeline_v3.sigs · 64ch · 1024Hz
APP SCREENSHOT SLOT Drop a 16:10 screenshot of your interface here
01 — NODES

Full control with nodes. App Mode when you don't need it.

Every model, every filter, every transform is a node. Connect them on an infinite canvas where every intermediate state is inspectable. New users start in App Mode — a clean, parameter-only view that hides the graph until you're ready.

  • Infinite zoomable canvas with snap-to-grid
  • Live preview on every edge — no re-run cycles
  • Reroute, group, comment — like a real DAW
  • One-click flip between App Mode and Graph Mode
NODE EDITORscreenshot / video — 4:3
02 — WORKFLOWS

Start from a published pipeline. Share yours back.

Every workflow is a file. Drag one in, run it on your data, fork it, publish your version. The community library covers ERP analysis, ICA decomposition, microstates, sleep staging, source localization — kept up to date by the people doing the research.

  • One-click fork — credits the original author
  • Versioned, like git — diff your changes
  • BIDS-compatible export for paper supplements
  • Private workspaces for unpublished work
WORKFLOW LIBRARYscreenshot / video — 4:3
03 — VISUALIZATION

Eight tabs. Every view you actually need.

Raw waveforms, spectrograms, topographies, ICA components, ERP overlays, source maps, connectivity matrices, statistical reports — each one is a tab away, all driven by the same node graph. Switch themes for poster prep, lab demos, or late-night sessions.

  • GPU-accelerated waveform rendering (60fps on 256ch)
  • Synchronized scrubbing across all views
  • Export-ready figures at any DPI
  • 4 built-in themes — including a high-contrast print mode
VISUALIZATION TABSscreenshot / video — 4:3
FROM THE LIBRARY

Workflows shared by the community.

Every pipeline below was published by a researcher. Open one, run it on your data, then share what you build on top.

Browse all 240+
PREVIEW · 4:3
ERPCLEAN

ERP CORE — minimal preprocessing

Reference workflow for event-related potentials. Re-referencing, baseline correction, ICA, epoching, averaging.

JL @j.luck
1.2k318
PREVIEW · 4:3
SLEEPCLASSIF

Sleep staging — YASA + custom epochs

Automated sleep staging pipeline with custom epoch overrides and quality flagging for human review.

MR @m.riedner
840211
PREVIEW · 4:3
SOURCEMNE

Source localization — eLORETA

Forward model from individual MRI, noise covariance estimation, eLORETA inverse solution with bootstrap.

TB @t.brookes
612184
FOR DEVELOPERS & PI HACKERS

A node is just a Python function.

Wrap any function with @node and it becomes a draggable block — typed inputs, typed outputs, full UI. Publish to the registry with one command. No build system. No yaml hell.

my_bandpass_node.py
PYTHON
# Drop this in ~/.signal-studio/nodes/ — it shows up in the palette.
from signal_studio import node, Signal
from scipy.signal import butter, filtfilt

@node(category="filter", color="#1d4ed8")
def bandpass(
    x: Signal,
    low: float = 1.0,
    high: float = 40.0,
    order: int = 4,
) -> Signal:
    b, a = butter(order, [low, high], fs=x.sfreq, btype="band")
    return x.with_data(filtfilt(b, a, x.data))
THE COMMUNITY

Built with everyone who's tired of writing the same script twice.

An open project belongs to the people who use it. Discord, GitHub, monthly community calls, a public roadmap. Every issue gets read.

0
PUBLIC WORKFLOWS
Forked, remixed, cited
0
COMMUNITY EXTENSIONS
Filters · classifiers · viz
0
DISCORD MEMBERS
Active across timezones
0
FREE & OPEN
MIT licensed, no exceptions
"I spent three years writing the same preprocessing script for every new dataset. Signal Studio exists because nobody else's PhD should look like that."
— FOUNDING NOTE · 2025

Open the workspace.
See your data differently.

Free, open source, runs locally. No account required to download. Onboarding takes about ninety seconds.

/* ============ Prefers reduced motion ============ */ const REDUCE_MOTION = window.matchMedia('(prefers-reduced-motion: reduce)').matches; /* ============ Nav — transparent over hero, opaque once scrolled past ============ */ (function () { const nav = document.querySelector('nav.topnav'); if (!nav) return; const hero = document.querySelector('.hero'); function update() { const heroBottom = hero ? hero.getBoundingClientRect().bottom : 0; if (heroBottom <= 72) { nav.classList.add('scrolled'); } else { nav.classList.remove('scrolled'); } } window.addEventListener('scroll', update, { passive: true }); update(); })(); /* ============ Rotating word (block layout — no width measurement) ============ */ (function () { const words = ['intuitive.', 'accessible.', 'inspiring.', 'explainable.']; const word = document.querySelector('.rotating-list .word'); if (!word) return; if (REDUCE_MOTION) return; let i = 0; setInterval(() => { i = (i + 1) % words.length; word.style.transition = 'transform .55s cubic-bezier(.7,0,.3,1), opacity .55s'; word.style.transform = 'translateY(-105%)'; word.style.opacity = '0'; setTimeout(() => { word.textContent = words[i]; word.style.transition = 'none'; word.style.transform = 'translateY(105%)'; word.style.opacity = '0'; // force reflow void word.offsetWidth; word.style.transition = 'transform .55s cubic-bezier(.7,0,.3,1), opacity .55s'; word.style.transform = 'translateY(0)'; word.style.opacity = '1'; }, 560); }, 2800); })(); /* ============ EEG signal background canvas — optimized ============ */ (function () { const c = document.getElementById('signalCanvas'); // Disabled: EEG canvas runs a parallel 2D loop that doubles compositing cost alongside WebGL fluid if (!c || REDUCE_MOTION || document.getElementById('fluidCanvas')) return; const ctx = c.getContext('2d', { alpha: true }); let w, h, dpr; function resize() { dpr = Math.min(window.devicePixelRatio || 1, 1.5); // cap DPR — was unlimited w = c.clientWidth; h = c.clientHeight; c.width = Math.round(w * dpr); c.height = Math.round(h * dpr); ctx.setTransform(dpr, 0, 0, dpr, 0, 0); } resize(); let resizeTimer; window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(resize, 150); }); const channels = 5; // was 7 const seeds = Array.from({ length: channels }, () => ({ phase: Math.random() * 1000, speed: 0.55 + Math.random() * 0.5, amp: 16 + Math.random() * 20, freq: 0.005 + Math.random() * 0.005, })); // Pause when hero is off-screen (huge win for scrolled-down pages) let visible = true; const heroEl = document.querySelector('.hero'); if (heroEl && 'IntersectionObserver' in window) { new IntersectionObserver(([e]) => { visible = e.isIntersecting; }, { threshold: 0 }) .observe(heroEl); } // Throttle to ~40fps for visible animation (smooth enough, less GPU) let lastT = 0; const FRAME_MS = 25; function draw(t) { if (!visible) { requestAnimationFrame(draw); return; } if (t - lastT < FRAME_MS) { requestAnimationFrame(draw); return; } lastT = t; ctx.clearRect(0, 0, w, h); ctx.lineWidth = 1; const step = 8; // was 4 — half the points per frame for (let ch = 0; ch < channels; ch++) { const s = seeds[ch]; const yBase = (h / (channels + 1)) * (ch + 1); ctx.beginPath(); ctx.strokeStyle = `rgba(29, 78, 216, ${0.18 + ch * 0.02})`; const phase = s.phase + t * 0.0008 * s.speed; for (let x = 0; x <= w; x += step) { const y = yBase + Math.sin(x * s.freq + phase) * s.amp + Math.sin(x * s.freq * 2.7 + phase * 1.6) * s.amp * 0.4; if (x === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); } requestAnimationFrame(draw); } requestAnimationFrame(draw); })(); /* ============ Cursor-follow soft glow in hero ============ */ (function () { if (REDUCE_MOTION) return; const hero = document.querySelector('.hero'); const glow = document.getElementById('heroGlow'); if (!hero || !glow) return; // No glow on coarse pointers (touch) if (window.matchMedia('(hover: none)').matches) { glow.style.display = 'none'; return; } let tX = window.innerWidth / 2, tY = 360; let cX = tX, cY = tY; let animating = false; hero.addEventListener('mousemove', (e) => { const rect = hero.getBoundingClientRect(); tX = e.clientX - rect.left; tY = e.clientY - rect.top; if (!animating) { animating = true; requestAnimationFrame(tick); } }, { passive: true }); hero.addEventListener('mouseleave', () => { tX = hero.clientWidth / 2; tY = 360; }); function tick() { cX += (tX - cX) * 0.10; cY += (tY - cY) * 0.10; glow.style.transform = `translate3d(${cX - 360}px, ${cY - 360}px, 0)`; if (Math.abs(tX - cX) < 0.4 && Math.abs(tY - cY) < 0.4) { animating = false; } else { requestAnimationFrame(tick); } } })(); /* ============ Animated number counters ============ */ (function () { const stats = document.querySelectorAll('[data-counter]'); if (!stats.length) return; const io = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { animate(entry.target); io.unobserve(entry.target); } }); }, { threshold: 0.4 }); stats.forEach(el => io.observe(el)); function animate(el) { const target = el.dataset.counter; const match = target.match(/^([\d.]+)(.*)$/); if (!match) { el.textContent = target; return; } const num = parseFloat(match[1]); const suffix = match[2]; if (REDUCE_MOTION) { el.textContent = target; return; } const dur = 1500; const start = performance.now(); function step(now) { const p = Math.min((now - start) / dur, 1); const eased = 1 - Math.pow(1 - p, 3); const val = num * eased; const display = Number.isInteger(num) ? Math.round(val) : val.toFixed(1); el.textContent = display + suffix; if (p < 1) requestAnimationFrame(step); } requestAnimationFrame(step); } })(); /* ============ Subtle parallax on hero grid (cheap — single transform) ============ */ (function () { if (REDUCE_MOTION) return; const grid = document.querySelector('.hero-grid'); if (!grid) return; let ticking = false; window.addEventListener('scroll', () => { if (ticking) return; ticking = true; requestAnimationFrame(() => { const y = window.scrollY; if (y < window.innerHeight) { grid.style.transform = `translate3d(0, ${y * 0.35}px, 0)`; } ticking = false; }); }, { passive: true }); })(); /* ============ Reveal on scroll ============ */ (function () { const els = document.querySelectorAll('.reveal'); if (REDUCE_MOTION) { els.forEach(el => el.classList.add('in')); return; } const io = new IntersectionObserver((entries) => { entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); } }); }, { threshold: 0.12 }); els.forEach(el => io.observe(el)); })();