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