1 // commradio.cxx -- class to manage a nav radio instance
3 // Written by Torsten Dreyer, February 2014
5 // Copyright (C) 2000 - 2011 Curtis L. Olson - http://www.flightgear.org/~curt
7 // This program is free software; you can redistribute it and/or
8 // modify it under the terms of the GNU General Public License as
9 // published by the Free Software Foundation; either version 2 of the
10 // License, or (at your option) any later version.
12 // This program is distributed in the hope that it will be useful, but
13 // WITHOUT ANY WARRANTY; without even the implied warranty of
14 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15 // General Public License for more details.
17 // You should have received a copy of the GNU General Public License
18 // along with this program; if not, write to the Free Software
19 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
26 #include "commradio.hxx"
29 #include <boost/foreach.hpp>
31 #include <simgear/sg_inlines.h>
32 #include <simgear/props/propertyObject.hxx>
33 #include <simgear/misc/strutils.hxx>
35 #include <ATC/CommStation.hxx>
36 #include <ATC/MetarPropertiesATISInformationProvider.hxx>
37 #include <ATC/CurrentWeatherATISInformationProvider.hxx>
38 #include <Airports/airport.hxx>
39 #include <Main/fg_props.hxx>
40 #include <Navaids/navlist.hxx>
42 #if defined(ENABLE_FLITE)
43 #include <Sound/soundmanager.hxx>
44 #include <simgear/sound/sample_group.hxx>
45 #include <Sound/VoiceSynthesizer.hxx>
48 #include "frequencyformatter.hxx"
50 namespace Instrumentation {
52 using simgear::PropertyObject;
55 #if defined(ENABLE_FLITE)
56 class AtisSpeaker: public SGPropertyChangeListener, SoundSampleReadyListener {
59 virtual ~AtisSpeaker();
60 virtual void valueChanged(SGPropertyNode * node);
61 virtual void SoundSampleReady(SGSharedPtr<SGSoundSample>);
65 return _spokenAtis.empty() == false;
68 SGSharedPtr<SGSoundSample> getSpokenAtis()
70 return _spokenAtis.pop();
73 void setStationId(const string & stationId)
75 _stationId = stationId;
80 SynthesizeRequest _synthesizeRequest;
81 SGLockedQueue<SGSharedPtr<SGSoundSample> > _spokenAtis;
85 AtisSpeaker::AtisSpeaker()
87 _synthesizeRequest.listener = this;
90 AtisSpeaker::~AtisSpeaker()
94 void AtisSpeaker::valueChanged(SGPropertyNode * node)
96 if (!fgGetBool("/sim/sound/working", false))
99 string newText = node->getStringValue();
100 if (_synthesizeRequest.text == newText) return;
102 _synthesizeRequest.text = newText;
104 string voice = "cmu_us_arctic_slt";
106 if (!_stationId.empty()) {
107 // lets play a bit with the voice so not every airports atis sounds alike
108 // but every atis of an airport has the same voice
110 // create a simple hash from the last two letters of the airport's id
111 unsigned char hash = 0;
112 string::iterator i = _stationId.end() - 1;
115 if( i != _stationId.begin() ) {
120 _synthesizeRequest.speed = (hash % 16) / 16.0;
121 _synthesizeRequest.pitch = (hash % 16) / 16.0;
124 voice = FLITEVoiceSynthesizer::getVoicePath(
125 static_cast<FLITEVoiceSynthesizer::voice_t>(hash % FLITEVoiceSynthesizer::VOICE_UNKNOWN) );
129 FGSoundManager * smgr = dynamic_cast<FGSoundManager*>(globals->get_soundmgr());
130 assert(smgr != NULL);
132 SG_LOG(SG_INSTR,SG_INFO,"AtisSpeaker voice is " << voice );
133 FLITEVoiceSynthesizer * synthesizer = dynamic_cast<FLITEVoiceSynthesizer*>(smgr->getSynthesizer(voice));
135 synthesizer->synthesize(_synthesizeRequest);
138 void AtisSpeaker::SoundSampleReady(SGSharedPtr<SGSoundSample> sample)
140 // we are now in the synthesizers worker thread!
141 _spokenAtis.push(sample);
145 SignalQualityComputer::~SignalQualityComputer()
149 class SimpleDistanceSquareSignalQualityComputer: public SignalQualityComputer {
151 SimpleDistanceSquareSignalQualityComputer()
152 : _altitudeAgl_ft(fgGetNode("/position/altitude-agl-ft", true))
156 ~SimpleDistanceSquareSignalQualityComputer()
160 double computeSignalQuality(double distance_nm) const
162 // Very simple line of sight propagation model. It's cheap but it does the trick for now.
163 // assume transmitter and receiver antennas are at some elevation above ground
164 // so we have at least a range of 5NM. Add the approx. distance to the horizon.
165 double range_nm = 5.0 + 1.23 * ::sqrt(SGMiscd::max(.0, _altitudeAgl_ft));
166 return distance_nm < range_nm ? 1.0 : (range_nm * range_nm / distance_nm / distance_nm);
170 PropertyObject<double> _altitudeAgl_ft;
173 class OnExitHandler {
175 virtual void onExit() = 0;
176 virtual ~OnExitHandler()
183 OnExit(OnExitHandler * onExitHandler)
184 : _onExitHandler(onExitHandler)
189 _onExitHandler->onExit();
192 OnExitHandler * _onExitHandler;
195 class OutputProperties: public OnExitHandler {
197 OutputProperties(SGPropertyNode_ptr rootNode)
198 : _rootNode(rootNode), _signalQuality_norm(0.0), _slantDistance_m(0.0), _trueBearingTo_deg(0.0), _trueBearingFrom_deg(0.0), _trackDistance_m(
199 0.0), _heightAboveStation_ft(0.0),
201 _PO_stationType(rootNode->getNode("station-type", true)), _PO_stationName(rootNode->getNode("station-name", true)), _PO_airportId(
202 rootNode->getNode("airport-id", true)), _PO_signalQuality_norm(rootNode->getNode("signal-quality-norm", true)), _PO_slantDistance_m(
203 rootNode->getNode("slant-distance-m", true)), _PO_trueBearingTo_deg(rootNode->getNode("true-bearing-to-deg", true)), _PO_trueBearingFrom_deg(
204 rootNode->getNode("true-bearing-from-deg", true)), _PO_trackDistance_m(rootNode->getNode("track-distance-m", true)), _PO_heightAboveStation_ft(
205 rootNode->getNode("height-above-station-ft", true))
208 virtual ~OutputProperties()
213 SGPropertyNode_ptr _rootNode;
215 std::string _stationType;
216 std::string _stationName;
217 std::string _airportId;
218 double _signalQuality_norm;
219 double _slantDistance_m;
220 double _trueBearingTo_deg;
221 double _trueBearingFrom_deg;
222 double _trackDistance_m;
223 double _heightAboveStation_ft;
226 PropertyObject<string> _PO_stationType;
227 PropertyObject<string> _PO_stationName;
228 PropertyObject<string> _PO_airportId;
229 PropertyObject<double> _PO_signalQuality_norm;
230 PropertyObject<double> _PO_slantDistance_m;
231 PropertyObject<double> _PO_trueBearingTo_deg;
232 PropertyObject<double> _PO_trueBearingFrom_deg;
233 PropertyObject<double> _PO_trackDistance_m;
234 PropertyObject<double> _PO_heightAboveStation_ft;
236 virtual void onExit()
238 _PO_stationType = _stationType;
239 _PO_stationName = _stationName;
240 _PO_airportId = _airportId;
241 _PO_signalQuality_norm = _signalQuality_norm;
242 _PO_slantDistance_m = _slantDistance_m;
243 _PO_trueBearingTo_deg = _trueBearingTo_deg;
244 _PO_trueBearingFrom_deg = _trueBearingFrom_deg;
245 _PO_trackDistance_m = _trackDistance_m;
246 _PO_heightAboveStation_ft = _heightAboveStation_ft;
250 /* ------------- The CommRadio implementation ---------------------- */
252 class MetarBridge: public SGReferenced, public SGPropertyChangeListener {
259 void requestMetarForId(std::string & id);
261 void setMetarPropertiesRoot(SGPropertyNode_ptr n)
263 _metarPropertiesNode = n;
265 void setAtisNode(SGPropertyNode * n)
271 virtual void valueChanged(SGPropertyNode *);
274 std::string _requestedId;
275 SGPropertyNode_ptr _realWxEnabledNode;
276 SGPropertyNode_ptr _metarPropertiesNode;
277 SGPropertyNode * _atisNode;
278 ATISEncoder _atisEncoder;
280 typedef SGSharedPtr<MetarBridge> MetarBridgeRef;
282 MetarBridge::MetarBridge()
287 MetarBridge::~MetarBridge()
291 void MetarBridge::bind()
293 _realWxEnabledNode = fgGetNode("/environment/realwx/enabled", true);
294 _metarPropertiesNode->getNode("valid", true)->addChangeListener(this);
297 void MetarBridge::unbind()
299 _metarPropertiesNode->getNode("valid", true)->removeChangeListener(this);
302 void MetarBridge::requestMetarForId(std::string & id)
304 std::string uppercaseId = simgear::strutils::uppercase(id);
305 if (_requestedId == uppercaseId) return;
306 _requestedId = uppercaseId;
308 if (_realWxEnabledNode->getBoolValue()) {
309 // trigger a METAR request for the associated metarproperties
310 _metarPropertiesNode->getNode("station-id", true)->setStringValue(uppercaseId);
311 _metarPropertiesNode->getNode("valid", true)->setBoolValue(false);
312 _metarPropertiesNode->getNode("time-to-live", true)->setDoubleValue(0.0);
314 // use the present weather to generate the ATIS.
315 if ( NULL != _atisNode && false == _requestedId.empty()) {
316 CurrentWeatherATISInformationProvider provider(_requestedId);
317 _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
322 void MetarBridge::clearMetar()
325 requestMetarForId(empty);
328 void MetarBridge::valueChanged(SGPropertyNode * node)
330 // check for raising edge of valid flag
331 if ( NULL == node || false == node->getBoolValue() || false == _realWxEnabledNode->getBoolValue()) return;
333 std::string responseId = simgear::strutils::uppercase(_metarPropertiesNode->getNode("station-id", true)->getStringValue());
335 // unrequested metar!?
336 if (responseId != _requestedId) return;
338 if ( NULL != _atisNode) {
339 MetarPropertiesATISInformationProvider provider(_metarPropertiesNode);
340 _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
344 /* ------------- The CommRadio implementation ---------------------- */
346 class CommRadioImpl: public CommRadio, OutputProperties {
349 CommRadioImpl(SGPropertyNode_ptr node);
350 virtual ~CommRadioImpl();
352 virtual void update(double dt);
358 string getSampleGroupRefname() const
360 return _rootNode->getPath();
364 MetarBridgeRef _metarBridge;
365 #if defined(ENABLE_FLITE)
366 AtisSpeaker _atisSpeaker;
368 FrequencyFormatter _useFrequencyFormatter;
369 FrequencyFormatter _stbyFrequencyFormatter;
370 const SignalQualityComputerRef _signalQualityComputer;
374 flightgear::CommStationRef _commStationForFrequency;
375 #if defined(ENABLE_FLITE)
376 SGSharedPtr<SGSampleGroup> _sampleGroup;
379 PropertyObject<bool> _serviceable;
380 PropertyObject<bool> _power_btn;
381 PropertyObject<bool> _power_good;
382 PropertyObject<double> _volume_norm;
383 PropertyObject<string> _atis;
384 PropertyObject<bool> _addNoise;
385 PropertyObject<double> _cutoffSignalQuality;
388 CommRadioImpl::CommRadioImpl(SGPropertyNode_ptr node)
390 fgGetNode("/instrumentation", true)->getNode(node->getStringValue("name", "comm"), node->getIntValue("number", 0), true)),
391 _num(node->getIntValue("number", 0)),
392 _metarBridge(new MetarBridge()),
393 _useFrequencyFormatter(_rootNode->getNode("frequencies/selected-mhz", true),
394 _rootNode->getNode("frequencies/selected-mhz-fmt", true), 0.025, 118.0, 137.0),
396 _stbyFrequencyFormatter(_rootNode->getNode("frequencies/standby-mhz", true),
397 _rootNode->getNode("frequencies/standby-mhz-fmt", true), 0.025, 118.0, 137.0),
399 _signalQualityComputer(new SimpleDistanceSquareSignalQualityComputer()),
403 _commStationForFrequency(NULL),
405 _serviceable(_rootNode->getNode("serviceable", true)),
406 _power_btn(_rootNode->getNode("power-btn", true)),
407 _power_good(_rootNode->getNode("power-good", true)),
408 _volume_norm(_rootNode->getNode("volume", true)),
409 _atis(_rootNode->getNode("atis", true)),
410 _addNoise(_rootNode->getNode("add-noise", true)),
411 _cutoffSignalQuality(_rootNode->getNode("cutoff-signal-quality", true))
415 CommRadioImpl::~CommRadioImpl()
419 void CommRadioImpl::bind()
421 _metarBridge->setAtisNode(_atis.node());
422 #if defined(ENABLE_FLITE)
423 _atis.node()->addChangeListener(&_atisSpeaker);
425 // link the metar node. /environment/metar[3] is comm1 and /environment[4] is comm2.
426 // see FGDATA/Environment/environment.xml
427 _metarBridge->setMetarPropertiesRoot(fgGetNode("/environment", true)->getNode("metar", _num + 3, true));
428 _metarBridge->bind();
431 void CommRadioImpl::unbind()
433 #if defined(ENABLE_FLITE)
434 _atis.node()->removeChangeListener(&_atisSpeaker);
435 if (_sampleGroup.valid()) {
436 globals->get_soundmgr()->remove(getSampleGroupRefname());
439 _metarBridge->unbind();
442 void CommRadioImpl::init()
444 // initialize power_btn to true if unset
445 string s = _power_btn.node()->getStringValue();
446 if (s.empty()) _power_btn = true;
448 // initialize squelch to a sane value if unset
449 s = _cutoffSignalQuality.node()->getStringValue();
450 if (s.empty()) _cutoffSignalQuality = 0.4;
452 // initialize add-noize to true if unset
453 s = _addNoise.node()->getStringValue();
454 if (s.empty()) _addNoise = true;
457 void CommRadioImpl::update(double dt)
459 if (dt < SGLimitsd::min()) return;
462 // Ensure all output properties get written on exit of this method
467 position = globals->get_aircraft_position();
469 catch (std::exception &) {
473 #if defined(ENABLE_FLITE)
475 static const char * atisSampleRefName = "atis";
476 static const char * noiseSampleRefName = "noise";
478 if (_atisSpeaker.hasSpokenAtis()) {
479 // the speaker has created a new atis sample
480 if (!_sampleGroup.valid()) {
481 // create a sample group for our instrument on the fly
482 _sampleGroup = globals->get_soundmgr()->find(getSampleGroupRefname(), true);
483 _sampleGroup->tie_to_listener();
485 SGSharedPtr<SGSoundSample> noise = new SGSoundSample("Sounds/radionoise.wav", globals->get_fg_root());
486 _sampleGroup->add(noise, noiseSampleRefName);
487 _sampleGroup->play_looped(noiseSampleRefName);
491 // remove previous atis sample
492 _sampleGroup->remove(atisSampleRefName);
493 // add and play the new atis sample
494 SGSharedPtr<SGSoundSample> sample = _atisSpeaker.getSpokenAtis();
495 _sampleGroup->add(sample, atisSampleRefName);
496 _sampleGroup->play_looped(atisSampleRefName);
499 if (_sampleGroup.valid()) {
501 // if noise is configured and there is a noise sample
502 // scale noise and signal volume by signalQuality.
503 SGSoundSample * s = _sampleGroup->find(noiseSampleRefName);
505 s->set_volume(1.0 - _signalQuality_norm);
506 s = _sampleGroup->find(atisSampleRefName);
507 s->set_volume(_signalQuality_norm);
510 // master volume for radio, mute on bad signal quality
511 _sampleGroup->set_volume(_signalQuality_norm >= _cutoffSignalQuality ? _volume_norm : 0.0);
516 if (false == (_power_btn)) {
521 if (_frequency != _useFrequencyFormatter.getFrequency()) {
522 _frequency = _useFrequencyFormatter.getFrequency();
526 if (_stationTTL <= 0.0) {
529 // Note: 122.375 must be rounded DOWN to 122370
530 // in order to be consistent with apt.dat et cetera.
531 int freqKhz = 10 * static_cast<int>(_frequency * 100 + 0.25);
532 _commStationForFrequency = flightgear::CommStation::findByFreq(freqKhz, position, NULL);
536 if (false == _commStationForFrequency.valid()) return;
538 _slantDistance_m = dist(_commStationForFrequency->cart(), SGVec3d::fromGeod(position));
540 SGGeodesy::inverse(position, _commStationForFrequency->geod(), _trueBearingTo_deg, _trueBearingFrom_deg, _trackDistance_m);
542 _heightAboveStation_ft = SGMiscd::max(0.0, position.getElevationFt() - _commStationForFrequency->airport()->elevation());
544 _signalQuality_norm = _signalQualityComputer->computeSignalQuality(_slantDistance_m * SG_METER_TO_NM);
545 _stationType = _commStationForFrequency->nameForType(_commStationForFrequency->type());
546 _stationName = _commStationForFrequency->ident();
547 _airportId = _commStationForFrequency->airport()->getId();
549 #if defined(ENABLE_FLITE)
550 _atisSpeaker.setStationId(_airportId);
552 switch (_commStationForFrequency->type()) {
553 case FGPositioned::FREQ_ATIS:
554 case FGPositioned::FREQ_AWOS: {
555 if (_signalQuality_norm > 0.01) {
556 _metarBridge->requestMetarForId(_airportId);
558 _metarBridge->clearMetar();
565 _metarBridge->clearMetar();
571 SGSubsystem * CommRadio::createInstance(SGPropertyNode_ptr rootNode)
573 return new CommRadioImpl(rootNode);
576 } // namespace Instrumentation