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 /* ------------- 8.3kHz Channel implementation ---------------------- */
346 class EightPointThreeFrequencyFormatter :
347 public FrequencyFormatterBase,
348 public SGPropertyChangeListener {
350 EightPointThreeFrequencyFormatter( SGPropertyNode_ptr root,
351 const char * channel,
355 const char * cnum ) :
356 _channel( root, channel ),
357 _frequency( root, frq ),
358 _channelSpacing( root, width ),
359 _formattedChannel( root, fmt ),
360 _channelNum( root, cnum )
362 // ensure properties exist.
364 _frequency.node(true);
365 _channelSpacing.node(true);
366 _channelNum.node(true);
367 _formattedChannel.node(true);
369 _channel.node()->addChangeListener( this, true );
370 _channelNum.node()->addChangeListener( this, true );
373 virtual ~EightPointThreeFrequencyFormatter()
375 _channel.node()->removeChangeListener( this );
376 _channelNum.node()->removeChangeListener( this );
380 EightPointThreeFrequencyFormatter( const EightPointThreeFrequencyFormatter & );
381 EightPointThreeFrequencyFormatter & operator = ( const EightPointThreeFrequencyFormatter & );
383 void valueChanged (SGPropertyNode * prop) {
384 if( prop == _channel.node() )
385 setFrequency(prop->getDoubleValue());
386 else if( prop == _channelNum.node() )
387 setChannel(prop->getIntValue());
390 void setChannel( int channel ) {
392 if( channel < 0 ) channel += 3040;
393 double f = 118.000 + 0.025*(channel/4) + 0.005*(channel%4);
394 if( f != _channel ) _channel = f;
397 void setFrequency( double channel ) {
398 // format as fixed decimal "nnn.nnn"
399 std::ostringstream buf;
403 << std::setprecision(3)
405 _formattedChannel = buf.str();
407 // sanitize range and round to nearest kHz.
408 unsigned c = static_cast<int>(SGMiscd::round(SGMiscd::clip( channel, 118.0, 136.99 ) * 1000));
410 if ( (c % 25) == 0 ) {
411 // legacy 25kHz channels continue to be just that.
412 _channelSpacing = 25.0;
413 _frequency = c / 1000.0;
415 int channelNum = (c-118000)/25*4;
416 if( channelNum != _channelNum ) _channelNum = channelNum;
418 if( _frequency != channel ) {
419 _channel = _frequency; //triggers recursion
422 _channelSpacing = 8.33;
424 // 25kHz base frequency: xxx.000, xxx.025, xxx.050, xxx.075
425 unsigned base25 = (c/25) * 25;
427 // add n*8.33 to the 25kHz frequency
428 unsigned subChannel = SGMisc<unsigned>::clip((c - base25)/5-1, 0, 2 );
430 _frequency = (base25 + 8.33 * subChannel)/1000.0;
432 int channelNum = (base25-118000)/25*4 + subChannel+1;
433 if( channelNum != _channelNum ) _channelNum = channelNum;
435 // set to correct channel on bogous input
436 double sanitizedChannel = (base25 + 5*(subChannel+1))/1000.0;
437 if( sanitizedChannel != channel ) {
438 _channel = sanitizedChannel; // triggers recursion
443 double getFrequency() const {
448 PropertyObject<double> _channel;
449 PropertyObject<double> _frequency;
450 PropertyObject<double> _channelSpacing;
451 PropertyObject<string> _formattedChannel;
452 PropertyObject<int> _channelNum;
457 /* ------------- The CommRadio implementation ---------------------- */
459 class CommRadioImpl: public CommRadio, OutputProperties {
462 CommRadioImpl(SGPropertyNode_ptr node);
463 virtual ~CommRadioImpl();
465 virtual void update(double dt);
471 string getSampleGroupRefname() const
473 return _rootNode->getPath();
477 MetarBridgeRef _metarBridge;
478 #if defined(ENABLE_FLITE)
479 AtisSpeaker _atisSpeaker;
481 SGSharedPtr<FrequencyFormatterBase> _useFrequencyFormatter;
482 SGSharedPtr<FrequencyFormatterBase> _stbyFrequencyFormatter;
483 const SignalQualityComputerRef _signalQualityComputer;
487 flightgear::CommStationRef _commStationForFrequency;
488 #if defined(ENABLE_FLITE)
489 SGSharedPtr<SGSampleGroup> _sampleGroup;
492 PropertyObject<bool> _serviceable;
493 PropertyObject<bool> _power_btn;
494 PropertyObject<bool> _power_good;
495 PropertyObject<double> _volume_norm;
496 PropertyObject<string> _atis;
497 PropertyObject<bool> _addNoise;
498 PropertyObject<double> _cutoffSignalQuality;
502 CommRadioImpl::CommRadioImpl(SGPropertyNode_ptr node)
504 fgGetNode("/instrumentation", true)->getNode(node->getStringValue("name", "comm"), node->getIntValue("number", 0), true)),
505 _num(node->getIntValue("number", 0)),
506 _metarBridge(new MetarBridge()),
507 _signalQualityComputer(new SimpleDistanceSquareSignalQualityComputer()),
511 _commStationForFrequency(NULL),
513 _serviceable(_rootNode->getNode("serviceable", true)),
514 _power_btn(_rootNode->getNode("power-btn", true)),
515 _power_good(_rootNode->getNode("power-good", true)),
516 _volume_norm(_rootNode->getNode("volume", true)),
517 _atis(_rootNode->getNode("atis", true)),
518 _addNoise(_rootNode->getNode("add-noise", true)),
519 _cutoffSignalQuality(_rootNode->getNode("cutoff-signal-quality", true))
521 if( node->getBoolValue("eight-point-three", false ) ) {
522 _useFrequencyFormatter = new EightPointThreeFrequencyFormatter(
523 _rootNode->getNode("frequencies", true),
526 "selected-channel-width-khz",
527 "selected-real-frequency-mhz",
530 _stbyFrequencyFormatter = new EightPointThreeFrequencyFormatter(
531 _rootNode->getNode("frequencies", true),
534 "standby-channel-width-khz",
535 "standby-real-frequency-mhz",
539 _useFrequencyFormatter = new FrequencyFormatter(
540 _rootNode->getNode("frequencies/selected-mhz", true),
541 _rootNode->getNode("frequencies/selected-mhz-fmt", true),
542 0.025, 118.0, 137.0);
544 _stbyFrequencyFormatter = new FrequencyFormatter(
545 _rootNode->getNode("frequencies/standby-mhz", true),
546 _rootNode->getNode("frequencies/standby-mhz-fmt", true),
547 0.025, 118.0, 137.0);
553 CommRadioImpl::~CommRadioImpl()
557 void CommRadioImpl::bind()
559 _metarBridge->setAtisNode(_atis.node());
560 #if defined(ENABLE_FLITE)
561 _atis.node()->addChangeListener(&_atisSpeaker);
563 // link the metar node. /environment/metar[3] is comm1 and /environment[4] is comm2.
564 // see FGDATA/Environment/environment.xml
565 _metarBridge->setMetarPropertiesRoot(fgGetNode("/environment", true)->getNode("metar", _num + 3, true));
566 _metarBridge->bind();
569 void CommRadioImpl::unbind()
571 #if defined(ENABLE_FLITE)
572 _atis.node()->removeChangeListener(&_atisSpeaker);
573 if (_sampleGroup.valid()) {
574 globals->get_soundmgr()->remove(getSampleGroupRefname());
577 _metarBridge->unbind();
580 void CommRadioImpl::init()
582 // initialize power_btn to true if unset
583 string s = _power_btn.node()->getStringValue();
584 if (s.empty()) _power_btn = true;
586 // initialize squelch to a sane value if unset
587 s = _cutoffSignalQuality.node()->getStringValue();
588 if (s.empty()) _cutoffSignalQuality = 0.4;
590 // initialize add-noize to true if unset
591 s = _addNoise.node()->getStringValue();
592 if (s.empty()) _addNoise = true;
595 void CommRadioImpl::update(double dt)
597 if (dt < SGLimitsd::min()) return;
600 // Ensure all output properties get written on exit of this method
605 position = globals->get_aircraft_position();
607 catch (std::exception &) {
611 #if defined(ENABLE_FLITE)
613 static const char * atisSampleRefName = "atis";
614 static const char * noiseSampleRefName = "noise";
616 if (_atisSpeaker.hasSpokenAtis()) {
617 // the speaker has created a new atis sample
618 if (!_sampleGroup.valid()) {
619 // create a sample group for our instrument on the fly
620 _sampleGroup = globals->get_soundmgr()->find(getSampleGroupRefname(), true);
621 _sampleGroup->tie_to_listener();
623 SGSharedPtr<SGSoundSample> noise = new SGSoundSample("Sounds/radionoise.wav", globals->get_fg_root());
624 _sampleGroup->add(noise, noiseSampleRefName);
625 _sampleGroup->play_looped(noiseSampleRefName);
629 // remove previous atis sample
630 _sampleGroup->remove(atisSampleRefName);
631 // add and play the new atis sample
632 SGSharedPtr<SGSoundSample> sample = _atisSpeaker.getSpokenAtis();
633 _sampleGroup->add(sample, atisSampleRefName);
634 _sampleGroup->play_looped(atisSampleRefName);
637 if (_sampleGroup.valid()) {
639 // if noise is configured and there is a noise sample
640 // scale noise and signal volume by signalQuality.
641 SGSoundSample * s = _sampleGroup->find(noiseSampleRefName);
643 s->set_volume(1.0 - _signalQuality_norm);
644 s = _sampleGroup->find(atisSampleRefName);
645 s->set_volume(_signalQuality_norm);
648 // master volume for radio, mute on bad signal quality
649 _sampleGroup->set_volume(_signalQuality_norm >= _cutoffSignalQuality ? _volume_norm : 0.0);
654 if (false == (_power_btn)) {
655 _metarBridge->clearMetar();
661 if (_frequency != _useFrequencyFormatter->getFrequency()) {
662 _frequency = _useFrequencyFormatter->getFrequency();
666 if (_stationTTL <= 0.0) {
669 // Note: 122.375 must be rounded DOWN to 122370
670 // in order to be consistent with apt.dat et cetera.
671 int freqKhz = 10 * static_cast<int>(_frequency * 100 + 0.25);
672 _commStationForFrequency = flightgear::CommStation::findByFreq(freqKhz, position, NULL);
676 if (false == _commStationForFrequency.valid()) return;
678 _slantDistance_m = dist(_commStationForFrequency->cart(), SGVec3d::fromGeod(position));
680 SGGeodesy::inverse(position, _commStationForFrequency->geod(), _trueBearingTo_deg, _trueBearingFrom_deg, _trackDistance_m);
682 _heightAboveStation_ft = SGMiscd::max(0.0, position.getElevationFt() - _commStationForFrequency->airport()->elevation());
684 _signalQuality_norm = _signalQualityComputer->computeSignalQuality(_slantDistance_m * SG_METER_TO_NM);
685 _stationType = _commStationForFrequency->nameForType(_commStationForFrequency->type());
686 _stationName = _commStationForFrequency->ident();
687 _airportId = _commStationForFrequency->airport()->getId();
689 #if defined(ENABLE_FLITE)
690 _atisSpeaker.setStationId(_airportId);
692 switch (_commStationForFrequency->type()) {
693 case FGPositioned::FREQ_ATIS:
694 case FGPositioned::FREQ_AWOS: {
695 if (_signalQuality_norm > 0.01) {
696 _metarBridge->requestMetarForId(_airportId);
698 _metarBridge->clearMetar();
705 _metarBridge->clearMetar();
711 SGSubsystem * CommRadio::createInstance(SGPropertyNode_ptr rootNode)
713 return new CommRadioImpl(rootNode);
716 } // namespace Instrumentation