| 5736 | // ─── TX stream ──────────────────────────────────────────────────────────────── |
| 5737 | |
| 5738 | bool AudioEngine::startTxStream(const QHostAddress& radioAddress, quint16 radioPort) |
| 5739 | { |
| 5740 | if (m_audioSource) return true; // already running |
| 5741 | |
| 5742 | // WASAPI silent-open recovery (#2929). If the previous open was driven |
| 5743 | // by the silence watchdog, m_txForceMonoOnNextOpen is true; consume it |
| 5744 | // here. A fresh (non-watchdog) start re-enables the one-shot retry budget. |
| 5745 | const bool isWatchdogRetry = m_txForceMonoOnNextOpen; |
| 5746 | m_txForceMonoOnNextOpen = false; |
| 5747 | if (!isWatchdogRetry) { |
| 5748 | m_txSilenceRetryDone = false; |
| 5749 | } |
| 5750 | m_txReceivedAnyBytes = false; |
| 5751 | |
| 5752 | m_txAddress = radioAddress; |
| 5753 | m_txPort = radioPort; |
| 5754 | m_txPacketCount = 0; |
| 5755 | m_txAccumulator.clear(); |
| 5756 | m_txMicChannelState.reset(); |
| 5757 | m_lastTxMicChannelLog.invalidate(); |
| 5758 | |
| 5759 | // TX mic capture uses Int16 — we convert to float32 after capture. |
| 5760 | // (makeFormat() returns Float for the RX sink, but mic hardware is Int16.) |
| 5761 | QAudioFormat fmt; |
| 5762 | fmt.setSampleRate(DEFAULT_SAMPLE_RATE); |
| 5763 | fmt.setChannelCount(2); |
| 5764 | fmt.setSampleFormat(QAudioFormat::Int16); |
| 5765 | QAudioDevice dev = QMediaDevices::defaultAudioInput(); |
| 5766 | bool txFallbackOccurred = false; |
| 5767 | QStringList txFallbackReasons; |
| 5768 | QStringList txFormatAttempts; |
| 5769 | const auto noteTxFallback = [&txFallbackOccurred, &txFallbackReasons](const QString& reason) { |
| 5770 | txFallbackOccurred = true; |
| 5771 | if (!reason.isEmpty() && !txFallbackReasons.contains(reason)) { |
| 5772 | txFallbackReasons << reason; |
| 5773 | } |
| 5774 | }; |
| 5775 | const auto noteTxAttempt = [&txFormatAttempts](const QAudioFormat& format) { |
| 5776 | const QString attempt = formatAudioAttempt(format.sampleRate(), |
| 5777 | format.channelCount(), |
| 5778 | format.sampleFormat()); |
| 5779 | if (!txFormatAttempts.contains(attempt)) { |
| 5780 | txFormatAttempts << attempt; |
| 5781 | } |
| 5782 | }; |
| 5783 | if (!m_inputDevice.isNull()) { |
| 5784 | const auto inputs = QMediaDevices::audioInputs(); |
| 5785 | if (devicePresent(inputs, m_inputDevice)) { |
| 5786 | dev = m_inputDevice; |
| 5787 | } else { |
| 5788 | qCWarning(lcAudio) << "AudioEngine: saved input device is unavailable, using the system default input instead"; |
| 5789 | noteTxFallback(QStringLiteral("saved input unavailable -> system default")); |
| 5790 | m_inputDevice = QAudioDevice{}; |
| 5791 | } |
| 5792 | } |
| 5793 | |
| 5794 | if (dev.isNull()) { |
| 5795 | qCWarning(lcAudio) << "AudioEngine: no audio input device available"; |
no test coverage detected