swift
afvclient.cpp
1 // SPDX-FileCopyrightText: Copyright (C) 2019 swift Project Community / Contributors
2 // SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
3 
5 
6 #ifdef Q_OS_WIN
7 # include "comdef.h"
8 #endif
9 
10 #include "core/application.h"
16 #include "misc/stringutils.h"
17 #include "misc/threadutils.h"
18 #include "misc/verify.h"
19 #include "sound/audioutilities.h"
20 
21 using namespace swift::core::context;
22 using namespace swift::core::afv::audio;
23 using namespace swift::core::afv::connection;
24 using namespace swift::misc;
25 using namespace swift::misc::audio;
26 using namespace swift::misc::physical_quantities;
27 using namespace swift::misc::simulation;
28 using namespace swift::misc::aviation;
29 using namespace swift::sound;
30 using namespace swift::sound::sample_provider;
31 
32 namespace swift::core::afv::clients
33 {
34  const QStringList &CAfvClient::getLogCategories()
35  {
37  return cats;
38  }
39 
40  CAfvClient::CAfvClient(const QString &apiServer, QObject *owner)
41  : CContinuousWorker(owner, "CAfvClient"), CIdentifiable("CAfvClient"),
42  m_connection(new CClientConnection(apiServer, this)), m_input(new CInput(SampleRate, this)),
43  m_output(new COutput(this)), m_voiceServerTimer(new QTimer(this))
44  {
45  this->setObjectName("AFV client: " + apiServer);
46  m_connection->setReceiveAudio(false);
47 
48  connect(m_input, &CInput::opusDataAvailable, this, &CAfvClient::opusDataAvailable);
49  connect(m_input, &CInput::inputVolumeStream, this, &CAfvClient::inputVolumeStream);
50 
51  connect(m_output, &COutput::outputVolumeStream, this, &CAfvClient::outputVolumeStream);
52  connect(m_connection, &CClientConnection::audioReceived, this, &CAfvClient::audioOutDataAvailable);
53  connect(m_voiceServerTimer, &QTimer::timeout, this, &CAfvClient::onTimerUpdate);
54 
55  // deferred init - use swift::misc:: singleShot to call in correct thread, "myself" NOT needed
56  swift::misc::singleShot(1000, this, [=, this] { this->deferredInit(); });
57  }
58 
60  {
61  QMutexLocker lock(&m_mutexCallsign);
62  return m_callsign;
63  }
64 
65  void CAfvClient::setCallsign(const QString &callsign)
66  {
67  QMutexLocker lock(&m_mutexCallsign);
68  m_callsign = callsign;
69  }
70 
72  {
73  QMutexLocker lock(&m_mutexConnection);
74  return m_connection->isConnected();
75  }
76 
77  void CAfvClient::initTransceivers()
78  {
79  {
80  QMutexLocker lock(&m_mutexTransceivers);
81  m_transceivers = { { 0, UniCom, 48.5, 11.5, 1000.0, 1000.0 }, { 1, UniCom, 48.5, 11.5, 1000.0, 1000.0 } };
82 
83  m_enabledTransceivers = { 0, 1 };
84  m_transmittingTransceivers = { { 0 } }; // TxTransceiverDto
85  }
86 
87  // init with context values
88  this->connectWithContexts();
89 
90  // update from context
91  this->onTimerUpdate();
92  }
93 
94  void CAfvClient::connectWithContexts()
95  {
96  if (m_connectedWithContext) { return; }
97  if (!hasContexts()) { return; }
98 
99  // Disconnect all previously connect signals between the AfvClient and the required contexts
100  for (auto *context : QVector<QObject *> { sApp->getIContextOwnAircraft(), sApp->getIContextNetwork() })
101  {
102  this->disconnect(context);
103  context->disconnect(this);
104  }
105 
106  connect(sApp->getIContextOwnAircraft(), &IContextOwnAircraft::changedAircraftCockpit, this,
107  &CAfvClient::onUpdateTransceiversFromContext, Qt::QueuedConnection);
108  connect(sApp->getIContextNetwork(), &IContextNetwork::muteRequestReceived, this,
109  &CAfvClient::toggleTransmissionCapability, Qt::QueuedConnection);
110 
111  m_connectedWithContext = true;
112  }
113 
114  void CAfvClient::fetchSimulatorSettings()
115  {
116  // call that in correct thread
117  if (!hasContexts()) { return; }
118 
120  {
121  // Method needs to be executed in the context thread
122  QPointer<CAfvClient> myself(this);
124  if (myself) { this->fetchSimulatorSettings(); }
125  });
126  return;
127  }
128 
129  const bool integrated = sApp->getIContextSimulator()->getSimulatorSettings().isComIntegrated();
130  const bool changed = integrated != m_integratedComUnit;
131 
132  m_integratedComUnit = integrated;
133  if (changed) { emit this->updatedFromOwnAircraftCockpit(); }
134  }
135 
136  void CAfvClient::connectTo(const QString &cid, const QString &password, const QString &callsign,
137  const QString &client)
138  {
139  if (QThread::currentThread() != thread())
140  {
141  // Method needs to be executed in the object thread since it will create new QObject children
142  QPointer<CAfvClient> myself(this);
143  QMetaObject::invokeMethod(this, [=, this]() {
144  if (myself) { connectTo(cid, password, callsign, client); }
145  });
146  return;
147  }
148 
149  // called in CAfvClient thread
150  this->connectWithContexts();
151  this->setCallsign(callsign);
152 
153  QPointer<CAfvClient> myself(this);
154  if (!this->isConnected() && m_retryConnectAttempt == 0)
155  {
156  // check if connect simply did NOT receive an answer
157  QTimer::singleShot(20 * 1000, this, [=, this] {
158  if (!myself) { return; }
159  if (m_retryConnectAttempt > 0) { return; } // already handled
160 
161  // this will reconnect ONLY if not already connected
162  this->retryConnectTo(cid, password, callsign, client, QStringLiteral("No connection afer 20secs"));
163  });
164  }
165 
166  // thread safe connect
167  {
168  QMutexLocker lock(&m_mutexConnection);
169 
170  // async connection
171  m_connection->connectTo(
172  cid, password, callsign, client,
173  { // this is the callback when the connection has been established
174  this, [=, this](bool authenticated) {
175  if (!myself) { return; } // cppcheck-suppress knownConditionTrueFalse
176 
177  // HF stations aliased
178  const QVector<StationDto> aliasedStations = m_connection->getAllAliasedStations();
179  this->setAliasedStations(aliasedStations); // threadsafe
180  this->onTimerUpdate();
181 
182  // const bool isConnected = this->isConnected(); // threadsafe
183  if (authenticated)
184  {
185  // restart timer, normally it should be started already, paranoia
186  // as I run in "my thread" starting timer should be OK
187  {
188  QMutexLocker lock2(&m_mutex);
189  if (m_voiceServerTimer) { m_voiceServerTimer->start(PositionUpdatesMs); }
190  }
191  m_retryConnectAttempt = 0;
192  emit this->connectionStatusChanged(Connected);
193  }
194  else
195  {
196  myself->retryConnectTo(
197  cid, password, callsign, client,
198  QStringLiteral("AFV authentication failed for '%1' callsign '%2'").arg(cid, callsign));
199  emit this->connectionStatusChanged(Disconnected);
200  }
201  } });
202  }
203  }
204 
205  void CAfvClient::disconnectFrom(bool stop)
206  {
207  if (QThread::currentThread() != thread())
208  {
209  // Method needs to be executed in the object thread since it will create new QObject children
210  QPointer<CAfvClient> myself(this);
211  QMetaObject::invokeMethod(this, [=, this]() {
212  if (myself) { disconnectFrom(stop); }
213  });
214  return;
215  }
216 
217  // we intentionally DO NOT STOP the timer here, but keep it for preset (own aircraft pos.)
218  // threadsafe
219  {
220  QMutexLocker lock(&m_mutexConnection);
221  m_connection->disconnectFrom();
222  }
223 
224  m_heartBeatFailures = 0;
225  m_retryConnectAttempt = 0;
226  m_fsdConnectMismatches = 0;
227 
228  emit connectionStatusChanged(Disconnected);
229 
230  if (stop) { this->stopAudio(); }
231  }
232 
233  QStringList CAfvClient::availableInputDevices() const
234  {
235  return CAudioDeviceInfoList::allInputDevices().getDeviceNames();
236  }
237 
238  QStringList CAfvClient::availableOutputDevices() const
239  {
240  return CAudioDeviceInfoList::allOutputDevices().getDeviceNames();
241  }
242 
243  void CAfvClient::setBypassEffects(bool value)
244  {
245  QMutexLocker lock(&m_mutexSampleProviders);
246  if (m_soundcardSampleProvider) { m_soundcardSampleProvider->setBypassEffects(value); }
247  }
248 
249  bool CAfvClient::isOutputMuted() const
250  {
251  const int v = this->getNormalizedMasterOutputVolume();
252  return v < 1;
253  }
254 
255  void CAfvClient::setOutputMuted(bool mute)
256  {
257  if (this->isOutputMuted() == mute) { return; }
258  this->setNormalizedMasterOutputVolume(mute ? 0 : 50);
259  emit this->changedOutputMute(mute);
260  }
261 
262  void CAfvClient::startAudio()
263  {
264  const CAudioDeviceInfo inputDevice = this->getInputDevice();
265  const CAudioDeviceInfo outputDevice = this->getOutputDevice();
266  this->startAudio(inputDevice, outputDevice);
267  }
268 
269  void CAfvClient::startAudio(const CAudioDeviceInfo &inputDevice, const CAudioDeviceInfo &outputDevice)
270  {
271  if (QThread::currentThread() != this->thread())
272  {
273  // Method needs to be executed in the object thread since it will create new QObject children
274  QPointer<CAfvClient> myself(this);
275  QMetaObject::invokeMethod(this, [=, this]() {
276  if (myself) { startAudio(inputDevice, outputDevice); }
277  });
278  return;
279  }
280 
281  const CAudioDeviceInfo useInputDevice =
282  inputDevice.isValid() ? inputDevice : CAudioDeviceInfo::getDefaultInputDevice();
283  const CAudioDeviceInfo useOutputDevice =
284  outputDevice.isValid() ? outputDevice : CAudioDeviceInfo::getDefaultOutputDevice();
285 
286  if (!useOutputDevice.isValid() || !useOutputDevice.isOutputDevice())
287  {
288  CLogMessage(this).error(u"No audio output device found. Audio For VATSIM (AFV) "
289  "is disabled");
290  return;
291  }
292 
293  // The input device is allowed to be invalid as AFV is also usable with "receive only".
294 
295  if (m_isStarted)
296  {
297  if (this->usesSameDevices(useInputDevice, useOutputDevice))
298  {
299  CLogMessage(this).info(u"Client already started for '%1'/'%2'")
300  << useInputDevice.getName() << useOutputDevice.getName();
301  return;
302  }
303  this->stopAudio();
304  }
305 
306  this->initTransceivers();
307 
308  // threadsafe block
309  {
310  // lock block 1
311  {
312  QMutexLocker lock { &m_mutexSampleProviders };
313  if (m_soundcardSampleProvider)
314  {
315  m_soundcardSampleProvider->disconnect();
316  m_soundcardSampleProvider->deleteLater();
317  }
318  m_soundcardSampleProvider = new CSoundcardSampleProvider(SampleRate, allTransceiverIds(), this);
319  connect(m_soundcardSampleProvider, &CSoundcardSampleProvider::receivingCallsignsChanged, this,
320  &CAfvClient::onReceivingCallsignsChanged);
321 
322  if (m_outputSampleProvider) { m_outputSampleProvider->deleteLater(); }
323  m_outputSampleProvider = new CVolumeSampleProvider(m_soundcardSampleProvider, this);
324  // m_outputSampleProvider->setGainRatio(outputVolume); // 2021-09 LT Disabled. Output volume is
325  // controlled independently for COM1/2
326  }
327 
328  // lock block 2
329  {
330  QMutexLocker lock(&m_mutex);
331 
332  m_output->start(useOutputDevice, m_outputSampleProvider);
333 
334  if (useInputDevice.getType() != CAudioDeviceInfo::Unknown) { m_input->start(useInputDevice); }
335  else
336  {
337  CLogMessage(this).warning(
338  u"No audio input device was found. Audio for VATSIM (AFV) will run in receive-only mode.");
339  }
340 
341  // runs in correct thread
342  m_voiceServerTimer->start(PositionUpdatesMs); // start for preset values
343  }
344  }
345 
346  this->setReceiveAudio(true); // threadsafe
347 
348  this->onSettingsChanged(); // make sure all settings are applied
349  m_isStarted = true;
350  CLogMessage(this).info(u"Started [Input: %1] [Output: %2]")
351  << useInputDevice.getName() << useOutputDevice.getName();
352 
353  this->onTimerUpdate(); // update values
354 
355  emit this->startedAudio(useInputDevice, useOutputDevice);
356 
357  if (this->isOutputMuted())
358  {
359  // un-mute after startup
360  this->setOutputMuted(false);
361  }
362  }
363 
364  void CAfvClient::startAudio(const QString &inputDeviceName, const QString &outputDeviceName)
365  {
366  const CAudioDeviceInfo i = CAudioDeviceInfoList::allInputDevices().findByName(inputDeviceName);
367  const CAudioDeviceInfo o = CAudioDeviceInfoList::allOutputDevices().findByName(outputDeviceName);
368  this->startAudio(i, o);
369  }
370 
371  void CAfvClient::stopAudio()
372  {
373  if (QThread::currentThread() != this->thread())
374  {
375  // Method needs to be executed in the object thread since it will create new QObject children
376  QPointer<CAfvClient> myself(this);
377  QMetaObject::invokeMethod(this, [=, this]() {
378  if (myself) stopAudio();
379  });
380  return;
381  }
382 
383  if (!m_isStarted)
384  {
385  CLogMessage(this).info(u"Client was NOT started, not stopping!");
386  return;
387  }
388 
389  m_isStarted = false;
390  this->setReceiveAudio(false); // threadsafe
391 
392  // stop input/output
393  {
394  QMutexLocker lock { &m_mutex };
395  m_input->stop();
396  m_output->stop();
397  }
398  CLogMessage(this).info(u"AFV Client stopped");
399 
400  if (this->isOutputMuted()) { this->setOutputMuted(false); }
401 
402  emit this->inputVolumePeakVU(0.0);
403  emit this->outputVolumePeakVU(0.0);
404  emit this->stoppedAudio();
405  }
406 
407  void CAfvClient::restartAudio()
408  {
409  if (!m_isStarted)
410  {
411  // just need to start
412  this->startAudio();
413  return;
414  }
415 
416  this->stopAudio();
417  QPointer<CAfvClient> myself(this);
418  QTimer::singleShot(1000, this, [=] {
419  if (myself) { myself->startAudio(); }
420  });
421  }
422 
423  /* disabled because NOT used
424  double CAfvClient::getDeviceInputVolume() const
425  {
426  if (m_input) { return m_input->getDeviceInputVolume(); }
427  return 0;
428  }
429 
430  bool CAfvClient::setDeviceInputVolume(double volume)
431  {
432  if (m_input) { return m_input->setDeviceInputVolume(volume); }
433  return false;
434  }
435 
436  double CAfvClient::getDeviceOutputVolume() const
437  {
438  if (m_output) { return m_output->getDeviceOutputVolume(); }
439  return 0;
440  }
441 
442  bool CAfvClient::setDeviceOutputVolume(double volume)
443  {
444  if (m_output) { return m_output->setDeviceOutputVolume(volume); }
445  return false;
446  }
447  */
448 
449  void CAfvClient::setReceiveAudio(bool receive)
450  {
451  QMutexLocker lock(&m_mutexConnection);
452  if (!m_connection) { return; }
453  m_connection->setReceiveAudio(receive);
454  }
455 
456  void CAfvClient::enableTransceiver(quint16 id, bool enable)
457  {
458  {
459  QMutexLocker lock(&m_mutexTransceivers);
460  if (enable) { m_enabledTransceivers.insert(id); }
461  else { m_enabledTransceivers.remove(id); }
462  }
463 
464  this->updateTransceivers();
465  }
466 
467  void CAfvClient::enableComUnit(CComSystem::ComUnit comUnit, bool enable)
468  {
469  this->enableTransceiver(comUnitToTransceiverId(comUnit), enable);
470  }
471 
472  bool CAfvClient::isEnabledTransceiver(quint16 id) const
473  {
474  // we double check, enabled and exist!
475  const auto enabledTransceivers = this->getEnabledTransceivers(); // threadsafe
476  if (!enabledTransceivers.contains(id)) { return false; }
477 
478  const auto transceivers = this->getTransceivers(); // threadsafe
479  return std::any_of(transceivers.cbegin(), transceivers.cend(),
480  [&id](const TransceiverDto &dto) { return dto.id == id; });
481  }
482 
483  bool CAfvClient::isEnabledComUnit(CComSystem::ComUnit comUnit) const
484  {
485  return this->isEnabledTransceiver(comUnitToTransceiverId(comUnit));
486  }
487 
488  void CAfvClient::updateComFrequency(quint16 id, quint32 frequencyHz)
489  {
490  if (id != 0 && id != 1) { return; }
491 
492  // Fix rounding issues like 128074999 Hz -> 128075000 Hz
493  quint32 roundedFrequencyHz = static_cast<quint32>(qRound(frequencyHz / 1000.0)) * 1000;
494  roundedFrequencyHz = this->getAliasFrequencyHz(roundedFrequencyHz);
495 
496  bool update = false;
497  {
498  QMutexLocker lockTransceivers(&m_mutexTransceivers);
499  if (m_transceivers.size() >= id + 1)
500  {
501  if (m_transceivers[id].frequencyHz != roundedFrequencyHz)
502  {
503  update = true;
504  m_transceivers[id].frequencyHz = roundedFrequencyHz;
505  }
506  }
507  }
508 
509  // outside lock to avoid deadlock
510  if (update)
511  {
512  this->updateTransceivers(false); // no frequency update
513  }
514  }
515 
516  void CAfvClient::updateComFrequency(CComSystem::ComUnit comUnit, const CFrequency &comFrequency)
517  {
518  const auto freqHz = static_cast<quint32>(comFrequency.valueInteger(CFrequencyUnit::Hz()));
519  this->updateComFrequency(comUnitToTransceiverId(comUnit), freqHz);
520  }
521 
522  void CAfvClient::updateComFrequency(CComSystem::ComUnit comUnit, const CComSystem &comSystem)
523  {
524  this->updateComFrequency(comUnit, comSystem.getFrequencyActive());
525  }
526 
527  void CAfvClient::updatePosition(double latitudeDeg, double longitudeDeg, double heightMeters)
528  {
529  QMutexLocker lock(&m_mutexTransceivers);
530  for (TransceiverDto &transceiver : m_transceivers)
531  {
532  transceiver.LatDeg = latitudeDeg;
533  transceiver.LonDeg = longitudeDeg;
534  transceiver.HeightAglM = heightMeters;
535  transceiver.HeightMslM = heightMeters;
536  }
537  }
538 
539  void CAfvClient::updateTransceivers(bool updateFrequencies)
540  {
541  // also update if NOT connected, values will be preset
542 
543  if (hasContexts())
544  {
546  this->updatePosition(ownAircraft.latitude().value(CAngleUnit::deg()),
547  ownAircraft.longitude().value(CAngleUnit::deg()),
548  ownAircraft.getAltitude().value(CLengthUnit::ft()));
549 
550  if (updateFrequencies)
551  {
552  this->updateComFrequency(CComSystem::Com1, ownAircraft.getCom1System());
553  this->updateComFrequency(CComSystem::Com2, ownAircraft.getCom2System());
554  }
555  }
556 
557  // threadsafe copies
558  const auto transceivers = this->getTransceivers();
559  const auto enabledTransceivers = this->getEnabledTransceivers();
560  const QString callsign = this->getCallsign(); // threadsafe
561 
562  // transceivers
563  QVector<TransceiverDto> newEnabledTransceivers;
564  for (const TransceiverDto &transceiver : transceivers)
565  {
566  if (enabledTransceivers.contains(transceiver.id)) { newEnabledTransceivers.push_back(transceiver); }
567  }
568 
569  // in connection and soundcard only use the enabled transceivers
570  {
571  QMutexLocker lock(&m_mutexConnection);
572  if (m_connection) { m_connection->updateTransceivers(callsign, newEnabledTransceivers); }
573  }
574  {
575  QMutexLocker lock(&m_mutexSampleProviders);
576  if (m_soundcardSampleProvider)
577  {
578  m_soundcardSampleProvider->updateRadioTransceivers(newEnabledTransceivers);
579  }
580  }
581  }
582 
583  void CAfvClient::setTransmittingTransceiver(quint16 transceiverID)
584  {
585  const TxTransceiverDto tx = { transceiverID };
586  this->setTransmittingTransceivers({ tx });
587  }
588 
589  void CAfvClient::setTransmittingComUnit(CComSystem::ComUnit comUnit)
590  {
591  this->setTransmittingTransceiver(comUnitToTransceiverId(comUnit));
592  }
593 
594  void CAfvClient::setTransmittingTransceivers(const QVector<TxTransceiverDto> &transceivers)
595  {
596  QMutexLocker lock(&m_mutexTransceivers);
597  m_transmittingTransceivers = transceivers;
598  }
599 
600  bool CAfvClient::isTransmittingTransceiver(quint16 id) const
601  {
602  QMutexLocker lock(&m_mutexTransceivers);
603  return std::any_of(m_transmittingTransceivers.cbegin(), m_transmittingTransceivers.cend(),
604  [&id](const TxTransceiverDto &dto) { return dto.id == id; });
605  }
606 
607  bool CAfvClient::isTransmittingComUnit(CComSystem::ComUnit comUnit) const
608  {
609  return this->isTransmittingTransceiver(comUnitToTransceiverId(comUnit));
610  }
611 
612  void CAfvClient::setRxTx(bool rx1, bool tx1, bool rx2, bool tx2)
613  {
615  if (tx1)
616  {
617  const TxTransceiverDto tx = { comUnitToTransceiverId(CComSystem::Com1) };
618  txs.push_back(tx);
619  }
620  if (tx2)
621  {
622  const TxTransceiverDto tx = { comUnitToTransceiverId(CComSystem::Com2) };
623  txs.push_back(tx);
624  }
625  this->setTransmittingTransceivers(txs);
626 
627  QSet<quint16> enabledTransceivers;
628  if (rx1 || tx1) { enabledTransceivers.insert(comUnitToTransceiverId(CComSystem::Com1)); }
629 
630  if (rx2 || tx2) { enabledTransceivers.insert(comUnitToTransceiverId(CComSystem::Com2)); }
631 
632  {
633  QMutexLocker lock(&m_mutexTransceivers);
634  m_enabledTransceivers = enabledTransceivers;
635  }
636 
637  // force update
638  this->onTimerUpdate();
639  }
640 
641  void CAfvClient::getRxTx(bool &rx1, bool &tx1, bool &rx2, bool &tx2) const
642  {
643  rx1 = false;
644  rx2 = false;
645  tx1 = false;
646  tx2 = false;
647 
648  const QSet<quint16> enabled = getEnabledTransceivers();
649  rx1 = enabled.contains(comUnitToTransceiverId(CComSystem::Com1));
650  rx2 = enabled.contains(comUnitToTransceiverId(CComSystem::Com2));
651 
652  const QVector<TxTransceiverDto> transmits = getTransmittingTransceivers();
653  for (const TxTransceiverDto &dto : transmits)
654  {
655  if (dto.id == comUnitToTransceiverId(CComSystem::Com1)) { tx1 = true; }
656  if (dto.id == comUnitToTransceiverId(CComSystem::Com2)) { tx2 = true; }
657  }
658  }
659 
660  QVector<TransceiverDto> CAfvClient::getTransceivers() const
661  {
662  QMutexLocker lock(&m_mutexTransceivers);
663  return m_transceivers;
664  }
665 
666  QSet<quint16> CAfvClient::getEnabledTransceivers() const
667  {
668  QMutexLocker lock(&m_mutexTransceivers);
669  return m_enabledTransceivers;
670  }
671 
672  QVector<TxTransceiverDto> CAfvClient::getTransmittingTransceivers() const
673  {
674  QMutexLocker lock(&m_mutexTransceivers);
675  return m_transmittingTransceivers;
676  }
677 
678  void CAfvClient::setPtt(bool active)
679  {
680  if (!m_isStarted)
681  {
682  CLogMessage(this).info(u"Voice client not started");
683  return;
684  }
685 
686  if (active && m_disableTransmissionCapability)
687  {
688  // Block transmissions
689  return;
690  }
691 
692  if (m_transmit == active) { return; }
693  m_transmit = active;
694 
695  // thread safe block
696  {
697  QMutexLocker lock(&m_mutexSampleProviders);
698  if (m_soundcardSampleProvider) { m_soundcardSampleProvider->pttUpdate(active, m_transmittingTransceivers); }
699 
711  }
712 
713  emit this->ptt(active, this->identifier());
714  }
715 
716  double CAfvClient::getInputVolumeDb() const
717  {
718  QMutexLocker lock(&m_mutex);
719  return m_inputVolumeDb;
720  }
721 
722  bool CAfvClient::setInputVolumeDb(double valueDb)
723  {
724  if (!CThreadUtils::isInThisThread(this))
725  {
726  // call in background thread of AFVClient to avoid lock issues
727  QPointer<CAfvClient> myself(this);
728  QTimer::singleShot(0, this, [=] {
729  if (!myself || !CAfvClient::hasContexts()) { return; }
730  myself->setInputVolumeDb(valueDb);
731  });
732  return true; // not exactly "true" as we do it async
733  }
734 
735  if (valueDb > MaxDbIn) { valueDb = MaxDbIn; }
736  else if (valueDb < MinDbIn) { valueDb = MinDbIn; }
737 
738  QMutexLocker lock(&m_mutex);
739  bool changed = !qFuzzyCompare(m_inputVolumeDb, valueDb);
740  if (changed)
741  {
742  m_inputVolumeDb = valueDb;
743  if (m_input)
744  {
745  const double gainRatio = qPow(10, valueDb / 20.0);
746  changed = m_input->setGainRatio(gainRatio);
747  }
748  }
749  return changed;
750  }
751 
752  double CAfvClient::getComOutputVolumeDb(CComSystem::ComUnit comUnit) const
753  {
754  QMutexLocker lock(&m_mutexVolume);
755  if (comUnit == CComSystem::Com1) return m_outputVolumeDbCom1;
756  if (comUnit == CComSystem::Com2) return m_outputVolumeDbCom2;
757  qFatal("Invalid COM unit");
758  return 0;
759  }
760 
761  double CAfvClient::getOutputGainRatio(CComSystem::ComUnit comUnit) const
762  {
763  QMutexLocker lock(&m_mutexVolume);
764  if (comUnit == CComSystem::Com1) return m_outputGainRatioCom1;
765  if (comUnit == CComSystem::Com2) return m_outputGainRatioCom2;
766  qFatal("Invalid COM unit");
767  return 0;
768  }
769 
770  int CAfvClient::getNormalizedInputVolume() const
771  {
772  const double db = this->getInputVolumeDb();
773  const double range = MaxDbIn - MinDbIn;
774  const int i = qRound((db - MinDbIn) / range * 100);
775  return i;
776  }
777 
778  int CAfvClient::getNormalizedMasterOutputVolume() const
779  {
780  QMutexLocker lock(&m_mutexVolume);
781  return m_outputMasterVolumeNormalized;
782  }
783 
784  int CAfvClient::getNormalizedComOutputVolume(CComSystem::ComUnit comUnit) const
785  {
786  QMutexLocker lock(&m_mutexVolume);
787  if (comUnit == CComSystem::Com1) { return m_outputVolumeCom1Normalized; }
788  if (comUnit == CComSystem::Com2) { return m_outputVolumeCom2Normalized; }
789  qFatal("Invalid ComUnit");
790  return 0;
791  }
792 
793  bool CAfvClient::setNormalizedInputVolume(int volume)
794  {
795  if (volume < 0) { volume = 0; }
796  else if (volume > 100) { volume = 100; }
797  const double range = MaxDbIn - MinDbIn;
798  const double dB = MinDbIn + (volume * range / 100.0);
799 
800  // converted to MinDbIn-MaxDbIn
801  return this->setInputVolumeDb(dB);
802  }
803 
804  bool CAfvClient::setNormalizedMasterOutputVolume(int volume)
805  {
806  if (!CThreadUtils::isInThisThread(this))
807  {
808  // call in background thread of AFVClient to avoid lock issues
809  QPointer<CAfvClient> myself(this);
810  QTimer::singleShot(0, this, [=] {
811  if (!myself || !CAfvClient::hasContexts()) { return; }
812  myself->setNormalizedMasterOutputVolume(volume);
813  });
814  return true; // not exactly "true" as we do it async
815  }
816 
817  bool changed = false;
818  {
819  QMutexLocker lock(&m_mutexVolume);
820  changed = m_outputMasterVolumeNormalized != volume;
821  if (changed) { m_outputMasterVolumeNormalized = volume; }
822  }
823 
824  // Trigger update of com volumes
825  int com1Normalized = getNormalizedComOutputVolume(CComSystem::Com1);
826  int com2Normalized = getNormalizedComOutputVolume(CComSystem::Com2);
827  setNormalizedComOutputVolume(CComSystem::Com1, com1Normalized);
828  setNormalizedComOutputVolume(CComSystem::Com2, com2Normalized);
829 
830  return changed;
831  }
832 
833  bool CAfvClient::setNormalizedComOutputVolume(CComSystem::ComUnit comUnit, int volume)
834  {
835  if (volume < 0) { volume = 0; }
836  else if (volume > 100) { volume = 100; }
837 
838  // Save original volume
839  if (comUnit == CComSystem::Com1) { m_outputVolumeCom1Normalized = volume; }
840  else if (comUnit == CComSystem::Com2) { m_outputVolumeCom2Normalized = volume; }
841  else { qFatal("Invalid ComUnit"); }
842 
843  // Calculate volume relative to master-output volume
844  volume = qRound((double)volume * getNormalizedMasterOutputVolume() / 100);
845 
846  // Asymetric
847  double range = MaxDbOut;
848  double dB = 0;
849  if (volume >= 50) { volume -= 50; }
850  else
851  {
852  dB = MinDbOut;
853  range = qAbs(MinDbOut);
854  }
855  dB += (volume * range / 50.0);
856 
857  // converted to MinDbOut-MaxDbOut
858  return this->setComOutputVolumeDb(comUnit, dB);
859  }
860 
861  double CAfvClient::getInputVolumePeakVU() const
862  {
863  QMutexLocker lock(&m_mutexInputStream);
864  return m_inputVolumeStream.PeakVU;
865  }
866 
867  double CAfvClient::getOutputVolumePeakVU() const
868  {
869  QMutexLocker lock(&m_mutexOutputStream);
870  return m_outputVolumeStream.PeakVU;
871  }
872 
873  void CAfvClient::opusDataAvailable(const OpusDataAvailableArgs &args)
874  {
875  const bool transmit = m_transmit;
876  const bool loopback = m_loopbackOn;
877  const bool transmitHistory = m_transmitHistory; // threadsafe
878  const auto transceivers = this->getTransceivers();
879 
880  if (loopback && transmit)
881  {
882  IAudioDto audioData;
883  audioData.audio = QByteArray(args.audio.data(), args.audio.size());
884  audioData.callsign = QStringLiteral("loopback");
885  audioData.lastPacket = false;
886  audioData.sequenceCounter = 0;
887 
888  const RxTransceiverDto com1 = { 0, transceivers.size() > 0 ? transceivers[0].frequencyHz : UniCom, 1.0 };
889  const RxTransceiverDto com2 = { 1, transceivers.size() > 1 ? transceivers[1].frequencyHz : UniCom, 1.0 };
890 
891  QMutexLocker lock(&m_mutexSampleProviders);
892  m_soundcardSampleProvider->addOpusSamples(audioData, { com1, com2 });
893  return;
894  }
895 
896  if (!this->isConnected()) { return; } // threadsafe
897 
898  const QString callsign = this->getCallsign(); // threadsafe
899  const auto transmittingTransceivers = this->getTransmittingTransceivers(); // threadsafe
900  if (!transmittingTransceivers.isEmpty())
901  {
902  if (transmit)
903  {
904  AudioTxOnTransceiversDto dto;
905  dto.callsign = callsign.toStdString();
906  dto.sequenceCounter = args.sequenceCounter;
907  dto.audio = std::vector<char>(args.audio.begin(), args.audio.end());
908  dto.lastPacket = false;
909  dto.transceivers =
910  std::vector<TxTransceiverDto>(transmittingTransceivers.begin(), transmittingTransceivers.end());
911  QMutexLocker lock(&m_mutexConnection);
912  m_connection->sendToVoiceServer(dto);
913  }
914 
915  if (!transmit && transmitHistory)
916  {
917  AudioTxOnTransceiversDto dto;
918  dto.callsign = callsign.toStdString();
919  dto.sequenceCounter = args.sequenceCounter;
920  dto.audio = std::vector<char>(args.audio.begin(), args.audio.end());
921  dto.lastPacket = true;
922  dto.transceivers =
923  std::vector<TxTransceiverDto>(transmittingTransceivers.begin(), transmittingTransceivers.end());
924  QMutexLocker lock(&m_mutexConnection);
925  m_connection->sendToVoiceServer(dto);
926  }
927  m_transmitHistory = transmit; // threadsafe
928  }
929  }
930 
931  void CAfvClient::audioOutDataAvailable(const AudioRxOnTransceiversDto &dto)
932  {
933  IAudioDto audioData;
934  audioData.audio = QByteArray(dto.audio.data(), static_cast<int>(dto.audio.size()));
935  audioData.callsign = QString::fromStdString(dto.callsign);
936  audioData.lastPacket = dto.lastPacket;
937  audioData.sequenceCounter = dto.sequenceCounter;
938 
939  QMutexLocker lock(&m_mutexSampleProviders);
940  m_soundcardSampleProvider->addOpusSamples(
941  audioData, QVector<RxTransceiverDto>(dto.transceivers.begin(), dto.transceivers.end()));
942  }
943 
944  void CAfvClient::inputVolumeStream(const InputVolumeStreamArgs &args)
945  {
946  // thread safe block
947  {
948  QMutexLocker lock(&m_mutexInputStream);
949  m_inputVolumeStream = args;
950  }
951  emit inputVolumePeakVU(args.PeakVU);
952  }
953 
954  void CAfvClient::outputVolumeStream(const OutputVolumeStreamArgs &args)
955  {
956  // thread safe block
957  {
958  QMutexLocker lock(&m_mutexOutputStream);
959  m_outputVolumeStream = args;
960  }
961  emit outputVolumePeakVU(args.PeakVU);
962  }
963 
964  QString CAfvClient::getReceivingCallsignsStringCom1() const
965  {
966  QMutexLocker lock(&m_mutex);
967  if (!m_soundcardSampleProvider) return {};
968  return m_soundcardSampleProvider->getReceivingCallsignsString(comUnitToTransceiverId(CComSystem::Com1));
969  }
970 
971  QString CAfvClient::getReceivingCallsignsStringCom2() const
972  {
973  QMutexLocker lock(&m_mutexSampleProviders);
974  if (!m_soundcardSampleProvider) return {};
975  return m_soundcardSampleProvider->getReceivingCallsignsString(comUnitToTransceiverId(CComSystem::Com2));
976  }
977 
978  CCallsignSet CAfvClient::getReceivingCallsignsCom1() const
979  {
980  QMutexLocker lock(&m_mutexSampleProviders);
981  if (!m_soundcardSampleProvider) return {};
982  return m_soundcardSampleProvider->getReceivingCallsigns(comUnitToTransceiverId(CComSystem::Com1));
983  }
984 
985  CCallsignSet CAfvClient::getReceivingCallsignsCom2() const
986  {
987  QMutexLocker lock(&m_mutexSampleProviders);
988  if (!m_soundcardSampleProvider) return {};
989  return m_soundcardSampleProvider->getReceivingCallsigns(comUnitToTransceiverId(CComSystem::Com2));
990  }
991 
992  QStringList CAfvClient::getReceivingCallsignsStringCom1Com2() const
993  {
994  QStringList coms;
995  QMutexLocker lock(&m_mutexSampleProviders);
996  if (!m_soundcardSampleProvider) { return { { QString(), QString() } }; }
997  coms << m_soundcardSampleProvider->getReceivingCallsignsString(comUnitToTransceiverId(CComSystem::Com1));
998  coms << m_soundcardSampleProvider->getReceivingCallsignsString(comUnitToTransceiverId(CComSystem::Com2));
999  return coms;
1000  }
1001 
1002  bool CAfvClient::updateVoiceServerUrl(const QString &url)
1003  {
1004  QMutexLocker lock(&m_mutexConnection);
1005  if (!m_connection) { return false; }
1006  return m_connection->updateVoiceServerUrl(url);
1007  }
1008 
1009  void CAfvClient::gracefulShutdown()
1010  {
1011  this->stopAudio();
1012  this->disconnectFrom();
1013  this->quitAndWait();
1014  Q_ASSERT_X(CThreadUtils::isInThisThread(this), Q_FUNC_INFO, "Needs to be back in current thread");
1015  }
1016 
1017  void CAfvClient::initialize()
1018  {
1019  CLogMessage(this).info(u"Initialize AFV client in thread %1") << CThreadUtils::currentThreadInfo();
1020  }
1021 
1022  void CAfvClient::cleanup()
1023  {
1024 #ifdef Q_OS_WIN
1025  if (m_winCoInitialized)
1026  {
1027  CoUninitialize();
1028  m_winCoInitialized = false;
1029  }
1030 #endif
1031  }
1032 
1033  void CAfvClient::onTimerUpdate()
1034  {
1035  if (hasContexts())
1036  {
1037  // for pilot client
1039  this->updateFromOwnAircraft(aircraft, false);
1040 
1041  // disconnect if NOT connected
1042  this->autoLogoffWithoutFsdNetwork();
1043 
1044  // get the settings in correct thread
1045  this->fetchSimulatorSettings();
1046  }
1047  else
1048  {
1049  // for AFV sample client
1050  this->updateTransceivers();
1051  }
1052 
1053  // connection check
1054  this->checkServerHeartbeat();
1055  }
1056 
1057  void CAfvClient::checkServerHeartbeat()
1058  {
1059  if (!this->isStarted()) { return; }
1060  if (!this->isConnected()) { return; }
1061 
1062  if (this->isVoiceServerAlive())
1063  {
1064  m_heartBeatFailures = 0;
1065  return;
1066  }
1067 
1068  // Heartbeat failure
1069  // it can happen that after connect we see an initial timeout
1070  const int failures = ++m_heartBeatFailures;
1071  if (failures < 2) { return; }
1072 
1073  QString un;
1074  QString pw;
1075  QString cs;
1076  QString client;
1077  {
1078  QMutexLocker lock(&m_mutexConnection);
1079  un = m_connection->getUserName();
1080  pw = m_connection->getPassword();
1081  cs = m_connection->getCallsign();
1082  client = m_connection->getClient();
1083  }
1084  if (un.isEmpty() || pw.isEmpty()) { return; }
1085 
1086  // make sure we are disconnected
1087  if (this->isConnected()) { this->disconnectFrom(false); }
1088 
1089  QPointer<CAfvClient> myself(this);
1090  QTimer::singleShot(5 * 1000, this, [=, this] {
1091  if (!myself) { return; }
1092  const QString reason = QStringLiteral("Heartbeat failed %1 times").arg(failures);
1093  this->retryConnectTo(un, pw, cs, client, reason);
1094  });
1095  }
1096 
1097  void CAfvClient::onSettingsChanged()
1098  {
1099  const CSettings audioSettings = m_audioSettings.get();
1100  const int iv = audioSettings.getInVolume();
1101  const int ov = audioSettings.getOutVolume();
1102  const int ov1 = audioSettings.getOutVolumeCom1();
1103  const int ov2 = audioSettings.getOutVolumeCom2();
1104 
1105  this->setNormalizedInputVolume(iv);
1106  this->setNormalizedMasterOutputVolume(ov);
1107  this->setNormalizedComOutputVolume(CComSystem::Com1, ov1);
1108  this->setNormalizedComOutputVolume(CComSystem::Com2, ov2);
1109  this->setBypassEffects(!audioSettings.isAudioEffectsEnabled());
1110  }
1111 
1112  void CAfvClient::autoLogoffWithoutFsdNetwork()
1113  {
1114  if (!hasContexts()) { return; }
1115  if (!this->isConnected())
1116  {
1117  m_fsdConnectMismatches = 0;
1118  return;
1119  }
1120 
1121  // AFV is connected
1123  {
1124  m_fsdConnectMismatches = 0;
1125  return;
1126  }
1127  if (++m_fsdConnectMismatches < 2) { return; } // avoid a single issue causing logoff
1128 
1129  CLogMessage(this).warning(u"Auto logoff AFV client because FSD no longer connected");
1130  this->disconnectFrom();
1131  }
1132 
1133  void CAfvClient::updateFromOwnAircraft(const CSimulatedAircraft &aircraft, bool withSignals)
1134  {
1135  if (!sApp || sApp->isShuttingDown()) { return; }
1136 
1137  TransceiverDto transceiverCom1;
1138  TransceiverDto transceiverCom2;
1139  transceiverCom1.id = comUnitToTransceiverId(CComSystem::Com1);
1140  transceiverCom2.id = comUnitToTransceiverId(CComSystem::Com2);
1141 
1142  // position
1143  const double latDeg = aircraft.latitude().value(CAngleUnit::deg());
1144  const double lngDeg = aircraft.longitude().value(CAngleUnit::deg());
1145  const double altM = aircraft.getAltitude().value(CLengthUnit::m());
1146 
1147  transceiverCom1.LatDeg = transceiverCom2.LatDeg = latDeg;
1148  transceiverCom1.LonDeg = transceiverCom2.LonDeg = lngDeg;
1149  transceiverCom1.HeightAglM = transceiverCom2.HeightAglM = altM;
1150  transceiverCom1.HeightMslM = transceiverCom2.HeightMslM = altM;
1151 
1152  // enabled, rx/tx, frequency
1153  const CComSystem com1 = aircraft.getCom1System();
1154  const CComSystem com2 = aircraft.getCom2System();
1155  const quint32 f1 = static_cast<quint32>(com1.getFrequencyActive().valueInteger(CFrequencyUnit::Hz()));
1156  const quint32 f2 = static_cast<quint32>(com2.getFrequencyActive().valueInteger(CFrequencyUnit::Hz()));
1157 
1158  transceiverCom1.frequencyHz = this->getAliasFrequencyHz(f1);
1159  transceiverCom2.frequencyHz = this->getAliasFrequencyHz(f2);
1160 
1161  QVector<TransceiverDto> newEnabledTransceivers;
1162  if (m_integratedComUnit)
1163  {
1164  const bool tx1 = com1.isTransmitEnabled();
1165  const bool rx1 = com1.isReceiveEnabled();
1166  const bool tx2 = com2.isTransmitEnabled(); // we only allow one (1) transmit
1167  const bool rx2 = com2.isReceiveEnabled();
1168 
1169  const int vol1 = com1.getVolumeReceive();
1170  const int vol2 = com2.getVolumeReceive();
1171 
1172  this->setNormalizedComOutputVolume(CComSystem::Com1, vol1);
1173  this->setNormalizedComOutputVolume(CComSystem::Com2, vol2);
1174 
1175  // enable, we currently treat receive as enable
1176  // flight sim cockpits normally use rx and tx
1177  // AFV uses tx and enable
1178  const bool e1 = rx1;
1179  const bool e2 = rx2;
1180 
1181  // transceivers
1182  const QVector<TransceiverDto> newTransceivers { transceiverCom1, transceiverCom2 };
1183  QSet<quint16> newEnabledTransceiverIds;
1184  QVector<TxTransceiverDto> newTransmittingTransceivers;
1185  if (e1)
1186  {
1187  newEnabledTransceivers.push_back(transceiverCom1);
1188  newEnabledTransceiverIds.insert(transceiverCom1.id);
1189  }
1190  if (e2)
1191  {
1192  newEnabledTransceivers.push_back(transceiverCom2);
1193  newEnabledTransceiverIds.insert(transceiverCom2.id);
1194  }
1195 
1196  // Transmitting transceivers, currently ALLOW ONLY ONE
1197  if (tx1 && e1) { newTransmittingTransceivers.push_back(transceiverCom1); }
1198  else if (tx2 && e2) { newTransmittingTransceivers.push_back(transceiverCom2); }
1199 
1200  // lock and update
1201  {
1202  QMutexLocker lock(&m_mutexTransceivers);
1203  m_transceivers = newTransceivers;
1204  m_enabledTransceivers = newEnabledTransceiverIds;
1205  m_transmittingTransceivers = newTransmittingTransceivers;
1206  }
1207  }
1208  else
1209  {
1210  // update position and frequencies, but keep enabled as it was
1211  const QSet<quint16> ids = getEnabledTransceivers();
1212  if (ids.contains(comUnitToTransceiverId(CComSystem::Com1)))
1213  {
1214  newEnabledTransceivers.push_back(transceiverCom1);
1215  }
1216 
1217  if (ids.contains(comUnitToTransceiverId(CComSystem::Com2)))
1218  {
1219  newEnabledTransceivers.push_back(transceiverCom2);
1220  }
1221  }
1222 
1223  // in connection and soundcard only use the enabled tarnsceivers
1224  const QString callsign = this->getCallsign(); // threadsafe
1225  {
1226  {
1227  QMutexLocker lock(&m_mutexConnection);
1228  if (m_connection)
1229  {
1230  // fire to network and forget
1231  m_connection->updateTransceivers(callsign, newEnabledTransceivers);
1232  }
1233  }
1234 
1235  {
1236  QMutexLocker lock(&m_mutexSampleProviders);
1237  if (m_soundcardSampleProvider)
1238  {
1239  m_soundcardSampleProvider->updateRadioTransceivers(newEnabledTransceivers);
1240  }
1241  }
1242  }
1243 
1244  if (withSignals) { emit this->updatedFromOwnAircraftCockpit(); }
1245  }
1246 
1247  void CAfvClient::onUpdateTransceiversFromContext(const CSimulatedAircraft &aircraft, const CIdentifier &originator)
1248  {
1249  if (originator == this->identifier()) { return; }
1250  this->updateFromOwnAircraft(aircraft);
1251  }
1252 
1253  void CAfvClient::onReceivingCallsignsChanged(const TransceiverReceivingCallsignsChangedArgs &args)
1254  {
1255  const CComSystem::ComUnit unit = transceiverIdToComUnit(args.transceiverID);
1256  CCallsignSet callsignsCom1;
1257  CCallsignSet callsignsCom2;
1258  switch (unit)
1259  {
1260  case CComSystem::Com1:
1261  default:
1262  callsignsCom1 = CCallsignSet(args.receivingCallsigns);
1263  callsignsCom2 = this->getReceivingCallsignsCom2();
1264  break;
1265 
1266  case CComSystem::Com2:
1267  callsignsCom2 = CCallsignSet(args.receivingCallsigns);
1268  callsignsCom1 = this->getReceivingCallsignsCom1();
1269  break;
1270  }
1271 
1272  emit this->receivedCallsignsChanged(callsignsCom1, callsignsCom2);
1273  emit this->receivingCallsignsChanged(args);
1274  }
1275 
1276  void CAfvClient::retryConnectTo(const QString &cid, const QString &password, const QString &callsign,
1277  const QString &client, const QString &reason)
1278  {
1279  if (this->isConnected()) { return; }
1280  m_retryConnectAttempt++;
1281 
1282  const int retrySecs = qMin(3 * 60, m_retryConnectAttempt * 30);
1283  const CStatusMessage msg = CStatusMessage(this).validationError(reason + ". Retry in %1secs. Attempt %2.")
1284  << retrySecs << m_retryConnectAttempt;
1285  this->reconnectTo(cid, password, callsign, client, retrySecs * 1000, msg);
1286  }
1287 
1288  void CAfvClient::reconnectTo(const QString &cid, const QString &password, const QString &callsign,
1289  const QString &client, int delayMs, const CStatusMessage &msg)
1290  {
1291  if (msg.isFailure())
1292  {
1294  emit this->afvConnectionFailure(msg);
1295  }
1296 
1297  QPointer<CAfvClient> myself(this);
1298  QTimer::singleShot(delayMs, this, [=, this] {
1299  if (!myself) { return; }
1300  if (myself->isConnected()) { return; }
1301  this->connectTo(cid, password, callsign, client);
1302  });
1303  }
1304 
1305  void CAfvClient::toggleTransmissionCapability(bool disableTransmission)
1306  {
1307  if (m_disableTransmissionCapability == disableTransmission) { return; }
1308  m_disableTransmissionCapability = disableTransmission;
1309 
1310  if (disableTransmission)
1311  {
1312  // Stop current transmissions
1313  setPtt(false);
1314  }
1315  }
1316 
1317  QVector<StationDto> CAfvClient::getAliasedStations() const
1318  {
1319  QMutexLocker lock(&m_mutex);
1320  return m_aliasedStations;
1321  }
1322 
1323  void CAfvClient::setAliasedStations(const QVector<StationDto> &stations)
1324  {
1325  QMutexLocker lock(&m_mutex);
1326  m_aliasedStations = stations;
1327  }
1328 
1329  quint32 CAfvClient::getAliasFrequencyHz(quint32 frequencyHz) const
1330  {
1331  // void rounding issues from float/double
1332  quint32 roundedFrequencyHz = static_cast<quint32>(qRound(frequencyHz / 1000.0)) * 1000;
1333 
1334  // change to aliased frequency if needed
1335  {
1336  QMutexLocker lock(&m_mutex);
1337  CFrequency roundedFrequency(static_cast<int>(roundedFrequencyHz), CFrequencyUnit::Hz());
1338  const auto it = std::find_if(m_aliasedStations.constBegin(), m_aliasedStations.constEnd(),
1339  [roundedFrequency](const StationDto &d) {
1340  if (d.frequencyAliasHz > 100000000 &&
1341  roundedFrequency.value(CFrequencyUnit::Hz()) > 100000000) // both VHF
1342  {
1343  const int aliasedFreqHz = qRound(d.frequencyAliasHz / 1000.0) * 1000;
1344  return CComSystem::isSameFrequency(
1345  CFrequency(aliasedFreqHz, CFrequencyUnit::Hz()), roundedFrequency);
1346  }
1347  return d.frequencyAliasHz == roundedFrequency.value(CFrequencyUnit::Hz());
1348  });
1349 
1350  if (it != m_aliasedStations.constEnd())
1351  {
1352  if (sApp && sApp->getIContextNetwork())
1353  {
1354  // Get the callsign for this frequency and fuzzy compare with our alias station
1355  const CFrequency f(static_cast<int>(roundedFrequencyHz), CFrequencyUnit::Hz());
1356  const CAtcStationList matchingAtcStations =
1358  const CAtcStation closest =
1359  matchingAtcStations
1361  .frontOrDefault();
1362 
1363  if (fuzzyMatchCallsign(it->name, closest.getCallsign().asString()))
1364  {
1365  // this is how it should be
1366  roundedFrequencyHz = it->frequencyHz;
1367  CLogMessage(this).debug(u"Aliasing '%1' %2Hz [VHF] to %3Hz [HF]")
1368  << closest.getCallsign() << frequencyHz << it->frequencyHz;
1369  }
1370  else
1371  {
1372  // Ups!
1373  CLogMessage(this).debug(
1374  u"Station '%1' NOT found! Candidate was '%2'. Using original frequency %3 Hz")
1375  << it->name << closest.getCallsign().asString() << roundedFrequencyHz;
1376  }
1377  }
1378  else
1379  {
1380  // without contexts always use HF frequency if found
1381  roundedFrequencyHz = it->frequencyHz; // we use this frequency
1382  CLogMessage(this).debug(u"Aliasing %1Hz [VHF] to %2Hz [HF] (no context)")
1383  << frequencyHz << it->frequencyHz;
1384  }
1385  }
1386  }
1387  return roundedFrequencyHz;
1388  }
1389 
1390  bool CAfvClient::isVoiceServerAlive() const
1391  {
1392  QMutexLocker lock(&m_mutexConnection);
1393  return m_connection && m_connection->isVoiceServerAlive();
1394  }
1395 
1396  const QString &CAfvClient::getVoiceServerUrl() const
1397  {
1398  QMutexLocker lock(&m_mutexConnection);
1399 
1400  static const QString e;
1401  if (!m_connection) { return e; }
1402  return m_connection->getVoiceServerUrl();
1403  }
1404 
1405  bool CAfvClient::fuzzyMatchCallsign(const QString &callsign, const QString &compareTo) const
1406  {
1407  if (callsign.isEmpty() || compareTo.isEmpty()) { return false; } // empty callsigns should NOT match
1408 
1409  QString prefixA;
1410  QString suffixA;
1411  QString prefixB;
1412  QString suffixB;
1413  this->getPrefixSuffix(callsign, prefixA, suffixA);
1414  this->getPrefixSuffix(compareTo, prefixB, suffixB);
1415  return (prefixA == prefixB) && (suffixA == suffixB);
1416  }
1417 
1418  void CAfvClient::getPrefixSuffix(const QString &callsign, QString &prefix, QString &suffix) const
1419  {
1420  thread_local const QRegularExpression separator("[(\\-|_)]");
1421  const QStringList parts = callsign.split(separator);
1422 
1423  // avoid issues if there are no parts, or only one
1424  prefix = parts.size() > 0 ? parts.first() : QString();
1425  suffix = parts.size() > 1 ? parts.last() : QString();
1426  }
1427 
1428  quint16 CAfvClient::comUnitToTransceiverId(CComSystem::ComUnit comUnit)
1429  {
1430  switch (comUnit)
1431  {
1432  case CComSystem::Com1: return 0;
1433  case CComSystem::Com2: return 1;
1434  default: break;
1435  }
1436  return 0;
1437  }
1438 
1439  CComSystem::ComUnit CAfvClient::transceiverIdToComUnit(quint16 id)
1440  {
1441  if (comUnitToTransceiverId(CComSystem::Com1) == id) { return CComSystem::Com1; }
1442  if (comUnitToTransceiverId(CComSystem::Com2) == id) { return CComSystem::Com2; }
1443  return CComSystem::Com1;
1444  }
1445 
1446  void CAfvClient::deferredInit()
1447  {
1448  // transceivers
1449  this->initTransceivers();
1450 
1451  // init by settings
1452  this->onSettingsChanged();
1453 
1454  // info
1455  CLogMessage(this).info(u"UserClient instantiated (deferred init)");
1456  }
1457 
1458  bool CAfvClient::hasContexts()
1459  {
1462  }
1463 
1464  bool CAfvClient::setComOutputVolumeDb(CComSystem::ComUnit comUnit, double valueDb)
1465  {
1466  if (!CThreadUtils::isInThisThread(this))
1467  {
1468  // call in background thread of AFVClient to avoid lock issues
1469  QPointer<CAfvClient> myself(this);
1470  QTimer::singleShot(0, this, [=] {
1471  if (!myself || !CAfvClient::hasContexts()) { return; }
1472  myself->setComOutputVolumeDb(comUnit, valueDb);
1473  });
1474  return true; // not exactly "true" as we do it async
1475  }
1476  if (comUnit != CComSystem::Com1 && comUnit != CComSystem::Com2) { return false; }
1477  if (valueDb > MaxDbOut) { valueDb = MaxDbOut; }
1478  else if (valueDb < MinDbOut) { valueDb = MinDbOut; }
1479 
1480  const double gainRatio = qPow(10, valueDb / 20.0);
1481  bool changed = false;
1482  {
1483  QMutexLocker lock(&m_mutexVolume);
1484  if (comUnit == CComSystem::Com1)
1485  {
1486  changed = !qFuzzyCompare(m_outputVolumeDbCom1, valueDb);
1487  if (changed)
1488  {
1489  m_outputVolumeDbCom1 = valueDb;
1490  m_outputGainRatioCom1 = gainRatio;
1491  }
1492  }
1493  else
1494  {
1495  changed = !qFuzzyCompare(m_outputVolumeDbCom2, valueDb);
1496  if (changed)
1497  {
1498  m_outputVolumeDbCom2 = valueDb;
1499  m_outputGainRatioCom2 = gainRatio;
1500  }
1501  }
1502  }
1503 
1504  // do NOT check on "changed", can be false, but "m_outputSampleProvider" is initialized
1505  // HINT: I do this tryLock here because I had deadlocks here, and I need to further investigate
1506  // As deadlocks mean (for the user) he needs to terminate the client I keep "trylock" that for now
1507  if (!m_mutexSampleProviders.tryLock(1000)) { return false; }
1508 
1509  if (m_soundcardSampleProvider)
1510  {
1511  changed = m_soundcardSampleProvider->setGainRatioForTransceiver(comUnit, gainRatio);
1512  }
1513  m_mutexSampleProviders.unlock();
1514  return changed;
1515  }
1516 
1517  const CAudioDeviceInfo &CAfvClient::getInputDevice() const
1518  {
1519  QMutexLocker lock(&m_mutex);
1520  if (m_input) { return m_input->device(); }
1521  static const CAudioDeviceInfo nullDevice;
1522  return nullDevice;
1523  }
1524 
1525  const CAudioDeviceInfo &CAfvClient::getOutputDevice() const
1526  {
1527  QMutexLocker lock(&m_mutex);
1528  if (m_output) { return m_output->device(); }
1529  static const CAudioDeviceInfo nullDevice;
1530  return nullDevice;
1531  }
1532 
1533  bool CAfvClient::usesSameDevices(const CAudioDeviceInfo &inputDevice, const CAudioDeviceInfo &outputDevice)
1534  {
1535  QMutexLocker lock(&m_mutex);
1536  if (!m_output || !m_input) { return false; }
1537  const CAudioDeviceInfo i = m_input->device();
1538  const CAudioDeviceInfo o = m_output->device();
1539  lock.unlock();
1540 
1541  return i.matchesNameTypeMachineName(inputDevice) && o.matchesNameTypeMachineName(outputDevice);
1542  }
1543 
1544  CAfvClient::ConnectionStatus CAfvClient::getConnectionStatus() const
1545  {
1546  return this->isConnected() ? Connected : Disconnected;
1547  }
1548 } // namespace swift::core::afv::clients
SWIFT_CORE_EXPORT swift::core::CApplication * sApp
Single instance of application object.
Definition: application.cpp:71
const context::IContextOwnAircraft * getIContextOwnAircraft() const
Direct access to contexts if a CCoreFacade has been initialized.
const context::IContextNetwork * getIContextNetwork() const
Direct access to contexts if a CCoreFacade has been initialized.
bool isShuttingDown() const
Is application shutting down?
const context::IContextSimulator * getIContextSimulator() const
Direct access to contexts if a CCoreFacade has been initialized.
ConnectionStatus
Connection status.
Definition: afvclient.h:53
bool isConnected() const
Is connected to network?
Definition: afvclient.cpp:71
void setCallsign(const QString &getCallsign)
Copy operations.
Definition: afvclient.cpp:65
void setReceiveAudio(bool value)
Receiving audio?
virtual swift::misc::aviation::CAtcStationList getOnlineStationsForFrequency(const swift::misc::physical_quantities::CFrequency &frequency) const =0
Online stations for frequency.
virtual bool isConnected() const =0
Network connected?
virtual swift::misc::simulation::CSimulatedAircraft getOwnAircraft() const =0
Get own aircraft.
virtual swift::misc::aviation::CAircraftSituation getOwnAircraftSituation() const =0
Get own aircraft.
virtual swift::misc::simulation::settings::CSimulatorSettings getSimulatorSettings() const =0
Get the current simulator settings.
Base class for a long-lived worker object which lives in its own thread.
Definition: worker.h:300
Base class with a member CIdentifier to be inherited by a class which has an identity in the environm...
Definition: identifiable.h:24
Value object encapsulating information identifying a component of a modular distributed swift process...
Definition: identifier.h:29
static const QString & vatsimSpecific()
VATSIM specific.
static const QString & audio()
Audio related.
Definition: logcategories.h:52
Class for emitting a log message.
Definition: logmessage.h:27
static void preformatted(const CStatusMessage &statusMessage)
Sends a verbatim, preformatted message to the log.
Derived & warning(const char16_t(&format)[N])
Set the severity to warning, providing a format string.
Derived & validationError(const char16_t(&format)[N])
Set the severity to error, providing a format string, and adding the validation category.
Derived & error(const char16_t(&format)[N])
Set the severity to error, providing a format string.
Derived & debug()
Set the severity to debug.
Derived & info(const char16_t(&format)[N])
Set the severity to info, providing a format string.
Streamable status message, e.g.
bool isFailure() const
Operation considered unsuccessful.
static QString currentThreadInfo()
Info about current thread, for debug messages.
Definition: threadutils.cpp:23
static bool isInThisThread(const QObject *toBeTested)
Is the current thread the object's thread?
Definition: threadutils.cpp:16
Value object encapsulating information of a audio device.
bool isOutputDevice() const
Output device.
bool isValid() const
Valid audio device object?
bool matchesNameTypeMachineName(const CAudioDeviceInfo &device) const
Matching name, type and machine.
const QString & getName() const
Get the device name.
Value object encapsulating information of audio related settings.
Definition: audiosettings.h:25
int getOutVolumeCom2() const
Get volume for com2 (audio) 0..100.
int getOutVolume() const
Get volume (audio) 0..100.
bool isAudioEffectsEnabled() const
Audio effects enabled?
int getInVolume() const
Get mic.volume (audio 0..100)
int getOutVolumeCom1() const
Get volume for com1 (audio) 0..100.
const geo::CCoordinateGeodetic & getPosition() const
Get position.
Value object encapsulating information about an ATC station.
Definition: atcstation.h:38
const CCallsign & getCallsign() const
Get callsign.
Definition: atcstation.h:84
Value object for a list of ATC stations.
const QString & asString() const
Get callsign (normalized)
Definition: callsign.h:96
Value object for a set of callsigns.
Definition: callsignset.h:26
COM system (aka "radio")
Definition: comsystem.h:37
bool isTransmitEnabled() const
Enabled?
Definition: modulator.cpp:82
int getVolumeReceive() const
Output volume 0..100.
Definition: modulator.cpp:54
bool isReceiveEnabled() const
Enabled?
Definition: modulator.cpp:88
swift::misc::physical_quantities::CFrequency getFrequencyActive() const
Active frequency.
Definition: modulator.cpp:30
CONTAINER findClosest(int number, const ICoordinateGeodetic &coordinate) const
Find 0..n objects closest to the given coordinate.
int valueInteger(MU unit) const
As integer value.
double value(MU unit) const
Value in given unit.
Comprehensive information of an aircraft.
geo::CLongitude longitude() const
Longitude.
const aviation::CComSystem & getCom2System() const
Get COM2 system.
geo::CLatitude latitude() const
Latitude.
const aviation::CAltitude & getAltitude() const
Get altitude.
const aviation::CComSystem & getCom1System() const
Get COM1 system.
Free functions in swift::misc.
auto singleShot(int msec, QObject *target, F &&task)
Starts a single-shot timer which will call a task in the thread of the given object when it times out...
Definition: threadutils.h:30
QByteArray::iterator begin()
char * data()
QByteArray::iterator end()
qsizetype size() const const
T & first()
T & last()
void push_back(QList< T >::parameter_type value)
qsizetype size() const const
bool invokeMethod(QObject *context, Functor &&function, Args &&... arguments)
QMetaObject::Connection connect(const QObject *sender, PointerToMemberFunction signal, Functor functor)
bool disconnect(const QMetaObject::Connection &connection)
void setObjectName(QAnyStringView name)
QThread * thread() const const
bool contains(const QSet< T > &other) const const
QSet< T >::iterator insert(QSet< T >::const_iterator it, const T &value)
QString arg(Args &&... args) const const
QString fromStdString(const std::string &str)
bool isEmpty() const const
QStringList split(QChar sep, Qt::SplitBehavior behavior, Qt::CaseSensitivity cs) const const
std::string toStdString() const const
QueuedConnection
QThread * currentThread()
void timeout()
bool lastPacket
Used to indicate to receiver that the sender has stopped sending.
Definition: dto.h:280
QByteArray audio
Opus compressed audio.
Definition: dto.h:279
QString callsign
Callsign that audio originates from.
Definition: dto.h:277
uint sequenceCounter
Receiver optionally uses this in reordering algorithm/gap detection.
Definition: dto.h:278
Receive transceiver DTO.
Definition: dto.h:206
Transceiver DTO.
Definition: dto.h:117
double LonDeg
Properties.
Definition: dto.h:123
double LatDeg
Properties.
Definition: dto.h:122
double HeightMslM
Properties.
Definition: dto.h:124
double HeightAglM
Properties.
Definition: dto.h:125
quint32 frequencyHz
Properties.
Definition: dto.h:121
quint16 id
Properties.
Definition: dto.h:120
Transmit transceiver DTO.
Definition: dto.h:220
Input volume stream arguments.
Definition: input.h:62
double PeakVU
Peak volume in VU.
Definition: input.h:70