SpikeGLX (.bin + .meta)
SpikeGLX records Neuropixels probes and NI-DAQ boards, writing one .bin data file and one .meta text file per stream. OpenReadout returns a stream as one trace (probe channels in µV, analog channels in V), with probe, headstage and electrode-site metadata.
Derived from the SpikeGLX authors’ public metadata documentation (Metadata help pages, imro-table help, ProbeTable) and checked against public streams (Neuropixels 1.0 AP/LF, 2.0, NI-DAQ analog and digital) with Neo (BSD-3) as a reference reader. See docs/provenance/spikeglx.md.
Crate: openreadout-spikeglx. A SpikeGLX run writes one .bin/.meta pair per stream (<run>_g0_t0.imec0.ap.bin, .imec0.lf.bin, .nidq.bin, .obx0.obx.bin). OpenReadout opens one stream — pass the .bin or the .meta (stream_paths) — as one trace with one sweep.
Detection
No signature in the .bin (it is headerless). A .bin whose same-name .meta contains nSavedChans= and typeThis= (or …SampRate=) is definite (looks_like_meta); so is such a .meta itself. A .bin without its .meta is not claimed; opening it directly fails as unsupported (exit 6) with a hint to keep the .meta beside it.
.meta (Meta, parse_meta, entries, get, number, counts)
One key=value per line (CRLF), split at the first =. Keys starting with ~ hold lists written (header)(entry)(entry)… (list_elements). Keys we use:
| key | use |
|---|---|
typeThis |
StreamKind: imec → Imec, nidq → Nidq, obx → Obx |
nSavedChans |
channels per sample |
imSampRate / niSampRate / obSampRate |
sample_rate_hz (calibrated, may be non-integer) |
firstSample |
start_s = firstSample / rate (sample index within the run) |
acqApLfSy / acqMnMaXaDw / acqXaDwSy |
acquired channel layout by kind |
snsSaveChanSubset |
acquired indices that were saved, all or inclusive ranges a:b,c (parse_subset) |
~snsChanMap |
channel names (AP0;0:0 → AP0) |
~imroTbl, imDatPrb_type |
per-channel gains for selectable-gain probes |
imAiRangeMax, imMaxInt, imChan0apGain, imChan0lfGain |
imec scaling |
niAiRangeMax, niMaxInt, niMNGain, niMAGain |
NI scaling |
obAiRangeMax, obMaxInt |
OneBox scaling |
fileSizeBytes, fileTimeSecs |
declared size and duration (check) |
appVersion, fileCreateTime, imDatPrb_pn, imDatPrb_sn, imDatHs_sn, imDatBs_sn, fileName |
trace extra: app_version (also format_version), created_at, probe_part_number, probe_serial_number, headstage_serial_number, basestation_serial_number, original_file_name |
Channels (SavedChannel, saved_channels)
Saved channel i is acquired channel subset[i] (acquired); its ChannelKind follows from the acquired layout: imec Ap, Lf, Sync (acqApLfSy); NI Mn, Ma, Xa, Xd (acqMnMaXaDw); OneBox Xa, Xd, Sync. prefix() gives AP, LF, SY, MN, MA, XA, XD; is_bits() is true for Sync and Xd. name comes from ~snsChanMap, else prefix + running number.
Scaling
The documentation’s rule is V = i × Vmax / Imax / gain.
- imec AP/LF (µV):
scale = (imAiRangeMax / Imax) × (1 / gain) × 10⁶(imec_scale).gain: for selectable-gain probes (has_selectable_gain: types 0, 1020, 1030, 1100, 1120–1123, 1200, 1300) the~imroTblentry of the channel (field 3 for AP, field 4 for LF; LF entry index = acquired − number of AP channels); elseimChan0apGain/imChan0lfGain; else, for type 1110, the imro header gains; elsefixed_gain(80 for types 21/24, 100 otherwise).Imax=imMaxInt, or when absentdefault_max_int(512 for 10-bit NP 1.0, 8192 for types 21/24, 2048 otherwise — the last from the ProbeTable,Source::Inferred). - NI MN/MA/XA (V):
scale = (1 / gain) × (niAiRangeMax / niMaxInt)(ni_scale),gain=niMNGain,niMAGain, or 1;niMaxIntdefaults to 32768. - OneBox XA (V):
obAiRangeMax / obMaxInt(32768 by default). - SY and XD words: raw (scale 1, no unit, channel
extra.bits).
Channel extra: kind, acquired_index, gain, bits, and the electrode site below.
Probe sites (SiteMap, ChannelSite, site_map)
From the SpikeGLX metadata help (imec section). ~snsGeomMap (SpikeGLX 20230202 and later; preferred) or the older ~snsShankMap (imec, and NI streams with MN channels) hold one entry per saved neural channel (imec AP and LF, NI MN), in saved order, after a header:
| map | header | entry | our fields |
|---|---|---|---|
~snsGeomMap |
(part-number,shanks,shank spacing µm,shank width µm) |
(s:x:z:u) |
trace extra.probe_geometry {source, part_number, shanks, shank_pitch_um, shank_width_um}; channel extra.shank, x_um (electrode centre from the shank’s left edge), z_um (from the centre of the bottom-most row), used |
~snsShankMap |
(shanks,columns,rows) (grid maxima) |
(s:c:r:u) |
trace extra.probe_geometry {source, shanks, columns, rows}; channel extra.shank, col, row, used |
Shank 0 is left-most with the tips pointing down; each shank has its own (x, z) origin. used is the map’s u-flag (drawn in the viewers, included in spatial averages). A map whose entry count differs from the saved neural channels, or with an unparsable entry, a shank beyond the header’s count or a flag other than 0/1, is ignored: no site is reported rather than a misaligned one. Grid indices are not converted to µm (that needs per-probe electrode pitches); LF streams written before the geometry map carry no map. Checked against probeinterface’s read_spikeglx (MIT), which places contacts from its own probe tables and the imro table: shank and z equal, x equal up to one constant per probe (probeinterface measures x from the left-most column plus shank × pitch).
Samples
The .bin is headerless int16 little-endian, interleaved by sample (all saved channels of sample 0, then sample 1, …) in saved order. Samples = file size / (2 × nSavedChans), so a stubbed or cut .bin still opens; a remainder → partial_sample. One read_trace returns at most 4 Mi samples per channel and MAX_READ_VALUES (64 Mi) values in all — about 174 k samples of a 385-channel probe stream; callers page with first_sample.
Run directories (sessions)
A SpikeGLX run directory (<run>_g0/, with the probe streams one level down in subdirectories named …_imec<N>, normally <run>_g0_imec0/; other subdirectories are ignored) is one recording (session_files, open_session) when it holds .bin files with their .meta and nothing else but SESSION_COMPANIONS (.meta, .txt, .log, .json, .md, .csv). Every stream — imec AP and LF bands, NI-DAQ, OneBox — is a trace of its own, in path order (probe subdirectories sort before the NI stream of the same run), each keeping its own rate and sample clock: streams are not aligned on their sync pulses. Several triggers or gates in one directory are separate traces. Nothing is resampled or realigned; every trace, channel and table names its file in extra.source_file / extra.source_files, info --view structure lists the member files and check checks each one (findings prefixed with the file). The composition itself is openreadout_core::session (SessionDataset).
check finding codes
truncated (the .bin is shorter than fileSizeBytes), bad_rate (errors); size_mismatch, partial_sample, no_declared_size, no_scaling, no_samples (warnings). A .meta whose saved-channel list disagrees with nSavedChans or the acquired layout fails to open as corrupt (exit 4). Most corpus .bin files are stubs cut from longer recordings, and check reports them as truncated.
Observed corpus values
| stream | probe / device | channels | rate | Imax, gain | notes |
|---|---|---|---|---|---|
sglx-noise4sam-g0-t0.imec0.ap |
NP 1.0 (type 0) | 384 AP + SY | 30000 | 512 (absent), 500 | 2019 metadata, stub |
sglx-noise4sam-g0-t0.imec0.lf |
NP 1.0 | 384 LF + SY | 2500 | 512, 125 | subset 384:768 |
sglx-noise4sam-g0-t0.nidq |
NI | 8 XA + 1 XD | 11574.074074 | 32768 (absent) | range 2 V |
sglx-test-20210920-0-g0-t0.imec0.ap |
NP 2.0 (type 24) | 384 + SY | 30000 | 8192, 80 | no imChan0apGain |
sglx-np2-with-sync.imec0.ap |
NP2013 | 384 + SY | 30000 | 2048, 100 | range 0.62 V |
sglx-np2-subset-with-sync.imec0.ap |
NP2013 | 120 + SY | 30000 | 2048, 100 | subset 0:35,72:95,192:227,264:287,384 |
sglx-np2-no-sync.exported.imec0.ap |
NP2013 | 384, no SY | 30000 | 2048, 100 | snsApLfSy=384,0,0 |
sglx-5-19-2022-ci1-g0-t0.imec0.ap/lf |
NP 1.0 | 384 + SY | 30000.061088 / 2500.005 | 512, 500/250 | calibrated rates |
zenodo5899237 Pt03.imec0.lf |
NP 1.0 (type 0), human intraoperative | 384 LF + SY | 2500 | 512, 250 | SpikeGLX 20201024, 1.1 GB, 1.47 M samples, ~snsShankMap absent from the LF meta |
zenodo21908762 sub-001_ses-001_g0_t0.imec0.ap |
NP2010 (type 24, four shanks) | 384 + SY | 30000 | 8192, 80 | SpikeGLX 20230411, ~snsGeomMap and ~snsShankMap both present, all sites on shank 2 |
Vocabulary (every public identifier in openreadout-spikeglx must appear here)
| identifier | meaning |
|---|---|
session_files, open_session, SESSION_COMPANIONS |
run directories (sessions) |
SpikeGlxReader, SpikeGlxDataset, FORMAT_ID, open, meta, stream_kind, stream_paths, looks_like_meta, MAX_META_LEN |
entry points |
Meta, entries, get, number, counts, parse_meta, list_elements, parse_subset |
.meta parsing |
StreamKind { Imec, Nidq, Obx }, name |
stream kinds |
ChannelKind { Ap, Lf, Sync, Mn, Ma, Xa, Xd }, prefix, is_bits |
channel kinds |
SavedChannel, index, acquired, kind, gain, scale, unit, saved_channels |
saved channels |
has_selectable_gain, fixed_gain, default_max_int, imec_scale, ni_scale |
scaling rules |
MAX_READ_VALUES |
read size limit |
SiteMap, source, part_number, shanks, shank_pitch_um, shank_width_um, columns, rows, sites, site_map |
probe site maps |
ChannelSite, shank, x_um, z_um, col, row, used |
one channel’s electrode site |
Performance / robustness merge (2026-09-23)
Both stream detection and open read at most 16 MiB plus one byte of the .meta sidecar, rejecting oversize sidecars without loading them whole.
Byte-source integration
The existing layouts are read through Input/Fs/SourceFile, including directory
sessions and companions where applicable. Path, memory and callback namespaces use
the same parser and scaling. See byte sources.
How this reader was derived, file by file: provenance log.