| 860 | // ─── FFT decode ────────────────────────────────────────────────────────────── |
| 861 | |
| 862 | void PanadapterStream::decodeFFT(const uchar* raw, int totalBytes, bool hasTrailer, quint32 streamId) |
| 863 | { |
| 864 | static constexpr int FFT_SUBHEADER_BYTES = 12; |
| 865 | if (totalBytes < VITA49_HEADER_BYTES + FFT_SUBHEADER_BYTES) return; |
| 866 | |
| 867 | const uchar* sub = raw + VITA49_HEADER_BYTES; |
| 868 | const quint16 startBin = qFromBigEndian<quint16>(sub + 0); |
| 869 | const quint16 numBins = qFromBigEndian<quint16>(sub + 2); |
| 870 | const quint16 binSize = qFromBigEndian<quint16>(sub + 4); |
| 871 | const quint16 totalBins = qFromBigEndian<quint16>(sub + 6); |
| 872 | const quint32 frameIndex = qFromBigEndian<quint32>(sub + 8); |
| 873 | |
| 874 | if (numBins == 0 || binSize == 0 || totalBins == 0) return; |
| 875 | |
| 876 | const int binDataOffset = VITA49_HEADER_BYTES + FFT_SUBHEADER_BYTES; |
| 877 | int binDataBytes = numBins * binSize; |
| 878 | const int available = totalBytes - binDataOffset - (hasTrailer ? 4 : 0); |
| 879 | if (available < binDataBytes) { |
| 880 | binDataBytes = available; |
| 881 | if (binDataBytes <= 0) return; |
| 882 | } |
| 883 | |
| 884 | const uchar* binData = raw + binDataOffset; |
| 885 | |
| 886 | // Per-stream frame assembly |
| 887 | auto& frame = m_frames[streamId]; |
| 888 | if (frameIndex != frame.frameIndex) { |
| 889 | if (frame.totalBins > 0 && !frame.isComplete()) |
| 890 | PerfTelemetry::instance().recordFrameRestart(PerfTelemetry::FrameKind::Panadapter); |
| 891 | frame.reset(frameIndex, totalBins); |
| 892 | } |
| 893 | |
| 894 | if (startBin + numBins > static_cast<quint16>(frame.buf.size())) |
| 895 | return; |
| 896 | |
| 897 | for (quint16 i = 0; i < numBins; ++i) |
| 898 | frame.buf[startBin + i] = qFromBigEndian<quint16>(binData + i * 2); |
| 899 | |
| 900 | frame.binsReceived += numBins; |
| 901 | |
| 902 | if (!frame.isComplete()) return; |
| 903 | |
| 904 | // Convert to dBm using per-stream range |
| 905 | QPair<float,float> dbmRange; |
| 906 | int yPixVal; |
| 907 | { |
| 908 | QMutexLocker lock(&m_streamMutex); |
| 909 | dbmRange = m_dbmRanges.value(streamId, {-130.0f, -40.0f}); |
| 910 | yPixVal = m_yPixels.value(streamId, 700); |
| 911 | } |
| 912 | auto [minDbm, maxDbm] = dbmRange; |
| 913 | const float range = maxDbm - minDbm; |
| 914 | if (range <= 0.0f) return; |
| 915 | |
| 916 | const int count = frame.buf.size(); |
| 917 | QVector<float> bins(count); |
| 918 | |
| 919 | int effectiveYPixels = std::max(yPixVal, 2); |
nothing calls this directly
no test coverage detected