Skip to content

Commit bf00b7d

Browse files
committed
dsp(core,io,nxdn): harden demod math and soft-metric scaling
1 parent 37f19d6 commit bf00b7d

13 files changed

Lines changed: 393 additions & 186 deletions

File tree

include/dsd-neo/dsp/demod_pipeline.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,10 +62,10 @@ void raw_demod(struct demod_state* fm);
6262
/**
6363
* Differential QPSK demodulator for CQPSK/LSM.
6464
*
65-
* Computes the phase difference between consecutive complex samples
66-
* (arg(z_n * conj(z_{n-1}))) using a fast atan2 approximation and writes
67-
* the resulting Q14-scaled symbols to the real result buffer. Maintains
68-
* history across blocks via demod_state.
65+
* Converts each carrier-corrected differential phasor to a real symbol using
66+
* `atan2f(Q, I) * (4/pi)`, matching OP25's `multiply_const_ff(4.0/pi)` stage.
67+
* The output range maps nominal CQPSK decision points to approximately
68+
* `{-3, -1, +1, +3}` for legacy slicers.
6969
*
7070
* @param fm Demodulator state (reads interleaved I/Q in lowpassed, writes phase deltas to result).
7171
*/

include/dsd-neo/io/rtl_stream_c.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -240,12 +240,14 @@ int rtl_stream_test_get_replay_state(RtlSdrContext* ctx, rtl_stream_test_replay_
240240
#endif
241241

242242
/**
243-
* @brief Get smoothed TED residual (EMA of Gardner error) from demod pipeline.
244-
* Positive: persistent early (nudge center right). Negative: late (nudge left).
243+
* @brief Get smoothed TED residual from demod pipeline in Q14 units.
244+
*
245+
* Positive values indicate persistent "sample early" bias (nudge center right),
246+
* negative values indicate "sample late" bias (nudge center left).
245247
* Returns 0 when unavailable.
246248
*
247249
* @param ctx Stream context (unused).
248-
* @return Residual value (coarse units, signed integer).
250+
* @return Signed Q14 residual (approximately float residual * 16384).
249251
*/
250252
int rtl_stream_ted_bias(const RtlSdrContext* ctx);
251253

src/core/audio/gain.c

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -152,48 +152,43 @@ AGF_END:; //do nothing
152152
// Kept identical to the original implementation in dsd_audio2.c.
153153
void
154154
agsm(dsd_opts* opts, dsd_state* state, short* input, int len) {
155-
int i;
156-
157155
UNUSED(opts);
158156

159-
//NOTE: This seems to be doing better now that I got it worked out properly
160-
//This may produce a mild buzz sound though on the low end
161-
162-
// float avg = 0.0f; //average of 20 samples (unused)
163-
float coeff = 0.0f; //gain coeffiecient
164-
float max = 0.0f; //the highest sample value
165-
float nom = 4800.0f; //nominator value for 48k
166-
float samp[960];
167-
memset(samp, 0.0f, 960 * sizeof(float));
168-
169-
//assign internal float from short input
170-
for (i = 0; i < len; i++) {
171-
samp[i] = (float)input[i];
157+
if (!input || len <= 0 || !state) {
158+
return;
172159
}
173160

174-
for (i = 0; i < len; i++) {
175-
if (fabsf(samp[i]) > max) {
176-
max = fabsf(samp[i]);
161+
/* Legacy target level for analog monitor path (PCM16 scale). */
162+
const float nom = 4800.0f;
163+
float max_abs = 0.0f;
164+
for (int i = 0; i < len; i++) {
165+
float v = fabsf((float)input[i]);
166+
if (v > max_abs) {
167+
max_abs = v;
177168
}
178169
}
179170

180-
/* average not used; remove to avoid dead store */
171+
/* Avoid divide-by-zero on silence and keep behavior stable. */
172+
if (max_abs < 1e-6f) {
173+
max_abs = 1e-6f;
174+
}
181175

182-
coeff = fabsf(nom / max);
176+
float coeff = fabsf(nom / max_abs);
183177

184-
//keep coefficient with tolerable range when silence to prevent crackle/buzz
178+
/* Keep coefficient in a conservative range to limit pumping/noise lift. */
185179
if (coeff > 3.0f) {
186180
coeff = 3.0f;
187181
}
188182

189-
//apply the coefficient to bring the max value to our desired maximum value
190-
for (i = 0; i < 20; i++) {
191-
samp[i] *= coeff;
192-
}
193-
194-
//return new sample values post agc
195-
for (i = 0; i < len; i++) {
196-
input[i] = (short)samp[i];
183+
/* Apply gain over the full block with explicit int16 saturation. */
184+
for (int i = 0; i < len; i++) {
185+
float scaled = (float)input[i] * coeff;
186+
if (scaled > 32767.0f) {
187+
scaled = 32767.0f;
188+
} else if (scaled < -32768.0f) {
189+
scaled = -32768.0f;
190+
}
191+
input[i] = (short)scaled;
197192
}
198193

199194
state->aout_gainA = coeff; //store for internal use

src/core/frames/dsd_dibit.c

Lines changed: 106 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -883,142 +883,145 @@ soft_symbol_frame_begin(dsd_state* state) {
883883
}
884884

885885
/**
886-
* \brief Convert a soft symbol to Viterbi cost metric.
886+
* \brief Map log-likelihood ratio proxy to 16-bit Viterbi metric.
887887
*
888-
* For 4-level modulations (C4FM, GFSK, QPSK), each dibit encodes 2 bits.
889-
* The MSB (bit_position=0) determines upper/lower half (+3/+1 vs -1/-3).
890-
* The LSB (bit_position=1) determines inner/outer level (+3/-3 vs +1/-1).
888+
* This decoder family expects costs in [0, 65535], where:
889+
* - 0 = strong bit 0
890+
* - 65535 = strong bit 1
891+
* - ~32768 = uncertain
892+
*/
893+
static inline uint16_t
894+
llr_to_viterbi_cost(float llr_log_p0_over_p1) {
895+
/* Clamp LLR to keep expf numerically stable and preserve monotonicity. */
896+
if (llr_log_p0_over_p1 >= 16.0f) {
897+
return 0;
898+
}
899+
if (llr_log_p0_over_p1 <= -16.0f) {
900+
return 65535;
901+
}
902+
float p1 = 1.0f / (1.0f + expf(llr_log_p0_over_p1));
903+
long q = lrintf(p1 * 65535.0f);
904+
if (q < 0L) {
905+
q = 0L;
906+
}
907+
if (q > 65535L) {
908+
q = 65535L;
909+
}
910+
return (uint16_t)q;
911+
}
912+
913+
static inline float
914+
min_sq_dist2(float x, float a, float b) {
915+
float da = x - a;
916+
float db = x - b;
917+
float d2a = da * da;
918+
float d2b = db * db;
919+
return (d2a < d2b) ? d2a : d2b;
920+
}
921+
922+
/**
923+
* \brief Convert a soft 4-level symbol to a Viterbi cost metric.
891924
*
892-
* Returns 0x0000 for confident '0', 0xFFFF for confident '1', ~0x7FFF for uncertain.
925+
* For 4-level modulations (C4FM, GFSK, QPSK), each dibit encodes 2 bits.
926+
* We use a max-log MAP proxy:
927+
* LLR ~= (d1^2 - d0^2) / (2*sigma^2)
928+
* where d0/d1 are distances to nearest ideal levels for each bit hypothesis.
893929
*
894-
* M17 uses GFSK with dibit mapping: 01->+3, 00->+1, 10->-1, 11->-3
895-
* So for M17: MSB=0 means positive (>center), MSB=1 means negative (<center)
896-
* LSB=0 means inner (±1), LSB=1 means outer (±3)
930+
* This is more mathematically consistent than direct linear confidence maps,
931+
* while preserving the decoder's expected [0..65535] metric domain.
897932
*/
898933
uint16_t
899934
soft_symbol_to_viterbi_cost(float symbol, const dsd_state* state, int bit_position) {
935+
if (!state) {
936+
return 32768U;
937+
}
938+
939+
float min_val = state->min;
940+
float lmid = state->lmid;
900941
float center = state->center;
901942
float umid = state->umid;
902-
float lmid = state->lmid;
903943
float max_val = state->max;
904-
float min_val = state->min;
905-
float confidence;
906-
int bit_value;
907944

908-
// Compute span for normalization, guard against zero
909-
float span = max_val - min_val;
910-
if (span < 1e-6f) {
911-
span = 1.0f;
912-
}
913-
914-
if (bit_position == 0) {
915-
// MSB: determines if symbol is in upper half (0) or lower half (1)
916-
// For M17 GFSK: positive symbols -> bit 0, negative -> bit 1
917-
if (symbol > center) {
918-
bit_value = 0;
919-
// Confidence based on distance from center toward max
920-
confidence = (symbol - center) / (max_val - center + 1e-6f);
921-
} else {
922-
bit_value = 1;
923-
// Confidence based on distance from center toward min
924-
confidence = (center - symbol) / (center - min_val + 1e-6f);
925-
}
926-
} else {
927-
// LSB: determines inner (0) or outer (1) level
928-
// For M17 GFSK: ±1 -> bit 0, ±3 -> bit 1
929-
float abs_sym = fabsf(symbol - center);
930-
float mid_threshold = (fabsf(umid - center) + fabsf(lmid - center)) / 2.0f;
931-
932-
if (abs_sym < mid_threshold) {
933-
// Inner level (±1)
934-
bit_value = 0;
935-
confidence = (mid_threshold - abs_sym) / (mid_threshold + 1e-6f);
936-
} else {
937-
// Outer level (±3)
938-
bit_value = 1;
939-
confidence = (abs_sym - mid_threshold) / (span / 2.0f - mid_threshold + 1e-6f);
945+
/* Guard against degenerate/unsorted thresholds by synthesizing a symmetric set. */
946+
if (!(min_val < lmid && lmid < center && center < umid && umid < max_val)) {
947+
float span = max_val - min_val;
948+
if (span < 1e-3f) {
949+
span = 2.0f;
940950
}
951+
float half = span * 0.5f;
952+
min_val = center - half;
953+
max_val = center + half;
954+
lmid = center - (span / 6.0f);
955+
umid = center + (span / 6.0f);
941956
}
942957

943-
// Clamp confidence to [0, 1]
944-
if (confidence < 0.0f) {
945-
confidence = 0.0f;
946-
}
947-
if (confidence > 1.0f) {
948-
confidence = 1.0f;
958+
/* Estimate ideal 4-PAM levels from decision-region midpoints. */
959+
float n3 = 0.5f * (min_val + lmid);
960+
float n1 = 0.5f * (lmid + center);
961+
float p1 = 0.5f * (center + umid);
962+
float p3 = 0.5f * (umid + max_val);
963+
964+
/* Noise scale proxy: 4-PAM span is roughly 6*sigma in this normalized model. */
965+
float sigma = (max_val - min_val) / 6.0f;
966+
if (sigma < 1e-3f) {
967+
sigma = 1e-3f;
949968
}
969+
float inv_2sigma2 = 0.5f / (sigma * sigma);
950970

951-
// Map to Viterbi cost:
952-
// bit_value=0: confident -> 0x0000, uncertain -> 0x7FFF
953-
// bit_value=1: confident -> 0xFFFF, uncertain -> 0x7FFF
954-
uint16_t cost;
955-
if (bit_value == 0) {
956-
// Strong 0 = 0x0000, weak 0 = 0x7FFF
957-
cost = (uint16_t)((1.0f - confidence) * 32767.0f);
971+
bit_position &= 1;
972+
float d0 = 0.0f;
973+
float d1 = 0.0f;
974+
if (bit_position == 0) {
975+
/* MSB: positive cluster (0) vs negative cluster (1). */
976+
d0 = min_sq_dist2(symbol, p1, p3);
977+
d1 = min_sq_dist2(symbol, n1, n3);
958978
} else {
959-
// Strong 1 = 0xFFFF, weak 1 = 0x7FFF
960-
cost = (uint16_t)(32767.0f + confidence * 32768.0f);
979+
/* LSB: inner levels (0) vs outer levels (1). */
980+
d0 = min_sq_dist2(symbol, n1, p1);
981+
d1 = min_sq_dist2(symbol, n3, p3);
961982
}
962983

963-
return cost;
984+
float llr = (d1 - d0) * inv_2sigma2; /* log(P(bit=0)/P(bit=1)) proxy */
985+
return llr_to_viterbi_cost(llr);
964986
}
965987

966988
/**
967989
* GMSK (binary) soft symbol to Viterbi cost.
968990
*
969991
* For GMSK modulation where each symbol represents a single bit.
970-
* D-STAR uses GMSK where: symbol > center -> bit 1, symbol < center -> bit 0
971-
* Distance from center indicates confidence in the decision.
992+
* Uses the same max-log MAP proxy from distances to the two class means.
972993
*/
973994
uint16_t
974995
gmsk_soft_symbol_to_viterbi_cost(float symbol, const dsd_state* state) {
975-
float center = state->center;
976-
float max_val = state->max;
977-
float min_val = state->min;
978-
float confidence;
979-
int bit_value;
980-
981-
float upper_span = max_val - center;
982-
if (upper_span < 1e-6f) {
983-
upper_span = 1e-6f;
984-
}
985-
float lower_span = center - min_val;
986-
if (lower_span < 1e-6f) {
987-
lower_span = 1e-6f;
996+
if (!state) {
997+
return 32768U;
988998
}
989999

990-
// GMSK: above center = 1, below center = 0
991-
if (symbol > center) {
992-
bit_value = 1;
993-
// Confidence based on distance from center toward max
994-
confidence = (symbol - center) / upper_span;
995-
} else {
996-
bit_value = 0;
997-
// Confidence based on distance from center toward min
998-
confidence = (center - symbol) / lower_span;
999-
}
1000-
1001-
// Clamp confidence to [0, 1]
1002-
if (confidence < 0.0f) {
1003-
confidence = 0.0f;
1004-
}
1005-
if (confidence > 1.0f) {
1006-
confidence = 1.0f;
1000+
float center = state->center;
1001+
float min_val = state->min;
1002+
float max_val = state->max;
1003+
if (!(min_val < center && center < max_val)) {
1004+
float span = max_val - min_val;
1005+
if (span < 1e-3f) {
1006+
span = 2.0f;
1007+
}
1008+
float half = span * 0.5f;
1009+
min_val = center - half;
1010+
max_val = center + half;
10071011
}
10081012

1009-
// Map to Viterbi cost:
1010-
// bit_value=0: confident -> 0x0000, uncertain -> 0x7FFF
1011-
// bit_value=1: confident -> 0xFFFF, uncertain -> 0x7FFF
1012-
uint16_t cost;
1013-
if (bit_value == 0) {
1014-
// Strong 0 = 0x0000, weak 0 = 0x7FFF
1015-
cost = (uint16_t)((1.0f - confidence) * 32767.0f);
1016-
} else {
1017-
// Strong 1 = 0xFFFF, weak 1 = 0x7FFF
1018-
cost = (uint16_t)(32767.0f + confidence * 32768.0f);
1013+
float mu0 = 0.5f * (min_val + center); /* bit 0 hypothesis */
1014+
float mu1 = 0.5f * (center + max_val); /* bit 1 hypothesis */
1015+
float sigma = (max_val - min_val) / 4.0f;
1016+
if (sigma < 1e-3f) {
1017+
sigma = 1e-3f;
10191018
}
1019+
float inv_2sigma2 = 0.5f / (sigma * sigma);
10201020

1021-
return cost;
1021+
float d0 = symbol - mu0;
1022+
float d1 = symbol - mu1;
1023+
float llr = ((d1 * d1) - (d0 * d0)) * inv_2sigma2;
1024+
return llr_to_viterbi_cost(llr);
10221025
}
10231026

10241027
void

0 commit comments

Comments
 (0)