Timestamp compensation: Physio16 realignment, filter recalibration, system delay, mock fidelity#15
Open
cboulay wants to merge 6 commits into
Open
Timestamp compensation: Physio16 realignment, filter recalibration, system delay, mock fidelity#15cboulay wants to merge 6 commits into
cboulay wants to merge 6 commits into
Conversation
…delays The PIB/Physio16 acquisition path leads the FPGA-filtered EEG by a fixed ~33 ms in decimated mode, so the same physical event lands at different sample indices in the EEG vs physio channels of the combined outlet. The existing timestamp offset can't fix this (EEG and physio share one timestamp), so add a per-channel delay line that buffers physio on the raw int32 ADC counts and emits zeros during warm-up. - PhysioDelayLine: header-only ring buffer applied in readPacketFormat2, serving both the native (int32) and float output paths. - The 33 ms skew is a physical delay the device quantizes to whole samples, so the applied delay is floor(33 ms x rate): 8 @ 250 Hz, 16 @ 500 Hz (both 32 ms), 33 @ 1000 Hz. Computed with integer division, not round(). - Store the delay in ampserver_config.cfg (<physioaligndelayms>, default 33). Remove the "Align Timestamps" control (GUI checkbox, CLI --align-timestamps, config field). Alignment now follows the user's attempted config: shouldAlign() returns true iff the configured mode is decimated, and gates both the timestamp offset and the physio delay. An already-running stream at an ambiguous rate (500/1000 Hz) is assumed to match the configured mode. Drops the "_aligned" LSL source-id suffix (decimated now always implies aligned). Recalibrate getFilterDelaySeconds DIN->EEG delays to measured, repeatable values (confirmed across machines/device versions): 250 Hz 112->111 ms, 500 Hz 66->61 ms. 1000 Hz unchanged at 36 ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make MockAmplifier reproduce the relative timing the real device exhibits so the app's PhysioDelayLine and timestamp alignment can be validated against it. - Inject a common reference pulse (jig emulation) into DIN pin-1 (active-low), EEG ch1, and physio ch1. DIN reflects the event immediately; EEG lags it by the FPGA anti-alias filter delay (111/61/36 ms decimated) and physio leads EEG by a fixed 33 ms (floor-quantized to samples), matching the device. - Add a decimated/native mode flag (set by setNativeRate/setDecimatedRate) and an activeRate() helper; native mode applies no skew. This also fixes a latent bug where the stream always ran at decimatedRate, ignoring native rates. - Replace the old free-running DIN counter with the active-low pin-1 reference. Verified end to end: MockAmpServer -p 1 + delay_capture_sweep against 127.0.0.1 yields DIN->EEG 27/30/36 samples and EEG-physio 8/16/33 samples at 250/500/1000 decimated, and 0 in native. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The streaming loop slept a fixed 5 ms *after* generating/sending each packet, so the effective rate sagged well below nominal (~816 Hz at 1000). The fixed 5 ms interval also can't represent non-1000 rates: 250 Hz -> 1 sample/5 ms = 200 Hz. Pace to a steady deadline with sleep_until, and set the interval to samplesPerPacket/rate so every rate is exact. Measured rates are now 250.0/500.1/1000.5 Hz (was 163/322/816), so the timestamp-derived delays line up with the sample-domain skews. A resync guard avoids bursting after a stall. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The EEG anti-alias delay rounds to the nearest sample instead of flooring, so DIN->EEG lands at 112/62/36 ms (was 108/60/36) -- closer to the device's 111/61/36, which the app applies as a continuous (non-quantized) timestamp offset anyway. The physio lead keeps flooring to match the device's own whole-sample quantization, so EEG-physio is unchanged at 32/32/33 ms. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Subtract a configurable system/pipeline latency (systemDelayMs, default 4.5 ms) from every pushed LSL timestamp -- EEG, Physio16, and DIN alike -- to compensate for the time between digitization and the sample being available to time-stamp in this client (device firmware + network transmission + read path). Measured ~5 ms in native mode via the audio latency test; the default deliberately under-compensates so a sample is never back-dated before its event. Applied unconditionally at the point the batch timestamp is captured, on top of the decimated-only FPGA filter offset. Document the full timestamp-compensation behaviour in the README (system delay, FPGA filter offset, Physio16 realignment), replacing the stale --align-timestamps section and correcting the filter-delay table to the measured 111/61/36 ms. Also gitignore the audio-latency-test recordings and Python caches.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Makes the LSL timestamps the app assigns reflect when a signal actually occurred, not when its bytes arrived — so EEG, Physio16, and DIN line up with each other and with external event markers. Adds three timestamp corrections (all measured on hardware) plus mock-server fidelity so the behaviour can be validated locally.
Changes
Physio16 realignment (decimated mode). The PIB/PNS acquisition path leads the FPGA-filtered EEG by a fixed ~33 ms, so the same event lands at different sample indices in the EEG vs physio channels of the combined stream. A new header-only
PhysioDelayLinebuffers the physio channels byfloor(33 ms × rate)samples (configphysioAlignDelayMs, default 33), emitting zeros during the warm-up window. Operates on raw int32 PIB counts so it serves both the native and float output paths.Filter-delay recalibration.
getFilterDelaySecondsupdated to the measured DIN→EEG group delays (250 → 111, 500 → 61 ms; 1000 unchanged at 36) from a 300 s DIN/EEG/Physio16 sweep — repeatable across machines and device versions.Removed the
--align-timestampstoggle. Alignment now follows the configured mode automatically: decimated ⇒ filter + physio corrections; native ⇒ neither. The app trusts the user's configured mode at the ambiguous 500/1000 Hz rates (where native/decimated are indistinguishable in the stream). GUI checkbox, CLI flag, and config field all removed; the_alignedLSL source-id suffix dropped.System-delay compensation (always applied). New
systemDelayMs(double, default 4.5 ms) subtracted from every pushed timestamp — EEG, Physio16, and DIN — to compensate the digitization→client pipeline (firmware + network + read path). Measured ~5 ms in native mode; the default deliberately under-compensates so a sample is never back-dated before its event.Mock-server fidelity. The mock now injects a common reference pulse into DIN pin-1 / EEG ch1 / physio ch1 with the device-accurate skews (EEG lagged by the filter delay, physio leading EEG by 33 ms; native = 0), distinguishes native vs decimated mode, and paces streaming to an accurate sample rate (was running ~0.8× nominal).
Docs. New README "Timestamp Compensation" section covering all three corrections; values persisted in
ampserver_config.cfg.Validation