| 265 | } |
| 266 | |
| 267 | void VirtualAudioBridge::feedDaxAudio(int channel, const QByteArray& pcm) |
| 268 | { |
| 269 | // This slot may run on PanadapterStream's network thread via |
| 270 | // Qt::DirectConnection — keep it lock-free and free of Qt-thread-affine |
| 271 | // calls. The silence-timer stop and ring-buffer readPos snap that used |
| 272 | // to live here have moved to feedSilenceToAllChannels (GUI-thread tick), |
| 273 | // which observes m_lastAudioMs to decide when to self-stop. |
| 274 | if (channel < 1 || channel > NUM_CHANNELS) return; |
| 275 | |
| 276 | auto* block = m_blocks[channel - 1]; |
| 277 | if (!block) return; |
| 278 | |
| 279 | // Note "real audio just arrived" so the GUI-thread silence timer can |
| 280 | // stop itself on its next tick. No locks, no Qt API touched here. |
| 281 | m_lastAudioMs.store(QDateTime::currentMSecsSinceEpoch(), |
| 282 | std::memory_order_relaxed); |
| 283 | |
| 284 | // Input: float32 stereo PCM @ 24 kHz from the radio (native DAX rate). |
| 285 | // After the float32 migration (502b934, b0c49d7), both PCC_IF_NARROW |
| 286 | // and PCC_IF_NARROW_REDUCED emit float32 stereo from PanadapterStream. |
| 287 | // Output: float32 stereo @ 24 kHz into the ring buffer — direct 1:1 copy. |
| 288 | // HAL plugin also runs at 24 kHz, so no resampling needed. |
| 289 | const auto* samples = reinterpret_cast<const float*>(pcm.constData()); |
| 290 | const int numSamples = pcm.size() / static_cast<int>(sizeof(float)); // total float count (L,R,L,R,...) |
| 291 | |
| 292 | const float chGain = m_channelGain[channel - 1].load(std::memory_order_relaxed); |
| 293 | auto& stats = m_rxTiming[channel - 1]; |
| 294 | if (!stats.windowElapsed.isValid()) |
| 295 | stats.windowElapsed.start(); |
| 296 | |
| 297 | uint32_t wp = block->writePos.load(std::memory_order_relaxed); |
| 298 | |
| 299 | for (int i = 0; i < numSamples; ++i) { |
| 300 | block->ringBuffer[wp % DaxShmBlock::RING_SIZE] = samples[i] * chGain; |
| 301 | ++wp; |
| 302 | } |
| 303 | |
| 304 | block->writePos.store(wp, std::memory_order_release); |
| 305 | stats.writtenSamples += static_cast<uint64_t>(numSamples); |
| 306 | |
| 307 | uint32_t currentRp = block->readPos.load(std::memory_order_acquire); |
| 308 | uint32_t backlogSamples = wp - currentRp; |
| 309 | stats.peakBacklogSamples = std::max(stats.peakBacklogSamples, backlogSamples); |
| 310 | |
| 311 | if (backlogSamples > DaxShmBlock::RING_SIZE) |
| 312 | ++stats.overrunEvents; |
| 313 | |
| 314 | if (backlogSamples > kRxMaxBacklogSamples) { |
| 315 | const uint32_t newRp = wp - kRxTargetBacklogSamples; |
| 316 | if (newRp > currentRp) { |
| 317 | const uint32_t trimmedSamples = newRp - currentRp; |
| 318 | block->readPos.store(newRp, std::memory_order_release); |
| 319 | currentRp = newRp; |
| 320 | backlogSamples = wp - currentRp; |
| 321 | |
| 322 | stats.trimmedSamples += trimmedSamples; |
| 323 | ++stats.trimEvents; |
| 324 |
nothing calls this directly
no test coverage detected