⚠️ CAUTION
This project is a work in progress. Blood pressure analysis is not yet implemented. Breathing signal extraction and visualisation are functional but should be treated as experimental.
spectHR is a free, open-source desktop application for Heart Rate Variability (HRV) analysis. It is built for researchers and students who want to move from a raw electrocardiogram (ECG) to interpretable HRV metrics without writing any code, and without trusting a black box.
Heart rate variability reflects the fluctuations inter-beat intervals (IBI). These fluctuations are regulated by the autonomic nervous system: higher HRV generally indicates flexible, healthy regulation; lower HRV is associated with stress, disease, or fatigue. By analysing the power in different frequency bands of the IBI series, spectHR lets you separate the parasympathetic (HF) and sympathetic/parasympathetic (LF) contributions to heart rate control in each named epoch of your recording.
The key principle behind spectHR is that you have to check data quality. Every step that could go wrong, R-peak detection, noisy intervals, epoch boundaries, is shown to you visually and can be corrected before any metrics are computed.
Generally, heart rate variability (HRV) reflects the continuous interplay between the two branches of the autonomic nervous system (ANS). The parasympathetic branch, acting via the vagus nerve, produces rapid beat-to-beat fluctuations — including respiratory sinus arrhythmia (RSA). The sympathetic branch exerts slower influences, mainly through vasomotor tone and blood pressure regulation. Because the two branches operate at different speeds, spectral analysis separates their contributions by frequency. (Mulder, 1989; Task Force of the European Society of Cardiology, 1996).
A note on the two-branch picture: both branches can be simultaneously active or withdrawn. HRV does not directly reveal what the sympathetic branch is doing. Broader conclusions about autonomic balance require additional signals such as blood pressure, breathing patterns or pre-ejection period.
The CARSPAN tradition, which spectHR inherits, utilizes the constructs mental effort and mental workload rather than stress. Mental effort is defined as the resources invested in response to task demands - a construct that can be experimentally manipulated and whose physiological cost can be measured independently of the subjective experience of strain.
Mulder (1980) showed that task performance suppresses HRV, particularly in the mid-frequency band (~0.10 Hz) that reflects baroreceptor-driven blood pressure oscillations. Mulder (1989) formalised the framework: the suppression reflects invested effort, not task difficulty. A person who disengages from a hard task shows little HRV change; one working hard at an easy task shows a marked decrease (Mulder, 1992). The pattern generalises across driving (de Waard, 1996; Mulder et al., 2004), aviation (De Rivecourt et al., 2008), clinical work (Peabody et al., 2023), and driving simulation studies (Arutyunova et al., 2024).
HRV does not measure perceived stress. It measures a physiological state: the cost of effort investment that may or may not correspond to what a person calls "feeling stressed." The gap between the two is where individual differences and coping strategies operate. Treating HRV as a direct readout of stress conflates distinct levels of analysis.
spectHR computes power in three bands (Mulder, 1989; Mulder, 1988), following the CARSPAN convention. Band boundaries are user-configurable to accommodate different research traditions.
Recording length. Frequency resolution is
Artefacts are critical. A single missed or spurious R-peak introduces broadband spectral energy that can swamp the genuine HRV signal (Mulder, 1988). This is why spectHR requires the user to inspect and verify the IBI series before computing any metrics.
Normalisation. Absolute spectral power in ms² scales with mean IBI squared, increasing at lower heart rates. The CARSPAN mMI² normalisation corrects for this automatically. For the Welch method the output unit is configurable: the default is mMI² (normalised, heart-rate-independent) but can be switched to ms² (raw IBI power, not normalised) via the "units" key in Edit Parameters. Whether to normalise HRV for mean heart rate is genuinely unresolved (de Geus et al., 2019), and no single recommendation fits all research questions.
Details
The two choices show the same frequencies (the rhythms of heart rate variability), but measured differently. ms²/Hz is the direct view. You take the sequence of beat-to-beat intervals in milliseconds, and ask how its variance is spread across frequencies. At any given frequency, this value tells you how much millisecond-squared variability sits there, per unit of frequency bandwidth. Integrate the curve over a band (for example HF, 0.15 to 0.40 Hz) and the result is band power in ms². That number is intuitive in absolute terms: an HF power of 1000 ms² means the HF rhythm contributes about √1000 ≈ 32 ms of variation to the IBI signal. The catch is that ms²/Hz depends strongly on mean heart rate. Someone with a slow resting heart (mean IBI 1200 ms) has more absolute room to fluctuate in milliseconds than someone with a fast heart (mean IBI 600 ms), even when both modulate their heart rate by the same proportion. So comparing absolute ms² between subjects, or between rest and exercise within one subject, is muddied by baseline differences in heart rate. mMI²/Hz removes that dependence. CARSPAN works from the R-peak event series and divides the spectrum by the squared mean heart rate. What comes out is dimensionless: it no longer asks "how many ms of fluctuation," but "what fraction of the mean does the rhythm modulate?" Because that fraction is small, the result is multiplied by 10⁶ so values land in a readable range. The unit name reflects this scaling: milli-Modulation-Index squared, mMI². The y-axis of an mMI²/Hz plot is therefore a relative modulation strength per Hz. Integrating over a band gives band power in mMI², with a clean interpretation tied directly to the coefficient of variation of heart rate: a 1% CoV contributes 100 mMI² of total power (since 0.01² × 10⁶ = 100), and a 10% CoV contributes 10 000 mMI². Read backwards, an HF band power of 250 mMI² means the high-frequency rhythm modulates heart rate by roughly √(250 × 10⁻⁶) ≈ 1.6%. In short: use ms²/Hz when you care about the absolute size of the millisecond fluctuations themselves. Use mMI²/Hz when you want to compare HRV rhythms across people, sessions, or conditions where mean heart rate differs, because it expresses the rhythms as a percentage of the heartbeat being modulated rather than as an absolute amplitude.spectHR operates on IBI time series. The core analyses - time-domain HRV, spectral band power, Poincaré geometry - require only inter-beat intervals. Simultaneous blood pressure or respiration recordings are not needed. That said, the most reliable conclusions about mental effort come from converging evidence across heart rate, HRV, and when available, blood pressure and baroreflex sensitivity. Stuiver and Mulder (2014) showed that the full cardiovascular pattern was more diagnostically informative than any single measure in isolation.
- Launch the application. On first run it creates a default workspace pointing to your Documents folder.
- Open WorkSpace → Edit Workspace to point spectHR at the folder where your data files live.
- Your files appear in the tree on the left. Click one to load it.
- Work through the tabs from left to right: clean the ECG, define your epochs, then read the results.
The window is divided into two areas. On the left is a narrow file tree listing all datasets in your configured data folder. On the right is the main analysis area, organised into six tabs:
| Tab | What you do there |
|---|---|
| Preprocessing | Verify and correct R-peak detection |
| IBI Series | Review the IBI timeseries |
| Poincaré | Explore beat-to-beat dynamics visually |
| Epochs | Draw and adjust your analysis segments |
| PSD | Read the frequency-domain power spectrum per epoch |
| Parameters | See all HRV metrics in a table and export to CSV |
This is the most important step. The quality of every metric downstream depends entirely on whether the R-peaks have been detected correctly.
When you select a file, spectHR loads the ECG, finds R-peaks automatically, and displays everything in the Preprocessing tab. The main plot shows the raw ECG signal in red. Every detected R-peak is marked with a vertical coloured line, and the IBI (in milliseconds) is printed as an arrow between consecutive peaks. A colour code immediately tells you which beats look suspicious, see IBI Classification for the full scheme.
If a breathing signal is available from the device's accelerometer, it appears as a green curve overlaid on the ECG. Light-blue shading marks inhalation phases. Below the main ECG is a thumbnail strip showing the entire recording. The navigation bar at the bottom lets you jump to the next or previous abnormal beat, pan, and zoom.
When you spot a problem, use the mode selector:
- Drag, shift a peak line to its correct position.
- Add, click anywhere on the ECG to insert a missing peak.
- Remove, click a peak line to delete a spurious detection.
All changes are saved automatically to the cache file when you leave the tab.
Tip
Right-clicking a file gives you Reload Raw, Invert ECG Polarity, and Retrigger ECG.
Once the peaks look correct, switch to the IBI Series tab to see heart rate over time in beats per minute. If a breathing signal is available it is shown as a faint green overlay.
HRV metrics are always computed within named segments. The Epochs tab shows a Gantt-style chart. If your recording contains marker events, epochs are built automatically. You can also add epochs manually via Actions → Add Epoch and trim them by dragging their edges. Any change here immediately updates the Poincaré plot, the PSD plots, and the metrics table.
The Poincaré tab shows a scatter plot where each point represents one heartbeat: its IBI on the x-axis and the following IBI on the y-axis. A healthy autonomic system produces a characteristic elliptical cloud elongated along the diagonal. The short axis (SD1) reflects rapid, beat-to-beat variability; the long axis (SD2) reflects slower variability. Each active epoch is drawn in a different colour with its own ellipse overlaid.
A note on SD1 and SD2
SD1 and SD2 carry no statistical information beyond RMSSD and SDNN respectively. Van Roon et al. (2025) demonstrate this rigorously: SD1
van Roon, A.M., Span, M.M., Lefrandt, J.D., & Riese, H. (2025). Overview of mathematical relations between Poincaré plot measures and time and frequency domain measures of heart rate variability. Entropy, 27(8), 861. https://doi.org/10.3390/e27080861
The PSD tab shows one power spectrum per active epoch. Four frequency bands are shaded (usefull defaults in brackets):
- FullRange (0.02–0.65 Hz), the total HRV spectrum.
- VLF (0.02–0.06 Hz), slow regulatory processes.
- LF (0.07–0.14 Hz), baroreflex, mixed sympathetic/parasympathetic.
- HF (0.15–0.40 Hz), respiratory sinus arrhythmia, parasympathetic tone.
Ranges are configurable. The shaded grey band is the confidence interval. Three PSD methods are available — carspan, carspan_strict, welch — selected in WorkSpace → Edit Parameters. The method takes effect immediately without restarting.
The Parameters tab shows a table with one row per epoch:
- Time-domain: count, mean, median, min, max, std, RMSSD, SDNN, SDSD
- Poincaré: SD1, SD2, SD2/SD1, ellipse area
- Frequency-domain: FullRange, VLF, LF, HF power and LF/HF ratio — in mMI² by default; Welch can be switched to ms² via Edit Parameters
Click Save to export to CSV.
All parameters are stored in a JSON workspace file, editable through the application menus.
WorkSpace → Edit Workspace changes the three folder paths (data, cache, export).
WorkSpace → Edit Parameters covers:
- Frequency Analysis, PSD method, band edges and colours, method-specific parameters, CI level.
- IBI Classification, window length, threshold multiplier, TL ceiling.
- ECG Preprocessing, high-pass filter settings.
Changes take effect immediately and are saved to disk.
spectHR reads .xdf files from LabStreamingLayer, designed for use with the Polar H10 via PolarBLE. Streams are identified automatically. Epochs are derived in one of two ways:
- Explicit markers — if any marker stream contains labels beginning with
start <label>/stop <label>(orend <label>), those are used to build named epochs in the usual way. - Keyboard stream fallback — if no explicit start/stop markers are present but a stream named
Keyboardexists, spectHR builds consecutive, non-overlapping epochs from every marker that ends with" pressed". Each such marker starts a new epoch named by the key that was pressed (the marker text with" pressed"stripped). The epoch ends when the next key is pressed, or at the end of the recording for the final epoch. If the same key is pressed more than once its epochs are numbered (a #1,a #2, …).
The plain-text RR-interval export format from the Polar app. See ExampleData/.
CARSPAN event files. If an .nff file with the same base name is present, spectHR loads the ECG channel from it. In that case the R-peak timestamps from the .evt file are authoritative: the ECG is filtered for display but R-peaks are not re-detected from it (the loader sets rtops_locked on the resulting CardioSeries).
After R-peak detection, each inter-beat interval is classified before any metrics are computed. The beat colour in the ECG plot reflects its label:
| Label | Meaning | Colour |
|---|---|---|
| N | Normal, within the expected range | Blue |
| S | Short, below the lower threshold | Magenta |
| L | Long, above the upper threshold | Cyan |
| TL | Too Long, exceeds the absolute ceiling; excluded from all metrics | Orange |
| SL | Short followed immediately by Long | Turquoise |
| SNS | Short-Normal-Short triplet | Light sea green |
| T | Degenerate (NaN or zero duration); excluded from all metrics | — |
Classification algorithm
Classification algorithm
Let
| Condition | Label |
|---|---|
| TL (excluded from all statistics) | |
|
|
T (excluded from all statistics) |
| L | |
| S | |
| beat |
SL |
| beat |
SNS |
| otherwise | N |
Default parameters (all configurable in Edit Parameters):
| Parameter | Default | JSON key |
|---|---|---|
| Window length |
51 beats | window_length |
| Threshold |
4.0 std | n_std |
| TL ceiling |
2.0 s | max_ibi_sec |
Comparison with CARSPAN
Users familiar with CARSPAN will notice several differences in the classification approach. Each is a deliberate design choice.
1. Refractory period, implemented at detection, not classification
CARSPAN enforces a hardware refractory period min_peak_distance_ms parameter passed to scipy.signal.find_peaks. Any two candidate peaks closer than 300 ms simply cannot both be detected. This approach prevents the false detection from entering the data in the first place. The effect on the classified IBI series is identical.
2. Window type, centred rather than causal
CARSPAN computes its running statistics over a causal (backward-looking) window of
3. Window unit, beats rather than seconds
CARSPAN's window is defined in seconds; spectHR's in beats. For a resting heart rate of ~70 bpm, 51 beats ≈ 44 seconds, close to CARSPAN's default of 60 seconds. A beat-based window always contains the same number of statistical observations regardless of heart rate.
4. Successive difference criterion, not implemented
CARSPAN also flags a beat if the difference between consecutive intervals (
5. Min/Max SD clipping, not implemented
CARSPAN clips the standard deviation used for thresholding to a minimum of 5 % and a maximum of 15 % of the local mean IBI. spectHR relies instead on the user's choice of n_std and max_ibi_sec. The absolute ceiling guards the upper extreme; the 300 ms minimum peak distance guards the lower extreme.
6. Automatic interpolation, not implemented
CARSPAN automatically corrects detected artefacts by linear interpolation, inserting estimated beats and adding normally-distributed noise to prevent variance reduction. spectHR does not perform automatic correction. Every flagged beat is shown in the Preprocessing tab and can be corrected manually. Intervals left uncorrected and labelled TL or T are excluded from all metric calculations. This reflects spectHR's core principle: you, not the algorithm, decide what to do with each artefact.
When a Polar H10 accelerometer stream is present, spectHR derives a respiration surrogate from the 3-axis chest-belt movement data by removing the gravity component, bandpassing to 0.10–0.70 Hz, and applying PCA to extract the dominant axis of motion as a single 1-D signal. The result is z-score normalised and overlaid in green on the ECG and heart rate plots. Inhalation phases are shaded in light blue.
SD1, beat-to-beat variability, equal to
SD2, longer-term variability, algebraically related to SDNN and RMSSD:
SD1/SD2, autonomic balance index.
Ellipse area
Important: SD1 and SD2 are fully determined by RMSSD and SDNN and therefore carry no additional statistical information (van Roon et al., 2025). Including all four in a statistical model introduces redundancy.
The IBI series (in ms) is resampled onto a uniform time grid with cubic interpolation, divided into overlapping segments, windowed, Fourier-transformed, and averaged. Averaging across segments reduces variance at the cost of frequency resolution. Default output: mMI²/Hz; configurable to ms²/Hz via the "units" workspace key.
Welch, algorithm detail
Welch, algorithm detail
Let scipy.signal.welch is invoked with scaling='density' and returns a one-sided PSD in ms²/Hz.
Band power
Because consecutive segments overlap, they are not statistically independent. The effective degrees of freedom are reduced accordingly (Percival & Walden, 1993):
where:
-
$K$ — number of segments -
$\rho$ — normalised window autocorrelation at lag equal to the segment step, computed numerically from the actual window samples:$\rho = \sum_n w[n], w[n + \text{step}] ;/; \sum_n w[n]^2$ -
$\chi^2_{\nu, p}$ — chi-square quantile at probability$p$ with$\nu$ degrees of freedom -
$\alpha$ — significance level (e.g. 0.05 for a 95% CI)
For non-overlapping segments
Default parameters (configurable in Edit Parameters):
| Parameter | Default | JSON key |
|---|---|---|
| Resampling frequency | 4 Hz | fs |
| Segment length | 256 samples | nperseg |
| Overlap | 128 samples | noverlap |
| FFT length | 1024 | nfft |
| Window | Hann | window |
| Output units | mMI² | units |
Note on resampling. A 4 Hz resampling rate gives a frequency resolution of nperseg, the segment length is reduced to the available count and the overlap is halved automatically.
CARSPAN takes a different approach. Instead of resampling the IBI series onto a uniform time grid, it operates directly on the R-peak times and computes the Fourier transform of those events — the algorithm Mulder (1988) developed for the original CARSPAN software. No resampling, no interpolation in time. The spectrum is computed on a native frequency grid
CARSPAN, algorithm detail
CARSPAN, algorithm detail
The R-peak event series
Following Rompelman (1975) and Mulder (1988), the heartbeat is modelled as a sequence of unit impulses at the R-peak times
This is the Integral Pulse Frequency Modulation (IPFM) representation. Its spectrum reflects heart-rate variability, not IBI variability directly. The manual's formula 3.20 normalises out the mean heart rate afterwards, which makes the two equivalent for spectral purposes.
Power spectral density (formula 3.19)
Treating the signal outside the analysis window
The 10% cosine bell, a Tukey window with α = 0.20). The window tapers the signal smoothly to zero at both ends, suppressing spectral leakage at the segment boundaries.
Two variants are available:
-
carspan(configurable) — anyscipy.signal.get_windowname (the"X% cosine bell"shorthand maps to Tukey α = X/50). Window applied by event index; amplitude includes the standard$2N / (T \cdot S_2)$ correction (with$S_2 = \sum_i w_i^2$ ) so the level remains approximately consistent across window choices. DC removal is off by default; setdc_removal: truein the workspace to enable it. -
carspan_strict— manual-faithful: Tukey 5 % cosine taper (α = 0.10) applied by event index, amplitude$2/T$ with no$N/S_2$ correction, and the regular-grid DC removal described below, applied unconditionally. This corresponds to what the reference CARSPAN implementation actually computes.
For a regular sinus rhythm the two variants produce the same spectral shape at LF/HF; the strict variant is noticeably cleaner at VLF because of the DC subtraction. Use carspan_strict to reproduce CARSPAN's reported values; use carspan for a tunable, variance-correct estimate.
Why they are not interchangeable. Several elements of the strict bundle are different code paths rather than parameter values, so carspan_strict cannot be reproduced exactly from the configurable carspan settings:
| Behaviour | carspan_strict |
carspan (configurable) |
Reachable via workspace? |
|---|---|---|---|
| Window function | CARSPAN cosine-bell index taper — sample 0 has a small non-zero weight (scipy's tukey zeros it) | any scipy.signal.get_window name |
✗ — no scipy window matches the CARSPAN taper exactly |
| Window length |
N − 1 events |
N events |
✗ |
| Amplitude pre-factor | 2 / T |
2N / (T · S₂) |
✗ |
| First event | skipped (CARSPAN convention) | included | ✗ |
| DC reference grid | regular-rate grid offset by ΔT, length N − 1
|
span-matched grid, length N
|
✗ |
| Regular-grid DC removal | always on |
opt-in via dc_removal: true
|
✓ |
| 3-MA + bin-average smoothing | on | on by default (smooth_for_display: true) |
✓ — already matches |
| mMI² mean convention | arithmetic mean of rate | harmonic mean of rate (N/T) |
✗ — hard-coded per method |
The closest the configurable mode can get is dc_removal: true, which cleans up the VLF leakage. The window, amplitude, skip-first, DC reference grid, and mean convention still differ. To reproduce CARSPAN exactly, select method: "carspan_strict" rather than tuning carspan.
Regular-grid DC removal (strict mode)
The manual writes Eq. 3.19 as a plain DFT of the windowed impulse train. The reference CARSPAN implementation goes one step further: before squaring, it subtracts the DFT of a perfectly periodic impulse train at the mean rate. The manual does not flag this as a separate step, but it is essential for matching CARSPAN's actual output, particularly at low frequencies.
The mechanism is straightforward. At
Available in configurable mode as well. Setting FrequencyAnalysis.carspan.dc_removal to true (also editable from Edit Parameters) enables the same subtraction in the configurable carspan variant. It defaults to false to preserve the historical behaviour of that variant; enable it when you need the VLF cleanup while retaining a custom window or the variance-correct
Native frequency grid
The grid runs from
Display grid and smoothing
The plot path and the integration path operate on separate arrays, so the 3-point moving average can smooth the on-screen curve without affecting the reported band-power values.
Plot path (psd()). Native spectrum → bin-averaged onto the freq_resolution display grid (default 0.01 Hz) → optional 3-point moving average when smooth_for_display = True (the default). Following the manual (§3.3):
"a moving average window over three frequency points (0.03 Hz bandwidth) is applied before plotting the spectral functions"
Integration path (band_power, band_powers). spectHR calls the back-end a second time with smooth=False and applies only the resample step — same display grid, but no moving average. The manual is equally explicit on this point:
"No smoothing of the spectra is carried out on the spectra before computing the spectral band values"
Band power (formula 3.28)
Band power in
where freq_resolution setting (default 0.01 Hz).
The manual writes this on the native grid (Δf = 1/T); spectHR uses the resampled grid because that is what the reference CARSPAN implementation integrates. The two values agree to within edge-bin rounding, since the resample step is energy-conservative.
Welch band power uses trapezoidal integration; only the CARSPAN back-end uses this rectangular summation.
Practical consequences. Toggling smooth_for_display does not affect band power — integration runs on a separately computed unsmoothed copy. Changing freq_resolution does shift Δf_disp and the bin boundaries, so it has a small effect on band power: energy is preserved overall, but edge-bin rounding shifts slightly. For typical band definitions the difference remains below 1 %. To reproduce CARSPAN's reported values exactly, leave freq_resolution at 0.01.
Normalisation to mMI² (formulae 3.20 and 3.29)
The raw spectrum is in units of events²/Hz. To make it dimensionless and independent of mean heart rate, it is divided by the squared mean of the event series. For unit impulses this equals the squared mean heart rate
The factor
Note on the mean. CARSPAN defines the mean rate as
Strict mode uses a different mean. The reference CARSPAN code defines the mean rate slightly differently from its own manual: it uses the arithmetic mean of the per-beat instantaneous rates rather than carspan_strict; carspan (configurable) and welch retain the simpler carspan_strict faithful to the reference implementation and the others faithful to the manual.
Default parameters (configurable in Edit Parameters):
| Parameter | Default | JSON key | Effect |
|---|---|---|---|
| Display grid resolution | 0.01 Hz | freq_resolution |
Smoothness of displayed curve; does not affect band power |
| Tapering window | 10% cosine bell |
window |
Any scipy.signal.get_window name, or "X% cosine bell" → Tukey α = X/50 |
| 3-point display smoothing | true |
smooth_for_display |
Matches CARSPAN plot convention |
| Regular-grid DC removal (configurable mode) | false |
dc_removal |
Subtract the DFT of a mean-rate impulse train before squaring; cleans up VLF. Strict mode applies this unconditionally. |
CARSPAN itself does not report confidence intervals; this is a spectHR addition. The CI reflects the variability of the spectral estimate that would be expected if the measurement were repeated under identical conditions. Both methods use a chi-square CI of the form:
The methods differ in how the degrees of freedom
All methods share the same configurable band definitions:
| Band | Default range | Reflects |
|---|---|---|
| FullRange | 0.02–0.50 Hz | Total spectral power across the HRV range |
| VLF | 0.02–0.06 Hz | Slow regulatory processes; requires long recordings |
| LF | 0.07–0.14 Hz | Baroreceptor reflex, mixed sympathetic/parasympathetic |
| HF | 0.15–0.40 Hz | Respiratory sinus arrhythmia, parasympathetic tone |
Band edges are configurable in Edit Parameters. If your participants breathe slowly, you may need to extend the HF band to lower frequencies.
| Method | PSD unit (default) | Band power unit (default) | Configurable? | Normalised by mean HR? |
|---|---|---|---|---|
| Welch | mMI²/Hz | mMI² | Yes (units: "mMI²" or "ms²") |
Yes (default) |
| CARSPAN | mMI²/Hz | mMI² | No | Yes, |
Both methods output mMI² by default, which is dimensionless and largely heart-rate independent, enabling valid comparisons across groups or conditions that differ in resting heart rate. Output of the Welch method can be switched to raw ms² output via the units workspace key; CARSPAN always outputs mMI².
Releases are compiled using Nuitka. They are fully self-contained.
Download spectHR-Windows-vX.Y.Z.zip from the Releases page, extract, and run spectHR.exe. No Python installation is required.
Download spectHR-Linux-vX.Y.Z.zip from the Releases page, extract, and run spectHR. No Python installation is required.
Download spectHR-macOS-vX.Y.Z.zip from the Releases page and extract spectHR.app. Drag it to /Applications. Because the app is not signed by Apple, macOS will block it the first time. Right-click spectHR.app in Finder, choose Open, and click Open in the dialog. Alternatively run xattr -dr com.apple.quarantine /Applications/spectHR.app in a Terminal.
spectHR is written in pure Python. The analysis library (src/spectHR/) and the GUI (src/spectUI/) are kept separate so the library can be used independently in scripts.
New HRV metrics can be added by decorating a method with @hrv_metric in CardioSeries, it will appear in the Parameters table and CSV automatically.
New file formats can be added by registering a loader function with @register_loader(".ext") in src/spectHR/DataSet/loaders/.
Fork the repository and open a pull request with a clear description of the change.
spectHR is released under the GNU LGPL-2.1 license. See the LICENSE file for details.
Arutyunova, K.R., Bakhchina, A.V., Konovalov, D.I., Margaryan, M., Filimonov, A.V., & Shishalov, I.S. (2024). Heart rate dynamics for cognitive load estimation in a driving simulation task. Scientific Reports, 14, 31656. https://doi.org/10.1038/s41598-024-79728-x
Billman, G.E. (2013). The LF/HF ratio does not accurately measure cardiac sympatho-vagal balance. Frontiers in Physiology, 4, 26. https://doi.org/10.3389/fphys.2013.00026
de Geus, E.J.C., Gianaros, P.J., Brindle, R.C., Jennings, J.R., & Berntson, G.G. (2019). Should heart rate variability be "corrected" for heart rate? Biological, quantitative, and interpretive considerations. Psychophysiology, 56(2), e13287. https://doi.org/10.1111/psyp.13287
Mulder, G. (1980). The heart of mental effort. Ph.D. Thesis, University of Groningen.
Mulder, L.J.M. (1985). Mental load, mental effort and attention. In A.W.K. Gaillard & W. Ritter (Eds.), Tutorials in Event Related Potential Research: Endogenous Components. Amsterdam: North Holland.
Mulder, L.J.M. (1988). Assessment of cardiovascular reactivity by means of spectral analysis. Ph.D. Thesis, University of Groningen.
Mulder, L.J.M. (1992). Measurement and analysis methods of heart rate and respiration for use in applied environments. Biological Psychology, 34, 205–236. https://doi.org/10.1016/0301-0511(92)90016-N
Mulder, L.J.M. (1989). Cardiovascular reactivity and mental workload. International Journal of Psychophysiology, 7, 321.
Mulder, L.J.M., de Waard, D., & Brookhuis, K.A. (2004). Estimating mental effort using heart rate and heart rate variability. In N.A. Stanton, A. Hedge, K. Brookhuis, E. Salas, & H. Hendrick (Eds.), Handbook of Human Factors and Ergonomics Methods (pp. 20.1–20.8). Boca Raton: CRC Press.
Peabody, J.E., Ryznar, R., Ziesmann, M.T., & Gillman, L. (2023). A systematic review of heart rate variability as a measure of stress in medical professionals. Cureus, 15(1), e34345. https://doi.org/10.7759/cureus.34345
De Rivecourt, M., Kuperus, M.N., Post, W.J., & Mulder, L.J.M. (2008). Cardiovascular and eye activity measures as indices for momentary changes in mental effort during simulated flight. Ergonomics, 51, 1295–1319. https://doi.org/10.1080/00140130802120267
Rompelman, O. (1980). Heart rate variability and the assessment of mental workload. In B.K.P. Horn (Ed.), Methods of Information in Medicine. Stuttgart: Schattauer.
Rompelman, O., Coenen, A.J.R.M., & Kitney, R.I. (1977). Measurement of heart-rate variability: Part 1 — Comparative study of heart-rate variability analysis methods. Medical and Biological Engineering and Computing, 15, 233–239. https://doi.org/10.1007/BF02441043
Stuiver, A., & Mulder, L.J.M. (2014). Cardiovascular state changes in simulated work environments. Frontiers in Neuroscience, 8, 399. https://doi.org/10.3389/fnins.2014.00399
Task Force of the European Society of Cardiology and the North American Society of Pacing and Electrophysiology (1996). Heart rate variability: standards of measurement, physiological interpretation and clinical use. Circulation, 93(5), 1043–1065. https://doi.org/10.1161/01.CIR.93.5.1043
Tegegne, B., Man, T., van Roon, A., Riese, H., & Snieder, H. (2019). To the Editor: 10-second ECG-based RMSSD as valid measure of HRV. Heart Rhythm, 16(3), e35. https://doi.org/10.1016/j.hrthm.2018.10.038
van Roon, A.M., Span, M.M., Lefrandt, J.D., & Riese, H. (2025). Overview of mathematical relations between Poincaré plot measures and time and frequency domain measures of heart rate variability. Entropy, 27(8), 861. https://doi.org/10.3390/e27080861
de Waard, D. (1996). The measurement of drivers' mental workload. Ph.D. Thesis, University of Groningen.



