| 880 | // ── Binary message handler (TX audio from TCI client) ─────────────────── |
| 881 | |
| 882 | void TciServer::onBinaryMessage(const QByteArray& data) |
| 883 | { |
| 884 | if (!m_audio) return; |
| 885 | if (data.size() < static_cast<int>(sizeof(TciAudioHeader))) return; |
| 886 | |
| 887 | // Parse header |
| 888 | TciAudioHeader hdr; |
| 889 | std::memcpy(&hdr, data.constData(), sizeof(hdr)); |
| 890 | |
| 891 | // Only accept TX_AUDIO_STREAM (type 2) |
| 892 | if (hdr.type != 2) return; |
| 893 | |
| 894 | const int payloadBytes = data.size() - static_cast<int>(sizeof(TciAudioHeader)); |
| 895 | if (payloadBytes <= 0) return; |
| 896 | |
| 897 | const char* payload = data.constData() + sizeof(TciAudioHeader); |
| 898 | |
| 899 | // ── Convert TX audio to float32 stereo ───────────────────────────────── |
| 900 | // WSJT-X channels field is garbage (FIFO reuse). readAudioData() writes |
| 901 | // hdr.length floats to data[0..length-1]. Take the first hdr.length floats. |
| 902 | QByteArray pcm; |
| 903 | |
| 904 | if (hdr.format == 3) { |
| 905 | int validFloats = static_cast<int>(hdr.length); |
| 906 | int availFloats = payloadBytes / static_cast<int>(sizeof(float)); |
| 907 | if (validFloats > availFloats) validFloats = availFloats; |
| 908 | if (validFloats <= 0) return; |
| 909 | |
| 910 | pcm = QByteArray(payload, |
| 911 | validFloats * static_cast<int>(sizeof(float))); |
| 912 | } else if (hdr.format == 0) { |
| 913 | int validSamples = static_cast<int>(hdr.length); |
| 914 | int availSamples = payloadBytes / static_cast<int>(sizeof(qint16)); |
| 915 | if (validSamples > availSamples) validSamples = availSamples; |
| 916 | if (validSamples <= 0) return; |
| 917 | |
| 918 | auto* src = reinterpret_cast<const qint16*>(payload); |
| 919 | pcm.resize(validSamples * static_cast<int>(sizeof(float))); |
| 920 | auto* dst = reinterpret_cast<float*>(pcm.data()); |
| 921 | for (int i = 0; i < validSamples; ++i) |
| 922 | dst[i] = src[i] / 32768.0f; |
| 923 | } |
| 924 | |
| 925 | if (pcm.isEmpty()) return; |
| 926 | |
| 927 | int inputFramesSrcRate = 0; // input frames at the client-declared rate (#3914) |
| 928 | bool duplicatedStereo = false; |
| 929 | |
| 930 | // ─── TX resampling: client-declared rate → 24kHz (radio native DAX) ── |
| 931 | // Resample from the rate the client declared in THIS frame (hdr.sampleRate), |
| 932 | // not a hardcoded 48k. WSJT-X sends 48 kHz — the common path, unchanged — but |
| 933 | // a client that negotiated 8/12/24 kHz (audio_samplerate) sends at that rate |
| 934 | // and must be resampled from it, or every tone is mis-pitched and digital |
| 935 | // decodes fail (#3306). Rebuild the per-session resampler only if the |
| 936 | // declared rate changes (rare, mid-stream); a 24 kHz client gets a 1:1 |
| 937 | // resampler so the mono/stereo canonicalization below still runs. |
| 938 | { |
| 939 | const int declaredRate = static_cast<int>(hdr.sampleRate); |
nothing calls this directly
no test coverage detected