From 4c3d44a63bd44fd14efb4a70c554f836e00695fe Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 30 May 2021 01:36:22 +0200 Subject: [PATCH] Sound update --- Moose Development/Moose/Core/Beacon.lua | 442 ++++++++++++++ Moose Development/Moose/Core/Settings.lua | 1 + Moose Development/Moose/Globals.lua | 32 +- Moose Development/Moose/Modules.lua | 4 +- Moose Development/Moose/Ops/ATIS.lua | 546 ++++++++++-------- Moose Development/Moose/Sound/Radio.lua | 462 +-------------- Moose Development/Moose/Sound/RadioQueue.lua | 6 +- Moose Development/Moose/Sound/SRS.lua | 160 ++++- .../Sound/{SoundFile.lua => SoundOutput.lua} | 58 +- Moose Development/Moose/Utilities/STTS.lua | 256 ++++++++ Moose Setup/Moose.files | 4 +- 11 files changed, 1235 insertions(+), 736 deletions(-) create mode 100644 Moose Development/Moose/Core/Beacon.lua rename Moose Development/Moose/Sound/{SoundFile.lua => SoundOutput.lua} (72%) create mode 100644 Moose Development/Moose/Utilities/STTS.lua diff --git a/Moose Development/Moose/Core/Beacon.lua b/Moose Development/Moose/Core/Beacon.lua new file mode 100644 index 000000000..37dba8c76 --- /dev/null +++ b/Moose Development/Moose/Core/Beacon.lua @@ -0,0 +1,442 @@ +--- **Core** - TACAN and other beacons. +-- +-- === +-- +-- ## Features: +-- +-- * Provide beacon functionality to assist pilots. +-- +-- === +-- +-- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky +-- +-- @module Core.Beacon +-- @image Core_Radio.JPG + +--- *In order for the light to shine so brightly, the darkness must be present.* -- Francis Bacon +-- +-- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. +-- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. +-- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is +-- attach to a cargo crate, for exemple. +-- +-- ## AA TACAN Beacon usage +-- +-- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. +-- Use @#BEACON:StopAATACAN}() to stop it. +-- +-- ## General Purpose Radio Beacon usage +-- +-- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with +-- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. +-- Use @{#BEACON:StopRadioBeacon}() to stop it. +-- +-- @type BEACON +-- @field #string ClassName Name of the class "BEACON". +-- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. +-- @extends Core.Base#BASE +BEACON = { + ClassName = "BEACON", + Positionable = nil, + name = nil, +} + +--- Beacon types supported by DCS. +-- @type BEACON.Type +-- @field #number NULL +-- @field #number VOR +-- @field #number DME +-- @field #number VOR_DME +-- @field #number TACAN TACtical Air Navigation system. +-- @field #number VORTAC +-- @field #number RSBN +-- @field #number BROADCAST_STATION +-- @field #number HOMER +-- @field #number AIRPORT_HOMER +-- @field #number AIRPORT_HOMER_WITH_MARKER +-- @field #number ILS_FAR_HOMER +-- @field #number ILS_NEAR_HOMER +-- @field #number ILS_LOCALIZER +-- @field #number ILS_GLIDESLOPE +-- @field #number PRMG_LOCALIZER +-- @field #number PRMG_GLIDESLOPE +-- @field #number ICLS Same as ICLS glideslope. +-- @field #number ICLS_LOCALIZER +-- @field #number ICLS_GLIDESLOPE +-- @field #number NAUTICAL_HOMER +BEACON.Type={ + NULL = 0, + VOR = 1, + DME = 2, + VOR_DME = 3, + TACAN = 4, + VORTAC = 5, + RSBN = 128, + BROADCAST_STATION = 1024, + HOMER = 8, + AIRPORT_HOMER = 4104, + AIRPORT_HOMER_WITH_MARKER = 4136, + ILS_FAR_HOMER = 16408, + ILS_NEAR_HOMER = 16424, + ILS_LOCALIZER = 16640, + ILS_GLIDESLOPE = 16896, + PRMG_LOCALIZER = 33024, + PRMG_GLIDESLOPE = 33280, + ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE + ICLS_LOCALIZER = 131328, + ICLS_GLIDESLOPE = 131584, + NAUTICAL_HOMER = 65536, +} + +--- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon +-- @type BEACON.System +-- @field #number PAR_10 ? +-- @field #number RSBN_5 Russian VOR/DME system. +-- @field #number TACAN TACtical Air Navigation system on ground. +-- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. +-- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. +-- @field #number VOR Very High Frequency Omni-Directional Range +-- @field #number ILS_LOCALIZER ILS localizer +-- @field #number ILS_GLIDESLOPE ILS glideslope. +-- @field #number PRGM_LOCALIZER PRGM localizer. +-- @field #number PRGM_GLIDESLOPE PRGM glideslope. +-- @field #number BROADCAST_STATION Broadcast station. +-- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. +-- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. +-- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. +-- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). +-- @field #number ICLS_LOCALIZER Carrier landing system. +-- @field #number ICLS_GLIDESLOPE Carrier landing system. +BEACON.System={ + PAR_10 = 1, + RSBN_5 = 2, + TACAN = 3, + TACAN_TANKER_X = 4, + TACAN_TANKER_Y = 5, + VOR = 6, + ILS_LOCALIZER = 7, + ILS_GLIDESLOPE = 8, + PRMG_LOCALIZER = 9, + PRMG_GLIDESLOPE = 10, + BROADCAST_STATION = 11, + VORTAC = 12, + TACAN_AA_MODE_X = 13, + TACAN_AA_MODE_Y = 14, + VORDME = 15, + ICLS_LOCALIZER = 16, + ICLS_GLIDESLOPE = 17, +} + +--- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. +-- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. +-- @param #BEACON self +-- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. +-- @return #BEACON Beacon object or #nil if the positionable is invalid. +function BEACON:New(Positionable) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) --#BEACON + + -- Debug. + self:F(Positionable) + + -- Set positionable. + if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid + self.Positionable = Positionable + self.name=Positionable:GetName() + self:I(string.format("New BEACON %s", tostring(self.name))) + return self + end + + self:E({"The passed positionable is invalid, no BEACON created", Positionable}) + return nil +end + + +--- Activates a TACAN BEACON. +-- @param #BEACON self +-- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". +-- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon +function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) + self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) + + -- Get frequency. + local Frequency=UTILS.TACANToFrequency(Channel, Mode) + + -- Check. + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + return self + end + + -- Beacon type. + local Type=BEACON.Type.TACAN + + -- Beacon system. + local System=BEACON.System.TACAN + + -- Check if unit is an aircraft and set system accordingly. + local AA=self.Positionable:IsAir() + if AA then + System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER + -- Check if "Y" mode is selected for aircraft. + if Mode~="Y" then + self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) + end + end + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug. + self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) + + -- Start beacon. + self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) + + -- Stop sheduler. + if Duration then + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. +-- @param #BEACON self +-- @param #number Channel ICLS channel. +-- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. +-- @param #number Duration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +function BEACON:ActivateICLS(Channel, Callsign, Duration) + self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) + + -- Attached unit. + local UnitID=self.Positionable:GetID() + + -- Debug + self:T2({"ICLS BEACON started!"}) + + -- Start beacon. + self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) + + -- Stop sheduler + if Duration then -- Schedule the stop of the BEACON if asked by the MD + self.Positionable:DeactivateBeacon(Duration) + end + + return self +end + +--- Activates a TACAN BEACON on an Aircraft. +-- @param #BEACON self +-- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels +-- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon +-- @param #boolean Bearing Can the BEACON be homed on ? +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a TACAN Beacon for a tanker +-- local myUnit = UNIT:FindByName("MyUnit") +-- local myBeacon = myUnit:GetBeacon() -- Creates the beacon +-- +-- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon +function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) + self:F({TACANChannel, Message, Bearing, BeaconDuration}) + + local IsValid = true + + if not self.Positionable:IsAir() then + self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) + IsValid = false + end + + local Frequency = self:_TACANToFrequency(TACANChannel, "Y") + if not Frequency then + self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) + IsValid = false + end + + -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing + -- or 14 (TACAN_AA_MODE_Y) if it does not + local System + if Bearing then + System = 5 + else + System = 14 + end + + if IsValid then -- Starts the BEACON + self:T2({"AA TACAN BEACON started !"}) + self.Positionable:SetCommand({ + id = "ActivateBeacon", + params = { + type = 4, + system = System, + callsign = Message, + frequency = Frequency, + } + }) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New(nil, + function() + self:StopAATACAN() + end, {}, BeaconDuration) + end + end + + return self +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopAATACAN() + self:F() + if not self.Positionable then + self:E({"Start the beacon first before stoping it !"}) + else + self.Positionable:SetCommand({ + id = 'DeactivateBeacon', + params = { + } + }) + end +end + + +--- Activates a general pupose Radio Beacon +-- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. +-- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. +-- They can home in on these specific frequencies : +-- * **Mi8** +-- * R-828 -> 20-60MHz +-- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM +-- * ARK9 -> 150-1300KHz +-- * **Huey** +-- * AN/ARC-131 -> 30-76 Mhz FM +-- @param #BEACON self +-- @param #string FileName The name of the audio file +-- @param #number Frequency in MHz +-- @param #number Modulation either radio.modulation.AM or radio.modulation.FM +-- @param #number Power in W +-- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. +-- @return #BEACON self +-- @usage +-- -- Let's create a beacon for a unit in distress. +-- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) +-- -- The beacon they use is battery-powered, and only lasts for 5 min +-- local UnitInDistress = UNIT:FindByName("Unit1") +-- local UnitBeacon = UnitInDistress:GetBeacon() +-- +-- -- Set the beacon and start it +-- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) +function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) + self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) + local IsValid = false + + -- Check the filename + if type(FileName) == "string" then + if FileName:find(".ogg") or FileName:find(".wav") then + if not FileName:find("l10n/DEFAULT/") then + FileName = "l10n/DEFAULT/" .. FileName + end + IsValid = true + end + end + if not IsValid then + self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) + end + + -- Check the Frequency + if type(Frequency) ~= "number" and IsValid then + self:E({"Frequency invalid. ", Frequency}) + IsValid = false + end + Frequency = Frequency * 1000000 -- Conversion to Hz + + -- Check the modulation + if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? + self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) + IsValid = false + end + + -- Check the Power + if type(Power) ~= "number" and IsValid then + self:E({"Power is invalid. ", Power}) + IsValid = false + end + Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that + + if IsValid then + self:T2({"Activating Beacon on ", Frequency, Modulation}) + -- Note that this is looped. I have to give this transmission a unique name, I use the class ID + trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) + + if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD + SCHEDULER:New( nil, + function() + self:StopRadioBeacon() + end, {}, BeaconDuration) + end + end +end + +--- Stops the AA TACAN BEACON +-- @param #BEACON self +-- @return #BEACON self +function BEACON:StopRadioBeacon() + self:F() + -- The unique name of the transmission is the class ID + trigger.action.stopRadioTransmission(tostring(self.ID)) + return self +end + +--- Converts a TACAN Channel/Mode couple into a frequency in Hz +-- @param #BEACON self +-- @param #number TACANChannel +-- @param #string TACANMode +-- @return #number Frequecy +-- @return #nil if parameters are invalid +function BEACON:_TACANToFrequency(TACANChannel, TACANMode) + self:F3({TACANChannel, TACANMode}) + + if type(TACANChannel) ~= "number" then + if TACANMode ~= "X" and TACANMode ~= "Y" then + return nil -- error in arguments + end + end + +-- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. +-- I have no idea what it does but it seems to work + local A = 1151 -- 'X', channel >= 64 + local B = 64 -- channel >= 64 + + if TACANChannel < 64 then + B = 1 + end + + if TACANMode == 'Y' then + A = 1025 + if TACANChannel < 64 then + A = 1088 + end + else -- 'X' + if TACANChannel < 64 then + A = 962 + end + end + + return (A + TACANChannel - B) * 1000000 +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/Settings.lua b/Moose Development/Moose/Core/Settings.lua index 3a557de65..74b5fa54b 100644 --- a/Moose Development/Moose/Core/Settings.lua +++ b/Moose Development/Moose/Core/Settings.lua @@ -236,6 +236,7 @@ do -- SETTINGS --- SETTINGS constructor. -- @param #SETTINGS self + -- @param #string PlayerName (Optional) Set settings for this player. -- @return #SETTINGS function SETTINGS:Set( PlayerName ) diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index bdde44b6d..fdc6db2c7 100644 --- a/Moose Development/Moose/Globals.lua +++ b/Moose Development/Moose/Globals.lua @@ -1,5 +1,4 @@ --- The order of the declarations is important here. Don't touch it. - +--- GLOBALS: The order of the declarations is important here. Don't touch it. --- Declare the event dispatcher based on the EVENT class _EVENTDISPATCHER = EVENT:New() -- Core.Event#EVENT @@ -10,9 +9,38 @@ _SCHEDULEDISPATCHER = SCHEDULEDISPATCHER:New() -- Core.ScheduleDispatcher#SCHEDU --- Declare the main database object, which is used internally by the MOOSE classes. _DATABASE = DATABASE:New() -- Core.Database#DATABASE +--- Settings _SETTINGS = SETTINGS:Set() _SETTINGS:SetPlayerMenuOn() +--- Register cargos. _DATABASE:_RegisterCargos() + +--- Register zones. _DATABASE:_RegisterZones() +--- Check if os etc is available. +BASE:I("Checking de-sanitization of os, io and lfs (Check /Scripts/MissionScripting.lua and commend out sanitizeModule(''). Use at your own risk!)") + +local __na=false +if os then + BASE:I("- os available") +else + BASE:I("- os NOT available! Some functions may not work.") + __na=true +end +if io then + BASE:I("- io available") +else + BASE:I("- io NOT available! Some functions may not work.") + __na=true +end +if lfs then + BASE:I("- lfs available") +else + BASE:I("- lfs NOT available! Some functions may not work.") + __na=true +end +if __na then + BASE:I("Check /Scripts/MissionScripting.lua and commend out the lines with sanitizeModule(''). Use at your own risk!)") +end \ No newline at end of file diff --git a/Moose Development/Moose/Modules.lua b/Moose Development/Moose/Modules.lua index 06f53d723..2ee24ee38 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -3,8 +3,10 @@ __Moose.Include( 'Scripts/Moose/Utilities/Routines.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Utils.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Profiler.lua' ) __Moose.Include( 'Scripts/Moose/Utilities/Templates.lua' ) +__Moose.Include( 'Scripts/Moose/Utilities/STTS.lua' ) __Moose.Include( 'Scripts/Moose/Core/Base.lua' ) +__Moose.Include( 'Scripts/Moose/Core/Beacon.lua' ) __Moose.Include( 'Scripts/Moose/Core/UserFlag.lua' ) __Moose.Include( 'Scripts/Moose/Core/Report.lua' ) __Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) @@ -110,7 +112,7 @@ __Moose.Include( 'Scripts/Moose/Actions/Act_Account.lua' ) __Moose.Include( 'Scripts/Moose/Actions/Act_Assist.lua' ) __Moose.Include( 'Scripts/Moose/Sound/UserSound.lua' ) -__Moose.Include( 'Scripts/Moose/Sound/SoundFile.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/SoundOutput.lua' ) __Moose.Include( 'Scripts/Moose/Sound/Radio.lua' ) __Moose.Include( 'Scripts/Moose/Sound/RadioQueue.lua' ) __Moose.Include( 'Scripts/Moose/Sound/RadioSpeech.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 96353d11d..3870ba197 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -88,6 +88,8 @@ -- @field #boolean usemarker Use mark on the F10 map. -- @field #number markerid Numerical ID of the F10 map mark point. -- @field #number relHumidity Relative humidity (used to approximately calculate the dew point). +-- @field #boolean useSRS If true, use SRS for transmission. +-- @field Sound.SRS#MSRS msrs Moose SRS object. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -1100,6 +1102,22 @@ function ATIS:MarkRunways(markall) end end +--- Use SRS Simple-Text-To-Speech for transmissions. No sound files necessary. +-- @param #ATIS self +-- @param #string PathToSRS Path to SRS directory. +-- @param #string Gender Gender: "male" or "female" (default). +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @param #string Voice Specific voice. Overrides `Gender` and `Culture`. +-- @return #ATIS self +function ATIS:SetSTTS(PathToSRS, Gender, Culture, Voice, Port) + self.useSRS=true + self.msrs=MSRS:New(PathToSRS, self.frequency, self.modulation) + self.msrs:SetGender(Gender) + self.msrs:SetCulture(Culture) + self.msrs:SetVoice(Voice) + return self +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1188,15 +1206,25 @@ end -- @param #string To To state. function ATIS:onafterCheckQueue(From, Event, To) - if #self.radioqueue.queue==0 then - self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + if self.useSRS then + self:Broadcast() + + self:__CheckQueue(-120) + else - self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + + if #self.radioqueue.queue==0 then + self:T(self.lid..string.format("Radio queue empty. Repeating message.")) + self:Broadcast() + else + self:T2(self.lid..string.format("Radio queue %d transmissions queued.", #self.radioqueue.queue)) + end + + -- Check back in 5 seconds. + self:__CheckQueue(-5) + end - - -- Check back in 5 seconds. - self:__CheckQueue(-5) end --- Broadcast ATIS radio message. @@ -1591,36 +1619,46 @@ function ATIS:onafterBroadcast(From, Event, To) if self.airbasename:find("AFB")==nil and self.airbasename:find("Airport")==nil and self.airbasename:find("Airstrip")==nil and self.airbasename:find("airfield")==nil and self.airbasename:find("AB")==nil then subtitle=subtitle.." Airport" end - self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + if not self.useSRS then + self.radioqueue:NewTransmission(string.format("%s/%s.ogg", self.theatre, self.airbasename), 3.0, self.soundpath, nil, nil, subtitle, self.subduration) + end local alltext=subtitle -- Information tag subtitle=string.format("Information %s", NATO) local _INFORMATION=subtitle - self:Transmission(ATIS.Sound.Information, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + if not self.useSRS then + self:Transmission(ATIS.Sound.Information, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Zulu Time subtitle=string.format("%s Zulu", ZULU) - self.radioqueue:Number2Transmission(ZULU, nil, 0.5) - self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + if not self.useSRS then + self.radioqueue:Number2Transmission(ZULU, nil, 0.5) + self:Transmission(ATIS.Sound.Zulu, 0.2, subtitle) + end alltext=alltext..";\n"..subtitle if not self.zulutimeonly then -- Sunrise Time subtitle=string.format("Sunrise at %s local time", SUNRISE) - self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunriseAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNRISE, nil, 0.2) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle -- Sunset Time subtitle=string.format("Sunset at %s local time", SUNSET) - self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) - self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) - self:Transmission(ATIS.Sound.TimeLocal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.SunsetAt, 0.5, subtitle) + self.radioqueue:Number2Transmission(SUNSET, nil, 0.5) + self:Transmission(ATIS.Sound.TimeLocal, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -1634,17 +1672,19 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle..", gusting" end local _WIND=subtitle - self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) - self.radioqueue:Number2Transmission(WINDFROM) - self:Transmission(ATIS.Sound.At, 0.2) - self.radioqueue:Number2Transmission(WINDSPEED) - if self.metric then - self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) - else - self:Transmission(ATIS.Sound.Knots, 0.2) - end - if turbulence>0 then - self:Transmission(ATIS.Sound.Gusting, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.WindFrom, 1.0, subtitle) + self.radioqueue:Number2Transmission(WINDFROM) + self:Transmission(ATIS.Sound.At, 0.2) + self.radioqueue:Number2Transmission(WINDSPEED) + if self.metric then + self:Transmission(ATIS.Sound.MetersPerSecond, 0.2) + else + self:Transmission(ATIS.Sound.Knots, 0.2) + end + if turbulence>0 then + self:Transmission(ATIS.Sound.Gusting, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1654,12 +1694,14 @@ function ATIS:onafterBroadcast(From, Event, To) else subtitle=string.format("Visibility %s SM", VISIBILITY) end - self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) - self.radioqueue:Number2Transmission(VISIBILITY) - if self.metric then - self:Transmission(ATIS.Sound.Kilometers, 0.2) - else - self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Visibilty, 1.0, subtitle) + self.radioqueue:Number2Transmission(VISIBILITY) + if self.metric then + self:Transmission(ATIS.Sound.Kilometers, 0.2) + else + self:Transmission(ATIS.Sound.StatuteMiles, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1699,57 +1741,63 @@ function ATIS:onafterBroadcast(From, Event, To) -- Actual output if wp then subtitle=string.format("Weather phenomena:%s", wpsub) - self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) - if precepitation==1 then - self:Transmission(ATIS.Sound.Rain, 0.5) - elseif precepitation==2 then - self:Transmission(ATIS.Sound.ThunderStorm, 0.5) - elseif precepitation==3 then - self:Transmission(ATIS.Sound.Snow, 0.5) - elseif precepitation==4 then - self:Transmission(ATIS.Sound.SnowStorm, 0.5) - end - if fog then - self:Transmission(ATIS.Sound.Fog, 0.5) - end - if dust then - self:Transmission(ATIS.Sound.Dust, 0.5) + if not self.useSRS then + self:Transmission(ATIS.Sound.WeatherPhenomena, 1.0, subtitle) + if precepitation==1 then + self:Transmission(ATIS.Sound.Rain, 0.5) + elseif precepitation==2 then + self:Transmission(ATIS.Sound.ThunderStorm, 0.5) + elseif precepitation==3 then + self:Transmission(ATIS.Sound.Snow, 0.5) + elseif precepitation==4 then + self:Transmission(ATIS.Sound.SnowStorm, 0.5) + end + if fog then + self:Transmission(ATIS.Sound.Fog, 0.5) + end + if dust then + self:Transmission(ATIS.Sound.Dust, 0.5) + end end alltext=alltext..";\n"..subtitle end -- Cloud base - self:Transmission(CloudCover, 1.0, CLOUDSsub) + if not self.useSRS then + self:Transmission(CloudCover, 1.0, CLOUDSsub) + end if CLOUDBASE and static then -- Base if self.metric then - subtitle=string.format("Cloudbase %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s meters", CLOUDBASE, CLOUDCEIL) else - subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) end - self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) - if tonumber(CLOUDBASE1000)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDBASE0100)>0 then - self.radioqueue:Number2Transmission(CLOUDBASE0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - -- Ceiling - self:Transmission(ATIS.Sound.CloudCeiling, 0.5) - if tonumber(CLOUDCEIL1000)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) - end - if tonumber(CLOUDCEIL0100)>0 then - self.radioqueue:Number2Transmission(CLOUDCEIL0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.CloudBase, 1.0, subtitle) + if tonumber(CLOUDBASE1000)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDBASE0100)>0 then + self.radioqueue:Number2Transmission(CLOUDBASE0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + -- Ceiling + self:Transmission(ATIS.Sound.CloudCeiling, 0.5) + if tonumber(CLOUDCEIL1000)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(CLOUDCEIL0100)>0 then + self.radioqueue:Number2Transmission(CLOUDCEIL0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1769,15 +1817,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _TEMPERATURE=subtitle - self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) - if temperature<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(TEMPERATURE) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Temperature, 1.0, subtitle) + if temperature<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(TEMPERATURE) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1796,15 +1846,17 @@ function ATIS:onafterBroadcast(From, Event, To) end end local _DEWPOINT=subtitle - self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) - if dewpoint<0 then - self:Transmission(ATIS.Sound.Minus, 0.2) - end - self.radioqueue:Number2Transmission(DEWPOINT) - if self.TDegF then - self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) - else - self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.DewPoint, 1.0, subtitle) + if dewpoint<0 then + self:Transmission(ATIS.Sound.Minus, 0.2) + end + self.radioqueue:Number2Transmission(DEWPOINT) + if self.TDegF then + self:Transmission(ATIS.Sound.DegreesFahrenheit, 0.2) + else + self:Transmission(ATIS.Sound.DegreesCelsius, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1813,51 +1865,53 @@ function ATIS:onafterBroadcast(From, Event, To) if self.qnhonly then subtitle=string.format("Altimeter %s.%s mmHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s mmHg", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.metric then if self.qnhonly then subtitle=string.format("Altimeter %s.%s hPa", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s hPa", QNH[1], QNH[2], QFE[1], QFE[2]) end else if self.qnhonly then subtitle=string.format("Altimeter %s.%s inHg", QNH[1], QNH[2]) else - subtitle=string.format("Altimeter QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) + subtitle=string.format("Altimeter: QNH %s.%s, QFE %s.%s inHg", QNH[1], QNH[2], QFE[1], QFE[2]) end end end local _ALTIMETER=subtitle - self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) - if not self.qnhonly then - self:Transmission(ATIS.Sound.QNH, 0.5) - end - self.radioqueue:Number2Transmission(QNH[1]) - - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) - end - self.radioqueue:Number2Transmission(QNH[2]) - - if not self.qnhonly then - self:Transmission(ATIS.Sound.QFE, 0.75) - self.radioqueue:Number2Transmission(QFE[1]) - if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then - self:Transmission(ATIS.Sound.Decimal, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.Altimeter, 1.0, subtitle) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QNH, 0.5) end - self.radioqueue:Number2Transmission(QFE[2]) - end + self.radioqueue:Number2Transmission(QNH[1]) + + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QNH[2]) - if self.PmmHg then - self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) - else - if self.metric then - self:Transmission(ATIS.Sound.HectoPascal, 0.1) + if not self.qnhonly then + self:Transmission(ATIS.Sound.QFE, 0.75) + self.radioqueue:Number2Transmission(QFE[1]) + if ATIS.ICAOPhraseology[UTILS.GetDCSMap()] then + self:Transmission(ATIS.Sound.Decimal, 0.2) + end + self.radioqueue:Number2Transmission(QFE[2]) + end + + if self.PmmHg then + self:Transmission(ATIS.Sound.MillimetersOfMercury, 0.1) else - self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + if self.metric then + self:Transmission(ATIS.Sound.HectoPascal, 0.1) + else + self:Transmission(ATIS.Sound.InchesOfMercury, 0.1) + end end end alltext=alltext..";\n"..subtitle @@ -1870,12 +1924,14 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." Right" end local _RUNACT=subtitle - self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) - self.radioqueue:Number2Transmission(runway) - if rwyLeft==true then - self:Transmission(ATIS.Sound.Left, 0.2) - elseif rwyLeft==false then - self:Transmission(ATIS.Sound.Right, 0.2) + if not self.useSRS then + self:Transmission(ATIS.Sound.ActiveRunway, 1.0, subtitle) + self.radioqueue:Number2Transmission(runway) + if rwyLeft==true then + self:Transmission(ATIS.Sound.Left, 0.2) + elseif rwyLeft==false then + self:Transmission(ATIS.Sound.Right, 0.2) + end end alltext=alltext..";\n"..subtitle @@ -1900,21 +1956,22 @@ function ATIS:onafterBroadcast(From, Event, To) end -- Transmit. - self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + if not self.useSRS then + self:Transmission(ATIS.Sound.RunwayLength, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1937,22 +1994,23 @@ function ATIS:onafterBroadcast(From, Event, To) subtitle=subtitle.." feet" end - -- Transmitt. - self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) - if tonumber(L1000)>0 then - self.radioqueue:Number2Transmission(L1000) - self:Transmission(ATIS.Sound.Thousand, 0.1) + -- Transmit. + if not self.useSRS then + self:Transmission(ATIS.Sound.Elevation, 1.0, subtitle) + if tonumber(L1000)>0 then + self.radioqueue:Number2Transmission(L1000) + self:Transmission(ATIS.Sound.Thousand, 0.1) + end + if tonumber(L0100)>0 then + self.radioqueue:Number2Transmission(L0100) + self:Transmission(ATIS.Sound.Hundred, 0.1) + end + if self.metric then + self:Transmission(ATIS.Sound.Meters, 0.1) + else + self:Transmission(ATIS.Sound.Feet, 0.1) + end end - if tonumber(L0100)>0 then - self.radioqueue:Number2Transmission(L0100) - self:Transmission(ATIS.Sound.Hundred, 0.1) - end - if self.metric then - self:Transmission(ATIS.Sound.Meters, 0.1) - else - self:Transmission(ATIS.Sound.Feet, 0.1) - end - alltext=alltext..";\n"..subtitle end @@ -1966,9 +2024,47 @@ function ATIS:onafterBroadcast(From, Event, To) end end subtitle=string.format("Tower frequency %s", freqs) - self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) - for _,freq in pairs(self.towerfrequency) do - local f=string.format("%.3f", freq) + if not self.useSRS then + self:Transmission(ATIS.Sound.TowerFrequency, 1.0, subtitle) + for _,freq in pairs(self.towerfrequency) do + local f=string.format("%.3f", freq) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + end + alltext=alltext..";\n"..subtitle + end + + -- ILS + local ils=self:GetNavPoint(self.ils, runway, rwyLeft) + if ils then + subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) + local f=string.format("%.2f", ils.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end + alltext=alltext..";\n"..subtitle + end + + -- Outer NDB + local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) + if ndb then + subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) + if not self.useSRS then + self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) f=UTILS.Split(f, ".") self.radioqueue:Number2Transmission(f[1], nil, 0.5) if tonumber(f[2])>0 then @@ -1977,41 +2073,6 @@ function ATIS:onafterBroadcast(From, Event, To) end self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - - alltext=alltext..";\n"..subtitle - end - - -- ILS - local ils=self:GetNavPoint(self.ils, runway, rwyLeft) - if ils then - subtitle=string.format("ILS frequency %.2f MHz", ils.frequency) - self:Transmission(ATIS.Sound.ILSFrequency, 1.0, subtitle) - local f=string.format("%.2f", ils.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - - alltext=alltext..";\n"..subtitle - end - - -- Outer NDB - local ndb=self:GetNavPoint(self.ndbouter, runway, rwyLeft) - if ndb then - subtitle=string.format("Outer NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.OuterNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end @@ -2019,51 +2080,55 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.ndbinner, runway, rwyLeft) if ndb then subtitle=string.format("Inner NDB frequency %.2f MHz", ndb.frequency) - self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) - local f=string.format("%.2f", ndb.frequency) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) - end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.InnerNDBFrequency, 1.0, subtitle) + local f=string.format("%.2f", ndb.frequency) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) + end alltext=alltext..";\n"..subtitle end -- VOR if self.vor then subtitle=string.format("VOR frequency %.2f MHz", self.vor) - self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) - local f=string.format("%.2f", self.vor) - f=UTILS.Split(f, ".") - self.radioqueue:Number2Transmission(f[1], nil, 0.5) - if tonumber(f[2])>0 then - self:Transmission(ATIS.Sound.Decimal, 0.2) - self.radioqueue:Number2Transmission(f[2]) + if not self.useSRS then + self:Transmission(ATIS.Sound.VORFrequency, 1.0, subtitle) + local f=string.format("%.2f", self.vor) + f=UTILS.Split(f, ".") + self.radioqueue:Number2Transmission(f[1], nil, 0.5) + if tonumber(f[2])>0 then + self:Transmission(ATIS.Sound.Decimal, 0.2) + self.radioqueue:Number2Transmission(f[2]) + end + self:Transmission(ATIS.Sound.MegaHertz, 0.2) end - self:Transmission(ATIS.Sound.MegaHertz, 0.2) - alltext=alltext..";\n"..subtitle end -- TACAN if self.tacan then subtitle=string.format("TACAN channel %dX", self.tacan) - self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) - self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.TACANChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.tacan), nil, 0.2) + self.radioqueue:NewTransmission("NATO Alphabet/Xray.ogg", 0.75, self.soundpath, nil, 0.2) + end alltext=alltext..";\n"..subtitle end -- RSBN if self.rsbn then subtitle=string.format("RSBN channel %d", self.rsbn) - self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) - + if not self.useSRS then + self:Transmission(ATIS.Sound.RSBNChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(self.rsbn), nil, 0.2) + end alltext=alltext..";\n"..subtitle end @@ -2071,17 +2136,19 @@ function ATIS:onafterBroadcast(From, Event, To) local ndb=self:GetNavPoint(self.prmg, runway, rwyLeft) if ndb then subtitle=string.format("PRMG channel %d", ndb.frequency) - self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) - self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) - + if not self.useSRS then + self:Transmission(ATIS.Sound.PRMGChannel, 1.0, subtitle) + self.radioqueue:Number2Transmission(tostring(ndb.frequency), nil, 0.5) + end alltext=alltext..";\n"..subtitle end -- Advice on initial... subtitle=string.format("Advise on initial contact, you have information %s", NATO) - self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) - self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) - + if not self.useSRS then + self:Transmission(ATIS.Sound.AdviceOnInitial, 0.5, subtitle) + self.radioqueue:NewTransmission(string.format("NATO Alphabet/%s.ogg", NATO), 0.75, self.soundpath) + end alltext=alltext..";\n"..subtitle -- Report ATIS text. @@ -2103,12 +2170,31 @@ end function ATIS:onafterReport(From, Event, To, Text) self:T(self.lid..string.format("Report:\n%s", Text)) - -- Remove line breaks - local text=string.gsub(Text, "[\r\n]", "") - env.info("FF: "..text) + if self.useSRS and self.msrs then - local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, Modulation) - msrs:PlayText(text) + -- Remove line breaks + local text=string.gsub(Text, "[\r\n]", "") + + -- Replace other stuff. + local text=string.gsub(text, "SM", "statute miles") + local text=string.gsub(text, "°C", "degrees Celsius") + local text=string.gsub(text, "°F", "degrees Fahrenheit") + local text=string.gsub(text, "inHg", "inches of Mercury") + local text=string.gsub(text, "mmHg", "millimeters of Mercury") + local text=string.gsub(text, "hPa", "hecto Pascals") + local text=string.gsub(text, "m/s", "meters per second") + + -- Replace ";" by "." + local text=string.gsub(text, ";", ". ") + env.info("FF: "..text) + + --local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, Modulation) + --msrs:PlayText(text) + + -- Play text-to-speech report. + self.msrs:PlayText(text) + + end end diff --git a/Moose Development/Moose/Sound/Radio.lua b/Moose Development/Moose/Sound/Radio.lua index 175bc2106..872ad6e26 100644 --- a/Moose Development/Moose/Sound/Radio.lua +++ b/Moose Development/Moose/Sound/Radio.lua @@ -1,13 +1,10 @@ ---- **Core** - Is responsible for everything that is related to radio transmission and you can hear in DCS, be it TACAN beacons, Radio transmissions. +--- **Sound** - Radio transmissions. -- -- === -- -- ## Features: -- -- * Provide radio functionality to broadcast radio transmissions. --- * Provide beacon functionality to assist pilots. --- --- The Radio contains 2 classes : RADIO and BEACON -- -- What are radio communications in DCS? -- @@ -35,13 +32,13 @@ -- -- ### Authors: Hugues "Grey_Echo" Bousquet, funkyfranky -- --- @module Core.Radio +-- @module Sound.Radio -- @image Core_Radio.JPG ---- Models the radio capability. +--- *It's not true I had nothing on, I had the radio on.* -- Marilyn Monroe -- --- ## RADIO usage +-- # RADIO usage -- -- There are 3 steps to a successful radio transmission. -- @@ -87,15 +84,15 @@ -- @field #string alias Name of the radio transmitter. -- @extends Core.Base#BASE RADIO = { - ClassName = "RADIO", - FileName = "", - Frequency = 0, - Modulation = radio.modulation.AM, - Subtitle = "", + ClassName = "RADIO", + FileName = "", + Frequency = 0, + Modulation = radio.modulation.AM, + Subtitle = "", SubtitleDuration = 0, - Power = 100, - Loop = false, - alias=nil, + Power = 100, + Loop = false, + alias = nil, } --- Create a new RADIO Object. This doesn't broadcast a transmission, though, use @{#RADIO.Broadcast} to actually broadcast. @@ -395,438 +392,3 @@ function RADIO:StopBroadcast() end return self end - - ---- After attaching a @{#BEACON} to your @{Wrapper.Positionable#POSITIONABLE}, you need to select the right function to activate the kind of beacon you want. --- There are two types of BEACONs available : the AA TACAN Beacon and the general purpose Radio Beacon. --- Note that in both case, you can set an optional parameter : the `BeaconDuration`. This can be very usefull to simulate the battery time if your BEACON is --- attach to a cargo crate, for exemple. --- --- ## AA TACAN Beacon usage --- --- This beacon only works with airborne @{Wrapper.Unit#UNIT} or a @{Wrapper.Group#GROUP}. Use @{#BEACON:AATACAN}() to set the beacon parameters and start the beacon. --- Use @#BEACON:StopAATACAN}() to stop it. --- --- ## General Purpose Radio Beacon usage --- --- This beacon will work with any @{Wrapper.Positionable#POSITIONABLE}, but **it won't follow the @{Wrapper.Positionable#POSITIONABLE}** ! This means that you should only use it with --- @{Wrapper.Positionable#POSITIONABLE} that don't move, or move very slowly. Use @{#BEACON:RadioBeacon}() to set the beacon parameters and start the beacon. --- Use @{#BEACON:StopRadioBeacon}() to stop it. --- --- @type BEACON --- @field #string ClassName Name of the class "BEACON". --- @field Wrapper.Controllable#CONTROLLABLE Positionable The @{#CONTROLLABLE} that will receive radio capabilities. --- @extends Core.Base#BASE -BEACON = { - ClassName = "BEACON", - Positionable = nil, - name=nil, -} - ---- Beacon types supported by DCS. --- @type BEACON.Type --- @field #number NULL --- @field #number VOR --- @field #number DME --- @field #number VOR_DME --- @field #number TACAN TACtical Air Navigation system. --- @field #number VORTAC --- @field #number RSBN --- @field #number BROADCAST_STATION --- @field #number HOMER --- @field #number AIRPORT_HOMER --- @field #number AIRPORT_HOMER_WITH_MARKER --- @field #number ILS_FAR_HOMER --- @field #number ILS_NEAR_HOMER --- @field #number ILS_LOCALIZER --- @field #number ILS_GLIDESLOPE --- @field #number PRMG_LOCALIZER --- @field #number PRMG_GLIDESLOPE --- @field #number ICLS Same as ICLS glideslope. --- @field #number ICLS_LOCALIZER --- @field #number ICLS_GLIDESLOPE --- @field #number NAUTICAL_HOMER -BEACON.Type={ - NULL = 0, - VOR = 1, - DME = 2, - VOR_DME = 3, - TACAN = 4, - VORTAC = 5, - RSBN = 128, - BROADCAST_STATION = 1024, - HOMER = 8, - AIRPORT_HOMER = 4104, - AIRPORT_HOMER_WITH_MARKER = 4136, - ILS_FAR_HOMER = 16408, - ILS_NEAR_HOMER = 16424, - ILS_LOCALIZER = 16640, - ILS_GLIDESLOPE = 16896, - PRMG_LOCALIZER = 33024, - PRMG_GLIDESLOPE = 33280, - ICLS = 131584, --leaving this in here but it is the same as ICLS_GLIDESLOPE - ICLS_LOCALIZER = 131328, - ICLS_GLIDESLOPE = 131584, - NAUTICAL_HOMER = 65536, - -} - ---- Beacon systems supported by DCS. https://wiki.hoggitworld.com/view/DCS_command_activateBeacon --- @type BEACON.System --- @field #number PAR_10 ? --- @field #number RSBN_5 Russian VOR/DME system. --- @field #number TACAN TACtical Air Navigation system on ground. --- @field #number TACAN_TANKER_X TACtical Air Navigation system for tankers on X band. --- @field #number TACAN_TANKER_Y TACtical Air Navigation system for tankers on Y band. --- @field #number VOR Very High Frequency Omni-Directional Range --- @field #number ILS_LOCALIZER ILS localizer --- @field #number ILS_GLIDESLOPE ILS glideslope. --- @field #number PRGM_LOCALIZER PRGM localizer. --- @field #number PRGM_GLIDESLOPE PRGM glideslope. --- @field #number BROADCAST_STATION Broadcast station. --- @field #number VORTAC Radio-based navigational aid for aircraft pilots consisting of a co-located VHF omnidirectional range (VOR) beacon and a tactical air navigation system (TACAN) beacon. --- @field #number TACAN_AA_MODE_X TACtical Air Navigation for aircraft on X band. --- @field #number TACAN_AA_MODE_Y TACtical Air Navigation for aircraft on Y band. --- @field #number VORDME Radio beacon that combines a VHF omnidirectional range (VOR) with a distance measuring equipment (DME). --- @field #number ICLS_LOCALIZER Carrier landing system. --- @field #number ICLS_GLIDESLOPE Carrier landing system. -BEACON.System={ - PAR_10 = 1, - RSBN_5 = 2, - TACAN = 3, - TACAN_TANKER_X = 4, - TACAN_TANKER_Y = 5, - VOR = 6, - ILS_LOCALIZER = 7, - ILS_GLIDESLOPE = 8, - PRMG_LOCALIZER = 9, - PRMG_GLIDESLOPE = 10, - BROADCAST_STATION = 11, - VORTAC = 12, - TACAN_AA_MODE_X = 13, - TACAN_AA_MODE_Y = 14, - VORDME = 15, - ICLS_LOCALIZER = 16, - ICLS_GLIDESLOPE = 17, -} - ---- Create a new BEACON Object. This doesn't activate the beacon, though, use @{#BEACON.ActivateTACAN} etc. --- If you want to create a BEACON, you probably should use @{Wrapper.Positionable#POSITIONABLE.GetBeacon}() instead. --- @param #BEACON self --- @param Wrapper.Positionable#POSITIONABLE Positionable The @{Positionable} that will receive radio capabilities. --- @return #BEACON Beacon object or #nil if the positionable is invalid. -function BEACON:New(Positionable) - - -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) --#BEACON - - -- Debug. - self:F(Positionable) - - -- Set positionable. - if Positionable:GetPointVec2() then -- It's stupid, but the only way I found to make sure positionable is valid - self.Positionable = Positionable - self.name=Positionable:GetName() - self:I(string.format("New BEACON %s", tostring(self.name))) - return self - end - - self:E({"The passed positionable is invalid, no BEACON created", Positionable}) - return nil -end - - ---- Activates a TACAN BEACON. --- @param #BEACON self --- @param #number Channel TACAN channel, i.e. the "10" part in "10Y". --- @param #string Mode TACAN mode, i.e. the "Y" part in "10Y". --- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon. --- @param #boolean Bearing If true, beacon provides bearing information. If false (or nil), only distance information is available. --- @param #number Duration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:ActivateTACAN(20, "Y", "TEXACO", true) -- Activate the beacon -function BEACON:ActivateTACAN(Channel, Mode, Message, Bearing, Duration) - self:T({channel=Channel, mode=Mode, callsign=Message, bearing=Bearing, duration=Duration}) - - -- Get frequency. - local Frequency=UTILS.TACANToFrequency(Channel, Mode) - - -- Check. - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - return self - end - - -- Beacon type. - local Type=BEACON.Type.TACAN - - -- Beacon system. - local System=BEACON.System.TACAN - - -- Check if unit is an aircraft and set system accordingly. - local AA=self.Positionable:IsAir() - if AA then - System=5 --NOTE: 5 is how you cat the correct tanker behaviour! --BEACON.System.TACAN_TANKER - -- Check if "Y" mode is selected for aircraft. - if Mode~="Y" then - self:E({"WARNING: The POSITIONABLE you want to attach the AA Tacan Beacon is an aircraft: Mode should Y !The BEACON is not emitting.", self.Positionable}) - end - end - - -- Attached unit. - local UnitID=self.Positionable:GetID() - - -- Debug. - self:I({string.format("BEACON Activating TACAN %s: Channel=%d%s, Morse=%s, Bearing=%s, Duration=%s!", tostring(self.name), Channel, Mode, Message, tostring(Bearing), tostring(Duration))}) - - -- Start beacon. - self.Positionable:CommandActivateBeacon(Type, System, Frequency, UnitID, Channel, Mode, AA, Message, Bearing) - - -- Stop sheduler. - if Duration then - self.Positionable:DeactivateBeacon(Duration) - end - - return self -end - ---- Activates an ICLS BEACON. The unit the BEACON is attached to should be an aircraft carrier supporting this system. --- @param #BEACON self --- @param #number Channel ICLS channel. --- @param #string Callsign The Message that is going to be coded in Morse and broadcasted by the beacon. --- @param #number Duration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self -function BEACON:ActivateICLS(Channel, Callsign, Duration) - self:F({Channel=Channel, Callsign=Callsign, Duration=Duration}) - - -- Attached unit. - local UnitID=self.Positionable:GetID() - - -- Debug - self:T2({"ICLS BEACON started!"}) - - -- Start beacon. - self.Positionable:CommandActivateICLS(Channel, UnitID, Callsign) - - -- Stop sheduler - if Duration then -- Schedule the stop of the BEACON if asked by the MD - self.Positionable:DeactivateBeacon(Duration) - end - - return self -end - - - - - - ---- Activates a TACAN BEACON on an Aircraft. --- @param #BEACON self --- @param #number TACANChannel (the "10" part in "10Y"). Note that AA TACAN are only available on Y Channels --- @param #string Message The Message that is going to be coded in Morse and broadcasted by the beacon --- @param #boolean Bearing Can the BEACON be homed on ? --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a TACAN Beacon for a tanker --- local myUnit = UNIT:FindByName("MyUnit") --- local myBeacon = myUnit:GetBeacon() -- Creates the beacon --- --- myBeacon:AATACAN(20, "TEXACO", true) -- Activate the beacon -function BEACON:AATACAN(TACANChannel, Message, Bearing, BeaconDuration) - self:F({TACANChannel, Message, Bearing, BeaconDuration}) - - local IsValid = true - - if not self.Positionable:IsAir() then - self:E({"The POSITIONABLE you want to attach the AA Tacan Beacon is not an aircraft ! The BEACON is not emitting", self.Positionable}) - IsValid = false - end - - local Frequency = self:_TACANToFrequency(TACANChannel, "Y") - if not Frequency then - self:E({"The passed TACAN channel is invalid, the BEACON is not emitting"}) - IsValid = false - end - - -- I'm using the beacon type 4 (BEACON_TYPE_TACAN). For System, I'm using 5 (TACAN_TANKER_MODE_Y) if the bearing shows its bearing - -- or 14 (TACAN_AA_MODE_Y) if it does not - local System - if Bearing then - System = 5 - else - System = 14 - end - - if IsValid then -- Starts the BEACON - self:T2({"AA TACAN BEACON started !"}) - self.Positionable:SetCommand({ - id = "ActivateBeacon", - params = { - type = 4, - system = System, - callsign = Message, - frequency = Frequency, - } - }) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New(nil, - function() - self:StopAATACAN() - end, {}, BeaconDuration) - end - end - - return self -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopAATACAN() - self:F() - if not self.Positionable then - self:E({"Start the beacon first before stoping it !"}) - else - self.Positionable:SetCommand({ - id = 'DeactivateBeacon', - params = { - } - }) - end -end - - ---- Activates a general pupose Radio Beacon --- This uses the very generic singleton function "trigger.action.radioTransmission()" provided by DCS to broadcast a sound file on a specific frequency. --- Although any frequency could be used, only 2 DCS Modules can home on radio beacons at the time of writing : the Huey and the Mi-8. --- They can home in on these specific frequencies : --- * **Mi8** --- * R-828 -> 20-60MHz --- * ARKUD -> 100-150MHz (canal 1 : 114166, canal 2 : 114333, canal 3 : 114583, canal 4 : 121500, canal 5 : 123100, canal 6 : 124100) AM --- * ARK9 -> 150-1300KHz --- * **Huey** --- * AN/ARC-131 -> 30-76 Mhz FM --- @param #BEACON self --- @param #string FileName The name of the audio file --- @param #number Frequency in MHz --- @param #number Modulation either radio.modulation.AM or radio.modulation.FM --- @param #number Power in W --- @param #number BeaconDuration How long will the beacon last in seconds. Omit for forever. --- @return #BEACON self --- @usage --- -- Let's create a beacon for a unit in distress. --- -- Frequency will be 40MHz FM (home-able by a Huey's AN/ARC-131) --- -- The beacon they use is battery-powered, and only lasts for 5 min --- local UnitInDistress = UNIT:FindByName("Unit1") --- local UnitBeacon = UnitInDistress:GetBeacon() --- --- -- Set the beacon and start it --- UnitBeacon:RadioBeacon("MySoundFileSOS.ogg", 40, radio.modulation.FM, 20, 5*60) -function BEACON:RadioBeacon(FileName, Frequency, Modulation, Power, BeaconDuration) - self:F({FileName, Frequency, Modulation, Power, BeaconDuration}) - local IsValid = false - - -- Check the filename - if type(FileName) == "string" then - if FileName:find(".ogg") or FileName:find(".wav") then - if not FileName:find("l10n/DEFAULT/") then - FileName = "l10n/DEFAULT/" .. FileName - end - IsValid = true - end - end - if not IsValid then - self:E({"File name invalid. Maybe something wrong with the extension ? ", FileName}) - end - - -- Check the Frequency - if type(Frequency) ~= "number" and IsValid then - self:E({"Frequency invalid. ", Frequency}) - IsValid = false - end - Frequency = Frequency * 1000000 -- Conversion to Hz - - -- Check the modulation - if Modulation ~= radio.modulation.AM and Modulation ~= radio.modulation.FM and IsValid then --TODO Maybe make this future proof if ED decides to add an other modulation ? - self:E({"Modulation is invalid. Use DCS's enum radio.modulation.", Modulation}) - IsValid = false - end - - -- Check the Power - if type(Power) ~= "number" and IsValid then - self:E({"Power is invalid. ", Power}) - IsValid = false - end - Power = math.floor(math.abs(Power)) --TODO Find what is the maximum power allowed by DCS and limit power to that - - if IsValid then - self:T2({"Activating Beacon on ", Frequency, Modulation}) - -- Note that this is looped. I have to give this transmission a unique name, I use the class ID - trigger.action.radioTransmission(FileName, self.Positionable:GetPositionVec3(), Modulation, true, Frequency, Power, tostring(self.ID)) - - if BeaconDuration then -- Schedule the stop of the BEACON if asked by the MD - SCHEDULER:New( nil, - function() - self:StopRadioBeacon() - end, {}, BeaconDuration) - end - end -end - ---- Stops the AA TACAN BEACON --- @param #BEACON self --- @return #BEACON self -function BEACON:StopRadioBeacon() - self:F() - -- The unique name of the transmission is the class ID - trigger.action.stopRadioTransmission(tostring(self.ID)) - return self -end - ---- Converts a TACAN Channel/Mode couple into a frequency in Hz --- @param #BEACON self --- @param #number TACANChannel --- @param #string TACANMode --- @return #number Frequecy --- @return #nil if parameters are invalid -function BEACON:_TACANToFrequency(TACANChannel, TACANMode) - self:F3({TACANChannel, TACANMode}) - - if type(TACANChannel) ~= "number" then - if TACANMode ~= "X" and TACANMode ~= "Y" then - return nil -- error in arguments - end - end - --- This code is largely based on ED's code, in DCS World\Scripts\World\Radio\BeaconTypes.lua, line 137. --- I have no idea what it does but it seems to work - local A = 1151 -- 'X', channel >= 64 - local B = 64 -- channel >= 64 - - if TACANChannel < 64 then - B = 1 - end - - if TACANMode == 'Y' then - A = 1025 - if TACANChannel < 64 then - A = 1088 - end - else -- 'X' - if TACANChannel < 64 then - A = 962 - end - end - - return (A + TACANChannel - B) * 1000000 -end - - diff --git a/Moose Development/Moose/Sound/RadioQueue.lua b/Moose Development/Moose/Sound/RadioQueue.lua index 1f5e9cd07..53207850f 100644 --- a/Moose Development/Moose/Sound/RadioQueue.lua +++ b/Moose Development/Moose/Sound/RadioQueue.lua @@ -77,7 +77,7 @@ RADIOQUEUE = { --- Create a new RADIOQUEUE object for a given radio frequency/modulation. -- @param #RADIOQUEUE self -- @param #number frequency The radio frequency in MHz. --- @param #number modulation (Optional) The radio modulation. Default radio.modulation.AM. +-- @param #number modulation (Optional) The radio modulation. Default `radio.modulation.AM` (=0). -- @param #string alias (Optional) Name of the radio queue. -- @return #RADIOQUEUE self The RADIOQUEUE object. function RADIOQUEUE:New(frequency, modulation, alias) @@ -245,8 +245,6 @@ end -- @return #RADIOQUEUE.Transmission Radio transmission table. function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration) - env.info("FF new transmission.") - -- Sanity checks. if not filename then self:E(self.lid.."ERROR: No filename specified.") @@ -288,7 +286,7 @@ end --- Create a new transmission and add it to the radio queue. -- @param #RADIOQUEUE self --- @param Sound.SoundFile#SOUNDFILE soundfile Sound file object to be added. +-- @param Sound.SoundOutput#SOUNDFILE soundfile Sound file object to be added. -- @param #number tstart Start time (abs) seconds. Default now. -- @param #number interval Interval in seconds after the last transmission finished. -- @return #RADIOQUEUE self diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index c4fc31c15..78cbaeaf2 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -71,7 +71,7 @@ MSRS = { modulations = {}, coalition = 0, gender = "female", - culture = "en-GB", + culture = nil, voice = nil, volume = 1, speed = 1, @@ -81,15 +81,19 @@ MSRS = { altitude = nil, } +--- Counter. +_MSRSuuid=0 + --- MSRS class version. -- @field #string version -MSRS.version="0.0.2" +MSRS.version="0.0.3" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- TODO: A lot. +-- TODO: Add functions to add/remove freqs and modulations. +-- TODO: Add coordinate. ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Constructor @@ -144,7 +148,7 @@ function MSRS:SetPath(Path) n=n+1 end - self.path=self.path.."/" + self.path=self.path --.."/" self:I(string.format("SRS path=%s", self:GetPath())) @@ -225,16 +229,40 @@ end -- @param #string Gender Gender: "male" or "female" (default). -- @return #MSRS self function MSRS:SetGender(Gender) - + self:I("Input gender to "..tostring(Gender)) + Gender=Gender or "female" - Gender=Gender:lower() + self.gender=Gender:lower() - self.gender=Gender + self:I("Setting gender to "..tostring(self.gender)) return self end +--- Set culture. +-- @param #MSRS self +-- @param #string Culture Culture, e.g. "en-GB" (default). +-- @return #MSRS self +function MSRS:SetCulture(Culture) + + self.culture=Culture + + return self +end + +--- Set to use a specific voice. Will override gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. +-- @return #MSRS self +function MSRS:SetVoice(Voice) + + self.voice=Voice + + return self +end + + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Transmission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -250,16 +278,20 @@ function MSRS:PlaySoundFile(Soundfile, Delay) self:ScheduleOnce(Delay, MSRS.PlaySoundFile, self, Soundfile, 0) else + -- Sound file name. local soundfile=Soundfile:GetName() + -- Get command. local command=self:_GetCommand() + -- Append file. command=command.." --file="..tostring(soundfile) - env.info(string.format("FF PlaySoundfile command=%s", command)) + -- Debug output. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) -- Execute SRS command. - os.execute(command) + local x=os.execute(command) end @@ -277,14 +309,17 @@ function MSRS:PlaySoundText(SoundText, Delay) self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) else + -- Get command. local command=self:_GetCommand(nil, nil, nil, SoundText.gender, SoundText.voice, SoundText.culture, SoundText.volume, SoundText.speed) + -- Append text. command=command..string.format(" --text=\"%s\"", tostring(SoundText.text)) - env.info(string.format("FF PlaySoundfile command=%s", command)) + -- Debug putput. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) -- Execute SRS command. - os.execute(command) + local x=os.execute(command) end @@ -302,17 +337,79 @@ function MSRS:PlayText(Text, Delay) self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, 0) else - local text=string.format("\"%s\"", Text) - + -- Get command line. local command=self:_GetCommand() + -- Append text. command=command..string.format(" --text=\"%s\"", tostring(Text)) - env.info(string.format("FF Text command=%s", command)) + -- Check that length of command is max 255 chars or os.execute() will not work! + if string.len(command)>255 then + + -- Create a tmp file. + local filename = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".bat" + + local script = io.open(filename, "w+") + script:write(command.." && exit") + script:close() + + -- Play command. + command=string.format("\"%s\"", filename) + + -- Play file in 0.05 seconds + timer.scheduleFunction(os.execute, command, timer.getTime()+0.05) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + else + + -- Debug output. + self:I(string.format("MSRS Text command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + end + + + end + + return self +end + + +--- Play text file via STTS. +-- @param #MSRS self +-- @param #string TextFile Full path to the file. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayTextFile(TextFile, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayTextFile, self, TextFile, 0) + else + + -- First check if text file exists! + local exists=UTILS.FileExists(TextFile) + if not exists then + self:E("ERROR: MSRS Text file does not exist! File="..tostring(TextFile)) + return self + end + + -- Get command line. + local command=self:_GetCommand() + + -- Append text file. + command=command..string.format(" --textFile=\"%s\"", tostring(TextFile)) + + -- Debug output. + self:I(string.format("MSRS TextFile command=%s", command)) + + -- Count length of command. + local l=string.len(command) -- Execute SRS command. local x=os.execute(command) - env.info(x) end @@ -338,8 +435,8 @@ end -- @return #string Command. function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port) - - local exe=self:GetPath().."DCS-SR-ExternalAudio.exe" + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" freqs=table.concat(freqs or self.frequencies, ",") modus=table.concat(modus or self.modulations, ",") coal=coal or self.coalition @@ -348,19 +445,34 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp culture=culture or self.culture volume=volume or self.volume speed=speed or self.speed - port=port or self.port + port=port or self.port - local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --gender=%s --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, gender, volume, speed) + env.info("FF gender="..tostring(gender)) + env.info("FF gender="..tostring(self.gender)) + -- This did not work well. Stopped if the transmission was a bit longer with no apparent error. + --local command=string.format("%s --freqs=%s --modulations=%s --coalition=%d --port=%d --volume=%.2f --speed=%d", exe, freqs, modus, coal, port, volume, speed) + + -- Command from orig STTS script. Works better for some unknown reason! + local command=string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\"", path, exe, freqs, modus, coal, port, "ROBOT") + + -- Set voice or gender/culture. if voice then + -- Use a specific voice (no need for gender and/or culture. command=command..string.format(" --voice=\"%s\"", tostring(voice)) + else + -- Add gender. + if gender and gender~="female" then + command=command..string.format(" --gender=%s", tostring(gender)) + end + -- Add culture. + if culture and culture~="en-GB" then + command=command..string.format(" -l %s", tostring(culture)) + end end - if culture then - command=command.." --culture="..tostring(culture) - end - - env.info("FF command="..command) + -- Debug output. + self:I("MSRS command="..command) return command end diff --git a/Moose Development/Moose/Sound/SoundFile.lua b/Moose Development/Moose/Sound/SoundOutput.lua similarity index 72% rename from Moose Development/Moose/Sound/SoundFile.lua rename to Moose Development/Moose/Sound/SoundOutput.lua index 884eeab6a..697d63941 100644 --- a/Moose Development/Moose/Sound/SoundFile.lua +++ b/Moose Development/Moose/Sound/SoundOutput.lua @@ -4,7 +4,8 @@ -- -- ## Features: -- --- * Add a sound file to the +-- * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions +-- * Create a SOUNDTEXT object for text-to-speech output -- -- === -- @@ -12,22 +13,19 @@ -- -- === -- --- @module Sound.Soundfile --- @image Sound_Soundfile.png --- +-- @module Sound.SoundOutput +-- @image Sound_SoundOutput.png do -- Sound Base --- @type SOUNDBASE - -- @field #string ClassName Name of the class + -- @field #string ClassName Name of the class. -- @extends Core.Base#BASE - --- Sound files used by other classes. + --- Basic sound output inherited by other classes. -- - -- # 1. USERFLAG constructor - -- - -- * @{#USERFLAG.New}(): Creates a new USERFLAG object. + -- This class is **not** meant to be used by "ordinary" users. -- -- @field #SOUNDBASE SOUNDBASE={ @@ -40,7 +38,7 @@ do -- Sound Base function SOUNDBASE:New() -- Inherit BASE. - local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE @@ -94,10 +92,11 @@ do -- Sound File -- Set file name. self:SetFileName(FileName) - -- Set path + -- Set path. self:SetPath(Path) - self.duration=Duration or 3 + -- Set duration. + self:SetDuration(Duration) -- Debug info: self:I(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path)) @@ -137,11 +136,11 @@ do -- Sound File --- Set sound file name. This must be a .ogg or .mp3 file! -- @param #SOUNDFILE self - -- @param #string FileName Name of the file. + -- @param #string FileName Name of the file. Default is "Hello World.mp3". -- @return #SOUNDFILE self function SOUNDFILE:SetFileName(FileName) --TODO: check that sound file is really .ogg or .mp3 - self.filename=FileName or "HelloWorld.mp3" + self.filename=FileName or "Hello World.mp3" return self end @@ -187,17 +186,27 @@ do -- Text-To-Speech -- @field #string ClassName Name of the class -- @field #string text Text to speak. -- @field #number duration Duration in seconds. - -- @field #string gender Gender. - -- @field #string voice Voice. - -- @field #string culture Culture. + -- @field #string gender Gender: "male", "female". + -- @field #string culture Culture, e.g. "en-GB". + -- @field #string voice Specific voice to use. Overrules `gender` and `culture` settings. -- @extends Core.Base#BASE - --- Sound files used by other classes. + --- Text-to-speech objects for other classes. -- - -- # 1. USERFLAG constructor + -- # Constructor -- - -- * @{#USERFLAG.New}(): Creates a new USERFLAG object. + -- * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object. + -- + -- Name: Microsoft Hazel Desktop, Culture: en-GB, Gender: Female, Age: Adult, Desc: Microsoft Hazel Desktop - English (Great Britain) + -- Name: Microsoft David Desktop, Culture: en-US, Gender: Male, Age: Adult, Desc: Microsoft David Desktop - English (United States) + -- Name: Microsoft Zira Desktop, Culture: en-US, Gender: Female, Age: Adult, Desc: Microsoft Zira Desktop - English (United States) + -- Name: Microsoft Hedda Desktop, Culture: de-DE, Gender: Female, Age: Adult, Desc: Microsoft Hedda Desktop - German + -- Name: Microsoft Helena Desktop, Culture: es-ES, Gender: Female, Age: Adult, Desc: Microsoft Helena Desktop - Spanish (Spain) + -- Name: Microsoft Hortense Desktop, Culture: fr-FR, Gender: Female, Age: Adult, Desc: Microsoft Hortense Desktop - French + -- Name: Microsoft Elsa Desktop, Culture: it-IT, Gender: Female, Age: Adult, Desc: Microsoft Elsa Desktop - Italian (Italy) + -- Name: Microsoft Irina Desktop, Culture: ru-RU, Gender: Female, Age: Adult, Desc: Microsoft Irina Desktop - Russian + -- Name: Microsoft Huihui Desktop, Culture: zh-CN, Gender: Female, Age: Adult, Desc: Microsoft Huihui Desktop - Chinese (Simplified) -- -- @field #SOUNDTEXT SOUNDTEXT={ @@ -216,8 +225,8 @@ do -- Text-To-Speech self:SetText(Text) self:SetDuration(Duration) - self:SetGender() - self:SetCulture() + --self:SetGender() + --self:SetCulture() -- Debug info: self:I(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration)) @@ -258,9 +267,10 @@ do -- Text-To-Speech return self end - --- Set the voice name. See the list from --help or if using google see: https://cloud.google.com/text-to-speech/docs/voices + --- Set to use a specific voice name. + -- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see https://cloud.google.com/text-to-speech/docs/voices -- @param #SOUNDTEXT self - -- @param #string Voice + -- @param #string Voice Voice name. Note that this will overrule `Gender` and `Culture`. -- @return #SOUNDTEXT self function SOUNDTEXT:SetVoice(Voice) diff --git a/Moose Development/Moose/Utilities/STTS.lua b/Moose Development/Moose/Utilities/STTS.lua new file mode 100644 index 000000000..97836a1ee --- /dev/null +++ b/Moose Development/Moose/Utilities/STTS.lua @@ -0,0 +1,256 @@ +--- **Utilities** DCS Simple Text-To-Speech (STTS). +-- +-- +-- +-- @module Utils.STTS +-- @image MOOSE.JPG + +--- [DCS Enum world](https://wiki.hoggitworld.com/view/DCS_enum_world) +-- @type STTS +-- @field #string DIRECTORY Path of the SRS directory. + +--- Simple Text-To-Speech +-- +-- Version 0.4 - Compatible with SRS version 1.9.6.0+ +-- +-- # DCS Modification Required +-- +-- You will need to edit MissionScripting.lua in DCS World/Scripts/MissionScripting.lua and remove the sanitisation. +-- To do this remove all the code below the comment - the line starts "local function sanitizeModule(name)" +-- Do this without DCS running to allow mission scripts to use os functions. +-- +-- *You WILL HAVE TO REAPPLY AFTER EVERY DCS UPDATE* +-- +-- # USAGE: +-- +-- Add this script into the mission as a DO SCRIPT or DO SCRIPT FROM FILE to initialise it +-- Make sure to edit the STTS.SRS_PORT and STTS.DIRECTORY to the correct values before adding to the mission. +-- Then its as simple as calling the correct function in LUA as a DO SCRIPT or in your own scripts. +-- +-- Example calls: +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2) +-- +-- Arguments in order are: +-- +-- * Message to say, make sure not to use a newline (\n) ! +-- * Frequency in MHz +-- * Modulation - AM/FM +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- * OPTIONAL - Vec3 Point i.e Unit.getByName("A UNIT"):getPoint() - needs Vec3 for Height! OR null if not needed +-- * OPTIONAL - Speed -10 to +10 +-- * OPTIONAL - Gender male, female or neuter +-- * OPTIONAL - Culture - en-US, en-GB etc +-- * OPTIONAL - Voice - a specfic voice by name. Run DCS-SR-ExternalAudio.exe with --help to get the ones you can use on the command line +-- * OPTIONAL - Google TTS - Switch to Google Text To Speech - Requires STTS.GOOGLE_CREDENTIALS path and Google project setup correctly +-- +-- +-- ## Example +-- +-- This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,null,-5,"male","en-GB") +-- +-- ## Example +-- +--This example will say the words "Hello DCS WORLD" on 251 MHz AM at maximum volume with a client called SRS and to the Blue coalition only centered on the position of the Unit called "A UNIT" +-- +-- STTS.TextToSpeech("Hello DCS WORLD","251","AM","1.0","SRS",2,Unit.getByName("A UNIT"):getPoint(),-5,"male","en-GB") +-- +-- Arguments in order are: +-- +-- * FULL path to the MP3 OR OGG to play +-- * Frequency in MHz - to use multiple separate with a comma - Number of frequencies MUST match number of Modulations +-- * Modulation - AM/FM - to use multiple +-- * Volume - 1.0 max, 0.5 half +-- * Name of the transmitter - ATC, RockFM etc +-- * Coalition - 0 spectator, 1 red 2 blue +-- +-- ## Example +-- +-- This will play that MP3 on 255MHz AM & 31 FM at half volume with a client called "Multiple" and to Spectators only +-- +-- STTS.PlayMP3("C:\\Users\\Ciaran\\Downloads\\PR-Music.mp3","255,31","AM,FM","0.5","Multiple",0) +-- +-- @field #STTS +STTS={ + ClassName="STTS", + DIRECTORY="", + SRS_PORT=5002, + GOOGLE_CREDENTIALS="C:\\Users\\Ciaran\\Downloads\\googletts.json", + EXECUTABLE="DCS-SR-ExternalAudio.exe", +} + +--- FULL Path to the FOLDER containing DCS-SR-ExternalAudio.exe - EDIT TO CORRECT FOLDER +STTS.DIRECTORY = "D:/DCS/_SRS" + +--- LOCAL SRS PORT - DEFAULT IS 5002 +STTS.SRS_PORT = 5002 + +--- Google credentials file +STTS.GOOGLE_CREDENTIALS = "C:\\Users\\Ciaran\\Downloads\\googletts.json" + +--- DONT CHANGE THIS UNLESS YOU KNOW WHAT YOU'RE DOING +STTS.EXECUTABLE = "DCS-SR-ExternalAudio.exe" + + +--- Function for UUID. +function STTS.uuid() + local random = math.random + local template ='yxxx-xxxxxxxxxxxx' + return string.gsub(template, '[xy]', function (c) + local v = (c == 'x') and random(0, 0xf) or random(8, 0xb) + return string.format('%x', v) + end) +end + +--- Round a number. +-- @param #number x Number. +-- @param #number n Precision. +function STTS.round(x, n) + n = math.pow(10, n or 0) + x = x * n + if x >= 0 then x = math.floor(x + 0.5) else x = math.ceil(x - 0.5) end + return x / n +end + +--- Function returns estimated speech time in seconds. +-- Assumptions for time calc: 100 Words per min, avarage of 5 letters for english word so +-- +-- * 5 chars * 100wpm = 500 characters per min = 8.3 chars per second +-- +-- So lengh of msg / 8.3 = number of seconds needed to read it. rounded down to 8 chars per sec map function: +-- +-- * (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min +-- +function STTS.getSpeechTime(length,speed,isGoogle) + + local maxRateRatio = 3 + + speed = speed or 1.0 + isGoogle = isGoogle or false + + local speedFactor = 1.0 + if isGoogle then + speedFactor = speed + else + if speed ~= 0 then + speedFactor = math.abs(speed) * (maxRateRatio - 1) / 10 + 1 + end + if speed < 0 then + speedFactor = 1/speedFactor + end + end + + local wpm = math.ceil(100 * speedFactor) + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) +end + +--- Text to speech function. +function STTS.TextToSpeech(message, freqs, modulations, volume, name, coalition, point, speed, gender, culture, voice, googleTTS) + if os == nil or io == nil then + env.info("[DCS-STTS] LUA modules os or io are sanitized. skipping. ") + return + end + + speed = speed or 1 + gender = gender or "female" + culture = culture or "" + voice = voice or "" + coalition=coalition or "0" + name=name or "ROBOT" + volume=1 + speed=1 + + + message = message:gsub("\"","\\\"") + + local cmd = string.format("start /min \"\" /d \"%s\" /b \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -h", STTS.DIRECTORY, STTS.EXECUTABLE, freqs or "305", modulations or "AM", coalition, STTS.SRS_PORT, name) + + if voice ~= "" then + cmd = cmd .. string.format(" -V \"%s\"",voice) + else + + if culture ~= "" then + cmd = cmd .. string.format(" -l %s",culture) + end + + if gender ~= "" then + cmd = cmd .. string.format(" -g %s",gender) + end + end + + if googleTTS == true then + cmd = cmd .. string.format(" -G \"%s\"",STTS.GOOGLE_CREDENTIALS) + end + + if speed ~= 1 then + cmd = cmd .. string.format(" -s %s",speed) + end + + if volume ~= 1.0 then + cmd = cmd .. string.format(" -v %s",volume) + end + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + cmd = cmd ..string.format(" -t \"%s\"",message) + + if string.len(cmd) > 255 then + local filename = os.getenv('TMP') .. "\\DCS_STTS-" .. STTS.uuid() .. ".bat" + local script = io.open(filename,"w+") + script:write(cmd .. " && exit" ) + script:close() + cmd = string.format("\"%s\"",filename) + timer.scheduleFunction(os.remove, filename, timer.getTime() + 1) + end + + if string.len(cmd) > 255 then + env.info("[DCS-STTS] - cmd string too long") + env.info("[DCS-STTS] TextToSpeech Command :\n" .. cmd.."\n") + end + os.execute(cmd) + + return STTS.getSpeechTime(message,speed,googleTTS) +end + +--- Play mp3 function. +-- @param #string pathToMP3 Path to the sound file. +-- @param #string freqs Frequencies, e.g. "305, 256". +-- @param #string modulations Modulations, e.g. "AM, FM". +-- @param #string volume Volume, e.g. "0.5". +function STTS.PlayMP3(pathToMP3, freqs, modulations, volume, name, coalition, point) + + local cmd = string.format("start \"\" /d \"%s\" /b /min \"%s\" -i \"%s\" -f %s -m %s -c %s -p %s -n \"%s\" -v %s -h", + STTS.DIRECTORY, STTS.EXECUTABLE, pathToMP3, freqs or "305", modulations or "AM", coalition or "0", STTS.SRS_PORT, name or "ROBOT", volume or "1") + + if point and type(point) == "table" and point.x then + local lat, lon, alt = coord.LOtoLL(point) + + lat = STTS.round(lat,4) + lon = STTS.round(lon,4) + alt = math.floor(alt) + + cmd = cmd .. string.format(" -L %s -O %s -A %s",lat,lon,alt) + end + + env.info("[DCS-STTS] MP3/OGG Command :\n" .. cmd.."\n") + os.execute(cmd) + +end \ No newline at end of file diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index 939bf7770..0bdf83161 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -4,8 +4,10 @@ Utilities/Utils.lua Utilities/Enums.lua Utilities/Profiler.lua Utilities/Templates.lua +Utilities/STTS.lua Core/Base.lua +Core/Beacon.lua Core/UserFlag.lua Core/Report.lua Core/Scheduler.lua @@ -111,7 +113,7 @@ Actions/Act_Account.lua Actions/Act_Assist.lua Sound/UserSound.lua -Sound/SoundFile.lua +Sound/SoundOutput.lua Sound/Radio.lua Sound/RadioQueue.lua Sound/RadioSpeech.lua