--- **Core** - Makes the radio talk. -- -- === -- -- ## Features: -- -- * Send text strings using a vocabulary that is converted in spoken language. -- * Possiblity to implement multiple language. -- -- === -- -- ### Authors: FlightControl -- -- @module Sound.RadioSpeech -- @image Core_Radio.JPG --- Makes the radio speak. -- -- # RADIOSPEECH usage -- -- # Developer Note -- -- Note while this class still works, it is no longer supported as the original author stopped active development of MOOSE -- Therefore, this class is considered to be deprecated -- -- -- @type RADIOSPEECH -- @extends Sound.RadioQueue#RADIOQUEUE RADIOSPEECH = { ClassName = "RADIOSPEECH", Vocabulary = { EN = {}, DE = {}, RU = {}, } } RADIOSPEECH.Vocabulary.EN = { ["1"] = { "1", 0.25 }, ["2"] = { "2", 0.25 }, ["3"] = { "3", 0.30 }, ["4"] = { "4", 0.35 }, ["5"] = { "5", 0.35 }, ["6"] = { "6", 0.42 }, ["7"] = { "7", 0.38 }, ["8"] = { "8", 0.20 }, ["9"] = { "9", 0.32 }, ["10"] = { "10", 0.35 }, ["11"] = { "11", 0.40 }, ["12"] = { "12", 0.42 }, ["13"] = { "13", 0.38 }, ["14"] = { "14", 0.42 }, ["15"] = { "15", 0.42 }, ["16"] = { "16", 0.52 }, ["17"] = { "17", 0.59 }, ["18"] = { "18", 0.40 }, ["19"] = { "19", 0.47 }, ["20"] = { "20", 0.38 }, ["30"] = { "30", 0.29 }, ["40"] = { "40", 0.35 }, ["50"] = { "50", 0.32 }, ["60"] = { "60", 0.44 }, ["70"] = { "70", 0.48 }, ["80"] = { "80", 0.26 }, ["90"] = { "90", 0.36 }, ["100"] = { "100", 0.55 }, ["200"] = { "200", 0.55 }, ["300"] = { "300", 0.61 }, ["400"] = { "400", 0.60 }, ["500"] = { "500", 0.61 }, ["600"] = { "600", 0.65 }, ["700"] = { "700", 0.70 }, ["800"] = { "800", 0.54 }, ["900"] = { "900", 0.60 }, ["1000"] = { "1000", 0.60 }, ["2000"] = { "2000", 0.61 }, ["3000"] = { "3000", 0.64 }, ["4000"] = { "4000", 0.62 }, ["5000"] = { "5000", 0.69 }, ["6000"] = { "6000", 0.69 }, ["7000"] = { "7000", 0.75 }, ["8000"] = { "8000", 0.59 }, ["9000"] = { "9000", 0.65 }, ["chevy"] = { "chevy", 0.35 }, ["colt"] = { "colt", 0.35 }, ["springfield"] = { "springfield", 0.65 }, ["dodge"] = { "dodge", 0.35 }, ["enfield"] = { "enfield", 0.5 }, ["ford"] = { "ford", 0.32 }, ["pontiac"] = { "pontiac", 0.55 }, ["uzi"] = { "uzi", 0.28 }, ["degrees"] = { "degrees", 0.5 }, ["kilometers"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, ["miles"] = { "miles", 0.45 }, ["meters"] = { "meters", 0.41 }, ["mi"] = { "miles", 0.45 }, ["feet"] = { "feet", 0.29 }, ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, ["returning to base"] = { "returning_to_base", 0.85 }, ["on route to ground target"] = { "on_route_to_ground_target", 1.05 }, ["intercepting bogeys"] = { "intercepting_bogeys", 1.00 }, ["engaging ground target"] = { "engaging_ground_target", 1.20 }, ["engaging bogeys"] = { "engaging_bogeys", 0.81 }, ["wheels up"] = { "wheels_up", 0.42 }, ["landing at base"] = { "landing at base", 0.8 }, ["patrolling"] = { "patrolling", 0.55 }, ["for"] = { "for", 0.31 }, ["and"] = { "and", 0.31 }, ["at"] = { "at", 0.3 }, ["dot"] = { "dot", 0.26 }, ["defender"] = { "defender", 0.45 }, } RADIOSPEECH.Vocabulary.RU = { ["1"] = { "1", 0.34 }, ["2"] = { "2", 0.30 }, ["3"] = { "3", 0.23 }, ["4"] = { "4", 0.51 }, ["5"] = { "5", 0.31 }, ["6"] = { "6", 0.44 }, ["7"] = { "7", 0.25 }, ["8"] = { "8", 0.43 }, ["9"] = { "9", 0.45 }, ["10"] = { "10", 0.53 }, ["11"] = { "11", 0.66 }, ["12"] = { "12", 0.70 }, ["13"] = { "13", 0.66 }, ["14"] = { "14", 0.80 }, ["15"] = { "15", 0.65 }, ["16"] = { "16", 0.75 }, ["17"] = { "17", 0.74 }, ["18"] = { "18", 0.85 }, ["19"] = { "19", 0.80 }, ["20"] = { "20", 0.58 }, ["30"] = { "30", 0.51 }, ["40"] = { "40", 0.51 }, ["50"] = { "50", 0.67 }, ["60"] = { "60", 0.76 }, ["70"] = { "70", 0.68 }, ["80"] = { "80", 0.84 }, ["90"] = { "90", 0.71 }, ["100"] = { "100", 0.35 }, ["200"] = { "200", 0.59 }, ["300"] = { "300", 0.53 }, ["400"] = { "400", 0.70 }, ["500"] = { "500", 0.50 }, ["600"] = { "600", 0.58 }, ["700"] = { "700", 0.64 }, ["800"] = { "800", 0.77 }, ["900"] = { "900", 0.75 }, ["1000"] = { "1000", 0.87 }, ["2000"] = { "2000", 0.83 }, ["3000"] = { "3000", 0.84 }, ["4000"] = { "4000", 1.00 }, ["5000"] = { "5000", 0.77 }, ["6000"] = { "6000", 0.90 }, ["7000"] = { "7000", 0.77 }, ["8000"] = { "8000", 0.92 }, ["9000"] = { "9000", 0.87 }, ["градусы"] = { "degrees", 0.5 }, ["километры"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, ["мили"] = { "miles", 0.45 }, ["mi"] = { "miles", 0.45 }, ["метров"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, ["ноги"] = { "feet", 0.37 }, ["br"] = { "br", 1.1 }, ["bra"] = { "bra", 0.3 }, ["возвращение на базу"] = { "returning_to_base", 1.40 }, ["на пути к наземной цели"] = { "on_route_to_ground_target", 1.45 }, ["перехват боги"] = { "intercepting_bogeys", 1.22 }, ["поражение наземной цели"] = { "engaging_ground_target", 1.53 }, ["привлечение болотных птиц"] = { "engaging_bogeys", 1.68 }, ["колёса вверх..."] = { "wheels_up", 0.92 }, ["посадка на базу"] = { "landing at base", 1.04 }, ["патрулирование"] = { "patrolling", 0.96 }, ["для"] = { "for", 0.27 }, ["и"] = { "and", 0.17 }, ["на сайте"] = { "at", 0.19 }, ["точка"] = { "dot", 0.51 }, ["защитник"] = { "defender", 0.45 }, } --- Create a new RADIOSPEECH object for a given radio frequency/modulation. -- @param #RADIOSPEECH self -- @param #number frequency The radio frequency in MHz. -- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. -- @return #RADIOSPEECH self The RADIOSPEECH object. function RADIOSPEECH:New(frequency, modulation) -- Inherit base local self = BASE:Inherit( self, RADIOQUEUE:New( frequency, modulation ) ) -- #RADIOSPEECH self.Language = "EN" self:BuildTree() return self end function RADIOSPEECH:SetLanguage( Langauge ) self.Language = Langauge end --- Add Sentence to the Speech collection. -- @param #RADIOSPEECH self -- @param #string RemainingSentence The remaining sentence during recursion. -- @param #table Speech The speech node. -- @param #string Sentence The full sentence. -- @param #string Data The speech data. -- @return #RADIOSPEECH self The RADIOSPEECH object. function RADIOSPEECH:AddSentenceToSpeech( RemainingSentence, Speech, Sentence, Data ) self:I( { RemainingSentence, Speech, Sentence, Data } ) local Token, RemainingSentence = RemainingSentence:match( "^ *([^ ]+)(.*)" ) self:I( { Token = Token, RemainingSentence = RemainingSentence } ) -- Is there a Token? if Token then -- We check if the Token is already in the Speech collection. if not Speech[Token] then -- There is not yet a vocabulary registered for this. Speech[Token] = {} if RemainingSentence and RemainingSentence ~= "" then -- We use recursion to iterate through the complete Sentence, and make a chain of Tokens. -- The last Speech node in the collection contains the Sentence and the Data to be spoken. -- This to ensure that during the actual speech: -- - Complete sentences are being understood. -- - Words without speech are ignored. -- - Incorrect sequence of words are ignored. Speech[Token].Next = {} self:AddSentenceToSpeech( RemainingSentence, Speech[Token].Next, Sentence, Data ) else -- There is no remaining sentence, so we add speech to the Sentence. -- The recursion stops here. Speech[Token].Sentence = Sentence Speech[Token].Data = Data end end end end --- Build the tree structure based on the language words, in order to find the correct sentences and to ignore incomprehensible words. -- @param #RADIOSPEECH self -- @return #RADIOSPEECH self The RADIOSPEECH object. function RADIOSPEECH:BuildTree() self.Speech = {} for Language, Sentences in pairs( self.Vocabulary ) do self:I( { Language = Language, Sentences = Sentences }) self.Speech[Language] = {} for Sentence, Data in pairs( Sentences ) do self:I( { Sentence = Sentence, Data = Data } ) self:AddSentenceToSpeech( Sentence, self.Speech[Language], Sentence, Data ) end end self:I( { Speech = self.Speech } ) return self end --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. function RADIOSPEECH:SpeakWords( Sentence, Speech, Language ) local OriginalSentence = Sentence -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. -- and then check if the character can be converted to a number or not. local Word, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Word = Word, Speech = Speech[Word], RemainderSentence = RemainderSentence } ) if Word then if Word ~= "" and tonumber(Word) == nil then -- Construct of words Word = Word:lower() if Speech[Word] then -- The end of the sentence has been reached. Now Speech.Next should be nil, otherwise there is an error. if Speech[Word].Next == nil then self:I( { Sentence = Speech[Word].Sentence, Data = Speech[Word].Data } ) self:NewTransmission( Speech[Word].Data[1] .. ".wav", Speech[Word].Data[2], Language .. "/" ) else if RemainderSentence and RemainderSentence ~= "" then return self:SpeakWords( RemainderSentence, Speech[Word].Next, Language ) end end end return RemainderSentence end return OriginalSentence else return "" end end --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. function RADIOSPEECH:SpeakDigits( Sentence, Speech, Langauge ) local OriginalSentence = Sentence -- lua does not parse UTF-8, so the match statement will fail on cyrillic using %a. -- therefore, the only way to parse the statement is to use blank, comma or dot as a delimiter. -- and then check if the character can be converted to a number or not. local Digits, RemainderSentence = Sentence:match( "^[., ]*([^ .,]+)(.*)" ) self:I( { Digits = Digits, Speech = Speech[Digits], RemainderSentence = RemainderSentence } ) if Digits then if Digits ~= "" and tonumber( Digits ) ~= nil then -- Construct numbers local Number = tonumber( Digits ) local Multiple = nil while Number >= 0 do if Number > 1000 then Multiple = math.floor( Number / 1000 ) * 1000 elseif Number > 100 then Multiple = math.floor( Number / 100 ) * 100 elseif Number > 20 then Multiple = math.floor( Number / 10 ) * 10 elseif Number >= 0 then Multiple = Number end Sentence = tostring( Multiple ) if Speech[Sentence] then self:I( { Speech = Speech[Sentence].Sentence, Data = Speech[Sentence].Data } ) self:NewTransmission( Speech[Sentence].Data[1] .. ".wav", Speech[Sentence].Data[2], Langauge .. "/" ) end Number = Number - Multiple Number = ( Number == 0 ) and -1 or Number end return RemainderSentence end return OriginalSentence else return "" end end --- Speak a sentence. -- @param #RADIOSPEECH self -- @param #string Sentence The sentence to be spoken. function RADIOSPEECH:Speak( Sentence, Language ) self:I( { Sentence, Language } ) local Language = Language or "EN" self:I( { Language = Language } ) -- If there is no node for Speech, then we start at the first nodes of the language. local Speech = self.Speech[Language] self:I( { Speech = Speech, Language = Language } ) self:NewTransmission( "_In.wav", 0.52, Language .. "/" ) repeat Sentence = self:SpeakWords( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) Sentence = self:SpeakDigits( Sentence, Speech, Language ) self:I( { Sentence = Sentence } ) -- Sentence = self:SpeakSymbols( Sentence, Speech ) -- -- self:I( { Sentence = Sentence } ) until not Sentence or Sentence == "" self:NewTransmission( "_Out.wav", 0.28, Language .. "/" ) end