@@ -1354,6 +1354,8 @@ CodewordStatus decodeFixedFrame(const std::vector<float>& interleaved_soft, Code
13541354 decoder.setMinSumFactor (0 .9375f );
13551355 size_t bytes_per_cw = getBytesPerCodeword (rate);
13561356
1357+ int perturbation_cw_count = 0 ; // How many CWs needed perturbation retry
1358+
13571359 for (int cw = 0 ; cw < FIXED_FRAME_CODEWORDS; ++cw) {
13581360 auto cw_bits = cw_soft_bits[cw];
13591361
@@ -1375,6 +1377,7 @@ CodewordStatus decodeFixedFrame(const std::vector<float>& interleaved_soft, Code
13751377 auto decoded = decoder.decodeSoft (cw_bits);
13761378 bool success = decoder.lastDecodeSuccess ();
13771379 int iterations = decoder.lastIterations ();
1380+ bool used_perturbation = false ; // Track if this CW needed perturbation retry
13781381
13791382 // Multi-strategy LDPC retry when decode fails:
13801383 // Uses decoder diversity (varying min-sum factor) + LLR perturbation
@@ -1407,19 +1410,17 @@ CodewordStatus decodeFixedFrame(const std::vector<float>& interleaved_soft, Code
14071410 decoder.setMinSumFactor (0 .9375f ); // restore default
14081411 }
14091412
1410- // Phase 1: Perturbation with decoder diversity (15 attempts)
1411- // Alternate min-sum factor between attempts for maximum diversity.
1412- // Different seeds × different factors explore the solution space broadly.
1413+ // Phase 1: Perturbation with decoder diversity (5 attempts)
1414+ // With ARQ, fast failure + retransmit beats slow recovery.
1415+ // Excessive perturbation (was 44 attempts across 6 phases) caused:
1416+ // - 200ms per failed frame → decoder falls behind real-time
1417+ // - High LDPC false positive rate (random noise → wrong codewords)
1418+ // - 5-10s audio backlog → sync detection degradation
1419+ // Reduced to 5 perturbation attempts (was 34) + 4 factor retries = 9 total.
14131420 if (!success) {
1414- static constexpr float sigmas1[] = {
1415- 0 .3f , 0 .7f , 0 .3f , 1 .0f , 0 .5f , 1 .5f , 0 .3f , 2 .0f ,
1416- 0 .5f , 0 .7f , 1 .0f , 2 .5f , 0 .3f , 1 .5f , 0 .5f
1417- };
1418- static constexpr float factors1[] = {
1419- 0 .75f , 0 .625f , 0 .875f , 0 .75f , 0 .625f , 0 .75f , 0 .5f , 0 .625f ,
1420- 0 .875f , 0 .75f , 0 .625f , 0 .875f , 0 .75f , 0 .5f , 0 .625f
1421- };
1422- for (int retry = 0 ; retry < 15 && !success; retry++) {
1421+ static constexpr float sigmas1[] = {0 .3f , 0 .7f , 1 .0f , 1 .5f , 2 .0f };
1422+ static constexpr float factors1[] = {0 .75f , 0 .625f , 0 .875f , 0 .75f , 0 .625f };
1423+ for (int retry = 0 ; retry < 5 && !success; retry++) {
14231424 decoder.setMinSumFactor (factors1[retry]);
14241425 std::mt19937 rng (data_hash + retry * 997 + retry * 31 );
14251426 std::normal_distribution<float > noise (0 .0f , sigmas1[retry]);
@@ -1431,113 +1432,18 @@ CodewordStatus decodeFixedFrame(const std::vector<float>& interleaved_soft, Code
14311432 if (decoder.lastDecodeSuccess ()) {
14321433 success = true ;
14331434 iterations = decoder.lastIterations ();
1435+ used_perturbation = true ;
14341436 LOG_MODEM (INFO, " CW[%d]: RETRY OK (perturb σ=%.1f f=%.3f, iters=%d)" , cw, sigmas1[retry], factors1[retry], iterations);
14351437 }
14361438 }
14371439 decoder.setMinSumFactor (0 .875f );
14381440 }
1439-
1440- // Phase 2: Clip ±10 + perturbation (5 attempts)
1441- if (!success) {
1442- static constexpr float sigmas2[] = {0 .3f , 0 .8f , 1 .5f , 2 .5f , 4 .0f };
1443- for (int retry = 0 ; retry < 5 && !success; retry++) {
1444- decoder.setMinSumFactor (retry % 2 == 0 ? 0 .625f : 0 .875f );
1445- std::mt19937 rng (data_hash + (retry + 15 ) * 997 + 12345 );
1446- std::normal_distribution<float > noise (0 .0f , sigmas2[retry]);
1447- auto clipped = cw_bits;
1448- for (float & llr : clipped) {
1449- llr = std::max (-10 .0f , std::min (10 .0f , llr));
1450- llr += noise (rng);
1451- }
1452- decoded = decoder.decodeSoft (clipped);
1453- if (decoder.lastDecodeSuccess ()) {
1454- success = true ;
1455- iterations = decoder.lastIterations ();
1456- LOG_MODEM (INFO, " CW[%d]: RETRY OK (clip10+perturb σ=%.1f, iters=%d)" , cw, sigmas2[retry], iterations);
1457- }
1458- }
1459- decoder.setMinSumFactor (0 .875f );
1460- }
1461-
1462- // Phase 3: Scale 0.5× + perturbation (3 attempts)
1463- if (!success) {
1464- static constexpr float sigmas3[] = {0 .5f , 1 .5f , 3 .0f };
1465- for (int retry = 0 ; retry < 3 && !success; retry++) {
1466- std::mt19937 rng (data_hash + (retry + 20 ) * 997 + 54321 );
1467- std::normal_distribution<float > noise (0 .0f , sigmas3[retry]);
1468- auto scaled = cw_bits;
1469- for (float & llr : scaled) {
1470- llr = llr * 0 .5f + noise (rng);
1471- }
1472- decoded = decoder.decodeSoft (scaled);
1473- if (decoder.lastDecodeSuccess ()) {
1474- success = true ;
1475- iterations = decoder.lastIterations ();
1476- LOG_MODEM (INFO, " CW[%d]: RETRY OK (scale50+perturb σ=%.1f, iters=%d)" , cw, sigmas3[retry], iterations);
1477- }
1478- }
1479- }
1480-
1481- // Phase 4: Clip ±6 + perturbation (3 attempts)
1482- if (!success) {
1483- static constexpr float sigmas4[] = {0 .5f , 1 .5f , 3 .0f };
1484- for (int retry = 0 ; retry < 3 && !success; retry++) {
1485- std::mt19937 rng (data_hash + (retry + 23 ) * 997 + 99999 );
1486- std::normal_distribution<float > noise (0 .0f , sigmas4[retry]);
1487- auto clipped = cw_bits;
1488- for (float & llr : clipped) {
1489- llr = std::max (-6 .0f , std::min (6 .0f , llr));
1490- llr += noise (rng);
1491- }
1492- decoded = decoder.decodeSoft (clipped);
1493- if (decoder.lastDecodeSuccess ()) {
1494- success = true ;
1495- iterations = decoder.lastIterations ();
1496- LOG_MODEM (INFO, " CW[%d]: RETRY OK (clip6+perturb σ=%.1f, iters=%d)" , cw, sigmas4[retry], iterations);
1497- }
1498- }
1499- }
1500-
1501- // Phase 5: Hard decision + perturbation (5 attempts)
1502- if (!success) {
1503- static constexpr float sigmas5[] = {0 .0f , 0 .2f , 0 .5f , 1 .0f , 1 .5f };
1504- for (int retry = 0 ; retry < 5 && !success; retry++) {
1505- std::mt19937 rng (data_hash + (retry + 26 ) * 997 + 33333 );
1506- std::normal_distribution<float > noise (0 .0f , sigmas5[retry]);
1507- auto hard = cw_bits;
1508- for (float & llr : hard) {
1509- llr = (llr >= 0 ) ? 1 .0f : -1 .0f ;
1510- llr += noise (rng);
1511- }
1512- decoded = decoder.decodeSoft (hard);
1513- if (decoder.lastDecodeSuccess ()) {
1514- success = true ;
1515- iterations = decoder.lastIterations ();
1516- LOG_MODEM (INFO, " CW[%d]: RETRY OK (hard+perturb σ=%.1f, iters=%d)" , cw, sigmas5[retry], iterations);
1517- }
1518- }
1519- }
1520-
1521- // Phase 6: Scale 0.25× + perturbation (3 attempts)
1522- if (!success) {
1523- static constexpr float sigmas6[] = {0 .3f , 1 .0f , 2 .0f };
1524- for (int retry = 0 ; retry < 3 && !success; retry++) {
1525- std::mt19937 rng (data_hash + (retry + 31 ) * 997 + 77777 );
1526- std::normal_distribution<float > noise (0 .0f , sigmas6[retry]);
1527- auto scaled = cw_bits;
1528- for (float & llr : scaled) {
1529- llr = llr * 0 .25f + noise (rng);
1530- }
1531- decoded = decoder.decodeSoft (scaled);
1532- if (decoder.lastDecodeSuccess ()) {
1533- success = true ;
1534- iterations = decoder.lastIterations ();
1535- LOG_MODEM (INFO, " CW[%d]: RETRY OK (scale25+perturb σ=%.1f, iters=%d)" , cw, sigmas6[retry], iterations);
1536- }
1537- }
1538- }
1441+ // Phases 2-6 REMOVED (2026-03-15): excessive perturbation caused false
1442+ // positives and decoder backlog. ARQ handles frame loss more efficiently.
15391443 }
15401444
1445+ if (used_perturbation && success) perturbation_cw_count++;
1446+
15411447 LOG_MODEM (INFO, " CW[%d]: %s (iters=%d, llr_avg=%.2f, |llr|_avg=%.2f)" ,
15421448 cw, success ? " OK" : " FAIL" , iterations, llr_avg, llr_abs_avg);
15431449
@@ -1569,9 +1475,23 @@ CodewordStatus decodeFixedFrame(const std::vector<float>& interleaved_soft, Code
15691475 }
15701476
15711477 if (!frame_valid) {
1572- LOG_MODEM (WARN, " LDPC false positive detected: all CWs decoded but frame invalid" );
1478+ LOG_MODEM (WARN, " LDPC false positive detected: all CWs decoded but frame invalid (perturbed_cws=%d)" ,
1479+ perturbation_cw_count);
15731480 bool recovered = false ;
15741481
1482+ // If any CW used perturbation retry, the false positive is almost certainly
1483+ // from the random noise injection finding a wrong-but-valid LDPC codeword.
1484+ // Skip expensive bit-flip recovery — it can't fix random garbage and risks
1485+ // producing wrong "recovered" data that passes CRC by coincidence.
1486+ if (perturbation_cw_count > 0 ) {
1487+ LOG_MODEM (WARN, " LDPC false positive: %d CWs used perturbation, skipping recovery" ,
1488+ perturbation_cw_count);
1489+ for (int cw = 0 ; cw < FIXED_FRAME_CODEWORDS; ++cw) {
1490+ status.decoded [cw] = false ;
1491+ }
1492+ return status;
1493+ }
1494+
15751495 // Helper: verify assembled frame without logging
15761496 auto verifyFrame = [](const Bytes& assembled) -> bool {
15771497 if (assembled.empty ()) return false ;
0 commit comments