diff --git a/Moose Development/Moose/Core/Point.lua b/Moose Development/Moose/Core/Point.lua index a3c83da1f..89807b436 100644 --- a/Moose Development/Moose/Core/Point.lua +++ b/Moose Development/Moose/Core/Point.lua @@ -2456,15 +2456,18 @@ do -- COORDINATE -- Write command as string and execute that. Idea by Grimes https://forum.dcs.world/topic/324201-mark-to-all-function/#comment-5273793 local s=string.format("trigger.action.markupToAll(7, %d, %d,", Coalition, MarkID) for _,vec in pairs(vecs) do - s=s..string.format("%s,", UTILS._OneLineSerialize(vec)) + --s=s..string.format("%s,", UTILS._OneLineSerialize(vec)) + s=s..string.format("{x=%.1f, y=%.1f, z=%.1f},", vec.x, vec.y, vec.z) end - s=s..string.format("%s, %s, %s, %s", UTILS._OneLineSerialize(Color), UTILS._OneLineSerialize(FillColor), tostring(LineType), tostring(ReadOnly)) - if Text and Text~="" then - s=s..string.format(", \"%s\"", Text) + s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", Color[1], Color[2], Color[3], Color[4]) + s=s..string.format("{%.3f, %.3f, %.3f, %.3f},", FillColor[1], FillColor[2], FillColor[3], FillColor[4]) + s=s..string.format("%d,", LineType or 1) + s=s..string.format("%s", tostring(ReadOnly)) + if Text and type(Text)=="string" and string.len(Text)>0 then + s=s..string.format(", \"%s\"", tostring(Text)) end s=s..")" - -- Execute string command local success=UTILS.DoString(s) diff --git a/Moose Development/Moose/Core/Zone.lua b/Moose Development/Moose/Core/Zone.lua index 15570ee87..3317df260 100644 --- a/Moose Development/Moose/Core/Zone.lua +++ b/Moose Development/Moose/Core/Zone.lua @@ -2114,7 +2114,7 @@ end function _ZONE_TRIANGLE:Fill(Coalition, FillColor, FillAlpha, ReadOnly) Coalition=Coalition or -1 FillColor = FillColor - FillAlpha = FillAlpha + FillAlpha = FillAlpha local newID = self.Coords[1]:MarkupToAllFreeForm({self.Coords[2],self.Coords[3]},Coalition,nil,nil,FillColor,FillAlpha,0,nil) self.DrawID[#self.DrawID+1] = newID return self.DrawID @@ -2194,7 +2194,7 @@ function ZONE_POLYGON_BASE:New( ZoneName, PointsArray ) self._Triangles = self:_Triangulate() -- set the polygon's surface area self.SurfaceArea = self:_CalculateSurfaceArea() - + end return self @@ -2495,6 +2495,7 @@ end -- @param #boolean ReadOnly (Optional) Mark is readonly and cannot be removed by users. Default false.s -- @return #ZONE_POLYGON_BASE self function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, IncludeTriangles) + if self._.Polygon and #self._.Polygon >= 3 then Coalition = Coalition or self:GetDrawCoalition() @@ -2516,7 +2517,22 @@ function ZONE_POLYGON_BASE:DrawZone(Coalition, Color, Alpha, FillColor, FillAlph self:ReDrawBorderline(Color,Alpha,LineType) end end - + + + if false then + local coords = self:GetVerticiesCoordinates() + + local coord=coords[1] --Core.Point#COORDINATE + + table.remove(coords, 1) + + coord:MarkupToAllFreeForm(coords, Coalition, Color, Alpha, FillColor, FillAlpha, LineType, ReadOnly, "Drew Polygon") + + if true then + return + end + end + return self end @@ -3117,18 +3133,18 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) self.ScanData.Scenery = {} self.ScanData.SceneryTable = {} self.ScanData.Units = {} - + local vectors = self:GetBoundingSquare() - + local minVec3 = {x=vectors.x1, y=0, z=vectors.y1} local maxVec3 = {x=vectors.x2, y=0, z=vectors.y2} - + local minmarkcoord = COORDINATE:NewFromVec3(minVec3) local maxmarkcoord = COORDINATE:NewFromVec3(maxVec3) local ZoneRadius = minmarkcoord:Get2DDistance(maxmarkcoord)/2 -- self:I("Scan Radius:" ..ZoneRadius) local CenterVec3 = self:GetCoordinate():GetVec3() - + --[[ this a bit shaky in functionality it seems local VolumeBox = { id = world.VolumeType.BOX, @@ -3138,7 +3154,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) } } --]] - + local SphereSearch = { id = world.VolumeType.SPHERE, params = { @@ -3146,13 +3162,13 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) radius = ZoneRadius, } } - + local function EvaluateZone( ZoneObject ) if ZoneObject then local ObjectCategory = Object.getCategory(ZoneObject) - + if ( ObjectCategory == Object.Category.UNIT and ZoneObject:isExist() and ZoneObject:isActive() ) or (ObjectCategory == Object.Category.STATIC and ZoneObject:isExist()) then local CoalitionDCSUnit = ZoneObject:getCoalition() @@ -3186,7 +3202,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) self:F2( { Name = ZoneObject:getName(), Coalition = CoalitionDCSUnit } ) end end - + -- trying with box search if ObjectCategory == Object.Category.SCENERY and self:IsVec3InZone(ZoneObject:getPoint()) then local SceneryType = ZoneObject:getTypeName() @@ -3205,7 +3221,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) -- Search objects. local inzoneunits = SET_UNIT:New():FilterZones({self}):FilterOnce() local inzonestatics = SET_STATIC:New():FilterZones({self}):FilterOnce() - + inzoneunits:ForEach( function(unit) local Unit = unit --Wrapper.Unit#UNIT @@ -3213,7 +3229,7 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) EvaluateZone(DCS) end ) - + inzonestatics:ForEach( function(static) local Static = static --Wrapper.Static#STATIC @@ -3221,19 +3237,19 @@ function ZONE_POLYGON:Scan( ObjectCategories, UnitCategories ) EvaluateZone(DCS) end ) - + local searchscenery = false for _,_type in pairs(ObjectCategories) do if _type == Object.Category.SCENERY then searchscenery = true end end - + if searchscenery then -- Search objects. world.searchObjects({Object.Category.SCENERY}, SphereSearch, EvaluateZone ) end - + end --- Count the number of different coalitions inside the zone. @@ -3449,7 +3465,7 @@ end end do -- ZONE_ELASTIC - + --- -- @type ZONE_ELASTIC -- @field #table points Points in 2D. @@ -3476,14 +3492,14 @@ do -- ZONE_ELASTIC function ZONE_ELASTIC:New(ZoneName, Points) local self=BASE:Inherit(self, ZONE_POLYGON_BASE:New(ZoneName, Points)) --#ZONE_ELASTIC - + -- Zone objects are added to the _DATABASE and SET_ZONE objects. _EVENTDISPATCHER:CreateEventNewZone( self ) - + if Points then self.points=Points end - + return self end @@ -3492,10 +3508,10 @@ do -- ZONE_ELASTIC -- @param DCS#Vec2 Vec2 Point in 2D (with x and y coordinates). -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddVertex2D(Vec2) - + -- Add vec2 to points. table.insert(self.points, Vec2) - + return self end @@ -3505,10 +3521,10 @@ do -- ZONE_ELASTIC -- @param DCS#Vec3 Vec3 Point in 3D (with x, y and z coordinates). Only the x and z coordinates are used. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddVertex3D(Vec3) - + -- Add vec2 from vec3 to points. table.insert(self.points, {x=Vec3.x, y=Vec3.z}) - + return self end @@ -3518,10 +3534,10 @@ do -- ZONE_ELASTIC -- @param Core.Set#SET_GROUP GroupSet Set of groups. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:AddSetGroup(GroupSet) - + -- Add set to table. table.insert(self.setGroups, GroupSet) - + return self end @@ -3533,13 +3549,13 @@ do -- ZONE_ELASTIC -- @param #boolean Draw Draw the zone. Default `nil`. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:Update(Delay, Draw) - + -- Debug info. self:T(string.format("Updating ZONE_ELASTIC %s", tostring(self.ZoneName))) - + -- Copy all points. local points=UTILS.DeepCopy(self.points or {}) - + if self.setGroups then for _,_setGroup in pairs(self.setGroups) do local setGroup=_setGroup --Core.Set#SET_GROUP @@ -3554,7 +3570,7 @@ do -- ZONE_ELASTIC -- Update polygon verticies from points. self._.Polygon=self:_ConvexHull(points) - + if Draw~=false then if self.DrawID or Draw==true then self:UndrawZone() @@ -3564,7 +3580,7 @@ do -- ZONE_ELASTIC return self end - + --- Start the updating scheduler. -- @param #ZONE_ELASTIC self -- @param #number Tstart Time in seconds before the updating starts. @@ -3573,9 +3589,9 @@ do -- ZONE_ELASTIC -- @param #boolean Draw Draw the zone. Default `nil`. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:StartUpdate(Tstart, dT, Tstop, Draw) - + self.updateID=self:ScheduleRepeat(Tstart, dT, 0, Tstop, ZONE_ELASTIC.Update, self, 0, Draw) - + return self end @@ -3584,46 +3600,46 @@ do -- ZONE_ELASTIC -- @param #number Delay Delay in seconds before the scheduler will be stopped. Default 0. -- @return #ZONE_ELASTIC self function ZONE_ELASTIC:StopUpdate(Delay) - + if Delay and Delay>0 then self:ScheduleOnce(Delay, ZONE_ELASTIC.StopUpdate, self) else - + if self.updateID then - + self:ScheduleStop(self.updateID) - + self.updateID=nil - + end - + end - + return self end - + --- Create a convec hull. -- @param #ZONE_ELASTIC self -- @param #table pl Points -- @return #table Points function ZONE_ELASTIC:_ConvexHull(pl) - + if #pl == 0 then return {} end - + table.sort(pl, function(left,right) return left.x < right.x end) - + local h = {} - + -- Function: ccw > 0 if three points make a counter-clockwise turn, clockwise if ccw < 0, and collinear if ccw = 0. local function ccw(a,b,c) return (b.x - a.x) * (c.y - a.y) > (b.y - a.y) * (c.x - a.x) end - + -- lower hull for i,pt in pairs(pl) do while #h >= 2 and not ccw(h[#h-1], h[#h], pt) do @@ -3631,7 +3647,7 @@ do -- ZONE_ELASTIC end table.insert(h,pt) end - + -- upper hull local t = #h + 1 for i=#pl, 1, -1 do @@ -3641,12 +3657,12 @@ do -- ZONE_ELASTIC end table.insert(h, pt) end - + table.remove(h, #h) - + return h - end - + end + end @@ -3673,13 +3689,17 @@ ZONE_OVAL = { --- Creates a new ZONE_OVAL from a center point, major axis, minor axis, and angle. --- ported from https://github.com/nielsvaes/CCMOOSE/blob/master/Moose%20Development/Moose/Shapes/Oval.lua +-- @param #ZONE_OVAL self +-- @param #string name Name of the zone. -- @param #table vec2 The center point of the oval -- @param #number major_axis The major axis of the oval -- @param #number minor_axis The minor axis of the oval -- @param #number angle The angle of the oval -- @return #ZONE_OVAL The new oval function ZONE_OVAL:New(name, vec2, major_axis, minor_axis, angle) + self = BASE:Inherit(self, ZONE_BASE:New()) + self.ZoneName = name self.CenterVec2 = vec2 self.MajorAxis = major_axis @@ -3717,7 +3737,7 @@ function ZONE_OVAL:NewFromDrawing(DrawingName) return self end ---- Gets the major axis of the oval. +--- Gets the major axis of the oval. -- @param #ZONE_OVAL self -- @return #number The major axis of the oval function ZONE_OVAL:GetMajorAxis() @@ -3913,7 +3933,7 @@ do -- ZONE_AIRBASE self._.ZoneAirbase = Airbase self._.ZoneVec2Cache = self._.ZoneAirbase:GetVec2() - + if Airbase:IsShip() then self.isShip=true self.isHelipad=false @@ -3921,11 +3941,11 @@ do -- ZONE_AIRBASE elseif Airbase:IsHelipad() then self.isShip=false self.isHelipad=true - self.isAirdrome=false + self.isAirdrome=false elseif Airbase:IsAirdrome() then self.isShip=false self.isHelipad=false - self.isAirdrome=true + self.isAirdrome=true end -- Zone objects are added to the _DATABASE and SET_ZONE objects. diff --git a/Moose Development/Moose/Ops/ATIS.lua b/Moose Development/Moose/Ops/ATIS.lua index 6819ee36c..064ab1880 100644 --- a/Moose Development/Moose/Ops/ATIS.lua +++ b/Moose Development/Moose/Ops/ATIS.lua @@ -890,7 +890,7 @@ _ATIS = {} --- ATIS class version. -- @field #string version -ATIS.version = "0.10.4" +ATIS.version = "1.0.0" ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list @@ -1528,12 +1528,12 @@ 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 PathToSRS Path to SRS directory (only necessary if SRS exe backend is used). -- @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. --- @param #string GoogleKey Path to Google JSON-Key. +-- @param #string GoogleKey Path to Google JSON-Key (SRS exe backend) or Google API key (DCS-gRPC backend). -- @return #ATIS self function ATIS:SetSRS(PathToSRS, Gender, Culture, Voice, Port, GoogleKey) if PathToSRS or MSRS.path then diff --git a/Moose Development/Moose/Sound/SRS.lua b/Moose Development/Moose/Sound/SRS.lua index 5c25018bb..36a300b67 100644 --- a/Moose Development/Moose/Sound/SRS.lua +++ b/Moose Development/Moose/Sound/SRS.lua @@ -4,8 +4,9 @@ -- -- **Main Features:** -- +-- * Incease immersion of your missions with more sound output -- * Play sound files via SRS --- * Play text-to-speach via SRS +-- * Play text-to-speech via SRS -- -- === -- @@ -41,16 +42,16 @@ -- @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 Specific voce. +-- @field #string voice Specific voice. Only used if no explicit provider voice specified. -- @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". +-- @field #string path Path to the SRS exe. -- @field #string Label Label showing up on the SRS radio overlay. Default is "ROBOT". No spaces allowed. --- @field #table AltBackend Table containing functions and variables to enable an alternate backend to transmit to SRS. --- @field #string ConfigFileName Name of the standard config file --- @field #string ConfigFilePath Path to the standard config file --- @field #boolean ConfigLoaded --- @field #string ttsprovider Default provider TTS backend, e.g. "Google" or "Microsoft", default is Microsoft +-- @field #string ConfigFileName Name of the standard config file. +-- @field #string ConfigFilePath Path to the standard config file. +-- @field #boolean ConfigLoaded If `true` if config file was loaded. +-- @field #table poptions Provider options. Each element is a data structure of type `MSRS.ProvierOptions`. +-- @field #string provider Provider of TTS (win, gcloud, azure, amazon). +-- @field #string backend Backend used as interface to SRS (MSRS.Backend.SRSEXE or MSRS.Backend.GRPC). -- @extends Core.Base#BASE --- *It is a very sad thing that nowadays there is so little useless information.* - Oscar Wilde @@ -60,94 +61,151 @@ -- # 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. --- +-- +-- * This script needs SRS version >= 1.9.6 +-- * You need to de-sanitize os, io and lfs in hte missionscripting.lua +-- * Optional: DCS-gRPC as backend to communicate with SRS (vide infra) +-- +-- ## Knwon Issues +-- +-- ### Pop-up Window +-- +-- The text-to-speech conversion of SRS is done via an external exe file. When this file is called, a windows `cmd` window is briefly opended. That puts DCS out of focus, which is annoying, +-- expecially in VR but unavoidable (if you have a solution, please feel free to share!). +-- +-- NOTE that this is not an issue if the mission is running on a server. +-- Also NOTE that using DCS-gRPC as backend will avoid the pop-up window. +-- -- # 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 +-- +-- -- 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 specific voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. +-- Note that this must be installed on your windows system. -- --- ## Set Google +-- Note that you can set voices for each provider via the @{#MSRS.SetVoiceProvider} function. Also shortcuts are available, *i.e.* +-- @{#MSRS.SetVoiceWindows}, @{#MSRS.SetVoiceGoogle}, @{#MSRS.SetVoiceAzure} and @{#MSRS.SetVoiceAmazon}. -- --- Use Google's text-to-speech engine with the @{#MSRS.SetGoogle} function, e.g. ':SetGoogle()'. --- By enabling this it also allows you to utilize SSML in your text for added flexibility. +-- For voices there are enumerators in this class to help you out on voice names: +-- +-- MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS +-- MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES. +-- +-- ## Set Coordinate +-- +-- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- Note that this is only a factor if SRS server has line-of-sight and/or distance limit enabled. +-- +-- ## Set SRS Port +-- +-- Use @{#MSRS.SetPort} to define the SRS port. Defaults to 5002. +-- +-- ## Set SRS Volume +-- +-- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest. +-- +-- ## Config file for many variables, auto-loaded by Moose +-- +-- See @{#MSRS.LoadConfigFile} for details on how to set this up. +-- +-- ## TTS Providers +-- +-- The default provider for generating speech from text is the native Windows TTS service. Note that you need to install the voices you want to use. +-- +-- **Pro-Tip** - use the command line with power shell to call `DCS-SR-ExternalAudio.exe` - it will tell you what is missing, +-- and also the Google Console error, in case you have missed a step in setting up your Google TTS. +-- For example, `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"` +-- plays a message on 255 MHz AM for the blue coalition in-game. +-- +-- ### Google +-- +-- In order to use Google Cloud for TTS you need to use @{#MSRS.SetProvider} and @{MSRS.SetProviderOptionsGoogle} functions: +-- +-- msrs:SetProvider(MSRS.Provider.GOOGLE) +-- msrs:SetProviderOptionsGoogle(CredentialsFile, AccessKey) +-- +-- The parameter `CredentialsFile` is used with the default 'DCS-SR-ExternalAudio.exe' backend and must be the full path to the credentials JSON file. +-- The `AccessKey` parameter is used with the DCS-gRPC backend (see below). +-- +-- You can set the voice to use with Google via @{#MSRS.SetVoiceGoogle}. +-- +-- When using Google it also allows you to utilize SSML in your text for more flexibility. -- For more information on setting up a cloud account, visit: https://cloud.google.com/text-to-speech -- Google's supported SSML reference: https://cloud.google.com/text-to-speech/docs/ssml -- +-- ### Amazon Web Service [Only DCS-gRPC backend] -- --- **Pro-Tipp** - use the command line with power shell to call DCS-SR-ExternalAudio.exe - it will tell you what is missing. --- and also the Google Console error, in case you have missed a step in setting up your Google TTS. --- E.g. `.\DCS-SR-ExternalAudio.exe -t "Text Message" -f 255 -m AM -c 2 -s 2 -z -G "Path_To_You_Google.Json"` --- Plays a message on 255AM for the blue coalition in-game. +-- In order to use Amazon Web Service (ASW) for TTS you need to use @{#MSRS.SetProvider} and @{MSRS.SetProviderOptionsAmazon} functions: -- --- ## Set Voice +-- msrs:SetProvider(MSRS.Provider.AMAZON) +-- msrs:SetProviderOptionsAmazon(AccessKey, SecretKey, Region) +-- +-- The parameters `AccessKey` and `SecretKey` are your ASW access and secret keys, respectively. The parameter `Region` is your [ASW region](https://docs.aws.amazon.com/general/latest/gr/pol.html). -- --- Use a specific voice with the @{#MSRS.SetVoice} function, e.g, `:SetVoice("Microsoft Hedda Desktop")`. --- Note that this must be installed on your windows system. --- If enabling SetGoogle(), you can use voices provided by Google --- Google's supported voices: https://cloud.google.com/text-to-speech/docs/voices --- For voices there are enumerators in this class to help you out on voice names: +-- You can set the voice to use with ASW via @{#MSRS.SetVoiceAmazon}. -- --- MSRS.Voices.Microsoft -- e.g. MSRS.Voices.Microsoft.Hedda - the Microsoft enumerator contains all voices known to work with SRS --- MSRS.Voices.Google -- e.g. MSRS.Voices.Google.Standard.en_AU_Standard_A or MSRS.Voices.Google.Wavenet.de_DE_Wavenet_C - The Google enumerator contains voices for EN, DE, IT, FR and ES. +-- ### Microsoft Azure [Only DCS-gRPC backend] -- --- ## Set Coordinate +-- In order to use Microsoft Azure for TTS you need to use @{#MSRS.SetProvider} and @{MSRS.SetProviderOptionsAzure} functions: -- --- Use @{#MSRS.SetCoordinate} to define the origin from where the transmission is broadcasted. +-- msrs:SetProvider(MSRS.Provider.AZURE) +-- msrs:SetProviderOptionsAmazon(AccessKey, Region) +-- +-- The parameter `AccessKey` is your Azure access key. The parameter `Region` is your [Azure region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- +-- You can set the voice to use with Azure via @{#MSRS.SetVoiceAzure}. +-- +-- ## Backend +-- +-- The default interface to SRS is via calling the 'DCS-SR-ExternalAudio.exe'. As noted above, this has the unavoidable drawback that a pop-up briefly appears +-- and DCS might be put out of focus. +-- +-- ## DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS -- --- ## Set SRS Port --- --- Use @{#MSRS.SetPort} to define the SRS port. Defaults to 5002. --- --- ## Set SRS Volume --- --- Use @{#MSRS.SetVolume} to define the SRS volume. Defaults to 1.0. Allowed values are between 0.0 and 1.0, from silent to loudest. --- --- ## Config file for many variables, auto-loaded by Moose --- --- See @{#MSRS.LoadConfigFile} for details on how to set this up. --- --- ## Set DCS-gRPC as an alternative to 'DCS-SR-ExternalAudio.exe' for TTS +-- Another interface to SRS is [DCS-gRPC](https://github.com/DCS-gRPC/rust-server). This does not call an exe file and therefore avoids the annoying pop-up window. +-- In addition to Windows and Google cloud, it also offers Microsoft Azure and Amazon Web Service as providers for TTS. -- -- Use @{#MSRS.SetDefaultBackendGRPC} to enable [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) as an alternate backend for transmitting text-to-speech over SRS. -- This can be useful if 'DCS-SR-ExternalAudio.exe' cannot be used in the environment, or to use Azure or AWS clouds for TTS. Note that DCS-gRPC does not (yet?) support -- all of the features and options available with 'DCS-SR-ExternalAudio.exe'. Of note, only text-to-speech is supported and it it cannot be used to transmit audio files. -- --- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism --- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua' +-- DCS-gRPC must be installed and configured per the [DCS-gRPC documentation](https://github.com/DCS-gRPC/rust-server) and already running via either the 'autostart' mechanism +-- or a Lua call to 'GRPC.load()' prior to use of the alternate DCS-gRPC backend. If a cloud TTS provider is being used, the API key must be set via the 'Config\dcs-grpc.lua' -- configuration file prior DCS-gRPC being started. DCS-gRPC can be used both with DCS dedicated server and regular DCS installations. --- +-- -- To use the default local Windows TTS with DCS-gRPC, Windows 2019 Server (or newer) or Windows 10/11 are required. Voices for non-local languages and dialects may need to -- be explicitly installed. -- -- To set the MSRS class to use the DCS-gRPC backend for all future instances, call the function `MSRS.SetDefaultBackendGRPC()`. -- --- **Note** - When using other classes that use MSRS with the alternate DCS-gRPC backend, pass them strings instead of nil values for non-applicable fields with filesystem paths, +-- **Note** - When using other classes that use MSRS with the alternate DCS-gRPC backend, pass them strings instead of nil values for non-applicable fields with filesystem paths, -- such as the SRS path or Google credential path. This will help maximize compatibility with other classes that were written for the default backend. -- -- Basic Play Text-To-Speech example using alternate DCS-gRPC backend (DCS-gRPC not previously started): @@ -157,8 +215,8 @@ -- -- Select the alternate DCS-gRPC backend for new MSRS instances -- MSRS.SetDefaultBackendGRPC() -- -- Create a SOUNDTEXT object. --- local text=SOUNDTEXT:New("All Enemies destroyed") --- -- MOOSE SRS +-- local text=SOUNDTEXT:New("All Enemies destroyed") +-- -- MOOSE SRS -- local msrs=MSRS:New('', 305.0) -- -- Text-to speech with default voice after 30 seconds. -- msrs:PlaySoundText(text, 30) @@ -182,26 +240,27 @@ MSRS = { lid = nil, port = 5002, name = "MSRS", + backend = "srsexe", frequencies = {}, modulations = {}, coalition = 0, gender = "female", - culture = nil, + culture = nil, voice = nil, - volume = 1, + volume = 1, speed = 1, coordinate = nil, + provider = "win", Label = "ROBOT", - AltBackend = nil, ConfigFileName = "Moose_MSRS.lua", ConfigFilePath = "Config\\", ConfigLoaded = false, - ttsprovider = "Microsoft", + poptions = {}, } --- MSRS class version. -- @field #string version -MSRS.version="0.1.3" +MSRS.version="0.3.0" --- Voices -- @type MSRS.Voices @@ -292,7 +351,7 @@ MSRS.Voices = { ["de_DE_Wavenet_C"] = "de-DE-Wavenet-C", -- Female ["de_DE_Wavenet_D"] = "de-DE-Wavenet-D", -- Male ["de_DE_Wavenet_E"] = "de-DE-Wavenet-E", -- Male - ["de_DE_Wavenet_F"] = "de-DE-Wavenet-F", -- Female + ["de_DE_Wavenet_F"] = "de-DE-Wavenet-F", -- Female ["es_ES_Wavenet_B"] = "es-ES-Wavenet-B", -- Male ["es_ES_Wavenet_C"] = "es-ES-Wavenet-C", -- Female ["es_ES_Wavenet_D"] = "es-ES-Wavenet-D", -- Female @@ -300,19 +359,44 @@ MSRS.Voices = { ["it_IT_Wavenet_B"] = "it-IT-Wavenet-B", -- Female ["it_IT_Wavenet_C"] = "it-IT-Wavenet-C", -- Male ["it_IT_Wavenet_D"] = "it-IT-Wavenet-D", -- Male - } , + } , }, } ---- --- @type MSRS.ProviderOptions --- @field #string key --- @field #string secret --- @field #string region --- @field #string defaultVoice --- @field #string voice ---- GRPC options +--- Backend options to communicate with SRS. +-- @type MSRS.Backend +-- @field #string SRSEXE Use `DCS-SR-ExternalAudio.exe`. +-- @field #string GRPC Use DCS-gRPC. +MSRS.Backend = { + SRSEXE = "srsexe", + GRPC = "grpc", +} + +--- Text-to-speech providers. These are compatible with the DCS-gRPC conventions. +-- @type MSRS.Provider +-- @field #string WINDOWS Microsoft windows (`win`). +-- @field #string GOOGLE Google (`gcloud`). +-- @field #string AZURE Microsoft Azure (`azure`). Only possible with DCS-gRPC backend. +-- @field #string AMAZON Amazon Web Service (`asw`). Only possible with DCS-gRPC backend. +MSRS.Provider = { + WINDOWS = "win", + GOOGLE = "gcloud", + AZURE = "azure", + AMAZON = "asw", +} + +--- Provider options. +-- @type MSRS.ProviderOptions +-- @field #string provider Provider. +-- @field #string credentials Google credentials JSON file (full path). +-- @field #string key Access key (DCS-gRPC with Google, ASW, AZURE as provider). +-- @field #string secret Secret key (DCS-gRPC with ASW as provider) +-- @field #string region Region. +-- @field #string defaultVoice Default voice (not used). +-- @field #string voice Voice used. + +--- GRPC options. -- @type MSRS.GRPCOptions -- @field #string plaintext -- @field #string srsClientName @@ -324,21 +408,12 @@ MSRS.Voices = { -- @field #MSRS.ProviderOptions aws -- @field #string DefaultProvider -MSRS.GRPCOptions = {} -- #MSRS.GRPCOptions -MSRS.GRPCOptions.gcloud = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.win = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.azure = {} -- #MSRS.ProviderOptions -MSRS.GRPCOptions.aws = {} -- #MSRS.ProviderOptions - -MSRS.GRPCOptions.win.defaultVoice = "Hedda" -MSRS.GRPCOptions.win.voice = "Hedda" - -MSRS.GRPCOptions.DefaultProvider = "win" - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- TODO list ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- DONE: Refactoring of input/config file. +-- DONE: Refactoring gRPC backend. -- TODO: Add functions to remove freqs and modulations. -- DONE: Add coordinate. -- DONE: Add google. @@ -349,77 +424,63 @@ MSRS.GRPCOptions.DefaultProvider = "win" -- Constructor ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Create a new MSRS object. +--- Create a new MSRS object. Required argument is the frequency and modulation. +-- Other parameters are read from the `Moose_MSRS.lua` config file. If you do not have that file set up you must set up and use the `DCS-SR-ExternalAudio.exe` (not DCS-gRPC) as backend, you need to still +-- set the path to the exe file via @{#MSRS.SetPath}. +-- -- @param #MSRS self --- @param #string PathToSRS Path to the directory, where SRS is located. +-- @param #string Path Path to SRS directory. Default `C:\\Program Files\\DCS-SimpleRadio-Standalone`. -- @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. --- @param #number Volume Volume - 1.0 is max, 0.0 is silence --- @param #table AltBackend Optional table containing tables 'Functions' and 'Vars' which add/replace functions and variables for the MSRS instance to allow alternate backends for transmitting to SRS. +-- @param #string Backend Backend used: `MSRS.Backend.SRSEXE` (default) or `MSRS.Backend.GRPC`. -- @return #MSRS self -function MSRS:New(PathToSRS, Frequency, Modulation, Volume, AltBackend) +function MSRS:New(Path, Frequency, Modulation, Backend) -- Defaults. Frequency = Frequency or 143 Modulation = Modulation or radio.modulation.AM - -- Inherit everything from FSM class. + -- Inherit everything from BASE class. local self=BASE:Inherit(self, BASE:New()) -- #MSRS - -- If AltBackend is supplied, initialize it, which will add/replace functions and variables in this MSRS instance. - if type( AltBackend ) == "table" or type( self.AltBackend ) == "table" then + self.lid = string.format("%s-%s | ", "unknown", self.version) - local Backend = UTILS.DeepCopy(AltBackend) or UTILS.DeepCopy(self.AltBackend) - - -- Add parameters to vars so alternate backends can use them if applicable - Backend.Vars = Backend.Vars or {} - Backend.Vars.PathToSRS = PathToSRS - Backend.Vars.Frequency = UTILS.DeepCopy(Frequency) - Backend.Vars.Modulation = UTILS.DeepCopy(Modulation) - Backend.Vars.Volume = Volume - - Backend.Functions = Backend.Functions or {} - - return self:_NewAltBackend(Backend) - end - if not self.ConfigLoaded then - - -- If no AltBackend table, the proceed with default initialisation - self:SetPath(PathToSRS) + + -- Defaults. + self:SetPath(Path) self:SetPort() self:SetFrequencies(Frequency) self:SetModulations(Modulation) self:SetGender() self:SetCoalition() self:SetLabel() - self:SetVolume(Volume) - + self:SetVolume() + else + + -- Default wverwrites from :New() - -- there might be some overwrites from :New() - - if PathToSRS then - self:SetPath(PathToSRS) + if Path then + self:SetPath(Path) end - + if Frequency then - self:SetFrequencies(Frequency) - self:SetModulations(Modulation) + self:SetFrequencies(Frequency) end - if Volume then - self:SetVolume(Volume) + if Modulation then + self:SetModulations(Modulation) end - + end - + self.lid = string.format("%s-%s | ", self.name, self.version) - + if not io or not os then self:E(self.lid.."***** ERROR - io or os NOT desanitized! MSRS will not work!") end - + return self end @@ -427,31 +488,80 @@ end -- User Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Set path to SRS install directory. More precisely, path to where the DCS- +--- Set backend to communicate with SRS. +-- There are two options: +-- +-- - `MSRS.Backend.SRSEXE`: This is the default and uses the `DCS-SR-ExternalAudio.exe`. +-- - `MSRS.Backend.GRPC`: Via DCS-gRPC. +-- -- @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. +-- @param #string Backend Backend used. Default is `MSRS.Backend.SRSEXE`. +-- @return #MSRS self +function MSRS:SetBackend(Backend) + + self.backend=Backend or MSRS.Backend.SRSEXE + + return self +end + +--- Set DCS-gRPC as backend to communicate with SRS. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetBackendGRPC() + + self:SetBackend(MSRS.Backend.GRPC) + + return self +end + +--- Set `DCS-SR-ExternalAudio.exe` as backend to communicate with SRS. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetBackendSRSEXE(Backend) + + self:SetBackend(MSRS.Backend.SRSEXE) + + return self +end + +--- Set the default backend. +-- @param #MSRS self +function MSRS.SetDefaultBackend(Backend) + MSRS.backend=Backend or MSRS.Backend.SRSEXE +end + +--- Set DCS-gRPC to be the default backend. +-- @param #MSRS self +function MSRS.SetDefaultBackendGRPC() + MSRS.backend=MSRS.Backend.GRPC +end + +--- Get currently set backend. +-- @param #MSRS self +-- @return #string Backend. +function MSRS:GetBackend() + return self.backend +end + +--- Set path to SRS install directory. More precisely, path to where the `DCS-SR-ExternalAudio.exe` is located. +-- @param #MSRS self +-- @param #string Path Path to the directory, where the sound file is located. Default is `C:\\Program Files\\DCS-SimpleRadio-Standalone`. -- @return #MSRS self function MSRS:SetPath(Path) - if Path==nil and not self.path then - self:E("ERROR: No path to SRS directory specified!") - return nil + -- Set path. + self.path=Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + + -- 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())) - if Path then - -- 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())) - end return self end @@ -474,7 +584,7 @@ function MSRS:SetVolume(Volume) end --- Get SRS volume. --- @param #MSRS self +-- @param #MSRS self -- @return #number Volume Volume - 1.0 is max, 0.0 is silence function MSRS:GetVolume() return self.volume @@ -535,13 +645,8 @@ end -- @return #MSRS self function MSRS:SetFrequencies(Frequencies) - -- Ensure table. - if type(Frequencies)~="table" then - Frequencies={Frequencies} - end - - self.frequencies=Frequencies - + self.frequencies=UTILS.EnsureTable(Frequencies, false) + return self end @@ -551,21 +656,17 @@ end -- @return #MSRS self function MSRS:AddFrequencies(Frequencies) - -- Ensure table. - if type(Frequencies)~="table" then - Frequencies={Frequencies} - end - - for _,_freq in pairs(Frequencies) do + for _,_freq in pairs(UTILS.EnsureTable(Frequencies, false)) do + self:T(self.lid..string.format("Adding frequency %s", tostring(_freq))) table.insert(self.frequencies,_freq) end - + return self end --- Get frequencies. -- @param #MSRS self --- @param #table Frequencies in MHz. +-- @return #table Frequencies in MHz. function MSRS:GetFrequencies() return self.frequencies end @@ -577,13 +678,12 @@ end -- @return #MSRS self function MSRS:SetModulations(Modulations) - -- Ensure table. - if type(Modulations)~="table" then - Modulations={Modulations} - end - - self.modulations=Modulations - + self.modulations=UTILS.EnsureTable(Modulations, false) + + -- Debug info. + self:T(self.lid.."Modulations:") + self:T(self.modulations) + return self end @@ -593,21 +693,16 @@ end -- @return #MSRS self function MSRS:AddModulations(Modulations) - -- Ensure table. - if type(Modulations)~="table" then - Modulations={Modulations} - end - - for _,_mod in pairs(Modulations) do + for _,_mod in pairs(UTILS.EnsureTable(Modulations, false)) do table.insert(self.modulations,_mod) end - + return self end --- Get modulations. -- @param #MSRS self --- @param #table Modulations. +-- @return #table Modulations. function MSRS:GetModulations() return self.modulations end @@ -617,173 +712,347 @@ end -- @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). +-- @param #string Culture Culture, *e.g.* "en-GB". -- @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. +--- Set to use a specific voice. Note that this will override any gender and culture settings as a voice already has a certain gender/culture. -- @param #MSRS self -- @param #string Voice Voice. -- @return #MSRS self function MSRS:SetVoice(Voice) self.voice=Voice - - --local defaultprovider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider or "win" - - --self.GRPCOptions[defaultprovider].voice = Voice - + return self end ---- Set to use a specific voice. Will override gender and culture settings. +--- Set to use a specific voice for a given provider. Note that this will override any gender and culture settings. -- @param #MSRS self -- @param #string Voice Voice. +-- @param #string Provider Provider. Default is as set by @{#MSRS.SetProvider}, which itself defaults to `MSRS.Provider.WINDOWS` if not set. -- @return #MSRS self -function MSRS:SetDefaultVoice(Voice) - - self.defaultVoice=Voice - local provider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider or "win" - self.GRPCOptions[provider].defaultVoice = Voice +function MSRS:SetVoiceProvider(Voice, Provider) + self.poptions=self.poptions or {} + + self.poptions[Provider or self:GetProvider()]=Voice + return self end ---- Set the coordinate from which the transmissions will be broadcasted. +--- Set to use a specific voice if Microsoft Windows' native TTS is use as provider. Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. Default `"Microsoft Hazel Desktop"`. +-- @return #MSRS self +function MSRS:SetVoiceWindows(Voice) + + self:SetVoiceProvider(Voice or "Microsoft Hazel Desktop", MSRS.Provider.WINDOWS) + + return self +end + +--- Set to use a specific voice if Google is use as provider. Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice Voice. Default `MSRS.Voices.Google.Standard.en_GB_Standard_A`. +-- @return #MSRS self +function MSRS:SetVoiceGoogle(Voice) + + self:SetVoiceProvider(Voice or MSRS.Voices.Google.Standard.en_GB_Standard_A, MSRS.Provider.GOOGLE) + + return self +end + + +--- Set to use a specific voice if Microsoft Azure is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice [Azure Voice](https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support). Default `"en-US-AriaNeural"`. +-- @return #MSRS self +function MSRS:SetVoiceAzure(Voice) + + self:SetVoiceProvider(Voice or "en-US-AriaNeural", MSRS.Provider.AZURE) + + return self +end + +--- Set to use a specific voice if Amazon Web Service is use as provider (only DCS-gRPC backend). Note that this will override any gender and culture settings. +-- @param #MSRS self +-- @param #string Voice [ASW Voice](https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). Default `"Brian"`. +-- @return #MSRS self +function MSRS:SetVoiceAmazon(Voice) + + self:SetVoiceProvider(Voice or "Brian", MSRS.Provider.AMAZON) + + return self +end + +--- Get voice. +-- @param #MSRS self +-- @param #string Provider Provider. Default is the currently set provider (`self.provider`). +-- @return #string Voice. +function MSRS:GetVoice(Provider) + + Provider=Provider or self.provider + + if Provider and self.poptions[Provider] and self.poptions[Provider].voice then + return self.poptions[Provider].voice + else + return self.voice + end + +end + +--- Set the coordinate from which the transmissions will be broadcasted. Note that this is only a factor if SRS has line-of-sight or distance enabled. -- @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 credentials. Also sets Google as default TTS provider. +--- **[Deprecated]** Use google text-to-speech credentials. Also sets Google as default TTS provider. -- @param #MSRS self -- @param #string PathToCredentials Full path to the google credentials JSON file, e.g. "C:\Users\username\Downloads\service-account-file.json". Can also be the Google API key. -- @return #MSRS self function MSRS:SetGoogle(PathToCredentials) - + if PathToCredentials then - - self.google=PathToCredentials - self.APIKey=PathToCredentials - self.provider = "gcloud" + + self.provider = MSRS.Provider.GOOGLE - self.GRPCOptions.DefaultProvider = "gcloud" - self.GRPCOptions.gcloud.key = PathToCredentials - self.ttsprovider = "Google" - + self:SetProviderOptionsGoogle(PathToCredentials, PathToCredentials) + end - + return self end ---- gRPC Backend: Use google text-to-speech set the API key. +--- **[Deprecated]** Use google text-to-speech set the API key (only for DCS-gRPC). -- @param #MSRS self -- @param #string APIKey API Key, usually a string of length 40 with characters and numbers. -- @return #MSRS self function MSRS:SetGoogleAPIKey(APIKey) if APIKey then - self.APIKey=APIKey - self.provider = "gcloud" - self.GRPCOptions.DefaultProvider = "gcloud" - self.GRPCOptions.gcloud.key = APIKey + + self.provider = MSRS.Provider.GOOGLE + + if self.poptions[MSRS.Provider.GOOGLE] then + self.poptions[MSRS.Provider.GOOGLE].key=APIKey + else + self:SetProviderOptionsGoogle(nil ,APIKey) + end + end return self end ---- Use Google text-to-speech as default. + +--- Set provider used to generate text-to-speech. +-- These options are available: +-- +-- - `MSRS.Provider.WINDOWS`: Microsoft Windows (default) +-- - `MSRS.Provider.GOOGLE`: Google Cloud +-- - `MSRS.Provider.AZURE`: Microsoft Azure (only with DCS-gRPC backend) +-- - `MSRS.Provier.AMAZON`: Amazone Web Service (only with DCS-gRPC backend) +-- +-- Note that all providers except Microsoft Windows need as additonal information the credentials of your account. +-- -- @param #MSRS self +-- @param #string Provider -- @return #MSRS self -function MSRS:SetTTSProviderGoogle() - self.ttsprovider = "Google" +function MSRS:SetProvider(Provider) + self.provider = Provider or MSRS.Provider.WINDOWS return self end ---- Use Microsoft text-to-speech as default. + +--- Get provider. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:GetProvider() + return self.provider or MSRS.Provider.WINDOWS +end + +--- Set provider options and credentials. +-- @param #MSRS self +-- @param #string Provider Provider. +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Region to use. +-- @return #MSRS.ProviderOptions Provider optionas table. +function MSRS:SetProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + + local option=MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + + if self then + + self.poptions=self.poptions or {} + + self.poptions[Provider]=option + + else + + MSRS.poptions=MSRS.poptions or {} + + MSRS.poptions[Provider]=option + + end + + return option +end + +--- Create MSRS.ProviderOptions. +-- @param #string Provider Provider. +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Region to use. +-- @return #MSRS.ProviderOptions Provider optionas table. +function MSRS._CreateProviderOptions(Provider, CredentialsFile, AccessKey, SecretKey, Region) + + local option={} --#MSRS.ProviderOptions + + option.provider=Provider + option.credentials=CredentialsFile + option.key=AccessKey + option.secret=SecretKey + option.region=Region + + return option +end + +--- Set provider options and credentials for Google Cloud. +-- @param #MSRS self +-- @param #string CredentialsFile Full path to your credentials file. For Google this is the path to a JSON file. This is used if `DCS-SR-ExternalAudio.exe` is used as backend. +-- @param #string AccessKey Your API access key. This is necessary if DCS-gRPC is used as backend. +-- @return #MSRS self +function MSRS:SetProviderOptionsGoogle(CredentialsFile, AccessKey) + + self:SetProviderOptions(MSRS.Provider.GOOGLE, CredentialsFile, AccessKey) + + return self +end + +--- Set provider options and credentials for Amazon Web Service (ASW). Only supported in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @param #string AccessKey Your API access key. +-- @param #string SecretKey Your secret key. +-- @param #string Region Your ASW [region](https://docs.aws.amazon.com/general/latest/gr/pol.html). +-- @return #MSRS self +function MSRS:SetProviderOptionsAmazon(AccessKey, SecretKey, Region) + + self:SetProviderOptions(MSRS.Provider.AMAZON, nil, AccessKey, SecretKey, Region) + + return self +end + +--- Set provider options and credentials for Microsoft Azure. Only supported in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @param #string AccessKey Your API access key. +-- @param #string Region Your Azure [region](https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- @return #MSRS self +function MSRS:SetProviderOptionsAzure(AccessKey, Region) + + self:SetProviderOptions(MSRS.Provider.AZURE, nil, AccessKey, nil, Region) + + return self +end + + +--- Get provider options. +-- @param #MSRS self +-- @param #string Provider Provider. Default is as set via @{#MSRS.SetProvider}. +-- @return #MSRS.ProviderOptions Provider options. +function MSRS:GetProviderOptions(Provider) + return self.poptions[Provider or self.provider] or {} +end + + +--- Use Google to provide text-to-speech. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderGoogle() + self:SetProvider(MSRS.Provider.GOOGLE) + return self +end + +--- Use Microsoft to provide text-to-speech. -- @param #MSRS self -- @return #MSRS self function MSRS:SetTTSProviderMicrosoft() - self.ttsprovider = "Microsoft" + self:SetProvider(MSRS.Provider.WINDOWS) return self end + +--- Use Microsoft Azure to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderAzure() + self:SetProvider(MSRS.Provider.AZURE) + return self +end + +--- Use Amazon Web Service (ASW) to provide text-to-speech. Only supported if used in combination with DCS-gRPC as backend. +-- @param #MSRS self +-- @return #MSRS self +function MSRS:SetTTSProviderAmazon() + self:SetProvider(MSRS.Provider.AMAZON) + 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 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) + 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 ---- Sets an alternate SRS backend to be used by MSRS to transmit over SRS for all new MSRS class instances. --- @param #table Backend A table containing a table `Functions` with new/replacement class functions and `Vars` with new/replacement variables. --- @return #boolean Returns 'true' on success. -function MSRS.SetDefaultBackend(Backend) - if type(Backend) == "table" then - MSRS.AltBackend = UTILS.DeepCopy(Backend) - else - return false - end - - return true -end - ---- Restores default SRS backend (DCS-SR-ExternalAudio.exe) to be used by all new MSRS class instances to transmit over SRS. --- @return #boolean Returns 'true' on success. -function MSRS.ResetDefaultBackend() - MSRS.AltBackend = nil - return true -end - ---- Sets DCS-gRPC as the default SRS backend for all new MSRS class instances. --- @return #boolean Returns 'true' on success. -function MSRS.SetDefaultBackendGRPC() - return MSRS.SetDefaultBackend(MSRS_BACKEND_DCSGRPC) -end - ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- Transmission Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- @@ -804,13 +1073,13 @@ function MSRS:PlaySoundFile(Soundfile, Delay) -- Get command. local command=self:_GetCommand() - + -- Append file. command=command..' --file="'..tostring(soundfile)..'"' - + -- Execute command. self:_ExecCommand(command) - + end return self @@ -826,16 +1095,24 @@ function MSRS:PlaySoundText(SoundText, Delay) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlaySoundText, self, SoundText, 0) else + + if self.backend==MSRS.Backend.GRPC then + + self:_DCSgRPCtts(SoundText.text, nil, SoundText.gender, SoundText.culture, SoundText.voice, SoundText.volume, SoundText.label, SoundText.coordinate) + + 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) + + end - -- 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) - end return self @@ -852,18 +1129,20 @@ function MSRS:PlayText(Text, Delay, Coordinate) if Delay and Delay>0 then self:ScheduleOnce(Delay, MSRS.PlayText, self, Text, nil, Coordinate) else - - -- Get command line. - local command=self:_GetCommand(nil,nil,nil,nil,nil,nil,nil,nil,nil,nil,Coordinate) - - -- Append text. - command=command..string.format(" --text=\"%s\"", tostring(Text)) - - -- Execute command. - self:_ExecCommand(command) - - end + if self.backend==MSRS.Backend.GRPC then + + self:T(self.lid.."Transmitting") + self:_DCSgRPCtts(Text, nil, nil , nil, nil, nil, nil, Coordinate) + + else + + self:PlayTextExt(Text, Delay, nil, nil, nil, nil, nil, nil, nil, Coordinate) + + end + + end + return self end @@ -886,27 +1165,28 @@ function MSRS:PlayTextExt(Text, Delay, Frequencies, Modulations, Gender, Culture self:ScheduleOnce(Delay, MSRS.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label, Coordinate) else - -- Ensure table. - if Frequencies and type(Frequencies)~="table" then - Frequencies={Frequencies} - end - - -- Ensure table. - if Modulations and type(Modulations)~="table" then - Modulations={Modulations} - end - - -- Get command line. - local command=self:_GetCommand(Frequencies, Modulations, nil, Gender, Voice, Culture, Volume, nil, nil, Label, Coordinate) - - -- Append text. - command=command..string.format(" --text=\"%s\"", tostring(Text)) - - -- Execute command. - self:_ExecCommand(command) - - end + Frequencies = Frequencies or self:GetFrequencies() + Modulations = Modulations or self:GetModulations() + if self.backend==MSRS.Backend.SRSEXE then + + -- Get command line. + local command=self:_GetCommand(UTILS.EnsureTable(Frequencies, false), UTILS.EnsureTable(Modulations, false), nil, Gender, Voice, Culture, Volume, nil, nil, Label, Coordinate) + + -- Append text. + command=command..string.format(" --text=\"%s\"", tostring(Text)) + + -- Execute command. + self:_ExecCommand(command) + + elseif self.backend==MSRS.Backend.GRPC then + + self:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate) + + end + + end + return self end @@ -921,31 +1201,31 @@ 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) + 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. + -- 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 @@ -954,127 +1234,28 @@ end -- Misc Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---- Adds or replaces functions and variables in the current MSRS class instance to enable an alternate backends for transmitting to SRS. --- @param #MSRS self --- @param #table A table containing a table `Functions` with new/replacement class functions and `Vars` with new/replacement variables. --- @return #MSRS self -function MSRS:_NewAltBackend(Backend) - BASE:T('Entering MSRS:_NewAltBackend()') - - -- Add/replace class instance functions with those defined in the alternate backend in Functions table - for funcName,funcDef in pairs(Backend.Functions) do - if type(funcDef) == 'function' then - BASE:T('MSRS (re-)defining function MSRS:' .. funcName ) - self[funcName] = funcDef - end - end - - -- Add/replace class instance variables with those defined in the alternate backend in Vars table - for varName,varVal in pairs(Backend.Vars) do - BASE:T('MSRS setting self.' .. varName) - self[varName] = UTILS.DeepCopy(varVal) - end - - -- If _MSRSbackendInit() is defined in the backend, then run it (it should return self) - if self._MSRSbackendInit and type(self._MSRSbackendInit) == 'function' then - return self:_MSRSbackendInit() - end - - return self -end - ---- 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) - self:T("SRS TTS command="..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) - - elseif false 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('Set oShell = CreateObject ("Wscript.Shell")\n')) - script:write(string.format('Dim strArgs\n')) - script:write(string.format('strArgs = "cmd /c %s"\n', filename)) - script:write(string.format('oShell.Run strArgs, 0, false')) - script:close() - - local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) - - -- Play file in 0.01 seconds - res=os.execute(runvbs) - - 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. +-- @return #number Latitude (or 0 if no input coordinate was given). +-- @return #number Longitude (or 0 if no input coordinate was given). +-- @return #number Altitude (or 0 if no input coordinate was given). function MSRS:_GetLatLongAlt(Coordinate) - - local lat, lon, alt=coord.LOtoLL(Coordinate) - + + local lat=0.0 + local lon=0.0 + local alt=0.0 + + if Coordinate then + lat, lon, alt=coord.LOtoLL(Coordinate) + end + return lat, lon, math.floor(alt) end +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Backend ExternalAudio.exe +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Get SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. -- @param #MSRS self @@ -1090,26 +1271,29 @@ end -- @param #string label Label, defaults to "ROBOT" (displayed sender name in the radio overlay of SRS) - No spaces allowed! -- @param Core.Point#COORDINATE coordinate Coordinate. -- @return #string Command. -function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port,label,coordinate) +function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, speed, port, label, coordinate) + + local path=self:GetPath() or STTS.DIRECTORY - 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 + voice=voice or self:GetVoice(self.provider) or self.voice culture=culture or self.culture volume=volume or self.volume speed=speed or self.speed port=port or self.port label=label or self.Label coordinate=coordinate or self.coordinate - + -- Replace modulation modus=modus:gsub("0", "AM") modus=modus:gsub("1", "FM") - + -- Command. local command=string.format('"%s\\%s" -f "%s" -m "%s" -c %s -p %s -n "%s" -v "%.1f"', path, exe, freqs, modus, coal, port, label,volume) @@ -1127,387 +1311,113 @@ function MSRS:_GetCommand(freqs, modus, coal, gender, voice, culture, volume, sp command=command..string.format(" -l %s", tostring(culture)) end end - + -- Set coordinate. if coordinate then local lat,lon,alt=self:_GetLatLongAlt(coordinate) command=command..string.format(" -L %.4f -O %.4f -A %d", lat, lon, alt) end - - -- Set google. - if self.google and self.ttsprovider == "Google" then - command=command..string.format(' --ssml -G "%s"', self.google) + + -- Set provider options + if self.provider==MSRS.Provider.GOOGLE then + local pops=self:GetProviderOptions() + command=command..string.format(' --ssml -G "%s"', pops.credentials) + elseif self.provider==MSRS.Provider.WINDOWS then + -- Nothing to do. + else + self:E("ERROR: SRS only supports WINWOWS and GOOGLE as TTS providers! Use DCS-gRPC backend for other providers such as ") end - + -- Debug output. self:T("MSRS command="..command) return command end ---- Get central SRS configuration to be able to play tts over SRS radio using the `DCS-SR-ExternalAudio.exe`. +--- Execute SRS command to play sound using the `DCS-SR-ExternalAudio.exe`. -- @param #MSRS self --- @param #string Path Path to config file, defaults to "C:\Users\\Saved Games\DCS\Config" --- @param #string Filename File to load, defaults to "Moose_MSRS.lua" --- @return #boolean success --- @usage --- 0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers, --- and also make config easier to use in the code. --- 1) Create a config file named "Moose_MSRS.lua" at this location "C:\Users\\Saved Games\DCS\Config" (or wherever your Saved Games folder resides). --- 2) The file needs the following structure: --- --- -- Moose MSRS default Config --- MSRS_Config = { --- Path = "C:\\Program Files\\DCS-SimpleRadio-Standalone", -- adjust as needed, note double \\ --- Port = 5002, -- adjust as needed --- Frequency = {127,243}, -- must be a table, 1..n entries! --- Modulation = {0,0}, -- must be a table, 1..n entries, one for each frequency! --- Volume = 1.0, -- 0.0 to 1.0 --- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue --- Coordinate = {0,0,0}, -- x,y,altitude - optional, all in meters --- Culture = "en-GB", --- Gender = "male", --- Google = "C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- path to google json key file - optional. --- Label = "MSRS", --- Voice = "Microsoft Hazel Desktop", --- Provider = "Microsoft", -- this is the default TTS provider, e.g. "Google" or "Microsoft" --- -- gRPC (optional) --- GRPC = { -- see https://github.com/DCS-gRPC/rust-server --- coalition = "blue", -- blue, red, neutral --- DefaultProvider = "gcloud", -- win, gcloud, aws, or azure, some of the values below depend on your cloud provider --- gcloud = { --- key = "", -- for gRPC Google API key --- --secret = "", -- needed for aws --- --region = "",-- needed for aws --- defaultVoice = MSRS.Voices.Google.Standard.en_GB_Standard_F, --- }, --- win = { --- defaultVoice = "Hazel", --- }, --- } --- } --- --- 3) The config file is automatically loaded when Moose starts. YOu can also load the config into the MSRS raw class manually before you do anything else: --- --- MSRS.LoadConfigFile() -- Note the "." here --- --- Optionally, your might want to provide a specific path and filename: --- --- MSRS.LoadConfigFile(nil,MyPath,MyFilename) -- Note the "." here --- --- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()` --- Optionally you can also load this per **single instance** if so needed, i.e. --- --- mysrs:LoadConfigFile(Path,Filename) --- --- 4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples: --- --- -- Needed once only --- MESSAGE.SetMSRS(MSRS.path,nil,MSRS.google,243,radio.modulation.AM,nil,nil, --- MSRS.Voices.Google.Standard.de_DE_Standard_B,coalition.side.BLUE) --- --- -- later on in your code --- --- MESSAGE:New("Test message!",15,"SPAWN"):ToSRS(243,radio.modulation.AM,nil,nil,MSRS.Voices.Google.Standard.fr_FR_Standard_C) --- --- -- Create new ATIS as usual --- atis=ATIS:New(AIRBASE.Caucasus.Batumi, 123, radio.modulation.AM) --- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) --- --Start ATIS --- atis:Start() -function MSRS:LoadConfigFile(Path,Filename) +-- @param #string command Command to executer +-- @return #number Return value of os.execute() command. +function MSRS:_ExecCommand(command) - if lfs == nil then - env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") - return false - end - local path = Path or lfs.writedir()..MSRS.ConfigFilePath - local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" - local pathandfile = path..file - local filexsists = UTILS.FileExists(pathandfile) - - if filexsists and not MSRS.ConfigLoaded then - assert(loadfile(path..file))() - -- now we should have a global var MSRS_Config - if MSRS_Config then - if self then - self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" - self.port = MSRS_Config.Port or 5002 - self.frequencies = MSRS_Config.Frequency or {127,243} - self.modulations = MSRS_Config.Modulation or {0,0} - self.coalition = MSRS_Config.Coalition or 0 - if MSRS_Config.Coordinate then - self.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1],MSRS_Config.Coordinate[2],MSRS_Config.Coordinate[3]) - end - self.culture = MSRS_Config.Culture or "en-GB" - self.gender = MSRS_Config.Gender or "male" - self.google = MSRS_Config.Google - if MSRS_Config.Provider then - self.ttsprovider = MSRS_Config.Provider - end - self.Label = MSRS_Config.Label or "MSRS" - self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel - if MSRS_Config.GRPC then - self.provider = MSRS_Config.GRPC.DefaultProvider - if MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider] then - self.APIKey = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].key - self.defaultVoice = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].defaultVoice - self.region = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].secret - self.secret = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].region - end - end - self.ConfigLoaded = true - else - MSRS.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" - MSRS.port = MSRS_Config.Port or 5002 - MSRS.frequencies = MSRS_Config.Frequency or {127,243} - MSRS.modulations = MSRS_Config.Modulation or {0,0} - MSRS.coalition = MSRS_Config.Coalition or 0 - if MSRS_Config.Coordinate then - MSRS.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1],MSRS_Config.Coordinate[2],MSRS_Config.Coordinate[3]) - end - MSRS.culture = MSRS_Config.Culture or "en-GB" - MSRS.gender = MSRS_Config.Gender or "male" - MSRS.google = MSRS_Config.Google - if MSRS_Config.Provider then - MSRS.ttsprovider = MSRS_Config.Provider - end - MSRS.Label = MSRS_Config.Label or "MSRS" - MSRS.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel - if MSRS_Config.GRPC then - MSRS.provider = MSRS_Config.GRPC.DefaultProvider - if MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider] then - MSRS.APIKey = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].key - MSRS.defaultVoice = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].defaultVoice - MSRS.region = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].secret - MSRS.secret = MSRS_Config.GRPC[MSRS_Config.GRPC.DefaultProvider].region - end - end - MSRS.ConfigLoaded = true - end - end - env.info("MSRS - Successfully loaded default configuration from disk!",false) - end - if not filexsists then - env.info("MSRS - Cannot find default configuration file!",false) - return false - end - - return true -end + -- Debug info. + self:T("SRS TTS command="..command) -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + -- Create a tmp file. + local filename=os.getenv('TMP').."\\MSRS-"..STTS.uuid()..".bat" ---- Alternate backend for MSRS to enable text-to-speech via DCS-gRPC. --- ### Author: **dogjutsu** --- A table containing functions and variables for MSRS to use DCS-gRPC [DCS-gRPC](https://github.com/DCS-gRPC/rust-server) 0.7.0 or newer as a backend to transmit over SRS. --- This is not a standalone class. Instead, variables and functions under the `Vars` and `Functions` tables get added to or replace MSRS variables/functions when activated. --- --- @type MSRS_BACKEND_DCSGRPC --- @field #number version Version number of this alternate backend. --- @field #table Functions A table of functions that will add or replace the default MSRS class functions. --- @field #table Vars A table of variables that will add or replace the default MSRS class variables. -MSRS_BACKEND_DCSGRPC = {} -MSRS_BACKEND_DCSGRPC.version = 0.1 + local script=io.open(filename, "w+") + script:write(command.." && exit") + script:close() -MSRS_BACKEND_DCSGRPC.Functions = {} -MSRS_BACKEND_DCSGRPC.Vars = { provider = 'win' } + -- Play command. + command=string.format('start /b "" "%s"', filename) ---- Called by @{#MSRS._NewAltBackend} (if present) immediately after an alternate backend functions and variables for MSRS are added/replaced. --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions._MSRSbackendInit = function (self) - BASE:I('Loaded MSRS DCS-gRPC alternate backend version ' .. self.AltBackend.version or 'unspecified') + local res=nil + if true then - return self -end + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend User Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + -- 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() ---- No-op replacement function for @{#MSRS.SetPath} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetPath = function (self) - return self -end + -- 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) ---- No-op replacement function for @{#MSRS.GetPath} (Not Applicable) --- @param #MSRS self --- @return #string Empty string -MSRS_BACKEND_DCSGRPC.Functions.GetPath = function (self) - return '' -end + -- Debug output. + self:T("MSRS execute command="..command) + self:T("MSRS execute VBS command="..runvbs) ---- No-op replacement function for @{#MSRS.SetVolume} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetVolume = function (self) - BASE:I('NOTE: MSRS:SetVolume() not used with DCS-gRPC backend.') - return self -end + -- Play file in 0.01 seconds + res=os.execute(runvbs) ---- No-op replacement function for @{#MSRS.GetVolume} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.GetVolume = function (self) - BASE:I('NOTE: MSRS:GetVolume() not used with DCS-gRPC backend.') - return 1 -end + -- Remove file in 1 second. + timer.scheduleFunction(os.remove, filename, timer.getTime()+1) + timer.scheduleFunction(os.remove, filenvbs, timer.getTime()+1) ---- No-op replacement function for @{#MSRS.SetGender} (Not Applicable) --- @param #MSRS self --- #string Gender Gender: "male" or "female" --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetGender = function (self, Gender) - -- Use DCS-gRPC default if not specified + elseif false then - if Gender then - self.gender=Gender:lower() - end - - -- Debug output. - self:T("Setting gender to "..tostring(self.gender)) - return self -end + -- Create a tmp file. + local filenvbs = os.getenv('TMP') .. "\\MSRS-"..STTS.uuid()..".vbs" ---- Replacement function for @{#MSRS.SetGoogle} to use google text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetGoogle = function (self) - self.provider = 'gcloud' - return self -end + -- VBS script + local script = io.open(filenvbs, "w+") + script:write(string.format('Set oShell = CreateObject ("Wscript.Shell")\n')) + script:write(string.format('Dim strArgs\n')) + script:write(string.format('strArgs = "cmd /c %s"\n', filename)) + script:write(string.format('oShell.Run strArgs, 0, false')) + script:close() ---- Replacement function for @{#MSRS.SetGoogle} to use google text-to-speech - here: Set the API key --- @param #MSRS self --- @param #string key --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetAPIKey = function (self, key) - self.APIKey = key - return self -end + local runvbs=string.format('cscript.exe //Nologo //B "%s"', filenvbs) ---- Replacement function for @{#MSRS.SetGoogle} to use google text-to-speech - here: Set the API key --- @param #MSRS self --- @param #string voice --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetDefaultVoice = function (self, voice) - self.defaultVoice = voice - return self -end + -- Play file in 0.01 seconds + res=os.execute(runvbs) ---- MSRS:SetAWS() Use AWS text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetAWS = function (self) - self.provider = 'aws' - return self -end - ---- MSRS:SetAzure() Use Azure text-to-speech. (API key set as part of DCS-gRPC configuration) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetAzure = function (self) - self.provider = 'azure' - return self -end - ---- MSRS:SetWin() Use local Windows OS text-to-speech (Windows Server 2019 / Windows 11 / Windows 10? or newer). (Default) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.SetWin = function (self) - self.provider = 'win' - return self -end - ---- Replacement function for @{#MSRS.Help} to display help. --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.Help = function (self) - env.info('For DCS-gRPC help, please see: https://github.com/DCS-gRPC/rust-server') - return self -end - -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend Transmission Functions -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- - ---- No-op replacement function for @{#MSRS.PlaySoundFile} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlaySoundFile = function (self) - BASE:E("ERROR: MSRS:PlaySoundFile() is not supported by the DCS-gRPC backend.") - return self -end - ---- Replacement function for @{#MSRS.PlaySoundText} --- @param #MSRS self --- @param Sound.SoundOutput#SOUNDTEXT SoundText Sound text. --- @param #number Delay Delay in seconds, before the sound file is played. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlaySoundText = function (self, SoundText, Delay) - - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlaySoundText, self, SoundText, 0) else - self:_DCSgRPCtts(tostring(SoundText.text)) + + -- 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 self -end ---- Replacement function for @{#MSRS.PlayText} --- @param #MSRS self --- @param #string Text Text message. --- @param #number Delay Delay in seconds, before the message is played. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayText = function (self, Text, Delay) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlayText, self, Text, 0) - else - self:_DCSgRPCtts(tostring(Text)) - end - - return self -end - ---- Replacement function for @{#MSRS.PlayText} --- @param #MSRS self --- @param #string Text Text message. --- @param #number Delay Delay in seconds, before the message is played. --- @param #table Frequencies Radio frequencies. --- @param #table Modulations Radio modulations. (Non-functional, DCS-gRPC sets automatically) --- @param #string Gender Gender. (Non-functional, only 'Voice' supported) --- @param #string Culture Culture. (Non-functional, only 'Voice' supported) --- @param #string Voice Voice. --- @param #number Volume Volume. (Non-functional, all transmissions full volume with DCS-gRPC) --- @param #string Label Label. --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayTextExt = function (self, Text, Delay, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) - if Delay and Delay>0 then - self:ScheduleOnce(Delay, self.PlayTextExt, self, Text, 0, Frequencies, Modulations, Gender, Culture, Voice, Volume, Label) - else - self:_DCSgRPCtts(tostring(Text), nil, Frequencies, Voice, Label) - end - - return self -end - ---- No-op replacement function for @{#MSRS.PlayTextFile} (Not Applicable) --- @param #MSRS self --- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions.PlayTextFile = function (self, TextFile, Delay) - BASE:E("ERROR: MSRS:PlayTextFile() is not supported by the DCS-gRPC backend.") - return self + return res end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- MSRS DCS-gRPC alternate backend Misc Functions +-- DCS-gRPC Backend Functions ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- -- DCS-gRPC v0.70 TTS API call: @@ -1548,101 +1458,239 @@ end --- Make DCS-gRPC API call to transmit text-to-speech over SRS. -- @param #MSRS self -- @param #string Text Text of message to transmit (can also be SSML). --- @param #string Optional plaintext version of message (for accessiblity) -- @param #table Frequencies Radio frequencies to transmit on. Can also accept a number in MHz. --- @param #string Voice Voice for the TTS provider to user. --- @param #string Label Label (SRS diplays as name of the transmitter). +-- @param #string Gender Gender. +-- @param #string Culture Culture. +-- @param #string Voice Voice. +-- @param #number Volume Volume. +-- @param #string Label Label. +-- @param Core.Point#COORDINATE Coordinate Coordinate. -- @return #MSRS self -MSRS_BACKEND_DCSGRPC.Functions._DCSgRPCtts = function (self, Text, Plaintext, Frequencies, Voice, Label) +function MSRS:_DCSgRPCtts(Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate) - BASE:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") - BASE:T({Text, Plaintext, Frequencies, Voice, Label}) + -- Debug info. + self:T("MSRS_BACKEND_DCSGRPC:_DCSgRPCtts()") + self:T({Text, Frequencies, Gender, Culture, Voice, Volume, Label, Coordinate}) - local options = self.ProviderOptions or MSRS.ProviderOptions or {} -- #MSRS.GRPCOptions - local ssml = Text or '' + local options = {} -- #MSRS.GRPCOptions + + local ssml = Text or '' + + -- Get frequenceies. + Frequencies = UTILS.EnsureTable(Frequencies, true) or self:GetFrequencies() + + -- Plain text (not really used. + options.plaintext=Text + + -- Name shows as sender. + options.srsClientName = Label or self.Label - local XmitFrequencies = Frequencies or self.Frequency - if type(XmitFrequencies)~="table" then - XmitFrequencies={XmitFrequencies} - end - - options.plaintext = Plaintext - options.srsClientName = Label or self.Label + -- Set position. + if self.coordinate then options.position = {} - if self.coordinate then - options.position.lat, options.position.lon, options.position.alt = self:_GetLatLongAlt(self.coordinate) + options.position.lat, options.position.lon, options.position.alt = self:_GetLatLongAlt(self.coordinate) + end + + -- Coalition (gRPC expects lower case) + options.coalition = UTILS.GetCoalitionName(self.coalition):lower() + + -- Provider (win, gcloud, ...) + local provider = self.provider or MSRS.Provider.WINDOWS + + -- Provider options: voice, credentials + options.provider = {} + options.provider[provider] = self:GetProviderOptions(provider) + + -- Voice + Voice=Voice or self:GetVoice(self.provider) or self.voice + + if Voice then + -- We use a specific voice + options.provider[provider].voice = Voice + else + -- DCS-gRPC doesn't directly support language/gender, but can use SSML + + local preTag, genderProp, langProp, postTag = '', '', '', '' + + local gender="" + if self.gender then + gender=string.format(' gender=\"\%s\"', self.gender) + end + local language="" + if self.culture then + language=string.format(' language=\"\%s\"', self.culture) end - options.position.lat = options.position.lat or 0.0 - options.position.lon = options.position.lon or 0.0 - options.position.alt = options.position.alt or 0.0 + if self.gender or self.culture then + ssml=string.format("%s", gender, language, Text) + end + end + + for _,freq in pairs(Frequencies) do + self:T("GRPC.tts") + self:T(ssml) + self:T(freq) + self:T(options) + GRPC.tts(ssml, freq*1e6, options) + end - if UTILS.GetCoalitionName(self.coalition) == 'Blue' then - options.coalition = 'blue' - elseif UTILS.GetCoalitionName(self.coalition) == 'Red' then - options.coalition = 'red' - end - - local provider = self.provider or self.GRPCOptions.DefaultProvider or MSRS.GRPCOptions.DefaultProvider - - options.provider = {} - - options.provider[provider] = {} - - if self.APIKey then - options.provider[provider].key = self.APIKey - end - - if self.defaultVoice then - options.provider[provider].defaultVoice = self.defaultVoice - end - - if self.voice then - options.provider[provider].voice = Voice or self.voice or self.defaultVoice - elseif ssml then - -- DCS-gRPC doesn't directly support language/gender, but can use SSML - -- Only use if a voice isn't explicitly set - local preTag, genderProp, langProp, postTag = '', '', '', '' +end - if self.gender then - genderProp = ' gender=\"' .. self.gender .. '\"' +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- +-- Config File +------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- + +--- Get central SRS configuration to be able to play tts over SRS radio using the `DCS-SR-ExternalAudio.exe`. +-- @param #MSRS self +-- @param #string Path Path to config file, defaults to "C:\Users\\Saved Games\DCS\Config" +-- @param #string Filename File to load, defaults to "Moose_MSRS.lua" +-- @return #boolean success +-- @usage +-- 0) Benefits: Centralize configuration of SRS, keep paths and keys out of the mission source code, making it safer and easier to move missions to/between servers, +-- and also make config easier to use in the code. +-- 1) Create a config file named "Moose_MSRS.lua" at this location "C:\Users\\Saved Games\DCS\Config" (or wherever your Saved Games folder resides). +-- 2) The file needs the following structure: +-- +-- -- Moose MSRS default Config +-- MSRS_Config = { +-- Path = C:\\Program Files\\DCS-SimpleRadio-Standalone, -- Path to SRS install directory. +-- Port = 5002, -- Port of SRS server. Default 5002. +-- Backend = "srsexe", -- Interface to SRS: "srsexe" or "grpc". +-- Frequency = {127, 243}, -- Default frequences. Must be a table 1..n entries! +-- Modulation = {0,0}, -- Default modulations. Must be a table, 1..n entries, one for each frequency! +-- Volume = 1.0, -- Default volume [0,1]. +-- Coalition = 0, -- 0 = Neutral, 1 = Red, 2 = Blue (only a factor if SRS server has encryption enabled). +-- Coordinate = {0,0,0}, -- x, y, alt (only a factor if SRS server has line-of-sight and/or distance limit enabled). +-- Culture = "en-GB", +-- Gender = "male", +-- Voice = "Microsoft Hazel Desktop", -- Voice that is used if no explicit provider voice is specified. +-- Label = "MSRS", +-- Provider = "win", --Provider for generating TTS (win, gcloud, azure, asw). +-- +-- -- Windows +-- win = { +-- voice = "Microsoft Hazel Desktop", +-- }, +-- -- Google Cloud +-- gcloud = { +-- voice = "en-GB-Standard-A", -- The Google Cloud voice to use (see https://cloud.google.com/text-to-speech/docs/voices). +-- credentials="C:\\Program Files\\DCS-SimpleRadio-Standalone\\yourfilename.json", -- Full path to credentials JSON file (only for SRS-TTS.exe backend) +-- key="Your access Key", -- Google API access key (only for DCS-gRPC backend) +-- }, +-- -- Amazon Web Service +-- aws = { +-- voice = "Brian", -- The default AWS voice to use (see https://docs.aws.amazon.com/polly/latest/dg/voicelist.html). +-- key="Your access Key", -- Your AWS key. +-- secret="Your secret key", -- Your AWS secret key. +-- region="eu-central-1", -- Your AWS region (see https://docs.aws.amazon.com/general/latest/gr/pol.html). +-- }, +-- -- Microsoft Azure +-- azure = { +-- voice="en-US-AriaNeural", --The default Azure voice to use (see https://learn.microsoft.com/azure/cognitive-services/speech-service/language-support). +-- key="Your access key", -- Your Azure access key. +-- region="westeurope", -- The Azure region to use (see https://learn.microsoft.com/en-us/azure/cognitive-services/speech-service/regions). +-- }, +-- } +-- +-- 3) The config file is automatically loaded when Moose starts. YOu can also load the config into the MSRS raw class manually before you do anything else: +-- +-- MSRS.LoadConfigFile() -- Note the "." here +-- +-- Optionally, your might want to provide a specific path and filename: +-- +-- MSRS.LoadConfigFile(nil,MyPath,MyFilename) -- Note the "." here +-- +-- This will populate variables for the MSRS raw class and all instances you create with e.g. `mysrs = MSRS:New()` +-- Optionally you can also load this per **single instance** if so needed, i.e. +-- +-- mysrs:LoadConfigFile(Path,Filename) +-- +-- 4) Use the config in your code like so, variable names are basically the same as in the config file, but all lower case, examples: +-- +-- -- Needed once only +-- MESSAGE.SetMSRS(MSRS.path,nil,MSRS.google,243,radio.modulation.AM,nil,nil, +-- MSRS.Voices.Google.Standard.de_DE_Standard_B,coalition.side.BLUE) +-- +-- -- later on in your code +-- +-- MESSAGE:New("Test message!",15,"SPAWN"):ToSRS(243,radio.modulation.AM,nil,nil,MSRS.Voices.Google.Standard.fr_FR_Standard_C) +-- +-- -- Create new ATIS as usual +-- atis=ATIS:New(AIRBASE.Caucasus.Batumi, 123, radio.modulation.AM) +-- atis:SetSRS(nil,nil,nil,MSRS.Voices.Google.Standard.en_US_Standard_H) +-- --Start ATIS +-- atis:Start() +function MSRS:LoadConfigFile(Path,Filename) + + if lfs == nil then + env.info("*****Note - lfs and os need to be desanitized for MSRS to work!") + return false + end + + local path = Path or lfs.writedir()..MSRS.ConfigFilePath + local file = Filename or MSRS.ConfigFileName or "Moose_MSRS.lua" + local pathandfile = path..file + local filexsists = UTILS.FileExists(pathandfile) + + if filexsists and not MSRS.ConfigLoaded then + + env.info("FF reading config file") + + -- Load global MSRS_Config + assert(loadfile(path..file))() + + if MSRS_Config then + + local Self = self or MSRS --#MSRS + + Self.path = MSRS_Config.Path or "C:\\Program Files\\DCS-SimpleRadio-Standalone" + Self.port = MSRS_Config.Port or 5002 + Self.backend = MSRS_Config.Backend or MSRS.Backend.SRSEXE + Self.frequencies = MSRS_Config.Frequency or {127,243} + Self.modulations = MSRS_Config.Modulation or {0,0} + Self.coalition = MSRS_Config.Coalition or 0 + if MSRS_Config.Coordinate then + Self.coordinate = COORDINATE:New( MSRS_Config.Coordinate[1], MSRS_Config.Coordinate[2], MSRS_Config.Coordinate[3] ) end - if self.culture then - langProp = ' language=\"' .. self.culture .. '\"' + Self.culture = MSRS_Config.Culture or "en-GB" + Self.gender = MSRS_Config.Gender or "male" + Self.Label = MSRS_Config.Label or "MSRS" + Self.voice = MSRS_Config.Voice --or MSRS.Voices.Microsoft.Hazel + + Self.provider = MSRS_Config.Provider or MSRS.Provider.WINDOWS + for _,provider in pairs(MSRS.Provider) do + if MSRS_Config[provider] then + Self.poptions[provider]=MSRS_Config[provider] + end end - - if self.culture or self.gender then - preTag = '' - postTag = '' - ssml = preTag .. Text .. postTag - end - - end - - for _,_freq in ipairs(XmitFrequencies) do - local freq = _freq*1000000 - BASE:T("GRPC.tts") - BASE:T(ssml) - BASE:T(freq) - BASE:T({options}) - GRPC.tts(ssml, freq, options) + + Self.ConfigLoaded = true + end + env.info("MSRS - Successfully loaded default configuration from disk!",false) + end + + if not filexsists then + env.info("MSRS - Cannot find default configuration file!",false) + return false + end + return true end ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- --- Manages radio transmissions. --- +-- -- The purpose of the MSRSQUEUE class is to manage SRS text-to-speech (TTS) messages using the MSRS class. -- This can be used to submit multiple TTS messages and the class takes care that they are transmitted one after the other (and not overlapping). --- +-- -- @type MSRSQUEUE -- @field #string ClassName Name of the class "MSRSQUEUE". -- @field #string lid ID for dcs.log. -- @field #table queue The queue of transmissions. -- @field #string alias Name of the radio queue. --- @field #number dt Time interval in seconds for checking the radio queue. +-- @field #number dt Time interval in seconds for checking the radio queue. -- @field #number Tlast Time (abs) when the last transmission finished. -- @field #boolean checking If `true`, the queue update function is scheduled to be called again. -- @extends Core.Base#BASE @@ -1688,13 +1736,13 @@ function MSRSQUEUE:New(alias) -- Inherit base local self=BASE:Inherit(self, BASE:New()) --#MSRSQUEUE - + self.alias=alias or "My Radio" - + self.dt=1.0 - + self.lid=string.format("MSRSQUEUE %s | ", self.alias) - + return self end @@ -1710,17 +1758,17 @@ end --- Add a transmission to the radio queue. -- @param #MSRSQUEUE self --- @param #MSRSQUEUE.Transmission transmission The transmission data table. +-- @param #MSRSQUEUE.Transmission transmission The transmission data table. -- @return #MSRSQUEUE self function MSRSQUEUE:AddTransmission(transmission) - + -- Init. transmission.isplaying=false transmission.Tstarted=nil -- Add to queue. table.insert(self.queue, transmission) - + -- Start checking. if not self.checking then self:_CheckRadioQueue() @@ -1766,13 +1814,13 @@ end -- @param Core.Point#COORDINATE coordinate Coordinate to be used -- @return #MSRSQUEUE.Transmission Radio transmission table. function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgroups, subtitle, subduration, frequency, modulation, gender, culture, voice, volume, label,coordinate) - + if self.TransmitOnlyWithPlayers then if self.PlayerSet and self.PlayerSet:CountAlive() == 0 then return self end end - + -- Sanity checks. if not text then self:E(self.lid.."ERROR: No text specified.") @@ -1780,10 +1828,10 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr end if type(text)~="string" then self:E(self.lid.."ERROR: Text specified is NOT a string.") - return nil + return nil end - + -- Create a new transmission object. local transmission={} --#MSRSQUEUE.Transmission transmission.text=text @@ -1806,10 +1854,10 @@ function MSRSQUEUE:NewTransmission(text, duration, msrs, tstart, interval, subgr transmission.volume = volume transmission.label = label transmission.coordinate = coordinate - - -- Add transmission to queue. + + -- Add transmission to queue. self:AddTransmission(transmission) - + return transmission end @@ -1823,27 +1871,27 @@ function MSRSQUEUE:Broadcast(transmission) else transmission.msrs:PlayText(transmission.text,nil,transmission.coordinate) end - + local function texttogroup(gid) -- Text to group. - trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true) + trigger.action.outTextForGroup(gid, transmission.subtitle, transmission.subduration, true) end - + if transmission.subgroups and #transmission.subgroups>0 then - + for _,_group in pairs(transmission.subgroups) do local group=_group --Wrapper.Group#GROUP - + if group and group:IsAlive() then local gid=group:GetID() - - self:ScheduleOnce(4, texttogroup, gid) + + self:ScheduleOnce(4, texttogroup, gid) end - + end - + end - + end --- Calculate total transmission duration of all transmission in the queue. @@ -1856,18 +1904,18 @@ function MSRSQUEUE:CalcTransmisstionDuration() local T=0 for _,_transmission in pairs(self.queue) do local transmission=_transmission --#MSRSQUEUE.Transmission - + if transmission.isplaying then - + -- Playing for dt seconds. local dt=Tnow-transmission.Tstarted - + T=T+transmission.duration-dt - + else T=T+transmission.duration end - + end return T @@ -1878,146 +1926,146 @@ end -- @param #number delay Delay in seconds before checking. function MSRSQUEUE:_CheckRadioQueue(delay) - -- Transmissions in queue. + -- Transmissions in queue. local N=#self.queue -- Debug info. self:T2(self.lid..string.format("Check radio queue %s: delay=%.3f sec, N=%d, checking=%s", self.alias, delay or 0, N, tostring(self.checking))) - + if delay and delay>0 then - + -- Delayed call. self:ScheduleOnce(delay, MSRSQUEUE._CheckRadioQueue, self) - + -- Checking on. self.checking=true - + else -- Check if queue is empty. if N==0 then - + -- Debug info. self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) - + -- Queue is now empty. Nothing to else to do. We start checking again, if a transmission is added. self.checking=false - + return end -- Get current abs time. local time=timer.getAbsTime() - + -- Checking on. self.checking=true - + -- Set dt. local dt=self.dt - - + + local playing=false local next=nil --#MSRSQUEUE.Transmission local remove=nil for i,_transmission in ipairs(self.queue) do local transmission=_transmission --#MSRSQUEUE.Transmission - + -- Check if transmission time has passed. - if time>=transmission.Tplay then - + if time>=transmission.Tplay then + -- Check if transmission is currently playing. if transmission.isplaying then - + -- Check if transmission is finished. if time>=transmission.Tstarted+transmission.duration then - + -- Transmission over. transmission.isplaying=false - + -- Remove ith element in queue. remove=i - + -- Store time last transmission finished. self.Tlast=time - + else -- still playing - + -- Transmission is still playing. playing=true - + dt=transmission.duration-(time-transmission.Tstarted) - + end - + else -- not playing yet - + local Tlast=self.Tlast - + if transmission.interval==nil then - + -- Not playing ==> this will be next. if next==nil then next=transmission end - + else - + if Tlast==nil or time-Tlast>=transmission.interval then - next=transmission + next=transmission else - + end end - + -- We got a transmission or one with an interval that is not due yet. No need for anything else. if next or Tlast then break end - + end - + else - + -- Transmission not due yet. - - end + + end end - + -- Found a new transmission. if next~=nil and not playing then -- Debug info. self:T(self.lid..string.format("Broadcasting text=\"%s\" at T=%.3f", next.text, time)) - + -- Call SRS. self:Broadcast(next) - + next.isplaying=true next.Tstarted=time dt=next.duration end - + -- Remove completed call from queue. if remove then -- Remove from queue. table.remove(self.queue, remove) N=N-1 - + -- Check if queue is empty. if #self.queue==0 then -- Debug info. self:T(self.lid..string.format("Check radio queue %s empty ==> disable checking", self.alias)) - + self.checking=false - + return end end - + -- Check queue. self:_CheckRadioQueue(dt) - + end - + end MSRS.LoadConfigFile()