/* Copyright (C) 2013 * swift Project Community / Contributors * * This file is part of swift project. It is subject to the license terms in the LICENSE file found in the top-level * directory of this distribution and at http://www.swift-project.org/license.html. No part of swift project, * including this file, may be copied, modified, propagated, or distributed except according to the terms * contained in the LICENSE file. */ #include "contextaudioimpl.h" #include "contextnetwork.h" #include "contextownaircraft.h" #include "contextapplication.h" #include "voicechannel.h" #include "voicevatlib.h" #include "blacksound/soundgenerator.h" #include "blackmisc/dbusserver.h" #include "blackmisc/audio/notificationsounds.h" #include "blackmisc/audio/voiceroomlist.h" #include "blackmisc/logmessage.h" #include "blackmisc/simplecommandparser.h" #include using namespace BlackMisc; using namespace BlackMisc::Aviation; using namespace BlackMisc::Audio; using namespace BlackMisc::Input; using namespace BlackMisc::Audio; using namespace BlackSound; namespace BlackCore { CContextAudio::CContextAudio(CCoreFacadeConfig::ContextMode mode, CCoreFacade *runtime) : IContextAudio(mode, runtime), m_voice(new CVoiceVatlib()) { // own aircraft may or may not be available const CCallsign ownCallsign = (this->getIContextOwnAircraft()) ? getIContextOwnAircraft()->getOwnAircraft().getCallsign() : CCallsign(); m_channel1 = m_voice->createVoiceChannel(); m_channel1->setOwnAircraftCallsign(ownCallsign); connect(m_channel1.data(), &IVoiceChannel::connectionStatusChanged, this, &CContextAudio::ps_connectionStatusChanged); connect(m_channel1.data(), &IVoiceChannel::userJoinedRoom, this, &CContextAudio::ps_userJoinedRoom); connect(m_channel1.data(), &IVoiceChannel::userLeftRoom, this, &CContextAudio::ps_userLeftRoom); m_channel2 = m_voice->createVoiceChannel(); m_channel2->setOwnAircraftCallsign(ownCallsign); connect(m_channel2.data(), &IVoiceChannel::connectionStatusChanged, this, &CContextAudio::ps_connectionStatusChanged); connect(m_channel2.data(), &IVoiceChannel::userJoinedRoom, this, &CContextAudio::ps_userJoinedRoom); connect(m_channel2.data(), &IVoiceChannel::userLeftRoom, this, &CContextAudio::ps_userLeftRoom); m_voiceInputDevice = m_voice->createInputDevice(); m_voiceOutputDevice = m_voice->createOutputDevice(); m_audioMixer = m_voice->createAudioMixer(); m_voice->connectVoice(m_voiceInputDevice.get(), m_audioMixer.get(), IAudioMixer::InputMicrophone); m_voice->connectVoice(m_channel1.data(), m_audioMixer.get(), IAudioMixer::InputVoiceChannel1); m_voice->connectVoice(m_channel2.data(), m_audioMixer.get(), IAudioMixer::InputVoiceChannel2); m_voice->connectVoice(m_audioMixer.get(), IAudioMixer::OutputOutputDevice1, m_voiceOutputDevice.get()); m_voice->connectVoice(m_audioMixer.get(), IAudioMixer::OutputVoiceChannel1, m_channel1.data()); m_voice->connectVoice(m_audioMixer.get(), IAudioMixer::OutputVoiceChannel2, m_channel2.data()); m_audioMixer->makeMixerConnection(IAudioMixer::InputVoiceChannel1, IAudioMixer::OutputOutputDevice1); m_audioMixer->makeMixerConnection(IAudioMixer::InputVoiceChannel2, IAudioMixer::OutputOutputDevice1); this->setVoiceOutputVolume(90); // Load sounds (init), not possible in own thread QTimer::singleShot(10 * 1000, this, SLOT(ps_initNotificationSounds())); m_unusedVoiceChannels.push_back(m_channel1); m_unusedVoiceChannels.push_back(m_channel2); } CContextAudio *CContextAudio::registerWithDBus(CDBusServer *server) { if (!server || this->m_mode != CCoreFacadeConfig::LocalInDbusServer) { return this; } server->addObject(IContextAudio::ObjectPath(), this); return this; } CContextAudio::~CContextAudio() { this->leaveAllVoiceRooms(); } CVoiceRoomList CContextAudio::getComVoiceRoomsWithAudioStatus() const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } return getComVoiceRooms(); } CVoiceRoom CContextAudio::getVoiceRoom(BlackMisc::Aviation::CComSystem::ComUnit comUnitValue, bool withAudioStatus) const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << withAudioStatus; } auto voiceChannel = m_voiceChannelMapping.value(comUnitValue); if (voiceChannel) { return voiceChannel->getVoiceRoom(); } else { return CVoiceRoom(); } } CVoiceRoomList CContextAudio::getComVoiceRooms() const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } CVoiceRoomList voiceRoomList; auto voiceChannelCom1 = m_voiceChannelMapping.value(BlackMisc::Aviation::CComSystem::Com1); if (voiceChannelCom1) { CVoiceRoom room = voiceChannelCom1->getVoiceRoom(); voiceRoomList.push_back(room); } else { voiceRoomList.push_back(CVoiceRoom()); } auto voiceChannelCom2 = m_voiceChannelMapping.value(BlackMisc::Aviation::CComSystem::Com2); if (voiceChannelCom2) { CVoiceRoom room = voiceChannelCom2->getVoiceRoom(); voiceRoomList.push_back(room); } else { voiceRoomList.push_back(CVoiceRoom()); } return voiceRoomList; } void CContextAudio::leaveAllVoiceRooms() { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO;} m_voiceChannelMapping.clear(); m_channel1->leaveVoiceRoom(); m_channel2->leaveVoiceRoom(); m_unusedVoiceChannels.push_back(m_channel1); m_unusedVoiceChannels.push_back(m_channel2); } CAudioDeviceInfoList CContextAudio::getAudioDevices() const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } CAudioDeviceInfoList devices = this->m_voiceOutputDevice->getOutputDevices(); devices = devices.join(this->m_voiceInputDevice->getInputDevices()); return devices; } CAudioDeviceInfoList CContextAudio::getCurrentAudioDevices() const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } CAudioDeviceInfoList devices; devices.push_back(this->m_voiceInputDevice->getCurrentInputDevice()); devices.push_back(this->m_voiceOutputDevice->getCurrentOutputDevice()); return devices; } void CContextAudio::setCurrentAudioDevice(const CAudioDeviceInfo &audioDevice) { Q_ASSERT(this->m_voice); Q_ASSERT(audioDevice.getType() != CAudioDeviceInfo::Unknown); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << audioDevice; } bool changed = false; if (audioDevice.getType() == CAudioDeviceInfo::InputDevice) { if (this->m_voiceInputDevice->getCurrentInputDevice() != audioDevice) { this->m_voiceInputDevice->setInputDevice(audioDevice); changed = true; } } else { if (this->m_voiceOutputDevice->getCurrentOutputDevice() != audioDevice) { this->m_voiceOutputDevice->setOutputDevice(audioDevice); changed = true; } } if (changed) { emit changedSelectedAudioDevices(this->getCurrentAudioDevices()); } } void CContextAudio::setVoiceOutputVolume(int volume) { Q_ASSERT(m_voiceOutputDevice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << volume; } bool wasMuted = isMuted(); bool changed = m_voiceOutputDevice->getOutputVolume() != volume; if (!changed) { return; } m_voiceOutputDevice->setOutputVolume(volume); m_outVolumeBeforeMute = m_voiceOutputDevice->getOutputVolume(); emit changedAudioVolume(volume); if ((volume > 0 && wasMuted) || (volume < 1 && !wasMuted)) { // inform about muted emit changedMute(volume < 1); } } int CContextAudio::getVoiceOutputVolume() const { Q_ASSERT(m_voiceOutputDevice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } return m_voiceOutputDevice->getOutputVolume(); } void CContextAudio::setMute(bool muted) { if (this->isMuted() == muted) { return; } // avoid roundtrips / unnecessary signals if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << muted; } int newVolume; if (muted) { Q_ASSERT(this->m_voiceOutputDevice); m_outVolumeBeforeMute = this->m_voiceOutputDevice->getOutputVolume(); newVolume = 0; } else { newVolume = m_outVolumeBeforeMute < MinUnmuteVolume ? MinUnmuteVolume : m_outVolumeBeforeMute; m_outVolumeBeforeMute = newVolume; } // do not call setVoiceOutputVolume -> infinite loop if (newVolume != m_voiceOutputDevice->getOutputVolume()) { m_voiceOutputDevice->setOutputVolume(newVolume); emit changedAudioVolume(newVolume); } // signal emit changedMute(muted); } bool CContextAudio::isMuted() const { if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } return m_voiceOutputDevice->getOutputVolume() < 1; } void CContextAudio::setComVoiceRooms(const CVoiceRoomList &newRooms) { Q_ASSERT(this->m_voice); Q_ASSERT(newRooms.size() == 2); Q_ASSERT(getIContextOwnAircraft()); if (m_debugEnabled) {CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << newRooms; } CVoiceRoomList currentRooms = getComVoiceRooms(); CVoiceRoom currentRoomCom1 = currentRooms[0]; CVoiceRoom currentRoomCom2 = currentRooms[1]; CVoiceRoom newRoomCom1 = newRooms[0]; CVoiceRoom newRoomCom2 = newRooms[1]; const CCallsign ownCallsign(this->getIContextOwnAircraft()->getOwnAircraft().getCallsign()); bool changed = false; // changed rooms? But only compare on "URL", not status as connected etc. if (currentRoomCom1.getVoiceRoomUrl() != newRoomCom1.getVoiceRoomUrl()) { auto oldVoiceChannel = m_voiceChannelMapping.value(BlackMisc::Aviation::CComSystem::Com1); if (oldVoiceChannel) { m_voiceChannelMapping.remove(BlackMisc::Aviation::CComSystem::Com1); // If the voice channel is not used by anybody else if (!m_voiceChannelMapping.values().contains(oldVoiceChannel)) { oldVoiceChannel->leaveVoiceRoom(); m_unusedVoiceChannels.push_back(oldVoiceChannel); } else { emit this->changedVoiceRooms(getComVoiceRooms(), false); } } if (newRoomCom1.isValid()) { auto newVoiceChannel = getVoiceChannelBy(newRoomCom1); newVoiceChannel->setOwnAircraftCallsign(ownCallsign); bool inUse = m_voiceChannelMapping.values().contains(newVoiceChannel); m_voiceChannelMapping.insert(BlackMisc::Aviation::CComSystem::Com1, newVoiceChannel); // If the voice channel is not used by anybody else if (!inUse) { newVoiceChannel->joinVoiceRoom(newRoomCom1); } else { emit this->changedVoiceRooms(getComVoiceRooms(), true); } } changed = true; } // changed rooms? But only compare on "URL", not status as connected etc. if (currentRoomCom2.getVoiceRoomUrl() != newRoomCom2.getVoiceRoomUrl()) { auto oldVoiceChannel = m_voiceChannelMapping.value(BlackMisc::Aviation::CComSystem::Com2); if (oldVoiceChannel) { m_voiceChannelMapping.remove(BlackMisc::Aviation::CComSystem::Com2); // If the voice channel is not used by anybody else if (!m_voiceChannelMapping.values().contains(oldVoiceChannel)) { oldVoiceChannel->leaveVoiceRoom(); m_unusedVoiceChannels.push_back(oldVoiceChannel); } else { emit this->changedVoiceRooms(getComVoiceRooms(), false); } } if (newRoomCom2.isValid()) { auto newVoiceChannel = getVoiceChannelBy(newRoomCom2); newVoiceChannel->setOwnAircraftCallsign(ownCallsign); bool inUse = m_voiceChannelMapping.values().contains(newVoiceChannel); m_voiceChannelMapping.insert(BlackMisc::Aviation::CComSystem::Com2, newVoiceChannel); // If the voice channel is not used by anybody else if (!inUse) { newVoiceChannel->joinVoiceRoom(newRoomCom2); } else { emit this->changedVoiceRooms(getComVoiceRooms(), true); } } changed = true; } // changed not yet used, but I keep it for debugging // changedVoiceRooms called by connectionStatusChanged; Q_UNUSED(changed); } void CContextAudio::setOwnCallsignForRooms(const CCallsign &callsign) { if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << callsign; } if (m_channel1) { m_channel1->setOwnAircraftCallsign(callsign); } if (m_channel2) { m_channel2->setOwnAircraftCallsign(callsign); } } CCallsignSet CContextAudio::getRoomCallsigns(BlackMisc::Aviation::CComSystem::ComUnit comUnitValue) const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } auto voiceChannel = m_voiceChannelMapping.value(comUnitValue); if (voiceChannel) { return voiceChannel->getVoiceRoomCallsigns(); } else { return CCallsignSet(); } } Network::CUserList CContextAudio::getRoomUsers(BlackMisc::Aviation::CComSystem::ComUnit comUnit) const { Q_ASSERT(this->m_voice); Q_ASSERT(this->getRuntime()); if (!this->getRuntime()->getIContextNetwork()) return Network::CUserList(); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } return this->getIContextNetwork()->getUsersForCallsigns(this->getRoomCallsigns(comUnit)); } void CContextAudio::playSelcalTone(const CSelcal &selcal) const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << selcal; } CAudioDeviceInfo outputDevice = m_voiceOutputDevice->getCurrentOutputDevice(); CSoundGenerator::playSelcal(90, selcal, outputDevice); } void CContextAudio::playNotification(CNotificationSounds::Notification notification, bool considerSettings) const { Q_ASSERT(this->m_voice); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO << notification; } bool play = !considerSettings || m_audioSettings.get().getNotificationFlag(notification); if (play) { CSoundGenerator::playNotificationSound(90, notification); } } void CContextAudio::ps_initNotificationSounds() { // not possible in own thread CSoundGenerator::playNotificationSound(0, CNotificationSounds::NotificationsLoadSounds); } void CContextAudio::enableAudioLoopback(bool enable) { Q_ASSERT(this->m_audioMixer); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } if (enable) { m_audioMixer->makeMixerConnection(IAudioMixer::InputMicrophone, IAudioMixer::OutputOutputDevice1); } else { m_audioMixer->removeMixerConnection(IAudioMixer::InputMicrophone, IAudioMixer::OutputOutputDevice1); } } bool CContextAudio::isAudioLoopbackEnabled() const { Q_ASSERT(this->m_audioMixer); if (m_debugEnabled) { CLogMessage(this, CLogCategory::contextSlot()).debug() << Q_FUNC_INFO; } return this->m_audioMixer->hasMixerConnection(IAudioMixer::InputMicrophone, IAudioMixer::OutputOutputDevice1); } bool CContextAudio::parseCommandLine(const QString &commandLine, const BlackMisc::CIdentifier &originator) { Q_UNUSED(originator); if (commandLine.isEmpty()) { return false; } CSimpleCommandParser parser( { ".vol", ".volume", // output volume ".mute", // mute ".unmute" // unmute }); parser.parse(commandLine); if (!parser.isKnownCommand()) { return false; } if (parser.matchesCommand(".mute")) { this->setMute(true); return true; } else if (parser.matchesCommand(".unmute")) { this->setMute(false); return true; } else if (parser.commandStartsWith("vol") && parser.countParts() > 1) { int v = parser.toInt(1); if (v >= 0 && v <= 300) { setVoiceOutputVolume(v); return true; } } return false; } void CContextAudio::ps_setVoiceTransmission(bool enable) { // FIXME: Use the 'active' channel instead of hardcoded COM1 if (enable) m_audioMixer->makeMixerConnection(IAudioMixer::InputMicrophone, IAudioMixer::OutputVoiceChannel1); else m_audioMixer->removeMixerConnection(IAudioMixer::InputMicrophone, IAudioMixer::OutputVoiceChannel1); } void CContextAudio::ps_connectionStatusChanged(BlackCore::IVoiceChannel::ConnectionStatus oldStatus, BlackCore::IVoiceChannel::ConnectionStatus newStatus) { Q_UNUSED(oldStatus); switch (newStatus) { case IVoiceChannel::Connected: emit this->changedVoiceRooms(getComVoiceRooms(), true); break; case IVoiceChannel::Disconnecting: break; case IVoiceChannel::Connecting: break; case IVoiceChannel::ConnectingFailed: case IVoiceChannel::DisconnectedError: CLogMessage(this).warning("Voice channel disconnecting error"); // intentional fall-through case IVoiceChannel::Disconnected: if (this->getIContextOwnAircraft()) { // good chance to update aircraft m_channel1->setOwnAircraftCallsign(this->getIContextOwnAircraft()->getOwnAircraft().getCallsign()); m_channel2->setOwnAircraftCallsign(this->getIContextOwnAircraft()->getOwnAircraft().getCallsign()); } emit this->changedVoiceRooms(getComVoiceRooms(), false); break; default: break; } } void CContextAudio::ps_userJoinedRoom(const CCallsign & /**callsign**/) { emit this->changedVoiceRoomMembers(); } void CContextAudio::ps_userLeftRoom(const CCallsign & /**callsign**/) { emit this->changedVoiceRoomMembers(); } QSharedPointer CContextAudio::getVoiceChannelBy(const CVoiceRoom &voiceRoom) { QSharedPointer voiceChannel; for (const auto &channel : m_voiceChannelMapping.values()) { if (channel->getVoiceRoom().getVoiceRoomUrl() == voiceRoom.getVoiceRoomUrl()) voiceChannel = channel; } // If we haven't found a valid voice channel pointer, get an unused one if (!voiceChannel) { Q_ASSERT(!m_unusedVoiceChannels.isEmpty()); voiceChannel = m_unusedVoiceChannels.takeFirst(); } return voiceChannel; } } // namespace