Rasterwave models analog image protocols as bounded, ordered state machines. It must accept arbitrary PCM chunk sizes, emit useful rows before EOF, and produce the same samples/events when driven one sample at a time or in one large call.
caller PCM
-> streaming linear rate conversion (12 kHz working rate)
-> quadrature downmix around 1900 Hz
-> two-stage low-pass + complex phase-difference FM demodulation
-> parallel VIS header and sync-pulse detectors
-> candidate/confirmation policy
-> robust line-clock regression
-> family row decoder
-> borrowed LineReady events
The acquisition frequency ring is fixed at eight seconds. It allows a VIS candidate to be confirmed by later sync pulses without discarding the early rows. Once locked, the decoder reuses fixed line and channel scratch buffers.
VIS parity alone is not enough under low SNR: a corrupted word can still have
valid even parity and name another supported mode. Rasterwave reports the VIS
candidate first, then requires compatible sync width and repeated line period
before emitting ImageStarted.
No-VIS inference is stricter. Six compatible pulses are scored against the
mode catalog. If line timing cannot distinguish modes such as Martin M1/M3 or
Scottie S1/S3, only ModeCandidate is emitted. A caller may then lock a manual
mode; the library does not choose arbitrarily.
Frequency offset is estimated relative to the 1900 Hz leader and refined from 1200 Hz sync pulses. All pixel frequencies use the same estimate.
Clock drift is distinct from carrier offset. Compatible sync edges receive a
line index that can skip missed pulses. A least-squares fit over the latest
eight (line_index, sample_position) points updates samples-per-line and
re-anchors the next line boundary from the fitted phase only when:
- slope is within +/-2000 ppm of the profile;
- at least four points exist;
- residual RMS is below the sync-jitter threshold.
Line boundaries use a cumulative fractional deadline. They never round every row independently.
Robot 36 alternates red-difference and blue-difference chroma. An even row can
be displayed immediately with neutral missing chroma (Provisional). The next
radio row then produces a revised final even row and a final odd row. Consumers
must key rows by (image_id, line_index, revision).
The encoder stores the source raster, mode state, oscillator recurrence, and a single cumulative sample deadline. It creates tone or pixel segments lazily and fills caller memory directly. The oscillator is phase-continuous across pixels, segments, and caller chunk boundaries.
The optional transmission envelope is a separate ordered state machine around that raster. It can prepend QSSTV calibration tones and append a silence gap, FSK/CW station ID, and final guard. The legacy constructor selects an empty envelope, while progress snapshots expose exact raster sample boundaries for monitoring and loopback decoders.
Radiofax shares PCM, oscillator, demodulation, and raster primitives with SSTV but has a separate protocol state machine:
APT selection -------------------> phasing/IOC/LPM lock -> image rows
configured IOC + phasing-only ---> phasing/IOC/LPM lock -> image rows
image rows -> confirmed APT stop | max-lines | EOF | signal-loss
IOC is not a fixed image size. The built-in square-sampling policy derives a
full line width, while page height remains open until stop or a configured
limit. The encoder and decoder use the complete round(pi * IOC) image line.
APT and suspected stop samples are held until
the control pattern is confirmed so they are not exposed as image pixels.
The default acquisition path detects IOC from APT and WMO line rates from
phasing. Phasing-only acquisition requires the IOC in FaxDecoderConfig; an
LPM outside the automatically inferred WMO set must also be configured. An
image-only stream has no timing or horizontal-phase acquisition marker and is
not acquired automatically.
Parameterized FM and AM subcarriers share the framing state machine. Receive configuration must name the modulation expected on the input. Low target-carrier level or coherence for the configured interval ends an active page as partial instead of allowing silence or wideband noise to synthesize more rows. A caller with receiver-specific squelch evidence may set the coherence threshold to zero and end the page explicitly at its integration boundary.
Radiofax timing has two independent errors: line-period error produces a
progressive slant, while line-phase error produces a constant circular shift.
The decoder models both with line_start(n) = phase + n * period.
Phasing edges are retained in a bounded 64-point history. Median interval and phase statistics reject threshold jitter before a confidence-gated clock is accepted. The page start is projected onto that fitted grid, so the expected phasing duration cannot introduce a separate horizontal offset. Rejected phasing cycles invalidate pending untrusted measurements instead of allowing a stale period to start a page.
Continuous fax paper has two explicit timing paths. Before a protocol lock, it
emits immediate NominalPaper rows. A trusted phasing capture uses the fitted
period directly and emits Calibrated rows; image content is not allowed to
retime that segment. For a mid-image join, a separate bounded image-content
tracker may publish a sparse affine model only after at least five consistent
observations over a long span. It freezes on large innovations and never treats
ordinary image structure as protocol evidence.
correct_fax_paper applies only to nominal rows. It evaluates sparse timing
models with slope-continuous interpolation and a four-sample fractional-delay
filter, so integer shifts remain exact and necessary fractional correction does
not receive a second linear blur. Consumers must not apply this transform to
rows whose basis is already Calibrated.
The core exposes synchronous methods taking &mut self. This is the correct
primitive for a future Promise API:
JS handle -> per-handle serial work queue -> owned Rust codec session
-> owned events back to JS
Different handles can run on a Node worker pool. Work for one handle must stay
ordered. The core does not embed Tokio, a thread pool, an Arc<Mutex<_>>, or a
Node-API dependency.
Borrowed events are the zero-copy Rust path. A binding must copy
DecodeEventRef with to_owned() before crossing an asynchronous boundary.
- no mutable process-global codec state;
- acquisition history has fixed capacity;
- locked SSTV audio buffers retain only pending rows;
- radiofax retains bounded phasing history plus bounded stop/fade evidence;
- owned complete images are created only when the caller asks to retain them;
- no
unsafecode in the core crate.