codec: wire decode_waveform_v2 into production; add MicL dB helper
Replaces the broken legacy int16 LE decoder in client.py with the
verified multi-channel codec. Three changes:
1. blastware_file.extract_body_bytes(a5_frames) — new helper that
factors out the body-reconstruction logic from write_blastware_file
so both writers (BW binary) and decoders (sample arrays) can use
the same canonical bytes.
2. waveform_codec.decode_a5_frames(a5_frames) — production entry point.
Returns the raw_samples dict consumers expect (Tran/Vert/Long as
int16 ADC counts; MicL as native ADC counts). Internally:
A5 frames → extract_body_bytes → decode_waveform_v2
→ decoded_to_adc_counts (geos ×16; mic pass-through)
3. waveform_codec.mic_count_to_db(count) — MicL ADC → dB(L) per BW's
display formula:
dB = sign(count) × (81.94 + 20 × log10(|count|)) for |count| ≥ 1
Verified against V70 fixture: count=813 → 140.14 dB (BW PSPL 140.1).
client.py:_decode_a5_waveform is reduced to a thin wrapper that calls
decode_a5_frames and populates event.raw_samples. Original implementation
preserved as _decode_a5_waveform_LEGACY (dead code; reference only).
Also fixed a tail-end bug in decode_waveform_v2 where trailer-section
"40 02" markers (containing ASCII serial bytes, NOT real segment headers)
were being mis-interpreted, producing 2 spurious samples per channel at
the end of each event. Added bytes [12:14] == "02 00" validation to
reject non-header markers.
7 new pytest tests cover the new helpers and dB conversion. Total:
71 passing (up from 64).
Known limitation (carried over from before): the walker still stops
mid-event on the loudest fixtures (SP0/SS0/SV0/event-b) at some
mid-segment edge cases not yet characterized. Every sample reached
is decoded correctly; the walker just doesn't reach all of them.
Loud events still yield 5,000–15,000 byte-exact samples each.
This commit is contained in:
+58
-11
@@ -1500,22 +1500,69 @@ def _decode_a5_waveform(
|
||||
(BULK_WAVEFORM_STREAM) frame payloads and populate event.raw_samples,
|
||||
event.total_samples, event.pretrig_samples, and event.rectime_seconds.
|
||||
|
||||
This requires ALL A5 frames (stop_after_metadata=False), not just the
|
||||
metadata-bearing subset.
|
||||
Wired up 2026-05-11 to the verified ``decode_waveform_v2`` codec (see
|
||||
``minimateplus/waveform_codec.py`` and ``docs/waveform_codec_re_status.md``).
|
||||
Replaces the legacy int16 LE decoder, which produced full-scale ±32K
|
||||
noise on every event because the body bytes are encoded, not raw
|
||||
samples.
|
||||
|
||||
── Waveform format (confirmed from 4-2-26 blast capture) ───────────────────
|
||||
The blast waveform is 4-channel interleaved signed 16-bit little-endian,
|
||||
8 bytes per sample-set:
|
||||
Output convention (preserved from the legacy decoder):
|
||||
``event.raw_samples`` is a dict with keys "Tran", "Vert", "Long",
|
||||
"MicL" mapping to lists of **int16 ADC counts**. Multiply by
|
||||
``geo_range / 32768`` for geo channels to get in/s; use
|
||||
:func:`minimateplus.waveform_codec.mic_count_to_db` for mic dB(L).
|
||||
|
||||
``total_samples`` / ``pretrig_samples`` / ``rectime_seconds`` are set
|
||||
to ``None`` so the caller backfills from compliance_config (the
|
||||
authoritative source — STRT fields aren't reliable).
|
||||
"""
|
||||
from .waveform_codec import decode_a5_frames
|
||||
|
||||
event.total_samples = None
|
||||
event.pretrig_samples = None
|
||||
event.rectime_seconds = None
|
||||
|
||||
if not frames_data:
|
||||
log.debug("_decode_a5_waveform: no frames provided")
|
||||
return
|
||||
|
||||
decoded = decode_a5_frames(frames_data)
|
||||
if decoded is None:
|
||||
log.warning("_decode_a5_waveform: codec returned no samples")
|
||||
return
|
||||
|
||||
event.raw_samples = decoded
|
||||
log.debug(
|
||||
"_decode_a5_waveform: decoded %d/%d/%d/%d samples (T/V/L/M)",
|
||||
len(decoded.get("Tran", [])),
|
||||
len(decoded.get("Vert", [])),
|
||||
len(decoded.get("Long", [])),
|
||||
len(decoded.get("MicL", [])),
|
||||
)
|
||||
|
||||
|
||||
def _decode_a5_waveform_LEGACY(
|
||||
frames_data: list[S3Frame],
|
||||
event: Event,
|
||||
) -> None:
|
||||
"""
|
||||
LEGACY decoder — kept for reference only. DO NOT CALL.
|
||||
|
||||
This is the int16 LE decoder that produced full-scale ±32K noise
|
||||
on every event. Retracted 2026-05-08; replaced 2026-05-11 with
|
||||
the verified codec in :mod:`minimateplus.waveform_codec`. See
|
||||
``docs/instantel_protocol_reference.md §7.6.1`` for the full history.
|
||||
|
||||
── Waveform format (LEGACY — WRONG) ────────────────────────────────
|
||||
Claimed 4-channel interleaved signed 16-bit little-endian, 8 bytes
|
||||
per sample-set:
|
||||
|
||||
[T_lo T_hi V_lo V_hi L_lo L_hi M_lo M_hi] × N
|
||||
|
||||
where T=Tran, V=Vert, L=Long, M=Mic. Channel ordering follows the
|
||||
Blastware convention [Tran, Vert, Long, Mic] = [ch0, ch1, ch2, ch3].
|
||||
where T=Tran, V=Vert, L=Long, M=Mic.
|
||||
|
||||
⚠️ Channel ordering is a confirmed CONVENTION — the physical ordering on
|
||||
the ADC mux is not independently verifiable from the saturating blast
|
||||
captures we have. The convention is consistent with Blastware labeling
|
||||
(Tran is always the first channel field in the A5 STRT+waveform stream).
|
||||
The body bytes are actually a tagged delta+RLE stream — this
|
||||
interpretation was wrong.
|
||||
|
||||
── Frame structure ──────────────────────────────────────────────────────────
|
||||
A5[0] (probe response):
|
||||
|
||||
Reference in New Issue
Block a user