From 6a0b9bf5356bab779326f1fd9e2b6ce188444433 Mon Sep 17 00:00:00 2001 From: Jeff Long Date: Sat, 12 Sep 2026 08:35:03 -0400 Subject: [PATCH] streaming: saturate a transmit sample past full scale instead of wrapping writebuf's HACKRF_FORMAT_FLOAT32 and HACKRF_FORMAT_FLOAT64 branches multiplied each part by 127.0 and cast the result to int8_t with nothing bounding it. A part whose magnitude exceeds 1.0 - a sample past the device's full scale - does not clip in that cast: the conversion is undefined for a value outside the destination's range and in practice takes the low eight bits, so a part just above full scale leaves the driver near the opposite sign's extreme. One sample of a smooth waveform is turned into a full-scale step of the wrong polarity. Both branches now bound each scaled part to [-127, 127] before the cast, so a part past full scale is clipped. std::min and std::max are used rather than std::clamp because the build selects -std=c++11. A transmit stream at unit average power, measured before this conversion, had a part past full scale on 9.72 % of its samples, and 7.97 % of samples reached the point of wrapping; with a local-oscillator shift ahead of the conversion, which rotates each sample's magnitude into the converted parts, those became 31.45 % and 29.46 %. On the air at 915 MHz, a coded QPSK link transmitting at full scale through the unbounded conversion needed 6.31 % of its coded bits corrected at the receiver, while the same link at 0.7 to 0.25 of full scale needed 0.0003 % to 0.017 % - three orders of magnitude fewer - with nothing else changed. At full scale BPSK and 8PSK acquired no lock at all and a frequency-shifted variant decoded no frame; all three carried every frame once the stream stayed inside full scale. The HACKRF_FORMAT_INT16 branch is left alone: it arithmetic-shifts right by eight, whose result is always in [-128, 127], and cannot wrap. Signed-off-by: Jeff Long --- HackRF_Streaming.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/HackRF_Streaming.cpp b/HackRF_Streaming.cpp index 9ea8514..6091095 100644 --- a/HackRF_Streaming.cpp +++ b/HackRF_Streaming.cpp @@ -546,6 +546,13 @@ void readbuf(int8_t * src, void * dst, uint32_t len,uint32_t format,size_t offse } +// The device takes each part as a signed eight-bit sample whose full scale is 127. A part +// past full scale saturates here; a direct cast of it wraps to the opposite sign. +static int8_t to_int8_saturating(double scaled){ + return (int8_t) std::max(-127.0,std::min(127.0,scaled)); +} + + void writebuf(const void * src, int8_t* dst, uint32_t len,uint32_t format,size_t offset) { if(format==HACKRF_FORMAT_INT8){ int8_t *samples_cs8=(int8_t *) src+offset*BYTES_PER_SAMPLE; @@ -563,14 +570,14 @@ void writebuf(const void * src, int8_t* dst, uint32_t len,uint32_t format,size_t }else if(format==HACKRF_FORMAT_FLOAT32){ float *samples_cf32=(float *) src+offset*BYTES_PER_SAMPLE; for (uint32_t i=0;i