Skip to content

Acquisition Module

Data acquisition functions for logging data from soundcards and National Instruments DAQ devices.

Acquisition Settings

MySettings is the object you pass to every acquisition call — it holds the device, channel, sampling, trigger, voltage-range, IEPE excitation and per-channel calibration configuration. Every constructor argument is keyword-only with a default, and the attributes are plain and mutable. The full per-attribute reference is below; for worked end-to-end recipes (IEPE on a cDAQ, calibration at logging time) see the Data Acquisition user guide.

MySettings

Bases: object

A container for every acquisition setting used by a recording.

A single MySettings instance configures all acquisition entry points — acquisition.log_data, acquisition.signal_generator, the streams recorders and the Logger GUI. All constructor arguments are keyword-only and have defaults, so override only the few you need; attributes are plain and may also be set after construction (settings.fs = 12800). See the Data Acquisition user guide for worked end-to-end examples.

Conventions used throughout:

  • Everything is in volts. Acquired data and generated output are in volts; engineering-unit scaling is applied at display/fit time from channel_sensitivities, never by rescaling the stored data.
  • Per-channel fields take a scalar or a sequence. iepe_excit_current_A and channel_sensitivities accept one value (broadcast to all channels) or a list of length channels, indexed in captured-column order — on a cDAQ chassis that is slot order (see input_channels_spec).
  • The output_* fields configure the generation/AO path and default to their input counterparts when left unset.

Attributes:

  • channels (int) –

    Number of input channels (default 2). On the soundcard backend this is clamped to the device's max_input_channels at start_stream time (with a printed warning) — so the default of 2 becomes 1 on a mono mic rather than failing with PortAudio -9998. The NI backend instead raises ValueError if the count exceeds the device's available AI channels.

  • fs (int) –

    Input sampling frequency in Hz (default 44100). On a DSA module (NI 9234) the driver coerces this to the nearest rate on its discrete divider ladder.

  • nbits (int) –

    Sample bit depth — 8, 16, 24 or 32 (default 16).

  • chunk_size (int) –

    Samples acquired per channel per callback (default 100; values below 10 are raised to 10). Also the upper bound on pretrig_samples.

  • num_chunks (int) –

    Chunks held in the oscilloscope circular buffer (default 6); recomputed from viewed_time when set.

  • viewed_time (float or None) –

    Oscilloscope display window in seconds (default 0.3). When set, overrides num_chunks as ceil(viewed_time * fs / chunk_size). Set to None to size the oscilloscope buffer directly from num_chunks instead.

  • stored_time (float) –

    Duration of the recorded capture in seconds (default 2).

  • pretrig_samples (int or None) –

    Samples retained before the trigger, or None (default) for untriggered recording. Must not exceed chunk_size (the pretrigger buffer holds only one chunk of pre-trigger context), and must be less than stored_time * fs — otherwise there would be no post-trigger data left to record.

  • pretrig_threshold (float) –

    Trigger level, as a magnitude, in the units the recorder stores (default 0.05). On NI that is volts. On a soundcard it is volts once VmaxSC is set and full-scale units while it is left at 1.0 (uncalibrated) — the samples are scaled by VmaxSC on the way in, and the threshold is compared after that scaling. The 0.05 default was chosen as "5% of full scale" for an uncalibrated device; on a characterised interface it means 50 mV, which can sit close to the noise floor, so raise it to a sensible fraction of the expected signal.

  • pretrig_channel (int) –

    Channel index monitored for the trigger (default 0).

  • pretrig_timeout (float) –

    Seconds to wait for the trigger event before recording anyway (default 20). It bounds only the wait for the threshold crossing: once the crossing happens, the post-trigger data is given stored_time + 5 seconds of its own to arrive, so a capture longer than the timeout is not cut short.

  • lpf_on (bool) –

    Digital low-pass toggle (default False). When on, log_data captures ABOVE fs and resamples down behind a linear-phase anti-alias FIR (analysis.resample_to_fs — passband to fs/2.56, 96 dB stopband at fs/2). fs keeps its normal meaning (the rate you log at); the toggle changes only HOW that rate is achieved — oversample + noise-reducing decimation instead of sampling at fs directly. How far above fs the capture runs is set by oversample; on a device with a published rate ladder it is one of that device's real rates, otherwise an integer multiple of fs under the device maximum. The logged settings record the capture rate as lpf_capture_fs. NOTE the capture rate can differ from fs with this toggle OFF too: a device that cannot run at fs at all (any sound card asked for 3 kHz) is captured at a rate it can run and resampled down regardless, because the alternative is letting the OS resample silently.

  • oversample (str) –

    How far above fs to capture when oversampling — 'auto' (default), 'lowest' or 'highest'. 'auto' is device-specific: a sound card (delta-sigma, already anti-aliased at its own rate) takes the lowest rate with headroom, while NI takes the highest, which is mandatory on the filterless multiplexed USB-6003/6212 and buys noise process gain on DSA modules. Override when you know better than the default — see streams.oversample_strategy.

  • capture_fs (float) –

    Force the rate the hardware runs at, in Hz, overriding the automatic choice (default None = auto). fs is what you want delivered; this is what the converter actually samples at before pydvma decimates down to fs. Auto picks the lowest rate the device can genuinely run that covers fs, which is right for a sound card (delta-sigma, already anti-aliased at its own rate). Set it explicitly to capture faster — worth doing on hardware with no anti-alias filter, where a high capture rate is the only protection. Must be at least fs.

  • device_driver (str) –

    Input backend — 'soundcard' (default), 'nidaq' or 'mock' (the hardware-free test backend).

  • device_index (int or None) –

    Index into the enumerated device list for the chosen driver; None picks a default (the system default input soundcard, or NI device 0). For a cDAQ chassis this indexes the chassis as a whole, not a module — list candidates with dvma.list_available_devices() (its nidaq section is indexed the same way device_index is).

  • device (str or int or None) –

    Name the hardware instead of numbering it (default None). A case-insensitive substring of the device name — device='U24XL' — is resolved to whichever index that interface currently occupies, choosing the best backend available for it and filling in device_index, device_name and device_hostapi together. On Windows that choice is material: one interface is listed once per host API and they differ in word length and in whether they will silently resample, so the resolved backend also depends on fs. The choice and its reason are printed. Raises ValueError listing the candidates if the name matches no device, or more than one. An int is taken as an index. Run streams.list_available_devices to see what is present and what pydvma knows about each device's voltage scale.

  • device_name (str or None) –

    Exact enumerated name that device_index is expected to refer to (default None). Device indices are positions in an enumeration, not identities, and the enumeration reorders — so when this is set, streams.start_stream verifies the two still agree before opening, follows the name to its new index if it moved, and refuses rather than record the wrong instrument if it has gone. Left unset, streams.Recorder.init_stream records the resolved name after the first capture, which arms the same check for every capture after that.

  • device_hostapi (str or None) –

    Host-API name that device_index was chosen on, e.g. 'Windows WDM-KS' (default None, also filled in after the first capture). The other half of the identity, and the half that matters on Windows: PortAudio lists one interface once per host API with the SAME name each time, so device_name alone cannot tell four candidates apart and the check would give up. Ignored on platforms that list each device once.

  • input_channels_spec (str or None) –

    Optional raw DAQmx physical-channel string for the AI task, e.g. 'cDAQ1Mod1/ai0:3,cDAQ1Mod3/ai0'. Overrides the auto-built '<dev>/ai0:N-1' string when set — use it for gappy or mixed-module layouts the count-based builder cannot express. nidaqmx backend only.

  • VmaxNI (float) –

    Full-scale input voltage for the NI AI task (default 5); ±VmaxNI is passed as min/max to add_ai_voltage_chan. Pick the smallest range covering the signal for best resolution. Fixed at ±5 V on the 9234 (other values are accepted but ignored by the hardware).

  • input_gain_db (float) –

    Preamp gain currently set on the audio interface, in dB (default None). pydvma cannot read this — it is a front-panel control no audio API exposes — so stating it here lets VmaxSC be derived from the device's published maximum input level, and records in the saved dataset what the front panel was set to. Takes precedence over an explicit VmaxSC. Only applies to interfaces characterised in pydvma._soundcard_specs; ignored otherwise. Changing the gain on the hardware invalidates the calibration, so re-state it. A characterised FIXED-GAIN interface (e.g. the ESI U24 XL) needs no stated gain: VmaxSC is derived automatically when this is None and VmaxSC is left at its default.

  • input_mode (str) –

    Which input the signal is on — 'line' (default), 'inst' or 'mic'. Sets the maximum input level used with input_gain_db; on a Scarlett 2i2 these are 22, 12 and 16 dBu at minimum gain respectively.

  • VmaxSC (float) –

    Soundcard input calibration (default 1.0) — the jack voltage corresponding to a normalised reading of 1.0. Default 1.0 treats normalised samples as volts at unit scale (no calibration); set it to your measured input sensitivity to calibrate captures in volts.

  • NI_mode (str) –

    NI terminal configuration — 'DAQmx_Val_RSE' (default), 'DAQmx_Val_NRSE', 'DAQmx_Val_Diff' or 'DAQmx_Val_PseudoDiff'. DSA modules (9234) are pseudo-differential only.

  • iepe_excit_current_A (float or sequence of float) –

    Per-channel IEPE / ICP excitation current in amps (default 0.0 = off on every channel). Scalar broadcasts; a sequence must be length channels. Only NI 9234-class DSA modules support it; the legal discrete values on the 9234 are 0.0 and 0.002 (2 mA), validated against the module that actually owns each channel. A channel with current > 0 is switched to AC coupling and the recorder blocks ~2 s after start for the sensor bias to settle through the AC-coupling HPF. Requires device_driver='nidaq' (raises ValueError otherwise). Never enable it on a channel wired to an AO output (e.g. a loopback) — the current drives back into the AO terminal.

  • channel_sensitivities (float or sequence of float) –

    Per-channel sensitivity in volts per engineering unit — V/g for an accelerometer, V/N for a force transducer, V/Pa for a microphone, etc. (default 1.0 = no calibration applied). Scalar broadcasts; a sequence must be length channels and every value must be non-zero. log_data stores the reciprocal as TimeData.channel_cal_factors, which plotting and modal fitting apply automatically, so a 100 mV/g accelerometer (0.1 here) gives a cal factor of 10 and plots read in g. The stored time_data array itself stays in volts.

  • output_device_driver (str) –

    Backend for the output/AO path; defaults to device_driver (same device as the input).

  • output_device_index (int or None) –

    Device index for the output path. None follows the resolved device_index when the output driver matches the input driver and the input device can play (for soundcard, when it reports output channels — a USB audio interface drives its own outputs; a capture on it then runs as ONE full-duplex stream, see pydvma.streams.Recorder.init_stream). Otherwise: soundcard falls back to the default output device (a microphone-only input cannot play the stimulus), and nidaq/mock to device 0 for cross-driver output.

  • output_channels (int) –

    Number of output (AO) channels (default 1).

  • output_channels_spec (str or None) –

    Raw DAQmx physical-channel string for the AO task, e.g. 'cDAQ1Mod2/ao0'; the output analogue of input_channels_spec. nidaqmx backend only.

  • output_fs (int) –

    Output sample rate in Hz; defaults to fs.

  • output_VmaxNI (float or None) –

    Full-scale output voltage for the NI AO task; defaults to VmaxNI. (The NI 9260 is limited to ±4.24 V.)

  • output_VmaxSC (float or None) –

    Full-scale output voltage for the soundcard AO path — the jack voltage corresponding to a ±1 sounddevice sample; defaults to VmaxSC.

  • use_output_as_ch0 (bool) –

    When True and an output array is passed to log_data, the generated drive signal is prepended as channel 0 of the recorded data (default False). Useful for transfer-function tests where the excitation should be the reference channel; the prepended column passes through with a cal factor of 1.

  • init_view_time (bool) –

    Show the time-domain oscilloscope view on launch (default True).

  • init_view_freq (bool) –

    Show the frequency-domain oscilloscope view on launch (default True).

  • init_view_levels (bool) –

    Show the channel-levels oscilloscope view on launch (default True).

Examples:

IEPE accelerometers on a cDAQ with per-channel calibration — 100 mV/g ICP accelerometers on the 9234's ai0/ai1 and a 2.3 mV/N force probe on ai2 (channel index 3 unused):

>>> settings = dvma.MySettings(
...     device_driver='nidaq',
...     device_index=0,                  # the cDAQ chassis
...     channels=3,
...     NI_mode='DAQmx_Val_PseudoDiff',  # required by the 9234
...     VmaxNI=5,                        # 9234 fixed at ±5 V
...     fs=12800,
...     iepe_excit_current_A=[0.002, 0.002, 0.0],  # 2 mA accels only
...     channel_sensitivities=[0.1, 0.1, 0.0023],  # V/g, V/g, V/N
... )
>>> dataset = dvma.log_data(settings)

Scalars broadcast to every channel — four identical IEPE-powered 100 mV/g accelerometers:

>>> settings = dvma.MySettings(
...     device_driver='nidaq', channels=4,
...     iepe_excit_current_A=0.002, channel_sensitivities=0.1,
... )
See Also

acquisition.log_data: Run a capture using these settings. list_available_devices: Print soundcard + nidaq devices by index. suggest_ni_settings: Safe NI ranges/rate/mode for a device.

Output_Signal_Settings

Bases: object

Pre-set values for the Logger GUI's "Generate output" panel.

A lightweight holder for the four output-generation fields the Logger GUI exposes. Pass an instance to gui.Logger(..., output_signal_settings=...) to pre-fill that panel; the GUI then feeds the chosen values to acquisition.signal_generator when you preview or play the output. It is only consumed by the GUI — for scripted output, call acquisition.signal_generator / log_data(output=...) directly (see the Data Acquisition user guide).

The signal duration is not stored here — it is a separate field in the GUI panel (and the T= argument of signal_generator when scripting).

Attributes:

  • type (str) –

    Output waveform, matching the panel's drop-down — one of 'None' (output off; the default), 'sweep' (a linear chirp from f1 to f2), 'gaussian' (band-limited Gaussian noise) or 'uniform' (band-limited uniform noise). Maps to signal_generator's sig.

  • amp (float) –

    Peak amplitude in volts (default 0). Clamped to ±settings.output_vmax() at generation time.

  • f1 (float) –

    Lower frequency in Hz (default 0). For 'sweep' the start frequency; for the noise types the lower band-pass corner. Passed through as f=[f1, f2].

  • f2 (float) –

    Upper frequency in Hz (default 0). For 'sweep' the end frequency; for noise the upper band-pass corner. The GUI rejects max(f1, f2) > fs/2 (Nyquist).

Examples:

>>> # band-limited noise, 0.1 V, 100-300 Hz, pre-loaded in the GUI
>>> oss = dvma.Output_Signal_Settings(type='gaussian',
...                                   amp=0.1, f1=100, f2=300)
>>> logger = dvma.Logger(settings, output_signal_settings=oss)

Main Acquisition Function

log_data

log_data(settings, test_name=None, rec=None, output=None, cancel_event=None)

Acquire one block of time-domain data and return it as a DataSet.

Two call modes depending on settings.pretrig_samples:

  • No pretrigger (pretrig_samples is None): starts / reuses a stream, waits for settings.stored_time seconds, and returns the most recent stored_time * fs samples from the circular buffer. If output is supplied it is played in parallel (soundcard play is blocking; NI play is non-blocking and synchronized against stored_time via WaitUntilTaskDone).
  • Pretrigger armed (pretrig_samples set): waits up to settings.pretrig_timeout seconds for the monitored channel to cross settings.pretrig_threshold, then a further stored_time + 5 seconds for the post-trigger half of the window to fill. The timeout therefore bounds the WAIT FOR THE EVENT only — a long capture cannot expire it. When an output stimulus is supplied the timeout clock starts once the stimulus is actually playing (the ~1 s settle sleep and AO task setup are not counted against it). On trigger, returns a window of stored_time * fs samples straddling the trigger sample so that pretrig_samples of pre-trigger data appears at the start of the returned buffer. On timeout with no trigger the function does not raise — it falls back to returning the tail of the buffer (same as the no-pretrigger path) with trigger_detected = False.
Parameters

settings : MySettings Acquisition configuration. See pydvma.options.MySettings. test_name : str or None Stored on the returned TimeData for labelling. rec : Recorder-like or None Ignored. Retained for backward compatibility with callers (the GUI's LogDataThread) that still pass a cached recorder. log_data always calls streams.start_stream(settings) itself so that switching device or backend between calls doesn't leave a stale recorder; a running stream whose signature matches is REUSED (soundcard and NI both), so the call is cheap for back-to-back captures and the buffer already holds real history — a capture never starts with the startup- latency zeros a freshly opened stream begins with. On top of that, the free-run dwell is topped up until the buffer really holds stored_time * fs delivered samples (_wait_for_buffer_fill), covering the fresh-stream case. A capture during which the acquisition host reported dropped input prints a loud warning and sets :data:LAST_CAPTURE_OVERFLOWS — gaps in the data are unrecoverable and quietly destroy TF coherence, so they must never pass silently. output : ndarray (N_samples, output_channels) or None Optional playback signal in volts. For NI it's passed through as-is (must stay within ±output_VmaxNI). For soundcard it's divided by output_VmaxSC to recover the ±1 normalised units sounddevice expects. cancel_event : threading.Event or None Optional cooperative stop. When supplied, every wait in the capture (the free-run dwell, and both phases of the armed wait) polls it every :data:CANCEL_POLL_INTERVAL seconds; if it is set, any playing stimulus is stopped and :class:CaptureCancelled is raised instead of returning data. None (the default) is the plain blocking behaviour — the Python API is unchanged. Used by the pydvma serve bridge, which runs this in a worker thread that cannot be killed from outside. Same-device soundcard playback (through the capture stream's duplex output) polls the event too and stops the stimulus mid-play. The one wait that cannot be interrupted is playback on a SEPARATE output device, which blocks inside sd.OutputStream.write.

Returns

DataSet A DataSet containing one TimeData in volts. If settings.use_output_as_ch0 is True and output was supplied, the output signal is prepended as an extra channel.

Raises

CaptureCancelled If cancel_event is set before the capture finishes. ValueError If pretrig_samples exceeds chunk_size, or leaves no post-trigger data (>= stored_time * fs).

Notes

A clipping warning is printed when |data| > 0.95 * Vmax anywhere in the capture, where Vmax is VmaxNI on the NI path and VmaxSC on the soundcard path. Both default to behaviour equivalent to the old ±1-normalised check when the user hasn't overridden them.

Pretrigger positioning (both backends, identical logic)

On a successful trigger, the returned buffer is stored_time * fs samples long and the first sample exceeding pretrig_threshold sits at exactly index pretrig_samples — samples [0, pretrig_samples) are pre-trigger context. See streams.Recorder for the state machine that gives this invariant.

pretrig_samples is capped at chunk_size (validated at call-time with a ValueError); the recorder only retains that much pre-trigger context. Larger windows require a larger chunk_size. It must also leave room for post-trigger data — pretrig_samples >= stored_time * fs is rejected the same way.

The threshold is compared in the units the recorder stores, which for a soundcard means volts once VmaxSC is set and full-scale units while it is 1.0 (uncalibrated). Calibrating a device therefore changes what a given threshold number means.

On a trigger timeout (pretrig_timeout elapses with nothing above threshold), the function does not raise — it returns the tail of the buffer (same shape as the no-pretrigger path), leaves trigger_detected False, and prints a "not detected" message.

Capture rate vs delivered rate

fs is the rate the returned TimeData is at. The rate the CONVERTER runs at can differ, and streams.select_capture_fs decides which:

  • The device cannot run at fs. Any sound card asked for 3 kHz — their ladders start at 44.1 kHz. Captured at the lowest rate it CAN run and resampled down. This happens with lpf_on off, because the alternative is the OS resampling silently at a quality that depends on whatever rate the device was left at (measured as poor as 12 dB of alias rejection).
  • lpf_on. Captured above fs deliberately, for the anti-alias chain and ~10·log10(M) dB of noise process gain. How far above is settings.oversample — see streams.oversample_strategy.
  • capture_fs. Forces a specific capture rate outright.

In every case the capture — pretrigger window included, whose sample-exact alignment survives the rate change — is resampled to fs behind a linear-phase anti-alias FIR (analysis.resample_to_fs: passband to fs/2.56, 96 dB stopband at fs/2, zero-phase), chunk_size and pretrig_samples are scaled to the capture rate internally so their user-facing meaning (at fs) is unchanged, and the returned settings record the real capture rate as lpf_capture_fs.

The log proceeds unfiltered (with a printed note) when there is no headroom to oversample into, or when the device refuses to OPEN a stream at a rate its capability probe accepted (PortAudio's check_input_settings can approve rates InputStream then rejects, e.g. via MME under a remote-desktop session).

On a sound card, playback shares the capture clock, so a stimulus passed as output is resampled onto the capture rate when input and output are the same device (streams.output_shares_input_clock).

Signal Generation

output_signal

output_signal(settings, output, cancel_event=None)

Play output (in volts) on the configured AO device.

For soundcard, divides by output_VmaxSC to recover the ±1 normalised float sounddevice expects, then writes through streams.setup_output_soundcard — which plays through the live capture stream itself when input and output are the same device (one full-duplex stream; a second stream there would silence the capture on macOS), or a separate sd.OutputStream otherwise. Either way the write blocks until the stimulus has been handed to the stream. For NI, the voltage array is passed straight through to setup_output_NI (the AO task is configured with ±output_VmaxNI rails).

cancel_event (a threading.Event or None) makes the SAME-DEVICE soundcard wait cancellable: the duplex playback wait polls it and stops the stimulus as soon as it is set, regardless of whether the cancel arrives before or during playback. The separate-device path cannot honour it mid-write (a blocking sd.OutputStream.write is uninterruptible) and ignores it.

signal_generator

signal_generator(settings, sig='gaussian', T=1, amplitude=0.1, f=None, selected_channels='all')

Create an output-ready waveform.

amplitude is in volts — the generated signal is bounded to ±amplitude and, as a safety ceiling, clipped to ±settings.output_vmax() (i.e. output_VmaxNI on the NI path, output_VmaxSC on the soundcard path). Returns (t, y) where y is shape (N, output_channels) in volts, ready to hand to log_data(..., output=y) or output_signal.

multisine_generator

multisine_generator(settings, spec)

Create a periodic random-phase multisine buffer for a BLA capture.

One period is N = spec['n_samples'] samples exciting integer DFT bins k1..k2 (inclusive, satisfying 1 <= k1 <= k2 <= (N-1)//2 -- violating this raises ValueError naming k1, k2 and N) with equal amplitudes and uniform random phases drawn from numpy.random.default_rng([seed, m]) — so a saved spec reproduces the waveform exactly. Both the excitation index q (0..n_exc-1) and the experiment index e (0..n_exc-1) are zero-based; experiment e applies the orthogonal DFT-matrix rotation (phase shift -2*pi*q*e/n_exc on excitation q), which keeps every channel's amplitude spectrum identical across the n_exc experiments of a realisation.

Unlike signal_generator this applies NO fade window and NO peak rescale — both would break exact periodicity. amp_rms sets the per-channel RMS (identical across realisations, keeping the excitation class constant); if the resulting peak exceeds settings.output_vmax() a ValueError is raised — lower the level. A ValueError is also raised for a degenerate n_exc (< 1), a degenerate period count (t_periods + p_periods < 1), an e outside [0, n_exc), or n_exc exceeding settings.output_channels.

Parameters:

  • settings (MySettings) –

    output_fs (the time axis), output_channels (checked against n_exc) and the output voltage rail are consulted.

  • spec (dict) –

    MultisineSpec dict with keys n_samples, k1, k2, p_periods, t_periods, seed, m, e, n_exc, amp_rms. See the web logger's Nonlin-stage guide for the design vocabulary (M realisations, P periods, excited bins k1/k2): https://torebutlin.github.io/pydvma/web-logger/nonlin/.

Returns a tuple (t, y) where y is a C-contiguous array with shape ((t_periods + p_periods) * n_samples, n_exc) in volts, ready for log_data(..., output=y) — C-contiguity matters because sounddevice's OutputStream.write raises TypeError on a Fortran-ordered buffer, which a naive tile-then-transpose produces whenever n_exc >= 2.

Stream Monitoring

stream_snapshot

stream_snapshot(rec)

Capture the live oscilloscope buffer as a TimeData.

Unlike log_data, which blocks for stored_time seconds and returns a fresh capture, this is a non-blocking snapshot of the oscilloscope-side circular buffer (osc_time_data) — i.e. the most recent num_chunks * chunk_size samples already in memory. Useful for "what is the stream doing right now?" diagnostics from a notebook while a stream is running.

Parameters

rec : Recorder-like Retained for backward compatibility; the actual snapshot is always taken from the module-level streams.REC. Callers can pass any value (typically streams.REC itself).

Returns

TimeData A single TimeData instance with test_name='stream_snapshot', carrying the oscilloscope axis and the live buffer. No channel_cal_factors or units are attached (this is meant as a quick-look tool, not a calibrated capture).

Notes

Requires a live stream: call streams.start_stream(settings) first, or use a function like log_data that does so internally. On the soundcard path the buffer holds voltages scaled by settings.VmaxSC; on the NI path it holds raw volts.