Files
pilotclient/src/core/context/contextaudio.cpp
2024-12-26 11:17:53 +01:00

618 lines
24 KiB
C++

// SPDX-FileCopyrightText: Copyright (C) 2013 swift Project Community / Contributors
// SPDX-License-Identifier: GPL-3.0-or-later OR LicenseRef-swift-pilot-client-1
#include "core/context/contextaudio.h"
#include "config/buildconfig.h"
#include "core/afv/clients/afvclient.h"
#include "core/context/contextaudioimpl.h"
#include "core/context/contextaudioproxy.h"
#include "core/context/contextnetwork.h" // for user login
#include "core/context/contextownaircraft.h" // for COM integration
#include "core/context/contextsimulator.h" // for COM intergration
#include "misc/dbusserver.h"
#include "misc/icons.h"
#include "misc/simplecommandparser.h"
#include "misc/stringutils.h"
#include "misc/verify.h"
#ifdef Q_OS_WIN
# include "comdef.h"
#endif
using namespace swift::misc;
using namespace swift::misc::aviation;
using namespace swift::misc::audio;
using namespace swift::misc::network;
using namespace swift::misc::physical_quantities;
using namespace swift::misc::simulation;
using namespace swift::sound;
using namespace swift::core::afv::clients;
//! \cond
namespace swift::core::context
{
IContextAudio::IContextAudio(CCoreFacadeConfig::ContextMode mode, CCoreFacade *runtime) : IContext(mode, runtime)
{
// void
}
void IContextAudio::onChangedLocalDevices(const CAudioDeviceInfoList &devices) { this->registerDevices(devices); }
const QString &IContextAudio::InterfaceName()
{
static const QString s(SWIFT_CORE_CONTEXTAUDIO_INTERFACENAME);
return s;
}
const QString &IContextAudio::ObjectPath()
{
static const QString s(SWIFT_CORE_CONTEXTAUDIO_OBJECTPATH);
return s;
}
IContextAudio *IContextAudio::create(CCoreFacade *runtime, CCoreFacadeConfig::ContextMode mode, CDBusServer *server,
QDBusConnection &connection)
{
// for audio no empty context is available
// since CContextAudioBaseImpl provides audio on either side (core/GUI) we do not use ContextAudioEmpty
// ContextAudioEmpty would cause issue, as it is initializing "common parts" during shutdown
switch (mode)
{
case CCoreFacadeConfig::Local: return new CContextAudio(mode, runtime);
case CCoreFacadeConfig::LocalInDBusServer:
{
auto *context = new CContextAudio(mode, runtime);
context->registerWithDBus(server);
return context;
}
case CCoreFacadeConfig::Remote:
return new CContextAudioProxy(CDBusServer::coreServiceName(connection), connection, mode, runtime);
case CCoreFacadeConfig::NotUsed:
SWIFT_VERIFY_X(false, Q_FUNC_INFO, "Empty context not supported for audio (since AFV)");
return nullptr;
default: SWIFT_VERIFY_X(false, Q_FUNC_INFO, "Unknown context mode"); return nullptr;
}
}
bool CContextAudioBase::parseCommandLine(const QString &commandLine, const CIdentifier &originator)
{
Q_UNUSED(originator)
if (commandLine.isEmpty()) { return false; }
CSimpleCommandParser parser({ ".vol", ".volume", // output volume
".mute", // mute
".unmute", // unmute
".aliased" });
parser.parse(commandLine);
if (!parser.isKnownCommand()) { return false; }
if (parser.matchesCommand(".mute"))
{
this->setOutputMute(true);
return true;
}
else if (parser.matchesCommand(".unmute"))
{
this->setOutputMute(false);
return true;
}
else if (parser.commandStartsWith("vol") && parser.countParts() > 1)
{
const int v = parser.toInt(1);
this->setMasterOutputVolume(v);
return true;
}
else if (afvClient() && parser.matchesCommand(".aliased") && parser.countParts() > 1)
{
const bool enable = parser.toBool(1, true);
afvClient()->enableAliasedStations(enable);
CLogMessage(this).info(u"Aliased stations are: %1") << boolToOnOff(enable);
return true;
}
return false;
}
CContextAudioBase::CContextAudioBase(CCoreFacadeConfig::ContextMode mode, CCoreFacade *runtime)
: IContextAudio(mode, runtime), CIdentifiable(this)
{
CContextAudioBase::registerHelp();
if (CContextAudioBase::isNoAudioSet()) { CLogMessage(this).info(u"Voice client disabled"); }
else { this->initVoiceClient(); }
// here we are in a base class of one context
// the whole context/facade system is not initialized when this code here is executed
QPointer<CContextAudioBase> myself(this);
QTimer::singleShot(5000, this, [=] {
if (!myself || !sApp || sApp->isShuttingDown()) { return; }
const CSettings as = m_audioSettings.getThreadLocal();
this->setMasterOutputVolume(as.getOutVolume());
this->setComOutputVolume(CComSystem::Com1, as.getOutVolumeCom1());
this->setComOutputVolume(CComSystem::Com2, as.getOutVolumeCom2());
m_selcalPlayer = new CSelcalPlayer(CAudioDeviceInfo::getDefaultOutputDevice(), this);
myself->changeDeviceSettings();
myself->onChangedAudioSettings();
myself->onChangedLocalDevices(m_activeLocalDevices);
});
}
CContextAudioBase::~CContextAudioBase() { this->gracefulShutdown(); }
void CContextAudioBase::initVoiceClient()
{
if (m_voiceClient || !sApp) { return; }
const CAudioDeviceInfoList devices = CAudioDeviceInfoList::allDevices();
if (devices != m_activeLocalDevices)
{
m_activeLocalDevices = devices;
emit this->changedLocalAudioDevices(devices);
}
#ifdef Q_OS_WIN
if (!m_winCoInitialized)
{
HRESULT hr = CoInitializeEx(nullptr, COINIT_MULTITHREADED);
// RPC_E_CHANGED_MODE: CoInitializeEx was already called by someone else in this thread with a different
// mode.
if (hr == RPC_E_CHANGED_MODE)
{
CLogMessage(this).debug(u"CoInitializeEx was already called with a different mode. Trying again.");
hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
}
// S_OK: The COM library was initialized successfully on this thread.
// S_FALSE: The COM library is already initialized on this thread. Reference count was incremented. This is
// not an error.
if (hr == S_OK || hr == S_FALSE) { m_winCoInitialized = true; }
}
#endif
m_voiceClient = new CAfvClient(sApp->getGlobalSetup().getAfvApiServerUrl().toQString(), this);
Q_ASSERT_X(m_voiceClient->thread() == qApp->thread(), Q_FUNC_INFO, "Should be in main thread");
m_voiceClient->start(); // thread
Q_ASSERT_X(m_voiceClient->owner() == this, Q_FUNC_INFO, "Wrong owner");
Q_ASSERT_X(m_voiceClient->thread() != qApp->thread(), Q_FUNC_INFO, "Must NOT be in main thread");
// connect(m_voiceClient, &CAfvClient::outputVolumePeakVU, this,
// &CContextAudioBase::outputVolumePeakVU, Qt::QueuedConnection); connect(m_voiceClient,
// &CAfvClient::inputVolumePeakVU, this, &CContextAudioBase::inputVolumePeakVU,
// Qt::QueuedConnection); connect(m_voiceClient, &CAfvClient::receivingCallsignsChanged, this,
// &CContextAudioBase::receivingCallsignsChanged, Qt::QueuedConnection); connect(m_voiceClient,
// &CAfvClient::updatedFromOwnAircraftCockpit, this, &CContextAudioBase::updatedFromOwnAircraftCockpit,
// Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::startedAudio, this, &CContextAudioBase::startedAudio, Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::stoppedAudio, this, &CContextAudioBase::stoppedAudio, Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::ptt, this, &CContextAudioBase::ptt, Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::changedOutputMute, this, &CContextAudioBase::changedOutputMute,
Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::connectionStatusChanged, this,
&CContextAudioBase::onAfvConnectionStatusChanged, Qt::QueuedConnection);
connect(m_voiceClient, &CAfvClient::afvConnectionFailure, this, &CContextAudioBase::onAfvConnectionFailure,
Qt::QueuedConnection);
}
void CContextAudioBase::terminateVoiceClient()
{
if (m_voiceClient)
{
m_voiceClient->gracefulShutdown();
Q_ASSERT_X(CThreadUtils::isInThisThread(m_voiceClient), Q_FUNC_INFO, "Needs to be back in current thread");
m_voiceClient->deleteLater();
m_voiceClient = nullptr;
#ifdef Q_OS_WIN
if (m_winCoInitialized)
{
CoUninitialize();
m_winCoInitialized = false;
}
#endif
}
}
void CContextAudioBase::gracefulShutdown()
{
this->terminateVoiceClient();
if (m_selcalPlayer)
{
m_selcalPlayer->gracefulShutdown();
m_selcalPlayer = nullptr;
}
QObject::disconnect(this);
}
void CContextAudioBase::setRxTx(bool rx1, bool tx1, bool rx2, bool tx2)
{
if (m_voiceClient) { m_voiceClient->setRxTx(rx1, tx1, rx2, tx2); }
}
void CContextAudioBase::getRxTx(bool &rx1, bool &tx1, bool &rx2, bool &tx2) const
{
if (m_voiceClient) { m_voiceClient->setRxTx(rx1, tx1, rx2, tx2); }
}
const CIdentifier &CContextAudioBase::audioRunsWhere() const
{
static const CIdentifier i("CContextAudioBaseImpl");
return i;
}
bool CContextAudioBase::isEnabledComUnit(CComSystem::ComUnit comUnit) const
{
if (!m_voiceClient) { return false; }
return m_voiceClient->isEnabledComUnit(comUnit);
}
bool CContextAudioBase::isTransmittingComUnit(CComSystem::ComUnit comUnit) const
{
if (!m_voiceClient) { return false; }
return m_voiceClient->isTransmittingComUnit(comUnit);
}
bool CContextAudioBase::connectAudioWithNetworkCredentials()
{
if (!m_voiceClient) { return false; }
if (!sApp || sApp->isShuttingDown() || !sApp->getIContextNetwork()) { return false; }
const CEcosystem ecoSystem = this->getIContextNetwork()->getConnectedServer().getEcosystem();
if (ecoSystem != CEcosystem::vatsim())
{
CLogMessage(this).info(u"Will not use AFV as ecosystem is '%1'") << ecoSystem.toQString(true);
return false;
}
const CUser connectedUser = this->getIContextNetwork()->getConnectedServer().getUser();
const QString client = "swift " % swift::config::CBuildConfig::getShortVersionString();
CCallsign cs = connectedUser.getCallsign();
this->unRegisterAudioCallsign(cs, this->identifier()); // un-register "myself"
if (this->hasRegisteredAudioCallsign(cs)) // anybody else using that callsign
{
//! \todo KB 2019-11 would need a better algorithm to really find a cs
cs = CCallsign(cs.asString() + "2");
}
CLogMessage(this).info(u"About to connect to voice as '%1' '%2'") << connectedUser.getId() << cs;
m_voiceClient->connectTo(connectedUser.getId(), connectedUser.getPassword(), cs.asString(), client);
this->registerAudioCallsign(cs, this->identifier()); // login can still fail, but we "block" this callsign
return true;
}
bool CContextAudioBase::isAudioConnected() const { return m_voiceClient && m_voiceClient->isConnected(); }
bool CContextAudioBase::isAudioStarted() const { return m_voiceClient && m_voiceClient->isStarted(); }
bool CContextAudioBase::isComUnitIntegrated() const
{
return m_voiceClient && m_voiceClient->isComUnitIntegrated();
}
const QList<QCommandLineOption> &CContextAudioBase::getCmdLineOptions()
{
static const QList<QCommandLineOption> opts { QCommandLineOption(
{ { "n", "noaudio" },
QCoreApplication::translate("CContextAudioBase", "No audio for GUI or core.", "noaudio") }) };
return opts;
}
bool CContextAudioBase::isNoAudioSet()
{
if (!sApp) { return false; }
return sApp->isParserOptionSet("noaudio");
}
QString CContextAudioBase::audioRunsWhereInfo() const
{
const QString s = QStringLiteral("[%1] Audio on '%2', '%3'.")
.arg(boolToEnabledDisabled(this->isAudioStarted()), audioRunsWhere().getMachineName(),
audioRunsWhere().getProcessName());
return s;
}
CAudioDeviceInfoList CContextAudioBase::getAudioDevices() const { return CAudioDeviceInfoList::allDevices(); }
CAudioDeviceInfoList CContextAudioBase::getAudioInputDevices() const
{
return this->getAudioDevices().getInputDevices();
}
CAudioDeviceInfoList CContextAudioBase::getAudioOutputDevices() const
{
return this->getAudioDevices().getOutputDevices();
}
CAudioDeviceInfoList CContextAudioBase::getCurrentAudioDevices() const
{
const QString inputDeviceName = m_inputDeviceSetting.get();
const CAudioDeviceInfo inputDevice = this->getAudioInputDevices().findByNameOrDefault(
inputDeviceName, CAudioDeviceInfo::getDefaultInputDevice());
const QString outputDeviceName = m_outputDeviceSetting.get();
const CAudioDeviceInfo outputDevice = this->getAudioOutputDevices().findByNameOrDefault(
outputDeviceName, CAudioDeviceInfo::getDefaultOutputDevice());
CAudioDeviceInfoList devices;
devices.push_back(inputDevice);
devices.push_back(outputDevice);
return devices;
}
void CContextAudioBase::setCurrentAudioDevices(const CAudioDeviceInfo &inputDevice,
const CAudioDeviceInfo &outputDevice)
{
if (!m_voiceClient) { return; }
if (!sApp) { return; }
if (!inputDevice.getName().isEmpty() && inputDevice.getName() != m_inputDeviceSetting.get())
{
Q_ASSERT_X(inputDevice.isInputDevice(), Q_FUNC_INFO, "Need input device");
const CStatusMessage m = m_inputDeviceSetting.setAndSave(inputDevice.getName());
CLogMessage::preformatted(m);
}
if (!outputDevice.getName().isEmpty() && outputDevice.getName() != m_outputDeviceSetting.get())
{
Q_ASSERT_X(outputDevice.isOutputDevice(), Q_FUNC_INFO, "Need output device");
const CStatusMessage m = m_outputDeviceSetting.setAndSave(outputDevice.getName());
CLogMessage::preformatted(m);
}
m_voiceClient->startAudio(inputDevice, outputDevice);
}
void CContextAudioBase::setMasterOutputVolume(int volume)
{
if (!m_voiceClient) { return; }
const bool wasMuted = this->isOutputMuted();
volume = CSettings::fixOutVolume(volume);
const int currentVolume = m_voiceClient->getNormalizedMasterOutputVolume();
const bool changedVoiceOutput = (currentVolume != volume);
if (changedVoiceOutput)
{
// TODO: KB 2020-05 the mute handling should entirely go to AFV client!
m_voiceClient->setNormalizedMasterOutputVolume(volume);
m_outMasterVolumeBeforeMute = volume;
emit this->changedAudioVolume(volume);
if ((volume > 0 && wasMuted) || (volume < 1 && !wasMuted))
{
// inform about muted
emit this->changedOutputMute(volume < 1);
}
}
CSettings as(m_audioSettings.getThreadLocal());
if (as.getOutVolume() != volume)
{
as.setOutVolume(volume);
m_audioSettings.set(as);
}
}
void CContextAudioBase::setComOutputVolume(CComSystem::ComUnit comUnit, int volume)
{
if (comUnit != CComSystem::Com1 && comUnit != CComSystem::Com2) { return; }
if (!m_voiceClient) { return; }
volume = CSettings::fixOutVolume(volume);
const int currentVolume = m_voiceClient->getNormalizedComOutputVolume(comUnit);
const bool changedVoiceOutput = (currentVolume != volume);
if (changedVoiceOutput)
{
m_voiceClient->setNormalizedComOutputVolume(comUnit, volume);
emit this->changedAudioVolume(volume);
}
CSettings as(m_audioSettings.getThreadLocal());
if (comUnit == CComSystem::Com1 && as.getOutVolumeCom1() != volume)
{
as.setOutVolumeCom1(volume);
m_audioSettings.set(as);
}
else if (comUnit == CComSystem::Com2 && as.getOutVolumeCom2() != volume)
{
as.setOutVolumeCom2(volume);
m_audioSettings.set(as);
}
}
int CContextAudioBase::getMasterOutputVolume() const
{
if (!m_voiceClient) { return 0; }
return m_voiceClient->getNormalizedMasterOutputVolume();
}
int CContextAudioBase::getComOutputVolume(CComSystem::ComUnit comUnit) const
{
if (!m_voiceClient) { return 0; }
return m_voiceClient->getNormalizedComOutputVolume(comUnit);
}
void CContextAudioBase::setOutputMute(bool muted)
{
if (!m_voiceClient) { return; }
if (this->isOutputMuted() == muted) { return; } // avoid roundtrips / unnecessary signals
if (muted) { m_outMasterVolumeBeforeMute = m_voiceClient->getNormalizedMasterOutputVolume(); }
m_voiceClient->setOutputMuted(muted);
if (!muted) { m_voiceClient->setNormalizedMasterOutputVolume(m_outMasterVolumeBeforeMute); }
}
bool CContextAudioBase::isOutputMuted() const
{
if (!m_voiceClient) { return false; }
return m_voiceClient->isOutputMuted();
}
void CContextAudioBase::playSelcalTone(const CSelcal &selcal)
{
using namespace std::chrono_literals;
const std::chrono::milliseconds ms = m_selcalPlayer->play(90, selcal);
if (ms > 10ms)
{
// Play additional notification
const QPointer<const CContextAudioBase> myself(this);
QTimer::singleShot(ms, this, [=] {
if (!sApp || sApp->isShuttingDown() || !myself) { return; }
this->playNotification(CNotificationSounds::NotificationTextMessageSupervisor, true);
});
}
}
void CContextAudioBase::playNotification(CNotificationSounds::NotificationFlag notification, bool considerSettings,
int volume)
{
if (isDebugEnabled())
{
CLogMessage(this, CLogCategories::contextSlot()).debug() << Q_FUNC_INFO << notification;
}
const CSettings settings = m_audioSettings.getThreadLocal();
const bool play = !considerSettings || settings.isNotificationFlagSet(notification);
if (!play) { return; }
if (volume < 0 || volume > 100)
{
volume = 90;
if (considerSettings) { volume = qMax(25, settings.getNotificationVolume()); }
}
m_notificationPlayer.play(notification, volume);
}
void CContextAudioBase::enableAudioLoopback(bool enable)
{
if (!m_voiceClient) { return; }
m_voiceClient->setLoopBack(enable);
}
bool CContextAudioBase::isAudioLoopbackEnabled() const
{
if (!m_voiceClient) { return false; }
return m_voiceClient->isLoopback();
}
void CContextAudioBase::setVoiceTransmission(bool enable)
{
if (!m_voiceClient) { return; }
m_voiceClient->setPtt(enable);
}
void CContextAudioBase::changeDeviceSettings()
{
const CAudioDeviceInfoList devices = this->getCurrentAudioDevices();
Q_ASSERT_X(devices.size() == 2, Q_FUNC_INFO, "Expect INPUT and OUTPUT device");
const CAudioDeviceInfo input = devices.front();
const CAudioDeviceInfo output = devices.back();
this->setCurrentAudioDevices(input, output);
}
void CContextAudioBase::onChangedAudioSettings()
{
const CSettings s = m_audioSettings.get();
const QString dir = s.getNotificationSoundDirectory();
m_notificationPlayer.updateDirectory(dir);
this->setMasterOutputVolume(s.getOutVolume());
this->setComOutputVolume(CComSystem::Com1, s.getOutVolumeCom1());
this->setComOutputVolume(CComSystem::Com2, s.getOutVolumeCom2());
}
void CContextAudioBase::audioIncreaseVolume(bool enabled)
{
if (!enabled) { return; }
const int v = qRound(this->getMasterOutputVolume() * 1.05);
this->setMasterOutputVolume(v);
}
void CContextAudioBase::audioDecreaseVolume(bool enabled)
{
if (!enabled) { return; }
const int v = qRound(this->getMasterOutputVolume() / 1.05);
this->setMasterOutputVolume(v);
}
void CContextAudioBase::audioIncreaseVolumeCom1(bool enabled)
{
if (!enabled) { return; }
if (isComUnitIntegrated()) { return; }
const int v = qRound(this->getComOutputVolume(CComSystem::Com1) * 1.05);
this->setComOutputVolume(CComSystem::Com1, v);
}
void CContextAudioBase::audioDecreaseVolumeCom1(bool enabled)
{
if (!enabled) { return; }
if (isComUnitIntegrated()) { return; }
const int v = qRound(this->getComOutputVolume(CComSystem::Com1) / 1.05);
this->setComOutputVolume(CComSystem::Com1, v);
}
void CContextAudioBase::audioIncreaseVolumeCom2(bool enabled)
{
if (!enabled) { return; }
if (isComUnitIntegrated()) { return; }
const int v = qRound(this->getComOutputVolume(CComSystem::Com2) * 1.05);
this->setComOutputVolume(CComSystem::Com2, v);
}
void CContextAudioBase::audioDecreaseVolumeCom2(bool enabled)
{
if (!enabled) { return; }
if (isComUnitIntegrated()) { return; }
const int v = qRound(this->getComOutputVolume(CComSystem::Com2) / 1.05);
this->setComOutputVolume(CComSystem::Com2, v);
}
void CContextAudioBase::xCtxNetworkConnectionStatusChanged(const CConnectionStatus &from,
const CConnectionStatus &to)
{
if (!m_voiceClient) { return; }
Q_UNUSED(from)
SWIFT_VERIFY_X(this->getIContextNetwork(), Q_FUNC_INFO, "Missing network context");
// we only change network connection of AFV client here
if (to.isConnected() && this->getIContextNetwork())
{
const bool connected = this->connectAudioWithNetworkCredentials();
Q_UNUSED(connected)
// one reason for not connecting is NOT using the VATSIM ecosystem
}
else if (to.isDisconnected()) { m_voiceClient->disconnectFrom(); }
}
void CContextAudioBase::onAfvConnectionStatusChanged(int status)
{
if (!m_voiceClient) { return; }
const CCallsign cs = m_voiceClient->getCallsign();
const CAfvClient::ConnectionStatus s = static_cast<CAfvClient::ConnectionStatus>(status);
switch (s)
{
case CAfvClient::Connected: this->registerAudioCallsign(cs, this->identifier()); break;
case CAfvClient::Disconnected: this->unRegisterAudioCallsign(cs, this->identifier()); break;
}
}
void CContextAudioBase::onAfvConnectionFailure(const CStatusMessage &msg)
{
if (!m_voiceClient) { return; }
emit this->voiceClientFailure(msg);
}
bool CContextAudioBase::isRunningWithLocalCore() { return sApp && sApp->isLocalContext(); }
} // namespace swift::core::context
//! \endcond