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/Functional/RAT.lua b/Moose Development/Moose/Functional/RAT.lua index 327e2e518..691f53338 100644 --- a/Moose Development/Moose/Functional/RAT.lua +++ b/Moose Development/Moose/Functional/RAT.lua @@ -5786,6 +5786,8 @@ end -- If desired, the @{#RATMANAGER} can be stopped by the @{#RATMANAGER.Stop}(stoptime) function. The parameter "stoptime" specifies the time delay in seconds after which the manager stops. -- When this happens, no new aircraft will be spawned and the population will eventually decrease to zero. -- +-- When you are using a time intervall like @{#RATMANAGER.dTspawn}(delay), @{#RATMANAGER} will ignore the amount set with @{#RATMANAGER.New}(). @{#RATMANAGER.dTspawn}(delay) will spawn infinite groups. +-- -- ## Example -- In this example, three different @{#RAT} objects are created (but not spawned manually). The @{#RATMANAGER} takes care that at least five aircraft of each type are alive and that the total number of aircraft -- spawned is 25. The @{#RATMANAGER} is started after 30 seconds and stopped after two hours. diff --git a/Moose Development/Moose/Functional/Range.lua b/Moose Development/Moose/Functional/Range.lua index fc850743c..7ba9dcfc7 100644 --- a/Moose Development/Moose/Functional/Range.lua +++ b/Moose Development/Moose/Functional/Range.lua @@ -36,7 +36,7 @@ -- -- === -- --- ## Sound files: Check out the pinned messages in the Moose discord *#func-range* channel. +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) -- -- === -- @@ -91,9 +91,9 @@ -- @field #boolean defaultsmokebomb If true, initialize player settings to smoke bomb. -- @field #boolean autosave If true, automatically save results every X seconds. -- @field #number instructorfreq Frequency on which the range control transmitts. --- @field Core.RadioQueue#RADIOQUEUE instructor Instructor radio queue. +-- @field Sound.RadioQueue#RADIOQUEUE instructor Instructor radio queue. -- @field #number rangecontrolfreq Frequency on which the range control transmitts. --- @field Core.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. +-- @field Sound.RadioQueue#RADIOQUEUE rangecontrol Range control radio queue. -- @field #string rangecontrolrelayname Name of relay unit. -- @field #string instructorrelayname Name of relay unit. -- @field #string soundpath Path inside miz file where the sound files are located. Default is "Range Soundfiles/". diff --git a/Moose Development/Moose/Globals.lua b/Moose Development/Moose/Globals.lua index d972cf38b..21d0c7eaa 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,10 +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() _DATABASE:_RegisterAirbases() +--- Check if os etc is available. +BASE:I("Checking de-sanitization of os, io and lfs:") +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 comment 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 0774e005e..ff11712bf 100644 --- a/Moose Development/Moose/Modules.lua +++ b/Moose Development/Moose/Modules.lua @@ -3,10 +3,11 @@ __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/UserSound.lua' ) __Moose.Include( 'Scripts/Moose/Core/Report.lua' ) __Moose.Include( 'Scripts/Moose/Core/Scheduler.lua' ) __Moose.Include( 'Scripts/Moose/Core/ScheduleDispatcher.lua' ) @@ -21,9 +22,6 @@ __Moose.Include( 'Scripts/Moose/Core/Point.lua' ) __Moose.Include( 'Scripts/Moose/Core/Velocity.lua' ) __Moose.Include( 'Scripts/Moose/Core/Message.lua' ) __Moose.Include( 'Scripts/Moose/Core/Fsm.lua' ) -__Moose.Include( 'Scripts/Moose/Core/Radio.lua' ) -__Moose.Include( 'Scripts/Moose/Core/RadioQueue.lua' ) -__Moose.Include( 'Scripts/Moose/Core/RadioSpeech.lua' ) __Moose.Include( 'Scripts/Moose/Core/Spawn.lua' ) __Moose.Include( 'Scripts/Moose/Core/SpawnStatic.lua' ) __Moose.Include( 'Scripts/Moose/Core/Timer.lua' ) @@ -124,6 +122,13 @@ __Moose.Include( 'Scripts/Moose/Actions/Act_Route.lua' ) __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/SoundOutput.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/Radio.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/RadioQueue.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/RadioSpeech.lua' ) +__Moose.Include( 'Scripts/Moose/Sound/SRS.lua' ) + __Moose.Include( 'Scripts/Moose/Tasking/CommandCenter.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Mission.lua' ) __Moose.Include( 'Scripts/Moose/Tasking/Task.lua' ) diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 91333138d..368d3c488 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -18,6 +18,7 @@ -- * Option to present information in imperial or metric units -- * Runway length and airfield elevation (optional) -- * Frequencies/channels of nav aids (ILS, VOR, NDB, TACAN, PRMG, RSBN) (optional) +-- * SRS Simple-Text-To-Speech (STTS) integration (no sound files necessary) -- -- === -- @@ -34,7 +35,7 @@ -- -- === -- --- ## Sound files: Check out the pinned messages in the Moose discord #ops-atis channel. +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) -- -- === -- @@ -59,7 +60,7 @@ -- @field #number frequency Radio frequency in MHz. -- @field #number modulation Radio modulation 0=AM or 1=FM. -- @field #number power Radio power in Watts. Default 100 W. --- @field Core.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. +-- @field Sound.RadioQueue#RADIOQUEUE radioqueue Radio queue for broadcasing messages. -- @field #string soundpath Path to sound files. -- @field #string relayunitname Name of the radio relay unit. -- @field #table towerfrequency Table with tower frequencies. @@ -88,6 +89,9 @@ -- @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. +-- @field #number dTQueueCheck Time interval to check the radio queue. Default 5 sec or 90 sec if SRS is used. -- @extends Core.Fsm#FSM --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -252,6 +256,16 @@ -- # Marks on the F10 Map -- -- You can place marks on the F10 map via the @{#ATIS.SetMapMarks}() function. These will contain info about the ATIS frequency, the currently active runway and some basic info about the weather (wind, pressure and temperature). +-- +-- # Text-To-Speech +-- +-- You can enable text-to-speech ATIS information with the @{#ATIS.SetSRS}() function. This uses [SRS](http://dcssimpleradio.com/) (Version >= 1.9.6.0) for broadcasing. +-- Advantages are that **no sound files** or radio relay units are necessary. Also the issue that FC3 aircraft hear all transmissions will be circumvented. +-- +-- The @{#ATIS.SetSRS}() requires you to specify the path to the SRS install directory or more specifically the path to the DCS-SR-ExternalAudio.exe file. +-- +-- Unfortunately, it is not possible to determine the duration of the complete transmission. So once the transmission is finished, there might be some radio silence before +-- the next iteration begins. You can fine tune the time interval between transmissions with the @{#ATIS.SetQueueUpdateTime}() function. The default interval is 90 seconds. -- -- # Examples -- @@ -283,7 +297,14 @@ -- atisAbuDhabi:SetTowerFrequencies({250.5, 119.2}) -- atisAbuDhabi:SetVOR(114.25) -- atisAbuDhabi:Start() +-- +-- ## SRS +-- +-- atis=ATIS:New("Batumi", 305, radio.modulation.AM) +-- atis:SetSRS("D:\\DCS\\_SRS\\", "male", "en-US") +-- atis:Start() -- +-- This uses a male voice with US accent. It requires SRS to be installed in the `D:\DCS\_SRS\` directory. Not that backslashes need to be escaped or simply use slashes (as in linux). -- -- @field #ATIS ATIS = { @@ -368,6 +389,7 @@ ATIS.Alphabet = { -- @field #number PersianGulf +2° (East). -- @field #number TheChannel -10° (West). -- @field #number Syria +5° (East). +-- @field #number MarianaIslands +2° (East). ATIS.RunwayM2T={ Caucasus=0, Nevada=12, @@ -375,6 +397,7 @@ ATIS.RunwayM2T={ PersianGulf=2, TheChannel=-10, Syria=5, + MarianaIslands=2, } --- Whether ICAO phraseology is used for ATIS broadcasts. @@ -385,6 +408,7 @@ ATIS.RunwayM2T={ -- @field #boolean PersianGulf true. -- @field #boolean TheChannel true. -- @field #boolean Syria true. +-- @field #boolean MarianaIslands true. ATIS.ICAOPhraseology={ Caucasus=true, Nevada=false, @@ -392,6 +416,7 @@ ATIS.ICAOPhraseology={ PersianGulf=true, TheChannel=true, Syria=true, + MarianaIslands=true, } --- Nav point data. @@ -564,7 +589,7 @@ _ATIS={} --- ATIS class version. -- @field #string version -ATIS.version="0.9.1" +ATIS.version="0.9.5" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -628,6 +653,7 @@ function ATIS:New(airbasename, frequency, modulation) self:SetAltimeterQNH(true) self:SetMapMarks(false) self:SetRelativeHumidity() + self:SetQueueUpdateTime() -- Start State. self:SetStartState("Stopped") @@ -955,7 +981,9 @@ end -- * 170° on the Normany map -- * 182° on the Persian Gulf map -- --- Likewise, to convert *magnetic* into *true* heading, one has to substract easterly and add westerly variation. +-- Likewise, to convert *true* into *magnetic* heading, one has to substract easterly and add westerly variation. +-- +-- Or you make your life simple and just include the sign so you don't have to bother about East/West. -- -- @param #ATIS self -- @param #number magvar Magnetic variation in degrees. Positive for easterly and negative for westerly variation. Default is magnatic declinaton of the used map, c.f. @{Utilities.UTils#UTILS.GetMagneticDeclination}. @@ -1100,6 +1128,35 @@ 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`. +-- @param #number Port SRS port. Default 5002. +-- @return #ATIS self +function ATIS:SetSRS(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) + self.msrs:SetPort(Port) + if self.dTQueueCheck<=10 then + self:SetQueueUpdateTime(90) + end + return self +end + +--- Set the time interval between radio queue updates. +-- @param #ATIS self +-- @param #number TimeInterval Interval in seconds. Default 5 sec. +-- @return #ATIS self +function ATIS:SetQueueUpdateTime(TimeInterval) + self.dTQueueCheck=TimeInterval or 5 +end + ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Start & Status ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -1171,7 +1228,13 @@ function ATIS:onafterStatus(From, Event, To) end -- Info text. - local text=string.format("State %s: Freq=%.3f MHz %s, Relay unit=%s (alive=%s)", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation), tostring(self.relayunitname), relayunitstatus) + local text=string.format("State %s: Freq=%.3f MHz %s", fsmstate, self.frequency, UTILS.GetModulationName(self.modulation)) + if self.useSRS then + text=text..string.format(", SRS path=%s (%s), gender=%s, culture=%s, voice=%s", + tostring(self.msrs.path), tostring(self.msrs.port), tostring(self.msrs.gender), tostring(self.msrs.culture), tostring(self.msrs.voice)) + else + text=text..string.format(", Relay unit=%s (alive=%s)", tostring(self.relayunitname), relayunitstatus) + end self:I(self.lid..text) self:__Status(-60) @@ -1188,15 +1251,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() + 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 + + + end - -- Check back in 5 seconds. - self:__CheckQueue(-5) + -- Check back in 5 seconds. + self:__CheckQueue(-math.abs(self.dTQueueCheck)) end --- Broadcast ATIS radio message. @@ -1322,11 +1395,14 @@ function ATIS:onafterBroadcast(From, Event, To) if time < 0 then time = 24*60*60 + time --avoid negative time around midnight - end + end local clock=UTILS.SecondsToClock(time) local zulu=UTILS.Split(clock, ":") local ZULU=string.format("%s%s", zulu[1], zulu[2]) + if self.useSRS then + ZULU=string.format("%s hours", zulu[1]) + end -- NATO time stamp. 0=Alfa, 1=Bravo, 2=Charlie, etc. @@ -1346,10 +1422,17 @@ function ATIS:onafterBroadcast(From, Event, To) local sunrise=coord:GetSunrise() sunrise=UTILS.Split(sunrise, ":") local SUNRISE=string.format("%s%s", sunrise[1], sunrise[2]) + if self.useSRS then + SUNRISE=string.format("%s %s hours", sunrise[1], sunrise[2]) + end local sunset=coord:GetSunset() sunset=UTILS.Split(sunset, ":") local SUNSET=string.format("%s%s", sunset[1], sunset[2]) + if self.useSRS then + SUNSET=string.format("%s %s hours", sunset[1], sunset[2]) + end + --------------------------------- --- Temperature and Dew Point --- @@ -1591,36 +1674,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 +1727,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 +1749,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 +1796,67 @@ 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 + local cbase=tostring(tonumber(CLOUDBASE1000)*1000+tonumber(CLOUDBASE0100)*100) + local cceil=tostring(tonumber(CLOUDCEIL1000)*1000+tonumber(CLOUDCEIL0100)*100) 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) + subtitle=string.format("Cloud base %s, ceiling %s meters", cbase, cceil) else - subtitle=string.format("Cloudbase %s, ceiling %s ft", CLOUDBASE, CLOUDCEIL) + --subtitle=string.format("Cloud base %s, ceiling %s feet", CLOUDBASE, CLOUDCEIL) + subtitle=string.format("Cloud base %s, ceiling %s feet", cbase, cceil) 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 +1876,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 +1905,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 +1924,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 +1983,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 +2015,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 +2053,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 +2083,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 +2132,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 +2139,58 @@ 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 self.useSRS then + subtitle=string.format("V O R frequency %.2f MHz", self.vor) + end + 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 +2198,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. @@ -2102,6 +2231,32 @@ end -- @param #string Text Report text. function ATIS:onafterReport(From, Event, To, Text) self:T(self.lid..string.format("Report:\n%s", Text)) + + if self.useSRS and self.msrs then + + -- 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, ";", " . ") + + --Debug output. + self:T("SRS TTS: "..text) + + -- Play text-to-speech report. + self.msrs:PlayText(text) + + end + end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Core/Radio.lua b/Moose Development/Moose/Sound/Radio.lua similarity index 50% rename from Moose Development/Moose/Core/Radio.lua rename to Moose Development/Moose/Sound/Radio.lua index 175bc2106..872ad6e26 100644 --- a/Moose Development/Moose/Core/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/Core/RadioQueue.lua b/Moose Development/Moose/Sound/RadioQueue.lua similarity index 84% rename from Moose Development/Moose/Core/RadioQueue.lua rename to Moose Development/Moose/Sound/RadioQueue.lua index a63677a98..80ac49752 100644 --- a/Moose Development/Moose/Core/RadioQueue.lua +++ b/Moose Development/Moose/Sound/RadioQueue.lua @@ -1,20 +1,24 @@ ---- **Core** - Queues Radio Transmissions. +--- **Sound** - Queues Radio Transmissions. -- -- === -- -- ## Features: -- --- * Managed Radio Transmissions. +-- * Manage Radio Transmissions -- -- === -- -- ### Authors: funkyfranky -- --- @module Core.RadioQueue +-- @module Sound.RadioQueue -- @image Core_Radio.JPG --- Manages radio transmissions. -- +-- The main goal of the RADIOQUEUE class is to string together multiple sound files to play a complete sentence. +-- The underlying problem is that radio transmissions in DCS are not queued but played "on top" of each other. +-- Therefore, to achive the goal, it is vital to know the precise duration how long it takes to play the sound file. +-- -- @type RADIOQUEUE -- @field #string ClassName Name of the class "RADIOQUEUE". -- @field #boolean Debugmode Debug mode. More info. @@ -35,6 +39,7 @@ -- @field #table numbers Table of number transmission parameters. -- @field #boolean checking Scheduler is checking the radio queue. -- @field #boolean schedonce Call ScheduleOnce instead of normal scheduler. +-- @field Sound.SRS#MSRS msrs Moose SRS class. -- @extends Core.Base#BASE RADIOQUEUE = { ClassName = "RADIOQUEUE", @@ -69,12 +74,14 @@ RADIOQUEUE = { -- @field #boolean isplaying If true, transmission is currently playing. -- @field #number Tplay Mission time (abs) in seconds when the transmission should be played. -- @field #number interval Interval in seconds before next transmission. +-- @field Sound.SoundOutput#SOUNDFILE soundfile Sound file object to play via SRS. +-- @field Sound.SoundOutput#SOUNDTEXT soundtext Sound TTS object to play via SRS. --- 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) @@ -125,9 +132,9 @@ function RADIOQUEUE:Start(delay, dt) -- Start Scheduler. if self.schedonce then - self:_CheckRadioQueueDelayed(delay) + self:_CheckRadioQueueDelayed(self.delay) else - self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, delay, dt) + self.RQid=self.scheduler:Schedule(nil, RADIOQUEUE._CheckRadioQueue, {self}, self.delay, self.dt) end return self @@ -170,6 +177,17 @@ function RADIOQUEUE:SetRadioPower(power) return self end +--- Set SRS. +-- @param #RADIOQUEUE self +-- @param #string PathToSRS Path to SRS. +-- @param #number Port SRS port. Default 5002. +-- @return #RADIOQUEUE self The RADIOQUEUE object. +function RADIOQUEUE:SetSRS(PathToSRS, Port) + self.msrs=MSRS:New(PathToSRS, self.frequency/1000000, self.modulation) + self.msrs:SetPort(Port) + return self +end + --- Set parameters of a digit. -- @param #RADIOQUEUE self -- @param #number digit The digit 0-9. @@ -202,7 +220,7 @@ end --- Add a transmission to the radio queue. -- @param #RADIOQUEUE self -- @param #RADIOQUEUE.Transmission transmission The transmission data table. --- @return #RADIOQUEUE self The RADIOQUEUE object. +-- @return #RADIOQUEUE self function RADIOQUEUE:AddTransmission(transmission) self:F({transmission=transmission}) @@ -221,7 +239,7 @@ function RADIOQUEUE:AddTransmission(transmission) return self end ---- Add a transmission to the radio queue. +--- Create a new transmission and add it to the radio queue. -- @param #RADIOQUEUE self -- @param #string filename Name of the sound file. Usually an ogg or wav file type. -- @param #number duration Duration in seconds the file lasts. @@ -230,7 +248,7 @@ end -- @param #number interval Interval in seconds after the last transmission finished. -- @param #string subtitle Subtitle of the transmission. -- @param #number subduration Duration [sec] of the subtitle being displayed. Default 5 sec. --- @return #RADIOQUEUE self The RADIOQUEUE object. +-- @return #RADIOQUEUE.Transmission Radio transmission table. function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, subtitle, subduration) -- Sanity checks. @@ -269,9 +287,36 @@ function RADIOQUEUE:NewTransmission(filename, duration, path, tstart, interval, -- Add transmission to queue. self:AddTransmission(transmission) + return transmission +end + +--- Add a SOUNDFILE to the radio queue. +-- @param #RADIOQUEUE self +-- @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 +function RADIOQUEUE:AddSoundFile(soundfile, tstart, interval) + --env.info(string.format("FF add soundfile: name=%s%s", soundfile:GetPath(), soundfile:GetFileName())) + local transmission=self:NewTransmission(soundfile:GetFileName(), soundfile.duration, soundfile:GetPath(), tstart, interval, soundfile.subtitle, soundfile.subduration) + transmission.soundfile=soundfile return self end +--- Add a SOUNDTEXT to the radio queue. +-- @param #RADIOQUEUE self +-- @param Sound.SoundOutput#SOUNDTEXT soundtext Text-to-speech text. +-- @param #number tstart Start time (abs) seconds. Default now. +-- @param #number interval Interval in seconds after the last transmission finished. +-- @return #RADIOQUEUE self +function RADIOQUEUE:AddSoundText(soundtext, tstart, interval) + + local transmission=self:NewTransmission("SoundText.ogg", soundtext.duration, nil, tstart, interval, soundtext.subtitle, soundtext.subduration) + transmission.soundtext=soundtext + return self +end + + --- Convert a number (as string) into a radio transmission. -- E.g. for board number or headings. -- @param #RADIOQUEUE self @@ -280,19 +325,9 @@ end -- @param #number interval Interval between the next call. -- @return #number Duration of the call in seconds. function RADIOQUEUE:Number2Transmission(number, delay, interval) - - --- Split string into characters. - local function _split(str) - local chars={} - for i=1,#str do - local c=str:sub(i,i) - table.insert(chars, c) - end - return chars - end -- Split string into characters. - local numbers=_split(number) + local numbers=UTILS.GetCharacters(number) local wait=0 for i=1,#numbers do @@ -325,6 +360,11 @@ end -- @param #RADIOQUEUE.Transmission transmission The transmission. function RADIOQUEUE:Broadcast(transmission) + if ((transmission.soundfile and transmission.soundfile.useSRS) or transmission.soundtext) and self.msrs then + self:_BroadcastSRS(transmission) + return + end + -- Get unit sending the transmission. local sender=self:_GetRadioSender() @@ -416,6 +456,19 @@ function RADIOQUEUE:Broadcast(transmission) end end +--- Broadcast radio message. +-- @param #RADIOQUEUE self +-- @param #RADIOQUEUE.Transmission transmission The transmission. +function RADIOQUEUE:_BroadcastSRS(transmission) + + if transmission.soundfile and transmission.soundfile.useSRS then + self.msrs:PlaySoundFile(transmission.soundfile) + elseif transmission.soundtext then + self.msrs:PlaySoundText(transmission.soundtext) + end + +end + --- Start checking the radio queue. -- @param #RADIOQUEUE self -- @param #number delay Delay in seconds before checking. @@ -547,7 +600,7 @@ function RADIOQUEUE:_GetRadioSender() return nil end ---- Get unit from which we want to transmit a radio message. This has to be an aircraft for subtitles to work. +--- Get unit from which we want to transmit a radio message. This has to be an aircraft or ground unit for subtitles to work. -- @param #RADIOQUEUE self -- @return DCS#Vec3 Vector 3D. function RADIOQUEUE:_GetRadioSenderCoord() diff --git a/Moose Development/Moose/Core/RadioSpeech.lua b/Moose Development/Moose/Sound/RadioSpeech.lua similarity index 90% rename from Moose Development/Moose/Core/RadioSpeech.lua rename to Moose Development/Moose/Sound/RadioSpeech.lua index d4cf22af1..f77c446a2 100644 --- a/Moose Development/Moose/Core/RadioSpeech.lua +++ b/Moose Development/Moose/Sound/RadioSpeech.lua @@ -11,7 +11,7 @@ -- -- ### Authors: FlightControl -- --- @module Core.RadioSpeech +-- @module Sound.RadioSpeech -- @image Core_Radio.JPG --- Makes the radio speak. @@ -162,31 +162,31 @@ RADIOSPEECH.Vocabulary.RU = { ["8000"] = { "8000", 0.92 }, ["9000"] = { "9000", 0.87 }, - ["степени"] = { "degrees", 0.5 }, - ["километров"] = { "kilometers", 0.65 }, + ["Ñ�тепени"] = { "degrees", 0.5 }, + ["километров"] = { "kilometers", 0.65 }, ["km"] = { "kilometers", 0.65 }, - ["миль"] = { "miles", 0.45 }, + ["миль"] = { "miles", 0.45 }, ["mi"] = { "miles", 0.45 }, - ["метры"] = { "meters", 0.41 }, + ["метры"] = { "meters", 0.41 }, ["m"] = { "meters", 0.41 }, - ["ноги"] = { "feet", 0.37 }, + ["ноги"] = { "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 }, + ["возвращаÑ�Ñ�ÑŒ на базу"] = { "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 }, + ["за"] = { "for", 0.27 }, + ["и"] = { "and", 0.17 }, + ["в"] = { "at", 0.19 }, ["dot"] = { "dot", 0.51 }, ["defender"] = { "defender", 0.45 }, } diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua new file mode 100644 index 000000000..d4253971c --- /dev/null +++ b/Moose Development/Moose/Sound/SRS.lua @@ -0,0 +1,675 @@ +--- **Sound** - Simple Radio Standalone (SRS) Integration. +-- +-- === +-- +-- **Main Features:** +-- +-- * Play sound files via SRS +-- * Play text-to-speach via SRS +-- +-- === +-- +-- ## Youtube Videos: None yet +-- +-- === +-- +-- ## Missions: None yet +-- +-- === +-- +-- ## Sound files: [MOOSE Sound Files](https://github.com/FlightControl-Master/MOOSE_SOUND/releases) +-- +-- === +-- +-- The goal of the [SRS](https://github.com/ciribob/DCS-SimpleRadioStandalone) project is to bring VoIP communication into DCS and to make communication as frictionless as possible. +-- +-- === +-- +-- ### Author: **funkyfranky** +-- @module Sound.MSRS +-- @image Sound_MSRS.png + +--- MSRS class. +-- @type MSRS +-- @field #string ClassName Name of the class. +-- @field #string lid Class id string for output to DCS log file. +-- @field #table frequencies Frequencies used in the transmissions. +-- @field #table modulations Modulations used in the transmissions. +-- @field #number coalition Coalition of the transmission. +-- @field #number port Port. Default 5002. +-- @field #string name Name. Default "DCS-STTS". +-- @field #number volume Volume between 0 (min) and 1 (max). Default 1. +-- @field #string culture Culture. Default "en-GB". +-- @field #string gender Gender. Default "female". +-- @field #string voice Specifc voce. +-- @field Core.Point#COORDINATE coordinate Coordinate from where the transmission is send. +-- @field #string path Path to the SRS exe. This includes the final slash "/". +-- @field #string google Full path google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @extends Core.Base#BASE + +--- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde +-- +-- === +-- +-- ![Banner Image](..\Presentations\ATIS\ATIS_Main.png) +-- +-- # The MSRS Concept +-- +-- This class allows to broadcast sound files or text via Simple Radio Standalone (SRS). +-- +-- ## Prerequisites +-- +-- This script needs SRS version >= 1.9.6. +-- +-- # Play Sound Files +-- +-- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "D:\\Sounds For DCS") +-- local msrs=MSRS:New("C:\\Path To SRS", 251, radio.modulation.AM) +-- msrs:PlaySoundFile(soundfile) +-- +-- # Play Text-To-Speech +-- +-- Basic example: +-- +-- -- Create a SOUNDTEXT object. +-- local text=SOUNDTEXT:New("All Enemies destroyed") +-- +-- -- MOOSE SRS +-- local msrs=MSRS:New("D:\\DCS\\_SRS\\", 305, radio.modulation.AM) +-- +-- -- Text-to speech with default voice after 2 seconds. +-- msrs:PlaySoundText(text, 2) +-- +-- ## Set Gender +-- +-- Use a specific gender with the @{#MSRS.SetGender} function, e.g. `SetGender("male")` or `:SetGender("female")`. +-- +-- ## Set Culture +-- +-- Use a specific "culture" with the @{#MSRS.SetCulture} function, e.g. `:SetCulture("en-US")` or `:SetCulture("de-DE")`. +-- +-- ## Set Voice +-- +-- Use a specifc voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. +-- +-- ## Set Coordinate +-- +-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- +-- @field #MSRS +MSRS = { + ClassName = "MSRS", + lid = nil, + port = 5002, + name = "MSRS", + frequencies = {}, + modulations = {}, + coalition = 0, + gender = "female", + culture = nil, + voice = nil, + volume = 1, + speed = 1, + coordinate = nil, +} + +--- MSRS class version. +-- @field #string version +MSRS.version="0.0.3" + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- TODO list +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +-- TODO: Add functions to add/remove freqs and modulations. +-- DONE: Add coordinate. +-- DONE: Add google. + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Constructor +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Create a new MSRS object. +-- @param #MSRS self +-- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #number Frequency Radio frequency in MHz. Default 143.00 MHz. Can also be given as a #table of multiple frequencies. +-- @param #number Modulation Radio modulation: 0=AM (default), 1=FM. See `radio.modulation.AM` and `radio.modulation.FM` enumerators. Can also be given as a #table of multiple modulations. +-- @return #MSRS self +function MSRS:New(PathToSRS, Frequency, Modulation) + + -- Defaults. + Frequency =Frequency or 143 + Modulation= Modulation or radio.modulation.AM + + -- Inherit everything from FSM class. + local self=BASE:Inherit(self, BASE:New()) -- #MSRS + + self:SetPath(PathToSRS) + self:SetPort() + self:SetFrequencies(Frequency) + self:SetModulations(Modulation) + self:SetGender() + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- User Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Set path to SRS install directory. More precisely, path to where the DCS- +-- @param #MSRS self +-- @param #string Path Path to the directory, where the sound file is located. This does **not** contain a final backslash or slash. +-- @return #MSRS self +function MSRS:SetPath(Path) + + if Path==nil then + self:E("ERROR: No path to SRS directory specified!") + return nil + end + + -- Set path. + self.path=Path + + -- Remove (back)slashes. + local n=1 ; local nmax=1000 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Debug output. + self:T(string.format("SRS path=%s", self:GetPath())) + + return self +end + +--- Get path to SRS directory. +-- @param #MSRS self +-- @return #string Path to the directory. This includes the final slash "/". +function MSRS:GetPath() + return self.path +end + +--- Set port. +-- @param #MSRS self +-- @param #number Port Port. Default 5002. +-- @return #MSRS self +function MSRS:SetPort(Port) + self.port=Port or 5002 +end + +--- Get port. +-- @param #MSRS self +-- @return #number Port. +function MSRS:GetPort() + return self.port +end + +--- Set frequencies. +-- @param #MSRS self +-- @param #table Frequencies Frequencies in MHz. Can also be given as a #number if only one frequency should be used. +-- @return #MSRS self +function MSRS:SetFrequencies(Frequencies) + + -- Ensure table. + if type(Frequencies)~="table" then + Frequencies={Frequencies} + end + + self.frequencies=Frequencies + + return self +end + +--- Get frequencies. +-- @param #MSRS self +-- @param #table Frequencies in MHz. +function MSRS:GetFrequencies() + return self.frequencies +end + + +--- Set modulations. +-- @param #MSRS self +-- @param #table Modulations Modulations. Can also be given as a #number if only one modulation should be used. +-- @return #MSRS self +function MSRS:SetModulations(Modulations) + + -- Ensure table. + if type(Modulations)~="table" then + Modulations={Modulations} + end + + self.modulations=Modulations + + return self +end + +--- Get modulations. +-- @param #MSRS self +-- @param #table Modulations. +function MSRS:GetModulations() + return self.modulations +end + +--- Set gender. +-- @param #MSRS self +-- @param #string Gender Gender: "male" or "female" (default). +-- @return #MSRS self +function MSRS:SetGender(Gender) + + Gender=Gender or "female" + + self.gender=Gender:lower() + + -- Debug output. + self:T("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 + +--- Set the coordinate from which the transmissions will be broadcasted. +-- @param #MSRS self +-- @param Core.Point#COORDINATE Coordinate Origin of the transmission. +-- @return #MSRS self +function MSRS:SetCoordinate(Coordinate) + + self.coordinate=Coordinate + + return self +end + +--- Use google text-to-speech. +-- @param #MSRS self +-- @param PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". +-- @return #MSRS self +function MSRS:SetGoogle(PathToCredentials) + + self.google=PathToCredentials + + return self +end + +--- Print SRS STTS help to DCS log file. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:Help() + + -- Path and exe. + local path=self:GetPath() or STTS.DIRECTORY + local exe=STTS.EXECUTABLE or "DCS-SR-ExternalAudio.exe" + + -- Text file for output. + local filename = os.getenv('TMP') .. "\\MSRS-help-"..STTS.uuid()..".txt" + + -- Print help. + local command=string.format("%s/%s --help > %s", path, exe, filename) + os.execute(command) + + local f=assert(io.open(filename, "rb")) + local data=f:read("*all") + f:close() + + -- Print to log file. + env.info("SRS STTS help output:") + env.info("======================================================================") + env.info(data) + env.info("======================================================================") + + return self +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Transmission Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Play sound file (ogg or mp3) via SRS. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDFILE Soundfile Sound file to play. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundFile(Soundfile, Delay) + + if Delay and Delay>0 then + 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) + + self:_ExecCommand(command) + + --[[ + + command=command.." > bla.txt" + + -- Debug output. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + + ]] + + end + + return self +end + +--- Play a SOUNDTEXT text-to-speech object. +-- @param #MSRS self +-- @param Sound.SoundFile#SOUNDTEXT SoundText Sound text. +-- @param #number Delay Delay in seconds, before the sound file is played. +-- @return #MSRS self +function MSRS:PlaySoundText(SoundText, Delay) + + if Delay and Delay>0 then + 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)) + + -- Execute command. + self:_ExecCommand(command) + + --[[ + command=command.." > bla.txt" + + -- Debug putput. + self:I(string.format("MSRS PlaySoundfile command=%s", command)) + + -- Execute SRS command. + local x=os.execute(command) + ]] + + end + + return self +end + +--- Play text message via STTS. +-- @param #MSRS self +-- @param #string Text Text message. +-- @param #number Delay Delay in seconds, before the message is played. +-- @return #MSRS self +function MSRS:PlayText(Text, Delay) + + if Delay and Delay>0 then + self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, 0) + else + + -- Get command line. + local command=self:_GetCommand() + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) + + -- Execute command. + self:_ExecCommand(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:T(string.format("MSRS TextFile command=%s", command)) + + -- Count length of command. + local l=string.len(command) + + -- Execute command. + self:_ExecCommand(command) + + end + + return self +end + + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Misc Functions +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #string command Command to executer +-- @return #number Return value of os.execute() command. +function MSRS:_ExecCommand(command) + + -- 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('start /b "" "%s"', filename) + + local res=nil + if true then + + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" + + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Dim WinScriptHost\n')) + script:write(string.format('Set WinScriptHost = CreateObject("WScript.Shell")\n')) + script:write(string.format('WinScriptHost.Run Chr(34) & "%s" & Chr(34), 0\n', filename)) + script:write(string.format('Set WinScriptHost = Nothing')) + script:close() + + -- Run visual basic script. This still pops up a window but very briefly and does not put the DCS window out of focus. + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) + + -- Debug output. + self:T("MSRS execute command="..command) + self:T("MSRS execute VBS command="..runvbs) + + -- Play file in 0.01 seconds + res=os.execute(runvbs) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) + + + else + + -- Debug output. + self:T("MSRS execute command="..command) + + -- Execute command + res=os.execute(command) + + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + + end + + + return res +end + +--- Get lat, long and alt from coordinate. +-- @param #MSRS self +-- @param Core.Point#Coordinate Coordinate Coordinate. Can also be a DCS#Vec3. +-- @return #number Latitude. +-- @return #number Longitude. +-- @return #number Altitude. +function MSRS:_GetLatLongAlt(Coordinate) + + local lat, lon, alt=coord.LOtoLL(Coordinate) + + return lat, lon, math.floor(alt) +end + + +--- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #table freqs Frequencies in MHz. +-- @param #table modus Modulations. +-- @param #number coal Coalition. +-- @param #string gender Gender. +-- @param #string voice Voice. +-- @param #string culture Culture. +-- @param #number volume Volume. +-- @param #number speed Speed. +-- @param #number port Port. +-- @return #string Command. +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port) + + 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 + gender=gender or self.gender + voice=voice or self.voice + culture=culture or self.culture + volume=volume or self.volume + speed=speed or self.speed + port=port or self.port + + -- Replace modulation + modus=modus:gsub("0", "AM") + modus=modus:gsub("1", "FM") + + -- 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\" -h", path, exe, freqs, modus, coal, port, "ROBOT") + + --local command=string.format('start /b "" /d "%s" "%s" -f %s -m %s -c %s -p %s -n "%s" > bla.txt', path, exe, freqs, modus, coal, port, "ROBOT") + + -- Command. + local command=string.format('%s/%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 + + -- Set coordinate. + if self.coordinate then + local lat,lon,alt=self:_GetLatLongAlt(self.coordinate) + command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt) + end + + -- Set google. + if self.google then + command=command..string.format(' -G "%s"', self.google) + end + + -- Debug output. + self:T("MSRS command="..command) + + return command +end + +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- diff --git a/Moose Development/Moose/Sound/SoundOutput.lua b/Moose Development/Moose/Sound/SoundOutput.lua new file mode 100644 index 000000000..01fd00483 --- /dev/null +++ b/Moose Development/Moose/Sound/SoundOutput.lua @@ -0,0 +1,408 @@ +--- **Sound** - Sound output classes. +-- +-- === +-- +-- ## Features: +-- +-- * Create a SOUNDFILE object (mp3 or ogg) to be played via DCS or SRS transmissions +-- * Create a SOUNDTEXT object for text-to-speech output vis SRS Simple-Text-To-Speech (STTS) +-- +-- === +-- +-- ### Author: **funkyfranky** +-- +-- === +-- +-- There are two classes, SOUNDFILE and SOUNDTEXT, defined in this section that deal with playing +-- sound files or arbitrary text (via SRS Simple-Text-To-Speech), respectively. +-- +-- The SOUNDFILE and SOUNDTEXT objects can be defined and used in other MOOSE classes. +-- +-- +-- @module Sound.SoundOutput +-- @image Sound_SoundOutput.png + +do -- Sound Base + + --- @type SOUNDBASE + -- @field #string ClassName Name of the class. + -- @extends Core.Base#BASE + + + --- Basic sound output inherited by other classes suche as SOUNDFILE and SOUNDTEXT. + -- + -- This class is **not** meant to be used by "ordinary" users. + -- + -- @field #SOUNDBASE + SOUNDBASE={ + ClassName = "SOUNDBASE", + } + + --- Constructor to create a new SOUNDBASE object. + -- @param #SOUNDBASE self + -- @return #SOUNDBASE self + function SOUNDBASE:New() + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDBASE + + + + return self + 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 + -- + -- @param #string Text The text string to analyze. + -- @param #number Speed Speed factor. Default 1. + -- @param #boolean isGoogle If true, google text-to-speech is used. + function SOUNDBASE: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 + + -- Words per minute. + local wpm = math.ceil(100 * speedFactor) + + -- Characters per second. + local cps = math.floor((wpm * 5)/60) + + if type(length) == "string" then + length = string.len(length) + end + + return math.ceil(length/cps) + end + +end + + +do -- Sound File + + --- @type SOUNDFILE + -- @field #string ClassName Name of the class + -- @field #string filename Name of the flag. + -- @field #string path Directory path, where the sound file is located. This includes the final slash "/". + -- @field #string duration Duration of the sound file in seconds. + -- @field #string subtitle Subtitle of the transmission. + -- @field #number subduration Duration in seconds how long the subtitle is displayed. + -- @field #boolean useSRS If true, sound file is played via SRS. Sound file needs to be on local disk not inside the miz file! + -- @extends Core.Base#BASE + + + --- Sound files used by other classes. + -- + -- # The SOUNDFILE Concept + -- + -- A SOUNDFILE object hold the important properties that are necessary to play the sound file, e.g. its file name, path, duration. + -- + -- It can be created with the @{#SOUNDFILE.New}(*FileName*, *Path*, *Duration*) function: + -- + -- local soundfile=SOUNDFILE:New("My Soundfile.ogg", "Sound File/", 3.5) + -- + -- ## SRS + -- + -- If sound files are supposed to be played via SRS, you need to use the @{#SOUNDFILE.SetPlayWithSRS}() function. + -- + -- # Location/Path + -- + -- ## DCS + -- + -- DCS can only play sound files that are located inside the mission (.miz) file. In particular, DCS cannot make use of files that are stored on + -- your hard drive. + -- + -- The default location where sound files are stored in DCS is the directory "l10n/DEFAULT/". This is where sound files are placed, if they are + -- added via the mission editor (TRIGGERS-->ACTIONS-->SOUND TO ALL). Note however, that sound files which are not added with a trigger command, + -- will be deleted each time the mission is saved! Therefore, this directory is not ideal to be used especially if many sound files are to + -- be included since for each file a trigger action needs to be created. Which is cumbersome, to say the least. + -- + -- The recommended way is to create a new folder inside the mission (.miz) file (a miz file is essentially zip file and can be opened, e.g., with 7-Zip) + -- and to place the sound files in there. Sound files in these folders are not wiped out by DCS on the next save. + -- + -- ## SRS + -- + -- SRS sound files need to be located on your local drive (not inside the miz). Therefore, you need to specify the full path. + -- + -- @field #SOUNDFILE + SOUNDFILE={ + ClassName = "SOUNDFILE", + filename = nil, + path = "l10n/DEFAULT/", + duration = 3, + subtitle = nil, + subduration = 0, + useSRS = false, + } + + --- Constructor to create a new SOUNDFILE object. + -- @param #SOUNDFILE self + -- @param #string FileName The name of the sound file, e.g. "Hello World.ogg". + -- @param #string Path The path of the directory, where the sound file is located. Default is "l10n/DEFAULT/" within the miz file. + -- @param #number Duration Duration in seconds, how long it takes to play the sound file. Default is 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:New(FileName, Path, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDFILE + + -- Set file name. + self:SetFileName(FileName) + + -- Set path. + self:SetPath(Path) + + -- Set duration. + self:SetDuration(Duration) + + -- Debug info: + self:T(string.format("New SOUNDFILE: file name=%s, path=%s", self.filename, self.path)) + + return self + end + + --- Set path, where the sound file is located. + -- @param #SOUNDFILE self + -- @param #string Path Path to the directory, where the sound file is located. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPath(Path) + + -- Init path. + self.path=Path or "l10n/DEFAULT/" + + -- Remove (back)slashes. + local nmax=1000 ; local n=1 + while (self.path:sub(-1)=="/" or self.path:sub(-1)==[[\]]) and n<=nmax do + self.path=self.path:sub(1,#self.path-1) + n=n+1 + end + + -- Append slash. + self.path=self.path.."/" + + return self + end + + --- Get path of the directory, where the sound file is located. + -- @param #SOUNDFILE self + -- @return #string Path. + function SOUNDFILE:GetPath() + local path=self.path or "l10n/DEFAULT/" + return path + end + + --- Set sound file name. This must be a .ogg or .mp3 file! + -- @param #SOUNDFILE self + -- @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 "Hello World.mp3" + return self + end + + --- Get the sound file name. + -- @param #SOUNDFILE self + -- @return #string Name of the soud file. This does *not* include its path. + function SOUNDFILE:GetFileName() + return self.filename + end + + + --- Set duration how long it takes to play the sound file. + -- @param #SOUNDFILE self + -- @param #string Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDFILE self + function SOUNDFILE:SetDuration(Duration) + self.duration=Duration or 3 + return self + end + + --- Get duration how long the sound file takes to play. + -- @param #SOUNDFILE self + -- @return #number Duration in seconds. + function SOUNDFILE:GetDuration() + return self.duration or 3 + end + + --- Get the complete sound file name inlcuding its path. + -- @param #SOUNDFILE self + -- @return #string Name of the sound file. + function SOUNDFILE:GetName() + local path=self:GetPath() + local filename=self:GetFileName() + local name=string.format("%s%s", path, filename) + return name + end + + --- Set whether sound files should be played via SRS. + -- @param #SOUNDFILE self + -- @param #boolean Switch If true or nil, use SRS. If false, use DCS transmission. + -- @return #SOUNDFILE self + function SOUNDFILE:SetPlayWithSRS(Switch) + if Switch==true or Switch==nil then + self.useSRS=true + else + self.useSRS=false + end + return self + end + +end + +do -- Text-To-Speech + + --- @type SOUNDTEXT + -- @field #string ClassName Name of the class + -- @field #string text Text to speak. + -- @field #number duration Duration in seconds. + -- @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 + + + --- Text-to-speech objects for other classes. + -- + -- # The SOUNDTEXT Concept + -- + -- A SOUNDTEXT object holds all necessary information to play a general text via SRS Simple-Text-To-Speech. + -- + -- It can be created with the @{#SOUNDTEXT.New}(*Text*, *Duration*) function. + -- + -- * @{#SOUNDTEXT.New}(*Text, Duration*): Creates a new SOUNDTEXT object. + -- + -- # Options + -- + -- ## Gender + -- + -- You can choose a gender ("male" or "femal") with the @{#SOUNDTEXT.SetGender}(*Gender*) function. + -- Note that the gender voice needs to be installed on your windows machine for the used culture (see below). + -- + -- ## Culture + -- + -- You can choose a "culture" (accent) with the @{#SOUNDTEXT.SetCulture}(*Culture*) function, where the default (SRS) culture is "en-GB". + -- + -- Other examples for culture are: "en-US" (US accent), "de-DE" (German), "it-IT" (Italian), "ru-RU" (Russian), "zh-CN" (Chinese). + -- + -- Note that the chosen culture needs to be installed on your windows machine. + -- + -- ## Specific Voice + -- + -- You can use a specific voice for the transmission with the @{SOUNDTEXT.SetVoice}(*VoiceName*) function. Here are some examples + -- + -- * 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) + -- + -- Note that this must be installed on your windos machine. Also note that this overrides any culture and gender settings. + -- + -- @field #SOUNDTEXT + SOUNDTEXT={ + ClassName = "SOUNDTEXT", + } + + --- Constructor to create a new SOUNDTEXT object. + -- @param #SOUNDTEXT self + -- @param #string Text The text to speak. + -- @param #number Duration Duration in seconds, how long it takes to play the text. Default is 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:New(Text, Duration) + + -- Inherit BASE. + local self=BASE:Inherit(self, BASE:New()) -- #SOUNDTEXT + + self:SetText(Text) + self:SetDuration(Duration or STTS.getSpeechTime(Text)) + --self:SetGender() + --self:SetCulture() + + -- Debug info: + self:T(string.format("New SOUNDTEXT: text=%s, duration=%.1f sec", self.text, self.duration)) + + return self + end + + --- Set text. + -- @param #SOUNDTEXT self + -- @param #string Text Text to speak. Default "Hello World!". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetText(Text) + + self.text=Text or "Hello World!" + + return self + end + + --- Set duration, how long it takes to speak the text. + -- @param #SOUNDTEXT self + -- @param #number Duration Duration in seconds. Default 3 seconds. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetDuration(Duration) + + self.duration=Duration or 3 + + return self + end + + --- Set gender. + -- @param #SOUNDTEXT self + -- @param #string Gender Gender: "male" or "female" (default). + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetGender(Gender) + + self.gender=Gender or "female" + + return self + end + + --- Set TTS culture - local for the voice. + -- @param #SOUNDTEXT self + -- @param #string Culture TTS culture. Default "en-GB". + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetCulture(Culture) + + self.culture=Culture or "en-GB" + + return self + end + + --- Set to use a specific voice name. + -- See the list from `DCS-SR-ExternalAudio.exe --help` or if using google see [google voices](https://cloud.google.com/text-to-speech/docs/voices). + -- @param #SOUNDTEXT self + -- @param #string VoiceName Voice name. Note that this will overrule `Gender` and `Culture`. + -- @return #SOUNDTEXT self + function SOUNDTEXT:SetVoice(VoiceName) + + self.voice=VoiceName + + return self + end + +end \ No newline at end of file diff --git a/Moose Development/Moose/Core/UserSound.lua b/Moose Development/Moose/Sound/UserSound.lua similarity index 98% rename from Moose Development/Moose/Core/UserSound.lua rename to Moose Development/Moose/Sound/UserSound.lua index b0f6fb393..8b94ad114 100644 --- a/Moose Development/Moose/Core/UserSound.lua +++ b/Moose Development/Moose/Sound/UserSound.lua @@ -1,4 +1,4 @@ ---- **Core** - Manage user sound. +--- **Sound** - Manage user sound. -- -- === -- @@ -16,7 +16,7 @@ -- -- === -- --- @module Core.UserSound +-- @module Sound.UserSound -- @image Core_Usersound.JPG do -- UserSound 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 Development/Moose/Utilities/Utils.lua b/Moose Development/Moose/Utilities/Utils.lua index c5ade4b22..29ac7f8c2 100644 --- a/Moose Development/Moose/Utilities/Utils.lua +++ b/Moose Development/Moose/Utilities/Utils.lua @@ -50,6 +50,7 @@ BIGSMOKEPRESET = { -- @field #string PersianGulf Persian Gulf map. -- @field #string TheChannel The Channel map. -- @field #string Syria Syria map. +-- @field #string MarianaIslands Mariana Islands map. DCSMAP = { Caucasus="Caucasus", NTTR="Nevada", @@ -57,6 +58,7 @@ DCSMAP = { PersianGulf="PersianGulf", TheChannel="TheChannel", Syria="Syria", + MarianaIslands="MarianaIslands" } @@ -696,7 +698,10 @@ function UTILS.IsInSphere( InVec3, Vec3, Radius ) return InSphere end --- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +--- Beaufort scale: returns Beaufort number and wind description as a function of wind speed in m/s. +-- @param #number speed Wind speed in m/s. +-- @return #number Beaufort number. +-- @return #string Beauford wind description. function UTILS.BeaufortScale(speed) local bn=nil local bd=nil @@ -756,6 +761,21 @@ function UTILS.Split(str, sep) return result end +--- Get a table of all characters in a string. +-- @param #string str Sting. +-- @return #table Individual characters. +function UTILS.GetCharacters(str) + + local chars={} + + for i=1,#str do + local c=str:sub(i,i) + table.insert(chars, c) + end + + return chars +end + --- Convert time in seconds to hours, minutes and seconds. -- @param #number seconds Time in seconds, e.g. from timer.getAbsTime() function. -- @param #boolean short (Optional) If true, use short output, i.e. (HH:)MM:SS without day. @@ -1205,6 +1225,9 @@ end -- * NTTR +12 (East), year ~ 2011 -- * Normandy -10 (West), year ~ 1944 -- * Persian Gulf +2 (East), year ~ 2011 +-- * The Cannel Map -10 (West) +-- * Syria +5 (East) +-- * Mariana Islands +2 (East) -- @param #string map (Optional) Map for which the declination is returned. Default is from env.mission.theatre -- @return #number Declination in degrees. function UTILS.GetMagneticDeclination(map) @@ -1225,6 +1248,8 @@ function UTILS.GetMagneticDeclination(map) declination=-10 elseif map==DCSMAP.Syria then declination=5 + elseif map==DCSMAP.MarianaIslands then + declination=2 else declination=0 end @@ -1353,6 +1378,8 @@ function UTILS.GMTToLocalTimeDifference() return 2 -- This map currently needs +2 elseif theatre==DCSMAP.Syria then return 3 -- Damascus is UTC+3 hours + elseif theatre==DCSMAP.MarianaIslands then + return 10 -- Guam is UTC+10 hours. else BASE:E(string.format("ERROR: Unknown Map %s in UTILS.GMTToLocal function. Returning 0", tostring(theatre))) return 0 diff --git a/Moose Development/Moose/Wrapper/Airbase.lua b/Moose Development/Moose/Wrapper/Airbase.lua index 62036f59e..ca6ba1ca1 100644 --- a/Moose Development/Moose/Wrapper/Airbase.lua +++ b/Moose Development/Moose/Wrapper/Airbase.lua @@ -337,41 +337,53 @@ AIRBASE.TheChannel = { -- * AIRBASE.Syria.Incirlik -- * AIRBASE.Syria.Damascus -- * AIRBASE.Syria.Bassel_Al_Assad +-- * AIRBASE.Syria.Rosh_Pina -- * AIRBASE.Syria.Aleppo --- * AIRBASE.Syria.Qabr_as_Sitt +-- * AIRBASE.Syria.Al_Qusayr -- * AIRBASE.Syria.Wujah_Al_Hajar -- * AIRBASE.Syria.Al_Dumayr +-- * AIRBASE.Syria.Gazipasa +-- * AIRBASE.Syria.Ru_Convoy_4 -- * AIRBASE.Syria.Hatay +-- * AIRBASE.Syria.Nicosia +-- * AIRBASE.Syria.Pinarbashi +-- * AIRBASE.Syria.Paphos +-- * AIRBASE.Syria.Kingsfield +-- * AIRBASE.Syria.Tha'lah -- * AIRBASE.Syria.Haifa -- * AIRBASE.Syria.Khalkhalah -- * AIRBASE.Syria.Megiddo +-- * AIRBASE.Syria.Lakatamia -- * AIRBASE.Syria.Rayak +-- * AIRBASE.Syria.Larnaca -- * AIRBASE.Syria.Mezzeh --- * AIRBASE.Syria.King_Hussein_Air_College --- * AIRBASE.Syria.Jirah +-- * AIRBASE.Syria.Gecitkale +-- * AIRBASE.Syria.Akrotiri +-- * AIRBASE.Syria.Naqoura +-- * AIRBASE.Syria.Gaziantep +-- * AIRBASE.Syria.CVN_71 +-- * AIRBASE.Syria.Sayqal +-- * AIRBASE.Syria.Tiyas +-- * AIRBASE.Syria.Shayrat -- * AIRBASE.Syria.Taftanaz +-- * AIRBASE.Syria.H4 +-- * AIRBASE.Syria.King_Hussein_Air_College -- * AIRBASE.Syria.Rene_Mouawad +-- * AIRBASE.Syria.Jirah -- * AIRBASE.Syria.Ramat_David +-- * AIRBASE.Syria.Qabr_as_Sitt -- * AIRBASE.Syria.Minakh -- * AIRBASE.Syria.Adana_Sakirpasa --- * AIRBASE.Syria.Marj_as_Sultan_South --- * AIRBASE.Syria.Hama --- * AIRBASE.Syria.Al_Qusayr -- * AIRBASE.Syria.Palmyra +-- * AIRBASE.Syria.Hama +-- * AIRBASE.Syria.Ercan +-- * AIRBASE.Syria.Marj_as_Sultan_South -- * AIRBASE.Syria.Tabqa -- * AIRBASE.Syria.Beirut_Rafic_Hariri -- * AIRBASE.Syria.An_Nasiriyah -- * AIRBASE.Syria.Abu_al_Duhur --- * AIRBASE.Syria.H4 --- * AIRBASE.Syria.Gaziantep --- * AIRBASE.Syria.Rosh_Pina --- * AIRBASE.Syria.Sayqal --- * AIRBASE.Syria.Shayrat --- * AIRBASE.Syria.Tiyas --- * AIRBASE.Syria.Tha_lah --- * AIRBASE.Syria.Naqoura -- --- @field Syria +--@field Syria AIRBASE.Syria={ ["Kuweires"]="Kuweires", ["Marj_Ruhayyil"]="Marj Ruhayyil", @@ -381,39 +393,71 @@ AIRBASE.Syria={ ["Incirlik"]="Incirlik", ["Damascus"]="Damascus", ["Bassel_Al_Assad"]="Bassel Al-Assad", + ["Rosh_Pina"]="Rosh Pina", ["Aleppo"]="Aleppo", - ["Qabr_as_Sitt"]="Qabr as Sitt", + ["Al_Qusayr"]="Al Qusayr", ["Wujah_Al_Hajar"]="Wujah Al Hajar", ["Al_Dumayr"]="Al-Dumayr", + ["Gazipasa"]="Gazipasa", + ["Ru_Convoy_4"]="Ru Convoy-4", ["Hatay"]="Hatay", + ["Nicosia"]="Nicosia", + ["Pinarbashi"]="Pinarbashi", + ["Paphos"]="Paphos", + ["Kingsfield"]="Kingsfield", + ["Tha'lah"]="Tha'lah", ["Haifa"]="Haifa", ["Khalkhalah"]="Khalkhalah", ["Megiddo"]="Megiddo", + ["Lakatamia"]="Lakatamia", ["Rayak"]="Rayak", + ["Larnaca"]="Larnaca", ["Mezzeh"]="Mezzeh", - ["King_Hussein_Air_College"]="King Hussein Air College", - ["Jirah"]="Jirah", + ["Gecitkale"]="Gecitkale", + ["Akrotiri"]="Akrotiri", + ["Naqoura"]="Naqoura", + ["Gaziantep"]="Gaziantep", + ["CVN_71"]="CVN-71", + ["Sayqal"]="Sayqal", + ["Tiyas"]="Tiyas", + ["Shayrat"]="Shayrat", ["Taftanaz"]="Taftanaz", + ["H4"]="H4", + ["King_Hussein_Air_College"]="King Hussein Air College", ["Rene_Mouawad"]="Rene Mouawad", + ["Jirah"]="Jirah", ["Ramat_David"]="Ramat David", + ["Qabr_as_Sitt"]="Qabr as Sitt", ["Minakh"]="Minakh", ["Adana_Sakirpasa"]="Adana Sakirpasa", - ["Marj_as_Sultan_South"]="Marj as Sultan South", - ["Hama"]="Hama", - ["Al_Qusayr"]="Al Qusayr", ["Palmyra"]="Palmyra", + ["Hama"]="Hama", + ["Ercan"]="Ercan", + ["Marj_as_Sultan_South"]="Marj as Sultan South", ["Tabqa"]="Tabqa", ["Beirut_Rafic_Hariri"]="Beirut-Rafic Hariri", ["An_Nasiriyah"]="An Nasiriyah", ["Abu_al_Duhur"]="Abu al-Duhur", - ["H4"]="H4", - ["Gaziantep"]="Gaziantep", - ["Rosh_Pina"]="Rosh Pina", - ["Sayqal"]="Sayqal", - ["Shayrat"]="Shayrat", - ["Tiyas"]="Tiyas", - ["Tha_lah"]="Tha'lah", - ["Naqoura"]="Naqoura", +} + + + +--- Airbases of the Mariana Islands map. +-- +-- * AIRBASE.MarianaIslands.Rota_International_Airport +-- * AIRBASE.MarianaIslands.Andersen +-- * AIRBASE.MarianaIslands.Northwest_Field +-- * AIRBASE.MarianaIslands.Antonio_B_Won_Pat_International_Airport +-- * AIRBASE.MarianaIslands.Saipan_International_Airport +-- * AIRBASE.MarianaIslands.Tinian_International_Airport +-- @field MarianaIslands +AIRBASE.MarianaIslands={ + ["Rota_International_Airport"]="Rota International Airport", + ["Andersen"]="Andersen", + ["Northwest_Field"]="Northwest_Field", + ["Antonio_B_Won_Pat_International_Airport"]="Antonio B. Won Pat International Airport", + ["Saipan_International_Airport"]="Saipan International Airport", + ["Tinian_International_Airport"]="Tinian International Airport", } @@ -1388,7 +1432,8 @@ function AIRBASE:GetRunwayData(magvar, mark) name==AIRBASE.PersianGulf.Abu_Dhabi_International_Airport or name==AIRBASE.PersianGulf.Dubai_Intl or name==AIRBASE.PersianGulf.Shiraz_International_Airport or - name==AIRBASE.PersianGulf.Kish_International_Airport then + name==AIRBASE.PersianGulf.Kish_International_Airport or + name==AIRBASE.MarianaIslands.Andersen then -- 1-->4, 2-->3, 3-->2, 4-->1 exception=1 diff --git a/Moose Setup/Moose.files b/Moose Setup/Moose.files index a99e9065e..e7bec2087 100644 --- a/Moose Setup/Moose.files +++ b/Moose Setup/Moose.files @@ -4,10 +4,11 @@ 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/UserSound.lua Core/Report.lua Core/Scheduler.lua Core/ScheduleDispatcher.lua @@ -22,9 +23,6 @@ Core/Point.lua Core/Velocity.lua Core/Message.lua Core/Fsm.lua -Core/Radio.lua -Core/RadioQueue.lua -Core/RadioSpeech.lua Core/Spawn.lua Core/SpawnStatic.lua Core/Timer.lua @@ -121,6 +119,13 @@ Actions/Act_Route.lua Actions/Act_Account.lua Actions/Act_Assist.lua +Sound/UserSound.lua +Sound/SoundOutput.lua +Sound/Radio.lua +Sound/RadioQueue.lua +Sound/RadioSpeech.lua +Sound/SRS.lua + Tasking/CommandCenter.lua Tasking/Mission.lua Tasking/Task.lua