Skip to content

Blackrock NSx and NEV

Blackrock Microsystems recording systems write continuous signals as NSx files (.ns1–.ns6) and spikes and events as NEV files. OpenReadout reads one file at a time: an NSx file (NsxDataset) is one trace whose sweeps are its data packets (a new packet follows each pause); a NEV file (NevDataset) is one table with a row per data packet. A directory of these files opens as one recording.

Derived from Blackrock’s public specification LB-0023 Rev 7.00 (“NEV and NSx file formats, FileSpec 3.0”) and LB-0110, checked against public corpus files (NSx 2.1, 2.3, 3.0 and 3.0 PTP; NEV 2.1, 2.3, 3.0). Spec 2.1 NSx details come from Neo (BSD-3), read as documentation and used as a reference reader. See docs/provenance/blackrock.md.

Crate: openreadout-blackrock. All integers are little-endian; char arrays end at the first NUL (bytes after it are ignored).

Detection

Bytes 0–7 (SIGNATURES): NEURALSG (NSx 2.1), NEURALCD (NSx 2.2/2.3), BRSMPGRP (NSx 3.0), NEURALEV (NEV ≤ 2.3), BREVENTS (NEV 3.0) → definite. An extension in EXTENSIONS without a signature → extension only.

NSx

NsxSpec: V21 (NEURALSG), V22 (NEURALCD, covers 2.2 and 2.3), V30 (BRSMPGRP); name(); packet_header_len() (0, 9, 13).

Basic header, spec 2.2+ (BASIC_HEADER_LEN = 314)

offset type our name
0 char[8] signature
8, 9 u8, u8 version (major, minor) → format_version
10 u32 bytes in headers (header_len; = 314 + 66 × channels, else header_length_mismatch)
14 char[16] label (trace name, e.g. 30 kS/s)
30 char[256] comment
286 u32 period: sample interval in 1/30000 s (PERIOD_CLOCK_HZ); rate = 30000 / period
290 u32 time_resolution: timestamp ticks per second (30000, or 10⁹ for PTP, PTP_RESOLUTION)
294 u16[8] Windows SYSTEMTIME, UTC (systemtime) → recorded_at
310 u32 channel count (≤ MAX_CHANNELS)

Channel headers (CC, CC_LEN = 66 each, → NsxChannel)

offset type our name
0 char[2] CC (else bad_channel_header)
2 u16 electrode_id
4 char[16] label (channel name; elec<id> when blank)
20, 21 u8, u8 connector, pin
22, 24 i16, i16 min_digital, max_digital
26, 28 i16, i16 min_analog, max_analog
30 char[16] units (uV, mV, …)
46, 50, 54 u32, u32, u16 highpass: corner (mHz), order, type (0 none, 1 Butterworth)
56, 60, 64 u32, u32, u16 lowpass

Scaling (cc_scaling): scale = (max_analog − min_analog) / (max_digital − min_digital), offset = min_analog − min_digital × scale, value in units. Equal digital limits → raw counts and bad_scaling.

Data packets (NsxPacket: offset, data_offset, timestamp, points)

0x01, timestamp (u32 in 2.2/2.3, u64 in 3.0), u32 number of points, then points × channels int16, point-major (all channels of a point together). Packets follow each other to the end of the file. A header byte other than 0x01 → bad_packet; a packet that declares more points than the file holds → truncated (the whole points present stay readable). Each packet is a sweep (NsxSweep: first_packet, packet_count, sample_count, timestamp); extra.sweep_starts_s = timestamp / resolution, extra.sweep_start_timestamps the raw ticks; check reports segments and, when a packet starts before the previous one ended, clock_reset.

PTP (spec 3.0, resolution 10⁹). When the first packet holds exactly one point, every packet does: the file is a sequence of 13 + 2 × channels-byte packets (ptp_stride, ptp_packet_count). Sweeps are gap-free runs: a gap is a timestamp step that departs from the period (period / 30000 s in ns) by more than half a sample. info checks only the first and last timestamps and assumes one sweep when they agree; check reads every timestamp.

Spec 2.1 (NEURALSG, SPEC21_HEADER_LEN = 32)

offset type use
8 char[16] label
24 u32 period
28 u32 channel count
32 u32 × channels electrode ids

Samples follow directly (no packet header, no timestamp): one sweep of (size − header) / (2 × channels) points; bytes of a partial point → partial_sample. The file carries no analog range. When a .nev with the same name sits next to it, each channel’s NEUEVWAV digitization factor (nV per count) gives scale = factor / 1000 µV (spec21_factor replaces 21516 with 152592.547 nV, the overflow Neo documents for old Cerebus systems) and scaling_source names the NEV; otherwise values are raw counts (no_scaling). Channel names are chan<id> below 129 and ainp<id − 128> from 129 (spec21_label, Neo’s convention).

NsxFile: spec, version, label, comment, period, time_resolution, recorded_at, header_len, channels, packets, sweeps, ptp_stride, ptp_packet_count, scaling_source, file_len, findings; sample_rate_hz(), frame_len(), sample_offset(), max_sweep_len(); built by parse_nsx.

NEV (NevFile, parse_nev)

Basic header (NEV_BASIC_LEN = 336) and extended headers (NEV_EXT_LEN = 32)

offset type our name
0 char[8] signature
8, 9 u8, u8 version (≥ 3 → 64-bit timestamps, wide_timestamps())
10 u16 flags (bit 0: every waveform sample is 16-bit)
12 u32 header_len (= 336 + 32 × extended headers)
16 u32 packet_len (bytes per data packet)
20 u32 time_resolution (ticks per second)
24 u32 sample_resolution (waveform samples per second)
28 u16[8] SYSTEMTIME, UTC → recorded_at
44 char[32] application
76 char[256] comment
332 u32 number of extended headers

Extended headers (ExtHeader: id, offset): NEUEVWAV → WaveformHeader (8 u16 electrode_id, 10 u8 connector, 11 u8 pin, 12 u16 digitization_nv, 14 u16 energy_threshold, 16 i16 high_threshold_uv, 18 i16 low_threshold_uv, 20 u8 sorted_units, 21 u8 sample_bytes (0/1 → 1), 22 u16 spike_width from spec 2.3); NEUEVLBL → labels (8 u16 id, 10 char[16]); DIGLABEL → digital_labels (8 char[16] label, 24 u8 mode: 0 serial, 1 parallel). Other ids are listed by info --view structure only.

Data packets (NevPacket, nev_packet)

Timestamp (u32, or u64 from spec 3.0), u16 packet id, then (payload_offset() = 6 or 10):

packet id kind column payload
0 0 (KIND_DIGITAL) u8 insertion reason (code), u8 reserved, u16 digital input (digital)
1 – 32767 (MAX_ELECTRODE_ID) 1 (KIND_SPIKE) u8 unit class (code: 0 unsorted, 255 noise), u8 reserved, waveform from waveform_offset() (8 or 12): waveform_samples() samples of sample_bytes() bytes, signed
0xFFFF (COMMENT_PACKET) 2 (KIND_COMMENT) u8 character set (0 ANSI, 1 UTF-16), u8 flag, u32 data, text (in info --view full, not in the table)
anything else 3 (KIND_OTHER) not decoded (video sync, tracking, button, configuration, log, recording events; 0x8001 in the 3.0 corpus file is undocumented)

Table columns: time_s (timestamp / resolution), packet_id, kind, code, digital, w0… (waveform in µV = raw × uv_per_count() = digitization factor / 1000; NaN outside spikes or past a shorter waveform). Rows = packet_count() = (file size − header) / packet size; a partial trailing packet → partial_packet. Table extra: signature, file_version, timestamp_resolution_hz, waveform_rate_hz, packet_size, recorded_at, application, comment, electrodes[], digital_labels[], kinds, code. info --view full adds up to MAX_COMMENTS comments with times and a count per packet id.

Recording directories (sessions)

A directory of .ns1–.ns6 and .nev files (plus SESSION_COMPANIONS: .ccf configuration, .txt, .log, .xml, .json, .md) is one recording (session_files, open_session): each NSx file is a trace with its own rate and clock (sampling groups are never merged), each NEV a table, in file-name order. A spec 2.1 NSx file takes its scaling from the NEV of the same name, exactly as when opened alone. NEV rows are not assigned to NSx sweeps. 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, bad_packet (errors); header_length_mismatch, bad_channel_header, bad_scaling, no_scaling, partial_scaling, partial_sample, partial_packet, clock_reset, timestamps_decrease, no_data, no_packets (warnings); segments, no_units, unknown_packets (info). A file whose basic header is cut off fails to open as corrupt (exit 4).

Observed corpus values

file spec channels rate sweeps notes
brk-2-1-l101210-001.ns2 2.1 6 (analog inputs 137–143) 1 kHz 1 3641 samples; factor 21516 in the NEV
brk-test2-test.ns5 2.1 2 30 kHz 1 6 samples, no NEV
brk-pause-correct.ns2 2.3 16 (mV) 1 kHz 2 packets at ticks 0 and 930261
brk-reset.ns2 2.3 16 (mV) 1 kHz 2 second packet starts at tick 96 (clock reset)
brk-filespec2-3001.ns5 2.3 10 (uV) 30 kHz 1 900300 samples
brk-file-spec-3-0.ns6 3.0 8 30 kHz 1 first packet at tick 26810899
brk-ptp-20231027-125608-001.ns2 3.0 PTP 65 1 kHz 1 2149 one-sample packets, ±240 ns jitter

Vocabulary (every public identifier in openreadout-blackrock must appear here)

identifier meaning
session_files, open_session, SESSION_COMPANIONS recording directories (sessions)
BlackrockReader, NsxDataset, NevDataset, FORMAT_ID, EXTENSIONS, SIGNATURES, open, nsx, nev, MAX_TABLE_READ, MAX_COMMENTS entry points and limits
NsxSpec { V21, V22, V30 }, name, packet_header_len NSx spec generations
NsxFile, spec, version, label, comment, period, time_resolution, recorded_at, header_len, channels, packets, sweeps, ptp_stride, ptp_packet_count, scaling_source, file_len, findings, sample_rate_hz, frame_len, sample_offset, max_sweep_len, parse_nsx NSx file
NsxChannel, index, electrode_id, connector, pin, min_digital, max_digital, min_analog, max_analog, units, highpass, lowpass, scale, offset NSx channels
NsxPacket, data_offset, timestamp, points, NsxSweep, first_packet, packet_count, sample_count NSx packets and sweeps
BASIC_HEADER_LEN, CC_LEN, SPEC21_HEADER_LEN, PERIOD_CLOCK_HZ, PTP_RESOLUTION, MAX_CHANNELS, cc_scaling, spec21_label, spec21_factor, systemtime NSx constants and helpers
NevFile, signature, flags, packet_len, sample_resolution, application, ext_headers, waveforms, labels, digital_labels, wide_timestamps, payload_offset, waveform_offset, sample_bytes, waveform_samples, max_waveform_samples, uv_per_count, parse_nev, NEV_BASIC_LEN, NEV_EXT_LEN NEV file
ExtHeader, id, WaveformHeader, digitization_nv, energy_threshold, high_threshold_uv, low_threshold_uv, sorted_units, spike_width NEV extended headers
NevPacket, packet_id, code, digital, waveform, text, nev_packet, COMMENT_PACKET, MAX_ELECTRODE_ID, KIND_DIGITAL, KIND_SPIKE, KIND_COMMENT, KIND_OTHER NEV packets and table kinds

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.