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