// commradio.cxx -- class to manage a nav radio instance // // Written by Torsten Dreyer, February 2014 // // Copyright (C) 2000 - 2011 Curtis L. Olson - http://www.flightgear.org/~curt // // This program is free software; you can redistribute it and/or // modify it under the terms of the GNU General Public License as // published by the Free Software Foundation; either version 2 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, but // WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU // General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program; if not, write to the Free Software // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. // #ifdef HAVE_CONFIG_H # include #endif #include "commradio.hxx" #include #include #include #include #include #include #include #include #include
#include #include #include #include #include "frequencyformatter.hxx" namespace Instrumentation { using simgear::PropertyObject; using std::string; class AtisSpeaker: public SGPropertyChangeListener, SoundSampleReadyListener { public: AtisSpeaker(); virtual ~AtisSpeaker(); virtual void valueChanged(SGPropertyNode * node); virtual void SoundSampleReady(SGSharedPtr); bool hasSpokenAtis() { return _spokenAtis.empty() == false; } SGSharedPtr getSpokenAtis() { return _spokenAtis.pop(); } void setStationId(const string & stationId) { _stationId = stationId; } private: SynthesizeRequest _synthesizeRequest; SGLockedQueue > _spokenAtis; string _stationId; }; AtisSpeaker::AtisSpeaker() { _synthesizeRequest.listener = this; if (!fgHasNode("/sim/atis/speed")) fgSetDouble("/sim/atis/speed", 1); if (!fgHasNode("/sim/atis/pitch")) fgSetDouble("/sim/atis/pitch", 1); if (!fgHasNode("/sim/atis/enabled")) fgSetBool("/sim/atis/enabled", true); } AtisSpeaker::~AtisSpeaker() { } void AtisSpeaker::valueChanged(SGPropertyNode * node) { using namespace simgear::strutils; if (!fgGetBool("/sim/sound/working", false)) return; string newText = node->getStringValue(); if (_synthesizeRequest.text == newText) return; SG_LOG(SG_INSTR,SG_INFO,"PATH= " << node->getPath() ) SG_LOG(SG_INSTR,SG_INFO,"newText:" << newText); _synthesizeRequest.text = newText; string voice = "cmu_us_arctic_slt"; if (!_stationId.empty()) { // lets play a bit with the voice so not every airports atis sounds alike // but every atis of an airport has the same voice // create a simple hash from the last two letters of the airport's id unsigned char hash = 0; string::iterator i = _stationId.end() - 1; hash += *i; if( i != _stationId.begin() ) { --i; hash += *i; } _synthesizeRequest.speed = (hash % 16) / 16.0 * fgGetDouble("/sim/atis/speed", 1); _synthesizeRequest.pitch = (hash % 16) / 16.0 * fgGetDouble("/sim/atis/pitch", 1); if( starts_with( _stationId, "K" ) || starts_with( _stationId, "C" ) || starts_with( _stationId, "P" ) ) { voice = FLITEVoiceSynthesizer::getVoicePath("cmu_us_arctic_slt"); } else if ( starts_with( _stationId, "EG" ) ) { voice = FLITEVoiceSynthesizer::getVoicePath("cstr_uk_female"); } else { // Pick a random voice from the available voices voice = FLITEVoiceSynthesizer::getVoicePath( static_cast(hash % FLITEVoiceSynthesizer::VOICE_UNKNOWN) ); } } FGSoundManager * smgr = globals->get_subsystem(); if (!smgr) { return; } SG_LOG(SG_INSTR, SG_DEBUG,"node->getPath()=" << node->getPath() << " AtisSpeaker voice is " << voice ); FLITEVoiceSynthesizer * synthesizer = dynamic_cast(smgr->getSynthesizer(voice)); synthesizer->synthesize(_synthesizeRequest); } void AtisSpeaker::SoundSampleReady(SGSharedPtr sample) { // we are now in the synthesizers worker thread! _spokenAtis.push(sample); } SignalQualityComputer::~SignalQualityComputer() { } class SimpleDistanceSquareSignalQualityComputer: public SignalQualityComputer { public: SimpleDistanceSquareSignalQualityComputer() : _altitudeAgl_ft(fgGetNode("/position/altitude-agl-ft", true)) { } ~SimpleDistanceSquareSignalQualityComputer() { } double computeSignalQuality(double distance_nm) const { // Very simple line of sight propagation model. It's cheap but it does the trick for now. // assume transmitter and receiver antennas are at some elevation above ground // so we have at least a range of 5NM. Add the approx. distance to the horizon. double range_nm = 5.0 + 1.23 * ::sqrt(SGMiscd::max(.0, _altitudeAgl_ft)); return distance_nm < range_nm ? 1.0 : (range_nm * range_nm / distance_nm / distance_nm); } private: PropertyObject _altitudeAgl_ft; }; class OnExitHandler { public: virtual void onExit() = 0; virtual ~OnExitHandler() { } }; class OnExit { public: OnExit(OnExitHandler * onExitHandler) : _onExitHandler(onExitHandler) { } ~OnExit() { _onExitHandler->onExit(); } private: OnExitHandler * _onExitHandler; }; class OutputProperties: public OnExitHandler { public: void bind(SGPropertyNode* rn) { _rootNode = rn; _PO_stationType = PropertyObject(_rootNode->getNode("station-type", true)); _PO_stationName = PropertyObject(_rootNode->getNode("station-name", true)); _PO_airportId = PropertyObject(_rootNode->getNode("airport-id", true)); _PO_signalQuality_norm = PropertyObject(_rootNode->getNode("signal-quality-norm", true)); _PO_slantDistance_m = PropertyObject(_rootNode->getNode("slant-distance-m", true)); _PO_trueBearingTo_deg = PropertyObject(_rootNode->getNode("true-bearing-to-deg", true)); _PO_trueBearingFrom_deg = PropertyObject(_rootNode->getNode("true-bearing-from-deg", true)); _PO_trackDistance_m = PropertyObject(_rootNode->getNode("track-distance-m", true)); _PO_heightAboveStation_ft = PropertyObject(_rootNode->getNode("height-above-station-ft", true)); } virtual ~OutputProperties() { } protected: SGPropertyNode_ptr _rootNode; std::string _stationType; std::string _stationName; std::string _airportId; double _signalQuality_norm = 0.0; double _slantDistance_m = 0.0; double _trueBearingTo_deg = 0.0; double _trueBearingFrom_deg = 0.0; double _trackDistance_m = 0.0; double _heightAboveStation_ft = 0.0; private: PropertyObject _PO_stationType; PropertyObject _PO_stationName; PropertyObject _PO_airportId; PropertyObject _PO_signalQuality_norm; PropertyObject _PO_slantDistance_m; PropertyObject _PO_trueBearingTo_deg; PropertyObject _PO_trueBearingFrom_deg; PropertyObject _PO_trackDistance_m; PropertyObject _PO_heightAboveStation_ft; virtual void onExit() { _PO_stationType = _stationType; _PO_stationName = _stationName; _PO_airportId = _airportId; _PO_signalQuality_norm = _signalQuality_norm; _PO_slantDistance_m = _slantDistance_m; _PO_trueBearingTo_deg = _trueBearingTo_deg; _PO_trueBearingFrom_deg = _trueBearingFrom_deg; _PO_trackDistance_m = _trackDistance_m; _PO_heightAboveStation_ft = _heightAboveStation_ft; } }; /* ------------- The CommRadio implementation ---------------------- */ class MetarBridge: public SGReferenced, public SGPropertyChangeListener { public: void bind(); void unbind(); void requestMetarForId(std::string & id); void clearMetar(); void setMetarPropertiesRoot(SGPropertyNode_ptr n) { _metarPropertiesNode = n; } void setAtisNode(SGPropertyNode * n) { _atisNode = n; } protected: virtual void valueChanged(SGPropertyNode *); private: std::string _requestedId; SGPropertyNode_ptr _realWxEnabledNode; SGPropertyNode_ptr _metarPropertiesNode; SGPropertyNode * _atisNode = nullptr; ATISEncoder _atisEncoder; }; typedef SGSharedPtr MetarBridgeRef; void MetarBridge::bind() { _realWxEnabledNode = fgGetNode("/environment/realwx/enabled", true); _metarPropertiesNode->getNode("valid", true)->addChangeListener(this); } void MetarBridge::unbind() { _metarPropertiesNode->getNode("valid", true)->removeChangeListener(this); } void MetarBridge::requestMetarForId(std::string & id) { std::string uppercaseId = simgear::strutils::uppercase(id); if (_requestedId == uppercaseId) return; _requestedId = uppercaseId; if (_realWxEnabledNode->getBoolValue()) { // trigger a METAR request for the associated metarproperties _metarPropertiesNode->getNode("station-id", true)->setStringValue(uppercaseId); _metarPropertiesNode->getNode("valid", true)->setBoolValue(false); _metarPropertiesNode->getNode("time-to-live", true)->setDoubleValue(0.0); } else { // use the present weather to generate the ATIS. if ( NULL != _atisNode && !_requestedId.empty()) { CurrentWeatherATISInformationProvider provider(_requestedId); _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider)); } } } void MetarBridge::clearMetar() { string empty; requestMetarForId(empty); } void MetarBridge::valueChanged(SGPropertyNode * node) { // check for raising edge of valid flag if ( NULL == node || !node->getBoolValue() || !_realWxEnabledNode->getBoolValue()) return; std::string responseId = simgear::strutils::uppercase(_metarPropertiesNode->getNode("station-id", true)->getStringValue()); // unrequested metar!? if (responseId != _requestedId) return; if ( NULL != _atisNode) { MetarPropertiesATISInformationProvider provider(_metarPropertiesNode); _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider)); } } /* ------------- 8.3kHz Channel implementation ---------------------- */ class EightPointThreeFrequencyFormatter : public FrequencyFormatterBase, public SGPropertyChangeListener { public: EightPointThreeFrequencyFormatter( SGPropertyNode_ptr root, const char * channel, const char * fmt, const char * width, const char * frq, const char * cnum ) : _channel( root, channel ), _frequency( root, frq ), _channelSpacing( root, width ), _formattedChannel( root, fmt ), _channelNum( root, cnum ) { // ensure properties exist. _channel.node(true); _frequency.node(true); _channelSpacing.node(true); _channelNum.node(true); _formattedChannel.node(true); _channel.node()->addChangeListener( this, true ); _channelNum.node()->addChangeListener( this, true ); } virtual ~EightPointThreeFrequencyFormatter() { _channel.node()->removeChangeListener( this ); _channelNum.node()->removeChangeListener( this ); } private: EightPointThreeFrequencyFormatter( const EightPointThreeFrequencyFormatter & ); EightPointThreeFrequencyFormatter & operator = ( const EightPointThreeFrequencyFormatter & ); void valueChanged (SGPropertyNode * prop) { if( prop == _channel.node() ) setFrequency(prop->getDoubleValue()); else if( prop == _channelNum.node() ) setChannel(prop->getIntValue()); } void setChannel( int channel ) { channel %= 3040; if( channel < 0 ) channel += 3040; double f = 118.000 + 0.025*(channel/4) + 0.005*(channel%4); if( f != _channel ) _channel = f; } void setFrequency( double channel ) { // format as fixed decimal "nnn.nnn" std::ostringstream buf; buf << std::fixed << std::setw(6) << std::setfill('0') << std::setprecision(3) << _channel; _formattedChannel = buf.str(); // sanitize range and round to nearest kHz. unsigned c = static_cast(SGMiscd::round(SGMiscd::clip( channel, 118.0, 136.99 ) * 1000)); if ( (c % 25) == 0 ) { // legacy 25kHz channels continue to be just that. _channelSpacing = 25.0; _frequency = c / 1000.0; int channelNum = (c-118000)/25*4; if( channelNum != _channelNum ) _channelNum = channelNum; if( _frequency != channel ) { const double channelValue = c / 1000.0; _channel = channelValue; } } else { _channelSpacing = 8.33; // 25kHz base frequency: xxx.000, xxx.025, xxx.050, xxx.075 unsigned base25 = (c/25) * 25; // add n*8.33 to the 25kHz frequency unsigned subChannel = SGMisc::clip((c - base25)/5-1, 0, 2 ); _frequency = (base25 + 8.33 * subChannel)/1000.0; int channelNum = (base25-118000)/25*4 + subChannel+1; if( channelNum != _channelNum ) _channelNum = channelNum; // set to correct channel on bogous input double sanitizedChannel = (base25 + 5*(subChannel+1))/1000.0; if( sanitizedChannel != channel ) { _channel = sanitizedChannel; // triggers recursion } } } double getFrequency() const { return _channel; } PropertyObject _channel; PropertyObject _frequency; PropertyObject _channelSpacing; PropertyObject _formattedChannel; PropertyObject _channelNum; }; /* ------------- The CommRadio implementation ---------------------- */ class CommRadioImpl: public CommRadio, OutputProperties { public: CommRadioImpl(SGPropertyNode_ptr node); virtual ~CommRadioImpl(); // Subsystem API. void bind() override; void init() override; void unbind() override; void update(double dt) override; private: bool _useEightPointThree = false; MetarBridgeRef _metarBridge; AtisSpeaker _atisSpeaker; SGSharedPtr _useFrequencyFormatter; SGSharedPtr _stbyFrequencyFormatter; const SignalQualityComputerRef _signalQualityComputer; double _stationTTL = 0.0; double _frequency = -1.0; flightgear::CommStationRef _commStationForFrequency; PropertyObject _volume_norm; PropertyObject _atis; PropertyObject _addNoise; PropertyObject _cutoffSignalQuality; SGPropertyNode_ptr _atis_enabled_node; bool _atis_enabled_prev; SGSharedPtr _atis_sample; std::string _soundPrefix; void stopAudio(); void updateAudio(); SGSampleGroup* _sampleGroup = nullptr; }; CommRadioImpl::CommRadioImpl(SGPropertyNode_ptr node) : _metarBridge(new MetarBridge), _signalQualityComputer(new SimpleDistanceSquareSignalQualityComputer), _atis_enabled_prev(false) { // set a special value to indicate we don't require a power supply node // by default setDefaultPowerSupplyPath("NO_DEFAULT"); readConfig(node, "comm"); _soundPrefix = name() + "_" + std::to_string(number()) + "_"; _useEightPointThree = node->getBoolValue("eight-point-three", false ); } CommRadioImpl::~CommRadioImpl() { } void CommRadioImpl::bind() { SGPropertyNode_ptr n = fgGetNode(nodePath(), true); OutputProperties::bind(n); _volume_norm = PropertyObject(_rootNode->getNode("volume", true)); _atis = PropertyObject(_rootNode->getNode("atis", true)); if (!fgHasNode("/sim/atis/enabled")) fgSetBool("/sim/atis/enabled", true); _atis_enabled_node = fgGetNode("/sim/atis/enabled"); _addNoise = PropertyObject(_rootNode->getNode("add-noise", true)); _cutoffSignalQuality = PropertyObject(_rootNode->getNode("cutoff-signal-quality", true)); _metarBridge->setAtisNode(_atis.node()); _atis.node()->addChangeListener(&_atisSpeaker); // link the metar node. /environment/metar[3] is comm1 and /environment[4] is comm2. // see FGDATA/Environment/environment.xml _metarBridge->setMetarPropertiesRoot(fgGetNode("/environment", true)->getNode("metar", number() + 3, true)); _metarBridge->bind(); if (_useEightPointThree) { _useFrequencyFormatter = new EightPointThreeFrequencyFormatter( _rootNode->getNode("frequencies", true), "selected-mhz", "selected-mhz-fmt", "selected-channel-width-khz", "selected-real-frequency-mhz", "selected-channel" ); _stbyFrequencyFormatter = new EightPointThreeFrequencyFormatter( _rootNode->getNode("frequencies", true), "standby-mhz", "standby-mhz-fmt", "standby-channel-width-khz", "standby-real-frequency-mhz", "standby-channel" ); } else { _useFrequencyFormatter = new FrequencyFormatter( _rootNode->getNode("frequencies/selected-mhz", true), _rootNode->getNode("frequencies/selected-mhz-fmt", true), 0.025, 118.0, 137.0); _stbyFrequencyFormatter = new FrequencyFormatter( _rootNode->getNode("frequencies/standby-mhz", true), _rootNode->getNode("frequencies/standby-mhz-fmt", true), 0.025, 118.0, 137.0); } } void CommRadioImpl::unbind() { _atis.node()->removeChangeListener(&_atisSpeaker); stopAudio(); _metarBridge->unbind(); AbstractInstrument::unbind(); } void CommRadioImpl::init() { initServicePowerProperties(_rootNode); string s; // initialize squelch to a sane value if unset s = _cutoffSignalQuality.node()->getStringValue(); if (s.empty()) _cutoffSignalQuality = 0.4; // initialize add-noize to true if unset s = _addNoise.node()->getStringValue(); if (s.empty()) _addNoise = true; auto soundManager = globals->get_subsystem(); if (soundManager) { _sampleGroup = soundManager->find("atc", true); } } void CommRadioImpl::update(double dt) { if (dt < SGLimitsd::min()) return; _stationTTL -= dt; // Ensure all output properties get written on exit of this method OnExit onExit(this); SGGeod position; try { position = globals->get_aircraft_position(); } catch (std::exception &) { return; } if (!isServiceableAndPowered()) { _metarBridge->clearMetar(); _atis = ""; _stationTTL = 0.0; stopAudio(); return; } if (_frequency != _useFrequencyFormatter->getFrequency()) { _frequency = _useFrequencyFormatter->getFrequency(); _stationTTL = 0.0; } if (_stationTTL <= 0.0) { _stationTTL = 30.0; int freqKhz = static_cast(_frequency * 1000 + 0.5); // make sure the frequency is integral multiple of 25, if not using 8.333 kHz spacing if (!_useEightPointThree && freqKhz % 25) { freqKhz += 5; } _commStationForFrequency = flightgear::CommStation::findByFreq(freqKhz, position, NULL); } if (!_commStationForFrequency.valid()) { stopAudio(); return; } _slantDistance_m = dist(_commStationForFrequency->cart(), SGVec3d::fromGeod(position)); SGGeodesy::inverse(position, _commStationForFrequency->geod(), _trueBearingTo_deg, _trueBearingFrom_deg, _trackDistance_m); _heightAboveStation_ft = SGMiscd::max(0.0, position.getElevationFt() - _commStationForFrequency->airport()->elevation()); _signalQuality_norm = _signalQualityComputer->computeSignalQuality(_slantDistance_m * SG_METER_TO_NM); _stationType = _commStationForFrequency->nameForType(_commStationForFrequency->type()); _stationName = _commStationForFrequency->ident(); _airportId = _commStationForFrequency->airport()->getId(); _atisSpeaker.setStationId(_airportId); switch (_commStationForFrequency->type()) { case FGPositioned::FREQ_ATIS: case FGPositioned::FREQ_AWOS: { if (_signalQuality_norm > 0.01) { _metarBridge->requestMetarForId(_airportId); } else { _metarBridge->clearMetar(); _atis = ""; } } break; default: _metarBridge->clearMetar(); _atis = ""; break; } updateAudio(); } void CommRadioImpl::updateAudio() { if (!_sampleGroup) return; const string noiseRef = _soundPrefix + "_noise"; const string atisRef = _soundPrefix + "_atis"; SGSoundSample* noiseSample = _sampleGroup->find(noiseRef); // create noise sample if necessary, and play forever if (_addNoise && !noiseSample) { SGSharedPtr noise = new SGSoundSample("Sounds/radionoise.wav", globals->get_fg_root()); _sampleGroup->add(noise, noiseRef); _sampleGroup->play_looped(noiseRef); noiseSample = noise; } bool atis_enabled = _atis_enabled_node->getBoolValue(); int atis_delta = 0; if (atis_enabled && !_atis_enabled_prev) atis_delta = 1; if (!atis_enabled && _atis_enabled_prev) atis_delta = -1; if (_atisSpeaker.hasSpokenAtis()) { // the speaker has created a new atis sample // remove previous atis sample _sampleGroup->remove(atisRef); if (!atis_delta && atis_enabled) atis_delta = 1; } if (atis_delta == 1) { // Start play of atis text. We store the most recent sample in _atis_sample // so that we can resume if /sim/atis/enabled is changed from false to // true. SGSharedPtr sample = _atisSpeaker.getSpokenAtis(); if (sample) _atis_sample = sample; else sample = _atis_sample; if (sample) { SG_LOG(SG_INSTR, SG_DEBUG, "starting looped play of atis sample."); _sampleGroup->add(sample, atisRef); _sampleGroup->play_looped(atisRef); } else { SG_LOG(SG_INSTR, SG_DEBUG, "no atis sample available"); } } if (atis_delta == -1) { // Stop play of atis text. _sampleGroup->remove(atisRef); } _atis_enabled_prev = atis_enabled; // adjust volumes const bool doSquelch = (_signalQuality_norm < _cutoffSignalQuality); double atisVolume = doSquelch ? 0.0 : _volume_norm; if (_addNoise) { const double noiseVol = (1.0 - _signalQuality_norm) * _volume_norm; atisVolume = _signalQuality_norm * _volume_norm; noiseSample->set_volume(doSquelch ? 0.0: noiseVol); } SGSoundSample* s = _sampleGroup->find(atisRef); if (s) { s->set_volume(atisVolume); } } void CommRadioImpl::stopAudio() { if (_sampleGroup) { _sampleGroup->remove(_soundPrefix + "_noise"); _sampleGroup->remove(_soundPrefix + "_atis"); } } SGSubsystem * CommRadio::createInstance(SGPropertyNode_ptr rootNode) { return new CommRadioImpl(rootNode); } // Register the subsystem. #if 0 SGSubsystemMgr::InstancedRegistrant registrantCommRadio( SGSubsystemMgr::FDM, {{"instrumentation", SGSubsystemMgr::Dependency::HARD}}); #endif } // namespace Instrumentation