From 13f31782a102caf7181e1a46662ab78830a054a8 Mon Sep 17 00:00:00 2001 From: Richard Senior Date: Fri, 8 Apr 2016 23:33:50 +0100 Subject: [PATCH] Improvements to spoken ATIS - Add section tag to support inclusion of ATIS fragments. - Add visibility, QNH and cloud tokens to support new ATIS formats. - Add support for starts-with, ends-with and contains comparisons in conditionals, including negated versions. - Strip and convert case in comparisons. - Speak VRB wind direction as "variable". - Speak zeroes in fractional part of QNH inHg. - Force US voice in US, Canada and Pacific; UK voice in UK. --- src/ATC/ATISEncoder.cxx | 129 ++++++++++++++++++++++++------ src/ATC/ATISEncoder.hxx | 19 ++++- src/Instrumentation/commradio.cxx | 18 +++-- 3 files changed, 137 insertions(+), 29 deletions(-) diff --git a/src/ATC/ATISEncoder.cxx b/src/ATC/ATISEncoder.cxx index 45675207b..f24e4469a 100644 --- a/src/ATC/ATISEncoder.cxx +++ b/src/ATC/ATISEncoder.cxx @@ -116,6 +116,7 @@ ATISEncoder::ATISEncoder() handlerMap.insert( std::make_pair( "text", &ATISEncoder::processTextToken )); handlerMap.insert( std::make_pair( "token", &ATISEncoder::processTokenToken )); handlerMap.insert( std::make_pair( "if", &ATISEncoder::processIfToken )); + handlerMap.insert( std::make_pair( "section", &ATISEncoder::processTokens )); handlerMap.insert( std::make_pair( "id", &ATISEncoder::getAtisId )); handlerMap.insert( std::make_pair( "airport-name", &ATISEncoder::getAirportName )); @@ -130,13 +131,17 @@ ATISEncoder::ATISEncoder() handlerMap.insert( std::make_pair( "wind-speed-kn", &ATISEncoder::getWindspeedKnots )); handlerMap.insert( std::make_pair( "gusts", &ATISEncoder::getGustsKnots )); handlerMap.insert( std::make_pair( "visibility-metric", &ATISEncoder::getVisibilityMetric )); + handlerMap.insert( std::make_pair( "visibility-miles", &ATISEncoder::getVisibilityMiles )); handlerMap.insert( std::make_pair( "phenomena", &ATISEncoder::getPhenomena )); handlerMap.insert( std::make_pair( "clouds", &ATISEncoder::getClouds )); + handlerMap.insert( std::make_pair( "clouds-brief", &ATISEncoder::getCloudsBrief )); handlerMap.insert( std::make_pair( "cavok", &ATISEncoder::getCavok )); handlerMap.insert( std::make_pair( "temperature-deg", &ATISEncoder::getTemperatureDeg )); handlerMap.insert( std::make_pair( "dewpoint-deg", &ATISEncoder::getDewpointDeg )); handlerMap.insert( std::make_pair( "qnh", &ATISEncoder::getQnh )); handlerMap.insert( std::make_pair( "inhg", &ATISEncoder::getInhg )); + handlerMap.insert( std::make_pair( "inhg-integer", &ATISEncoder::getInhgInteger )); + handlerMap.insert( std::make_pair( "inhg-fraction", &ATISEncoder::getInhgFraction )); handlerMap.insert( std::make_pair( "trend", &ATISEncoder::getTrend )); } @@ -250,6 +255,8 @@ string ATISEncoder::processTokenToken( SGPropertyNode_ptr token ) string ATISEncoder::processIfToken( SGPropertyNode_ptr token ) { + using namespace simgear::strutils; + SGPropertyNode_ptr n; if( (n = token->getChild("empty", false )).valid() ) { @@ -264,14 +271,50 @@ string ATISEncoder::processIfToken( SGPropertyNode_ptr token ) processTokens(token->getChild("else",false)); } + if( (n = token->getChild("contains", false )).valid() ) { + return checkCondition( n, true, &contains, "contains") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + + if( (n = token->getChild("not-contains", false )).valid() ) { + return checkCondition( n, false, &contains, "not-contains") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + + if( (n = token->getChild("ends-with", false )).valid() ) { + return checkCondition( n, true, &ends_with, "ends-with") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + + if( (n = token->getChild("not-ends-with", false )).valid() ) { + return checkCondition( n, false, &ends_with, "not-ends-with") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + if( (n = token->getChild("equals", false )).valid() ) { - return checkEqualsCondition( n, true) ? + return checkCondition( n, true, &equals, "equals") ? processTokens(token->getChild("then",false)) : processTokens(token->getChild("else",false)); } if( (n = token->getChild("not-equals", false )).valid() ) { - return checkEqualsCondition( n, false) ? + return checkCondition( n, false, &equals, "not-equals") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + + if( (n = token->getChild("starts-with", false )).valid() ) { + return checkCondition( n, true, &starts_with, "starts-with") ? + processTokens(token->getChild("then",false)) : + processTokens(token->getChild("else",false)); + } + + if( (n = token->getChild("not-starts-with", false )).valid() ) { + return checkCondition( n, false, &starts_with, "not-starts-with") ? processTokens(token->getChild("then",false)) : processTokens(token->getChild("else",false)); } @@ -291,27 +334,31 @@ bool ATISEncoder::checkEmptyCondition( SGPropertyNode_ptr node, bool isEmpty ) return processToken( n1 ).empty() == isEmpty; } -bool ATISEncoder::checkEqualsCondition( SGPropertyNode_ptr node, bool isEqual ) +bool ATISEncoder::checkCondition( SGPropertyNode_ptr node, bool notInverted, + bool (*fp)(const string &, const string &), const string &name ) { + using namespace simgear::strutils; + SGPropertyNode_ptr n1 = node->getNode( "token", 0, false ); SGPropertyNode_ptr n2 = node->getNode( "token", 1, false ); if( n1.valid() && n2.valid() ) { - bool comp = processToken( n1 ).compare( processToken( n2 ) ) == 0; - return comp == isEqual; + bool comp = fp( processToken( n1 ), processToken( n2 ) ); + return comp == notInverted; } if( n1.valid() && !n2.valid() ) { - SGPropertyNode_ptr t = node->getNode( "text", 0, false ); - if( t.valid() ) { - bool comp = processToken( n1 ).compare( processTextToken( t ) ) == 0; - return comp == isEqual; + SGPropertyNode_ptr t1 = node->getNode( "text", 0, false ); + if( t1.valid() ) { + string n1s = lowercase( strip( processToken( n1 ) ) ); + string t1s = lowercase( strip( processTextToken( t1 ) ) ); + return fp( n1s, t1s ) == notInverted; } - SG_LOG(SG_ATC, SG_WARN, "missing or node for (not)-equals"); + SG_LOG(SG_ATC, SG_WARN, "missing or node for " << name); return false; } - SG_LOG(SG_ATC, SG_WARN, "missing node for (not)-equals"); + SG_LOG(SG_ATC, SG_WARN, "missing node for " << name); return false; } @@ -408,7 +455,10 @@ string ATISEncoder::getTransitionLevel( SGPropertyNode_ptr ) string ATISEncoder::getWindDirection( SGPropertyNode_ptr ) { - return getSpokenNumber( _atis->getWindDeg(), true, 3 ); + string variable = globals->get_locale()->getLocalizedString("variable", "atc", "variable" ); + + bool vrb = _atis->getWindMinDeg() == 0 && _atis->getWindMaxDeg() == 359; + return vrb ? variable : getSpokenNumber( _atis->getWindDeg(), true, 3 ); } string ATISEncoder::getWindMinDirection( SGPropertyNode_ptr ) @@ -452,6 +502,20 @@ string ATISEncoder::getVisibilityMetric( SGPropertyNode_ptr ) return reply.append( getSpokenNumber( v/1000 ).SPACE.append( km ) ); } +string ATISEncoder::getVisibilityMiles( SGPropertyNode_ptr ) +{ + string feet = globals->get_locale()->getLocalizedString("feet", "atc", "feet" ); + + int v = _atis->getVisibilityMeters(); + int vft = round( v * SG_METER_TO_FEET / 100 ) * 100; // Rounded to 100 feet + int vsm = round( v * SG_METER_TO_SM ); + + string reply; + if( vsm < 1 ) return reply.append( getSpokenAltitude( vft ) ).SPACE.append( feet ); + if( v >= 9999 ) return reply.append( getSpokenNumber(10) ); + return reply.append( getSpokenNumber( vsm ) ); +} + string ATISEncoder::getPhenomena( SGPropertyNode_ptr ) { return _atis->getPhenomena(); @@ -471,6 +535,19 @@ string ATISEncoder::getClouds( SGPropertyNode_ptr ) return reply; } +string ATISEncoder::getCloudsBrief( SGPropertyNode_ptr ) +{ + string reply; + + ATISInformationProvider::CloudEntries cloudEntries = _atis->getClouds(); + + for( ATISInformationProvider::CloudEntries::iterator it = cloudEntries.begin(); it != cloudEntries.end(); it++ ) { + if( false == reply.empty() ) reply.append(",").SPACE; + reply.append( it->second ).SPACE.append( getSpokenAltitude(it->first) ); + } + return reply; +} + string ATISEncoder::getTemperatureDeg( SGPropertyNode_ptr ) { return getSpokenNumber( _atis->getTemperatureDeg() ); @@ -486,19 +563,25 @@ string ATISEncoder::getQnh( SGPropertyNode_ptr ) return getSpokenNumber( _atis->getQnh() ); } -string ATISEncoder::getInhg( SGPropertyNode_ptr ) +string ATISEncoder::getInhgInteger( SGPropertyNode_ptr ) { - string DECIMAL = globals->get_locale()->getLocalizedString("dp", "atc", "decimal" ); - double intpart = .0; - int fractpart = 1000 * ::modf( _atis->getQnh() * 100.0 / SG_INHG_TO_PA, &intpart ); - fractpart += 5; - fractpart /= 10; + double qnh = _atis->getQnh() * 100 / SG_INHG_TO_PA; + return getSpokenNumber( (int)qnh, true, 2 ); +} - string reply; - reply.append( getSpokenNumber( (int)intpart ) ) - .SPACE.append( DECIMAL ).SPACE - .append( getSpokenNumber( fractpart ) ); - return reply; +string ATISEncoder::getInhgFraction( SGPropertyNode_ptr ) +{ + double qnh = _atis->getQnh() * 100 / SG_INHG_TO_PA; + int f = round(100 * (qnh - (int)qnh)); + return getSpokenNumber( f, true, 2 ); +} + +string ATISEncoder::getInhg( SGPropertyNode_ptr node) +{ + string DECIMAL = globals->get_locale()->getLocalizedString("dp", "atc", "decimal" ); + return getInhgInteger(node) + .SPACE.append(DECIMAL).SPACE + .append(getInhgFraction(node)); } string ATISEncoder::getTrend( SGPropertyNode_ptr ) diff --git a/src/ATC/ATISEncoder.hxx b/src/ATC/ATISEncoder.hxx index 410eb842c..57958a1a1 100644 --- a/src/ATC/ATISEncoder.hxx +++ b/src/ATC/ATISEncoder.hxx @@ -85,11 +85,15 @@ protected: virtual std::string getGustsKnots( SGPropertyNode_ptr ); virtual std::string getCavok( SGPropertyNode_ptr ); virtual std::string getVisibilityMetric( SGPropertyNode_ptr ); + virtual std::string getVisibilityMiles( SGPropertyNode_ptr ); virtual std::string getPhenomena( SGPropertyNode_ptr ); virtual std::string getClouds( SGPropertyNode_ptr ); + virtual std::string getCloudsBrief( SGPropertyNode_ptr ); virtual std::string getTemperatureDeg( SGPropertyNode_ptr ); virtual std::string getDewpointDeg( SGPropertyNode_ptr ); virtual std::string getQnh( SGPropertyNode_ptr ); + virtual std::string getInhgInteger( SGPropertyNode_ptr ); + virtual std::string getInhgFraction( SGPropertyNode_ptr ); virtual std::string getInhg( SGPropertyNode_ptr ); virtual std::string getTrend( SGPropertyNode_ptr ); @@ -105,11 +109,24 @@ protected: std::string processTextToken( SGPropertyNode_ptr baseNode ); std::string processTokenToken( SGPropertyNode_ptr baseNode ); std::string processIfToken( SGPropertyNode_ptr baseNode ); + bool checkEmptyCondition( SGPropertyNode_ptr node, bool isEmpty ); - bool checkEqualsCondition( SGPropertyNode_ptr node, bool isEmpty ); + + // Wrappers that can be passed as function pointers to checkCondition + // @see simgear::strutils::starts_with + // @see simgear::strutils::ends_with + static bool contains(const string &s, const string &substring) + { return s.find(substring) != std::string::npos; }; + static bool equals(const string &s1, const string &s2) + { return s1 == s2; }; + + bool checkCondition( SGPropertyNode_ptr node, bool notInverted, + bool (*fp)(const std::string &, const std::string &), + const std::string &name ); FGAirportRef airport; ATISInformationProvider * _atis; + }; #endif diff --git a/src/Instrumentation/commradio.cxx b/src/Instrumentation/commradio.cxx index 854ee87e6..ee81cb581 100644 --- a/src/Instrumentation/commradio.cxx +++ b/src/Instrumentation/commradio.cxx @@ -93,6 +93,8 @@ AtisSpeaker::~AtisSpeaker() } void AtisSpeaker::valueChanged(SGPropertyNode * node) { + using namespace simgear::strutils; + if (!fgGetBool("/sim/sound/working", false)) return; @@ -120,13 +122,19 @@ void AtisSpeaker::valueChanged(SGPropertyNode * node) _synthesizeRequest.speed = (hash % 16) / 16.0; _synthesizeRequest.pitch = (hash % 16) / 16.0; - // pick a voice - voice = FLITEVoiceSynthesizer::getVoicePath( - static_cast(hash % FLITEVoiceSynthesizer::VOICE_UNKNOWN) ); + 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(); + FGSoundManager * smgr = globals->get_subsystem(); assert(smgr != NULL); SG_LOG(SG_INSTR,SG_INFO,"AtisSpeaker voice is " << voice ); -- 2.39.2