Skip to main content

Read CED Spike2 (.smr/.smrx) data files in Python via the native SON64 library.

Project description

spike2io — Spike2 Files in Python

A Python library for loading Spike2 .smr / .smrx files. It reads a file and exposes its channels as typed Python objects returning NumPy arrays.

Work on this code was supported by a grant from the NIH NIDDK (grant: R21DK140694).


Why this library?

CED provides an official Python package, sonpy, but on its own it has limitations:

  • Narrow platform/version coverage — as of 1.9.12 (June 2026) the macOS and Linux wheels are built for CPython 3.14 only
  • Older versions of sonpy may appear to have better coverage but were buggy

spike2io provides a consistent, NumPy-based interface on top of two interchangeable backends, so you get the same API whichever one is available:

Backend What it uses Platforms Python
ceds64 Bundled CFFI extension + CED's SON64 DLLs Windows x64 3.9 – 3.14
sonpy CED's own sonpy package Windows / macOS / Linux 3.9 – 3.14 on Windows, 3.14 only on macOS/Linux

The ceds64 backend is bundled — on Windows there is nothing else to install. It is also the default when both are present, because it can return WaveMark/RealMark sample payloads, which sonpy cannot.


Installation

pip install spike2io

Windows (64-bit) — the CED libraries ship with the package, so no other Python dependency is needed. They do require Microsoft's Visual C++ 2012 Redistributable (x64), which many systems already have; install it if importing ced reports that the ceds64 backend failed to load.

macOS / Linux — reading goes through CED's sonpy, which currently publishes macOS/Linux wheels for CPython 3.14 only:

pip install spike2io[sonpy]

Optional plotting helpers (.plot() methods) need matplotlib:

pip install spike2io[plot]

Requirements: Python >= 3.9 on Windows, >= 3.14 on macOS/Linux. NumPy.


Example Files

A repository of example .smr / .smrx files is available for testing:

https://github.com/JimHokanson/spike2_example_files

The examples below use files from that repository.


Usage

Reading a file

import ced

f = ced.read_file("my_recording.smr")
# ... work with f ...
f.close()

Files can also be used as context managers, which closes them even if an error is raised:

import ced

with ced.read_file("my_recording.smr") as f:
    w = f.waveforms[0]
    segments = w.get_data()

By default the first available backend is used, preferring ceds64. Pass backend= to force one:

import ced

f = ced.read_file("my_recording.smr", backend="sonpy")

If you pass a .s2rx resource file, the matching .smrx / .smr recording is opened instead.


Waveform channels

Waveform channels hold continuous analog signals (e.g. pressure, EMG, voltage). Both Spike2 waveform types appear here — Adc (scaled 16-bit) and RealWave (32-bit float).

import ced
from matplotlib import pyplot as plt

with ced.read_file("Demo1.smr") as f:
    # First waveform channel (not necessarily channel number 1)
    w = f.waveforms[0]

    print(w.name)    # Data
    print(w.units)   # volt
    print(w.fs)      # 2000.0  — samples per second

    # get_data() returns a list of segments, one per contiguous
    # recording run. Demo1.smr was paused 5 times, so it has 6.
    segments = w.get_data()

    plt.figure()
    for seg in segments:
        seg.plot()          # convenience method: seg.time vs seg.data
    plt.ylabel(f"{w.name} ({w.units})")
    plt.xlabel("Time (s)")
    plt.show()

A recording with no pauses returns a single-element list. Each segment has:

Attribute Description
seg.time NumPy array of sample times (seconds)
seg.data NumPy array of sample values (physical units)
seg.n_samples Number of samples in this segment
seg.start_time Time in seconds of the first sample
seg.start_tick Time in file ticks of the first sample

Sample times come from the file's own clock, so channels stay aligned with each other and with event times. A channel does not necessarily start at time zero — Spike2 staggers waveform channels across the sampling multiplexer, and a channel can also start well into a recording. Demo1.smr above starts at 17.76225 s.

To read part of a channel, give either a time range (seconds, inclusive) or a sample range (1-based, inclusive):

import ced

with ced.read_file("Demo1.smr") as f:
    w = f.waveforms[0]
    segments = w.get_data(time_range=(20.0, 25.0))
    segments = w.get_data(sample_range=(1, 1000))

get_data() with no arguments returns everything and never raises. A range reaching past the end of the file raises ValueError; pass out_of_range='clamp' (or 'warning') to return what exists instead.

Data is returned as float64 by default. Use return_format='single' for float32, or 'int16' for the raw stored values of an Adc channel.


Event channels

Spike2 distinguishes rising-edge, falling-edge, and both-edge (level) event channels. Which lists are populated depends on the file — Demo_Jim.smrx has two falling-edge channels and one both-edge channel, and no rising-edge channels.

import ced

with ced.read_file("Demo_Jim.smrx") as f:
    print([c.name for c in f.event_falls])   # ['Stimulus', 'Response']
    print([c.name for c in f.event_both])    # ['Memory']

    fall = f.event_falls[0]
    d = fall.get_data()
    print(d.n_events)      # 752
    print(d.times[:3])     # [0.06286 0.85558 1.97999]  — seconds
    d.plot()               # event times as tick marks

Both-edge channels also report the signal level in force before the first transition:

import ced

with ced.read_file("Demo_Jim.smrx") as f:
    both = f.event_both[0]
    d = both.get_data()

    print(d.n_events)      # 752
    print(d.start_level)   # 1  (0 = low, 1 = high)
    print(d.times[:3])     # transition times in seconds
    d.plot()               # square wave

get_data() accepts the same time_range and out_of_range arguments as waveform channels. get_times() is the same method under a clearer name.


Marker channels

Marker channels carry a timestamp plus four 8-bit codes. The Keyboard channel records keys pressed during acquisition, and its codes are decoded to characters automatically.

import ced

with ced.read_file("Demo_Jim.smrx") as f:
    mk = f.markers[0]
    d = mk.get_data()

    print(mk.name)         # Keyboard
    print(d.n_events)      # 38
    print(d.times[:3])     # marker times in seconds
    print("".join(d.c1[:5]))  # thisi

    d.plot()               # vertical lines, labelled with c1

Codes are available as d.c1 through d.c4. Pass to_char=False to keep them as raw uint8.


Listing all channels

import ced

with ced.read_file("my_recording.smrx") as f:
    print("Waveform channels  :", len(f.waveforms))
    print("Event-rise channels:", len(f.event_rises))
    print("Event-fall channels:", len(f.event_falls))
    print("Event-both channels:", len(f.event_both))
    print("Marker channels    :", len(f.markers))

    for w in f.waveforms:
        print(f"  {w.name}  ({w.units})")

f.all_chan_objects holds every channel in file order, and f.chan_info is a table of [id, index, name, type] rows.

Channels can also be looked up by name:

import ced

with ced.read_file("my_recording.smrx") as f:
    w = f.get_channels("nerve")                  # partial match, case-insensitive
    chans = f.get_channels(["nerve", "signal"])  # several at once

Cursor positions

Spike2 stores cursor positions in a .s2rx sidecar file next to the recording. When one is present it is parsed automatically and exposed as f.meta_file:

import ced

with ced.read_file("Demo_Jim.smrx") as f:
    print(f.meta_file.get_vertical_cursor_positions())    # [115.68]
    print(f.meta_file.get_horizontal_cursor_positions())  # []

Positions are in seconds. A cursor that was never moved has no stored position and is omitted, so a file whose cursors are all untouched returns an empty list. f.meta_file is None when there is no .s2rx file.

Note, this functionality is undocumented by CED. The .s2rx file is a XML file, so it is easy to read. However, the behavior of the components is not documented There may be other features that could be extracted from this file ....


Low-Level API

ced.ffi_ceds64 is a thin wrapper over the CED DLL, and ced.ffi_sonpy presents the same function names on top of sonpy. Use these if you need direct access to file, channel and marker primitives. Channel numbers are 1-based and times are in file ticks.

from ced import ffi_ceds64 as ffi

fhand = ffi.open("Demo.smr")

print(ffi.chan_title(fhand, 1))   # Sinewave
print(ffi.chan_units(fhand, 1))   # Volts
print(ffi.chan_type(fhand, 1))    # 1        — ChanType.ADC
print(ffi.time_base(fhand))       # 9.999999999999999e-06  — seconds per tick

# n_read, int16 samples, tick of the first sample
n_read, data, t_first = ffi.read_wave_s(fhand, chan=1, n_max=5000, t_from=0)

ffi.close(fhand)

Both modules are documented inline; see their docstrings for the full set of calls. Note they are interchangeable only for reading — edit_marker, write_ext_marks, extra_data and the mask_* functions exist in ffi_ceds64 only.


Relationship to the MATLAB library

This package is the Python counterpart of matlab_spike2. The high-level API mirrors the MATLAB interface:

MATLAB Python
file = ced.file(path) f = ced.read_file(path)
w = file.waveforms(1) w = f.waveforms[0]
d = w.getData() data = w.get_data()
plot(d(i).time, d(i).data) seg.plot()

Known Limitations

  • WaveMark / RealMark / TextMark channels are read, but on the sonpy backend the numeric sample payload is unavailable — sonpy 1.9.12 exposes no attribute for it, so .data comes back empty. Timestamps and codes are correct on both backends.
  • MATLAB's return_format options for both-edge event channels ('time_series1', 'time_series2', 'switch_times', 'starts_and_stops') are not yet ported. Python returns transition times and a starting level, equivalent to MATLAB's default 'times'.
  • Writing files is available through the backend modules but is not covered by the high-level API.
  • Some example files (example1.smr, example2.smr) cannot be opened by the bundled ceds64 backend, which returns error −13. They read correctly with backend="sonpy". Unfortunately I have no idea why this is :/
  • There are limited error checks on low-level DLL return codes.

License

MIT — see LICENSE for details.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

spike2io-0.1.0.tar.gz (547.6 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

spike2io-0.1.0-py3-none-win_amd64.whl (557.0 kB view details)

Uploaded Python 3Windows x86-64

spike2io-0.1.0-py3-none-any.whl (49.1 kB view details)

Uploaded Python 3

File details

Details for the file spike2io-0.1.0.tar.gz.

File metadata

  • Download URL: spike2io-0.1.0.tar.gz
  • Upload date:
  • Size: 547.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for spike2io-0.1.0.tar.gz
Algorithm Hash digest
SHA256 cd57f9b61194dbc617ecf69165dae75ae623116b826dadf23508abbc4a345b43
MD5 10335b1ad7b370514df0708bbe716582
BLAKE2b-256 6e1a549b68a778d53e04b6cb64f5bcae9b9037bcff8c74d23d7e5a202ffc54d6

See more details on using hashes here.

File details

Details for the file spike2io-0.1.0-py3-none-win_amd64.whl.

File metadata

  • Download URL: spike2io-0.1.0-py3-none-win_amd64.whl
  • Upload date:
  • Size: 557.0 kB
  • Tags: Python 3, Windows x86-64
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for spike2io-0.1.0-py3-none-win_amd64.whl
Algorithm Hash digest
SHA256 c8983a00417dd46f26dab319b4d5946c1b4970b13a834d401515d3e6384d0125
MD5 f52f92de02caaf21a7cc694cbdb164a1
BLAKE2b-256 9ccf353cd56d79fc8aa56e7a238ea98323c6e475a73f0f1388e20b7d672de751

See more details on using hashes here.

File details

Details for the file spike2io-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: spike2io-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 49.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.14.4

File hashes

Hashes for spike2io-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9729729a28ae940859e278f095ec2e71035dd1bac07182e1ca261b323612e290
MD5 56fcc330dcffd06142e8c39ae831c2e1
BLAKE2b-256 f59f7b7dcfb78f9185150d40f51799bdc6db71ad876ed3b301c0724f70b5d7c4

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page