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 using namespace simgear::strutils;
98 if (!fgGetBool("/sim/sound/working", false))
101 string newText = node->getStringValue();
102 if (_synthesizeRequest.text == newText) return;
104 _synthesizeRequest.text = newText;
106 string voice = "cmu_us_arctic_slt";
108 if (!_stationId.empty()) {
109 // lets play a bit with the voice so not every airports atis sounds alike
110 // but every atis of an airport has the same voice
112 // create a simple hash from the last two letters of the airport's id
113 unsigned char hash = 0;
114 string::iterator i = _stationId.end() - 1;
117 if( i != _stationId.begin() ) {
122 _synthesizeRequest.speed = (hash % 16) / 16.0;
123 _synthesizeRequest.pitch = (hash % 16) / 16.0;
125 if( starts_with( _stationId, "K" ) || starts_with( _stationId, "C" ) ||
126 starts_with( _stationId, "P" ) ) {
127 voice = FLITEVoiceSynthesizer::getVoicePath("cmu_us_arctic_slt");
128 } else if ( starts_with( _stationId, "EG" ) ) {
129 voice = FLITEVoiceSynthesizer::getVoicePath("cstr_uk_female");
131 // Pick a random voice from the available voices
132 voice = FLITEVoiceSynthesizer::getVoicePath(
133 static_cast<FLITEVoiceSynthesizer::voice_t>(hash % FLITEVoiceSynthesizer::VOICE_UNKNOWN) );
137 FGSoundManager * smgr = globals->get_subsystem<FGSoundManager>();
138 assert(smgr != NULL);
140 SG_LOG(SG_INSTR,SG_INFO,"AtisSpeaker voice is " << voice );
141 FLITEVoiceSynthesizer * synthesizer = dynamic_cast<FLITEVoiceSynthesizer*>(smgr->getSynthesizer(voice));
143 synthesizer->synthesize(_synthesizeRequest);
146 void AtisSpeaker::SoundSampleReady(SGSharedPtr<SGSoundSample> sample)
148 // we are now in the synthesizers worker thread!
149 _spokenAtis.push(sample);
153 SignalQualityComputer::~SignalQualityComputer()
157 class SimpleDistanceSquareSignalQualityComputer: public SignalQualityComputer {
159 SimpleDistanceSquareSignalQualityComputer()
160 : _altitudeAgl_ft(fgGetNode("/position/altitude-agl-ft", true))
164 ~SimpleDistanceSquareSignalQualityComputer()
168 double computeSignalQuality(double distance_nm) const
170 // Very simple line of sight propagation model. It's cheap but it does the trick for now.
171 // assume transmitter and receiver antennas are at some elevation above ground
172 // so we have at least a range of 5NM. Add the approx. distance to the horizon.
173 double range_nm = 5.0 + 1.23 * ::sqrt(SGMiscd::max(.0, _altitudeAgl_ft));
174 return distance_nm < range_nm ? 1.0 : (range_nm * range_nm / distance_nm / distance_nm);
178 PropertyObject<double> _altitudeAgl_ft;
181 class OnExitHandler {
183 virtual void onExit() = 0;
184 virtual ~OnExitHandler()
191 OnExit(OnExitHandler * onExitHandler)
192 : _onExitHandler(onExitHandler)
197 _onExitHandler->onExit();
200 OnExitHandler * _onExitHandler;
203 class OutputProperties: public OnExitHandler {
205 OutputProperties(SGPropertyNode_ptr rootNode)
206 : _rootNode(rootNode), _signalQuality_norm(0.0), _slantDistance_m(0.0), _trueBearingTo_deg(0.0), _trueBearingFrom_deg(0.0), _trackDistance_m(
207 0.0), _heightAboveStation_ft(0.0),
209 _PO_stationType(rootNode->getNode("station-type", true)), _PO_stationName(rootNode->getNode("station-name", true)), _PO_airportId(
210 rootNode->getNode("airport-id", true)), _PO_signalQuality_norm(rootNode->getNode("signal-quality-norm", true)), _PO_slantDistance_m(
211 rootNode->getNode("slant-distance-m", true)), _PO_trueBearingTo_deg(rootNode->getNode("true-bearing-to-deg", true)), _PO_trueBearingFrom_deg(
212 rootNode->getNode("true-bearing-from-deg", true)), _PO_trackDistance_m(rootNode->getNode("track-distance-m", true)), _PO_heightAboveStation_ft(
213 rootNode->getNode("height-above-station-ft", true))
216 virtual ~OutputProperties()
221 SGPropertyNode_ptr _rootNode;
223 std::string _stationType;
224 std::string _stationName;
225 std::string _airportId;
226 double _signalQuality_norm;
227 double _slantDistance_m;
228 double _trueBearingTo_deg;
229 double _trueBearingFrom_deg;
230 double _trackDistance_m;
231 double _heightAboveStation_ft;
234 PropertyObject<string> _PO_stationType;
235 PropertyObject<string> _PO_stationName;
236 PropertyObject<string> _PO_airportId;
237 PropertyObject<double> _PO_signalQuality_norm;
238 PropertyObject<double> _PO_slantDistance_m;
239 PropertyObject<double> _PO_trueBearingTo_deg;
240 PropertyObject<double> _PO_trueBearingFrom_deg;
241 PropertyObject<double> _PO_trackDistance_m;
242 PropertyObject<double> _PO_heightAboveStation_ft;
244 virtual void onExit()
246 _PO_stationType = _stationType;
247 _PO_stationName = _stationName;
248 _PO_airportId = _airportId;
249 _PO_signalQuality_norm = _signalQuality_norm;
250 _PO_slantDistance_m = _slantDistance_m;
251 _PO_trueBearingTo_deg = _trueBearingTo_deg;
252 _PO_trueBearingFrom_deg = _trueBearingFrom_deg;
253 _PO_trackDistance_m = _trackDistance_m;
254 _PO_heightAboveStation_ft = _heightAboveStation_ft;
258 /* ------------- The CommRadio implementation ---------------------- */
260 class MetarBridge: public SGReferenced, public SGPropertyChangeListener {
267 void requestMetarForId(std::string & id);
269 void setMetarPropertiesRoot(SGPropertyNode_ptr n)
271 _metarPropertiesNode = n;
273 void setAtisNode(SGPropertyNode * n)
279 virtual void valueChanged(SGPropertyNode *);
282 std::string _requestedId;
283 SGPropertyNode_ptr _realWxEnabledNode;
284 SGPropertyNode_ptr _metarPropertiesNode;
285 SGPropertyNode * _atisNode;
286 ATISEncoder _atisEncoder;
288 typedef SGSharedPtr<MetarBridge> MetarBridgeRef;
290 MetarBridge::MetarBridge()
295 MetarBridge::~MetarBridge()
299 void MetarBridge::bind()
301 _realWxEnabledNode = fgGetNode("/environment/realwx/enabled", true);
302 _metarPropertiesNode->getNode("valid", true)->addChangeListener(this);
305 void MetarBridge::unbind()
307 _metarPropertiesNode->getNode("valid", true)->removeChangeListener(this);
310 void MetarBridge::requestMetarForId(std::string & id)
312 std::string uppercaseId = simgear::strutils::uppercase(id);
313 if (_requestedId == uppercaseId) return;
314 _requestedId = uppercaseId;
316 if (_realWxEnabledNode->getBoolValue()) {
317 // trigger a METAR request for the associated metarproperties
318 _metarPropertiesNode->getNode("station-id", true)->setStringValue(uppercaseId);
319 _metarPropertiesNode->getNode("valid", true)->setBoolValue(false);
320 _metarPropertiesNode->getNode("time-to-live", true)->setDoubleValue(0.0);
322 // use the present weather to generate the ATIS.
323 if ( NULL != _atisNode && false == _requestedId.empty()) {
324 CurrentWeatherATISInformationProvider provider(_requestedId);
325 _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
330 void MetarBridge::clearMetar()
333 requestMetarForId(empty);
336 void MetarBridge::valueChanged(SGPropertyNode * node)
338 // check for raising edge of valid flag
339 if ( NULL == node || false == node->getBoolValue() || false == _realWxEnabledNode->getBoolValue()) return;
341 std::string responseId = simgear::strutils::uppercase(_metarPropertiesNode->getNode("station-id", true)->getStringValue());
343 // unrequested metar!?
344 if (responseId != _requestedId) return;
346 if ( NULL != _atisNode) {
347 MetarPropertiesATISInformationProvider provider(_metarPropertiesNode);
348 _atisNode->setStringValue(_atisEncoder.encodeATIS(&provider));
352 /* ------------- 8.3kHz Channel implementation ---------------------- */
354 class EightPointThreeFrequencyFormatter :
355 public FrequencyFormatterBase,
356 public SGPropertyChangeListener {
358 EightPointThreeFrequencyFormatter( SGPropertyNode_ptr root,
359 const char * channel,
363 const char * cnum ) :
364 _channel( root, channel ),
365 _frequency( root, frq ),
366 _channelSpacing( root, width ),
367 _formattedChannel( root, fmt ),
368 _channelNum( root, cnum )
370 // ensure properties exist.
372 _frequency.node(true);
373 _channelSpacing.node(true);
374 _channelNum.node(true);
375 _formattedChannel.node(true);
377 _channel.node()->addChangeListener( this, true );
378 _channelNum.node()->addChangeListener( this, true );
381 virtual ~EightPointThreeFrequencyFormatter()
383 _channel.node()->removeChangeListener( this );
384 _channelNum.node()->removeChangeListener( this );
388 EightPointThreeFrequencyFormatter( const EightPointThreeFrequencyFormatter & );
389 EightPointThreeFrequencyFormatter & operator = ( const EightPointThreeFrequencyFormatter & );
391 void valueChanged (SGPropertyNode * prop) {
392 if( prop == _channel.node() )
393 setFrequency(prop->getDoubleValue());
394 else if( prop == _channelNum.node() )
395 setChannel(prop->getIntValue());
398 void setChannel( int channel ) {
400 if( channel < 0 ) channel += 3040;
401 double f = 118.000 + 0.025*(channel/4) + 0.005*(channel%4);
402 if( f != _channel ) _channel = f;
405 void setFrequency( double channel ) {
406 // format as fixed decimal "nnn.nnn"
407 std::ostringstream buf;
411 << std::setprecision(3)
413 _formattedChannel = buf.str();
415 // sanitize range and round to nearest kHz.
416 unsigned c = static_cast<int>(SGMiscd::round(SGMiscd::clip( channel, 118.0, 136.99 ) * 1000));
418 if ( (c % 25) == 0 ) {
419 // legacy 25kHz channels continue to be just that.
420 _channelSpacing = 25.0;
421 _frequency = c / 1000.0;
423 int channelNum = (c-118000)/25*4;
424 if( channelNum != _channelNum ) _channelNum = channelNum;
426 if( _frequency != channel ) {
427 _channel = _frequency; //triggers recursion
430 _channelSpacing = 8.33;
432 // 25kHz base frequency: xxx.000, xxx.025, xxx.050, xxx.075
433 unsigned base25 = (c/25) * 25;
435 // add n*8.33 to the 25kHz frequency
436 unsigned subChannel = SGMisc<unsigned>::clip((c - base25)/5-1, 0, 2 );
438 _frequency = (base25 + 8.33 * subChannel)/1000.0;
440 int channelNum = (base25-118000)/25*4 + subChannel+1;
441 if( channelNum != _channelNum ) _channelNum = channelNum;
443 // set to correct channel on bogous input
444 double sanitizedChannel = (base25 + 5*(subChannel+1))/1000.0;
445 if( sanitizedChannel != channel ) {
446 _channel = sanitizedChannel; // triggers recursion
451 double getFrequency() const {
456 PropertyObject<double> _channel;
457 PropertyObject<double> _frequency;
458 PropertyObject<double> _channelSpacing;
459 PropertyObject<string> _formattedChannel;
460 PropertyObject<int> _channelNum;
465 /* ------------- The CommRadio implementation ---------------------- */
467 class CommRadioImpl: public CommRadio, OutputProperties {
470 CommRadioImpl(SGPropertyNode_ptr node);
471 virtual ~CommRadioImpl();
473 virtual void update(double dt);
479 string getSampleGroupRefname() const
481 return _rootNode->getPath();
485 MetarBridgeRef _metarBridge;
486 #if defined(ENABLE_FLITE)
487 AtisSpeaker _atisSpeaker;
489 SGSharedPtr<FrequencyFormatterBase> _useFrequencyFormatter;
490 SGSharedPtr<FrequencyFormatterBase> _stbyFrequencyFormatter;
491 const SignalQualityComputerRef _signalQualityComputer;
495 flightgear::CommStationRef _commStationForFrequency;
496 #if defined(ENABLE_FLITE)
497 SGSharedPtr<SGSampleGroup> _sampleGroup;
500 PropertyObject<bool> _serviceable;
501 PropertyObject<bool> _power_btn;
502 PropertyObject<bool> _power_good;
503 PropertyObject<double> _volume_norm;
504 PropertyObject<string> _atis;
505 PropertyObject<bool> _addNoise;
506 PropertyObject<double> _cutoffSignalQuality;
510 CommRadioImpl::CommRadioImpl(SGPropertyNode_ptr node)
512 fgGetNode("/instrumentation", true)->getNode(node->getStringValue("name", "comm"), node->getIntValue("number", 0), true)),
513 _num(node->getIntValue("number", 0)),
514 _metarBridge(new MetarBridge()),
515 _signalQualityComputer(new SimpleDistanceSquareSignalQualityComputer()),
519 _commStationForFrequency(NULL),
521 _serviceable(_rootNode->getNode("serviceable", true)),
522 _power_btn(_rootNode->getNode("power-btn", true)),
523 _power_good(_rootNode->getNode("power-good", true)),
524 _volume_norm(_rootNode->getNode("volume", true)),
525 _atis(_rootNode->getNode("atis", true)),
526 _addNoise(_rootNode->getNode("add-noise", true)),
527 _cutoffSignalQuality(_rootNode->getNode("cutoff-signal-quality", true))
529 if( node->getBoolValue("eight-point-three", false ) ) {
530 _useFrequencyFormatter = new EightPointThreeFrequencyFormatter(
531 _rootNode->getNode("frequencies", true),
534 "selected-channel-width-khz",
535 "selected-real-frequency-mhz",
538 _stbyFrequencyFormatter = new EightPointThreeFrequencyFormatter(
539 _rootNode->getNode("frequencies", true),
542 "standby-channel-width-khz",
543 "standby-real-frequency-mhz",
547 _useFrequencyFormatter = new FrequencyFormatter(
548 _rootNode->getNode("frequencies/selected-mhz", true),
549 _rootNode->getNode("frequencies/selected-mhz-fmt", true),
550 0.025, 118.0, 137.0);
552 _stbyFrequencyFormatter = new FrequencyFormatter(
553 _rootNode->getNode("frequencies/standby-mhz", true),
554 _rootNode->getNode("frequencies/standby-mhz-fmt", true),
555 0.025, 118.0, 137.0);
561 CommRadioImpl::~CommRadioImpl()
565 void CommRadioImpl::bind()
567 _metarBridge->setAtisNode(_atis.node());
568 #if defined(ENABLE_FLITE)
569 _atis.node()->addChangeListener(&_atisSpeaker);
571 // link the metar node. /environment/metar[3] is comm1 and /environment[4] is comm2.
572 // see FGDATA/Environment/environment.xml
573 _metarBridge->setMetarPropertiesRoot(fgGetNode("/environment", true)->getNode("metar", _num + 3, true));
574 _metarBridge->bind();
577 void CommRadioImpl::unbind()
579 #if defined(ENABLE_FLITE)
580 _atis.node()->removeChangeListener(&_atisSpeaker);
581 if (_sampleGroup.valid()) {
582 globals->get_subsystem<SGSoundMgr>()->remove(getSampleGroupRefname());
585 _metarBridge->unbind();
588 void CommRadioImpl::init()
590 // initialize power_btn to true if unset
591 string s = _power_btn.node()->getStringValue();
592 if (s.empty()) _power_btn = true;
594 // initialize squelch to a sane value if unset
595 s = _cutoffSignalQuality.node()->getStringValue();
596 if (s.empty()) _cutoffSignalQuality = 0.4;
598 // initialize add-noize to true if unset
599 s = _addNoise.node()->getStringValue();
600 if (s.empty()) _addNoise = true;
603 void CommRadioImpl::update(double dt)
605 if (dt < SGLimitsd::min()) return;
608 // Ensure all output properties get written on exit of this method
613 position = globals->get_aircraft_position();
615 catch (std::exception &) {
619 #if defined(ENABLE_FLITE)
621 static const char * atisSampleRefName = "atis";
622 static const char * noiseSampleRefName = "noise";
624 if (_atisSpeaker.hasSpokenAtis()) {
625 // the speaker has created a new atis sample
626 if (!_sampleGroup.valid()) {
627 // create a sample group for our instrument on the fly
628 SGSoundMgr * smgr = globals->get_subsystem<SGSoundMgr>();
630 _sampleGroup = smgr->find(getSampleGroupRefname(), true);
631 _sampleGroup->tie_to_listener();
633 SGSharedPtr<SGSoundSample> noise = new SGSoundSample("Sounds/radionoise.wav", globals->get_fg_root());
634 _sampleGroup->add(noise, noiseSampleRefName);
635 _sampleGroup->play_looped(noiseSampleRefName);
639 // remove previous atis sample
640 _sampleGroup->remove(atisSampleRefName);
641 // add and play the new atis sample
642 SGSharedPtr<SGSoundSample> sample = _atisSpeaker.getSpokenAtis();
643 _sampleGroup->add(sample, atisSampleRefName);
644 _sampleGroup->play_looped(atisSampleRefName);
647 if (_sampleGroup.valid()) {
649 // if noise is configured and there is a noise sample
650 // scale noise and signal volume by signalQuality.
651 SGSoundSample * s = _sampleGroup->find(noiseSampleRefName);
653 s->set_volume(1.0 - _signalQuality_norm);
654 s = _sampleGroup->find(atisSampleRefName);
655 s->set_volume(_signalQuality_norm);
658 // master volume for radio, mute on bad signal quality
659 _sampleGroup->set_volume(_signalQuality_norm >= _cutoffSignalQuality ? _volume_norm : 0.0);
664 if (false == (_power_btn) || false == (_serviceable)) {
665 _metarBridge->clearMetar();
671 if (_frequency != _useFrequencyFormatter->getFrequency()) {
672 _frequency = _useFrequencyFormatter->getFrequency();
676 if (_stationTTL <= 0.0) {
679 // Note: 122.375 must be rounded DOWN to 122370
680 // in order to be consistent with apt.dat et cetera.
681 int freqKhz = 10 * static_cast<int>(_frequency * 100 + 0.25);
682 _commStationForFrequency = flightgear::CommStation::findByFreq(freqKhz, position, NULL);
686 if (false == _commStationForFrequency.valid()) return;
688 _slantDistance_m = dist(_commStationForFrequency->cart(), SGVec3d::fromGeod(position));
690 SGGeodesy::inverse(position, _commStationForFrequency->geod(), _trueBearingTo_deg, _trueBearingFrom_deg, _trackDistance_m);
692 _heightAboveStation_ft = SGMiscd::max(0.0, position.getElevationFt() - _commStationForFrequency->airport()->elevation());
694 _signalQuality_norm = _signalQualityComputer->computeSignalQuality(_slantDistance_m * SG_METER_TO_NM);
695 _stationType = _commStationForFrequency->nameForType(_commStationForFrequency->type());
696 _stationName = _commStationForFrequency->ident();
697 _airportId = _commStationForFrequency->airport()->getId();
699 #if defined(ENABLE_FLITE)
700 _atisSpeaker.setStationId(_airportId);
702 switch (_commStationForFrequency->type()) {
703 case FGPositioned::FREQ_ATIS:
704 case FGPositioned::FREQ_AWOS: {
705 if (_signalQuality_norm > 0.01) {
706 _metarBridge->requestMetarForId(_airportId);
708 _metarBridge->clearMetar();
715 _metarBridge->clearMetar();
721 SGSubsystem * CommRadio::createInstance(SGPropertyNode_ptr rootNode)
723 return new CommRadioImpl(rootNode);
726 } // namespace Instrumentation