Skip to content

Commit 3703cf0

Browse files
committed
✨ parse audio settings
semver: minor
1 parent 3c8f62d commit 3703cf0

14 files changed

Lines changed: 516 additions & 42 deletions

File tree

EliteAPI.Tests/AudioTests.cs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using EliteAPI.Options.Audio;
2+
using FluentAssertions;
3+
4+
namespace EliteAPI.Tests.Audio;
5+
6+
public class AudioParserTests
7+
{
8+
[Fact]
9+
public void Parse_Sample_File()
10+
{
11+
string xml = File.ReadAllText("TestFiles/Audio/Custom.4.3.audio");
12+
13+
var content = AudioParser.Parse(xml);
14+
15+
content.Should().NotBeNull();
16+
content.Should().NotBeEmpty();
17+
}
18+
19+
[Fact]
20+
public void Parse_Should_Read_Volume_Settings()
21+
{
22+
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8"" ?>
23+
<Root PresetName=""Custom"" MajorVersion=""4"" MinorVersion=""3"">
24+
<SoundEffectsVolume Value=""83.03371429"" Muted=""0"" />
25+
<MusicVolume Value=""35.53370667"" Muted=""1"" />
26+
</Root>";
27+
28+
var settings = AudioParser.Parse(xml).ToDictionary(s => s.Name);
29+
30+
settings.Should().ContainKeys("SoundEffectsVolume", "MusicVolume");
31+
32+
var effects = settings["SoundEffectsVolume"];
33+
effects.IsVolume.Should().BeTrue();
34+
effects.IsToggle.Should().BeFalse();
35+
effects.Value.Should().BeApproximately(83.03f, 0.01f);
36+
effects.IsMuted.Should().BeFalse();
37+
effects.IsEnabled.Should().BeNull();
38+
39+
var music = settings["MusicVolume"];
40+
music.IsVolume.Should().BeTrue();
41+
music.Value.Should().BeApproximately(35.53f, 0.01f);
42+
music.IsMuted.Should().BeTrue();
43+
}
44+
45+
[Fact]
46+
public void Parse_Should_Read_Toggle_Settings()
47+
{
48+
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8"" ?>
49+
<Root>
50+
<CombatMusicEnabled Enabled=""1"" />
51+
<StreamerModeEnabled Enabled=""0"" />
52+
</Root>";
53+
54+
var settings = AudioParser.Parse(xml).ToDictionary(s => s.Name);
55+
56+
var combat = settings["CombatMusicEnabled"];
57+
combat.IsToggle.Should().BeTrue();
58+
combat.IsVolume.Should().BeFalse();
59+
combat.IsEnabled.Should().BeTrue();
60+
combat.Value.Should().BeNull();
61+
combat.IsMuted.Should().BeNull();
62+
63+
var streamer = settings["StreamerModeEnabled"];
64+
streamer.IsToggle.Should().BeTrue();
65+
streamer.IsEnabled.Should().BeFalse();
66+
}
67+
68+
[Fact]
69+
public void Parse_Should_Skip_Unknown_Element_Shapes()
70+
{
71+
const string xml = @"<?xml version=""1.0"" encoding=""UTF-8"" ?>
72+
<Root>
73+
<MusicVolume Value=""50"" Muted=""0"" />
74+
<SomeUnknownThing Foo=""bar"" />
75+
<CombatMusicEnabled Enabled=""1"" />
76+
</Root>";
77+
78+
var settings = AudioParser.Parse(xml).ToArray();
79+
80+
settings.Should().HaveCount(2);
81+
settings.Select(s => s.Name).Should().NotContain("SomeUnknownThing");
82+
}
83+
84+
[Fact]
85+
public void Parse_Should_Return_Empty_For_Malformed_Xml()
86+
{
87+
var settings = AudioParser.Parse("not valid xml <<<");
88+
89+
settings.Should().NotBeNull();
90+
settings.Should().BeEmpty();
91+
}
92+
93+
[Fact]
94+
public void Parse_Real_Preset_Contains_Known_Settings()
95+
{
96+
string xml = File.ReadAllText("TestFiles/Audio/Custom.4.3.audio");
97+
98+
var settings = AudioParser.Parse(xml).ToDictionary(s => s.Name);
99+
100+
settings.Should().ContainKey("MusicVolume");
101+
settings.Should().ContainKey("SoundEffectsVolume");
102+
settings.Should().ContainKey("CombatMusicEnabled");
103+
104+
settings["MusicVolume"].IsVolume.Should().BeTrue();
105+
settings["MusicVolume"].IsMuted.Should().BeTrue();
106+
settings["SoundEffectsVolume"].IsMuted.Should().BeFalse();
107+
settings["CombatMusicEnabled"].IsEnabled.Should().BeTrue();
108+
}
109+
}

EliteAPI.Tests/BindingTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using EliteAPI.Bindings;
1+
using EliteAPI.Options.Bindings;
22
using FluentAssertions;
33

44
namespace EliteAPI.Tests.Bindings;

EliteAPI.Tests/EliteAPI.Tests.csproj

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
<None Update="TestFiles\Bindings\test.binds">
3030
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
3131
</None>
32+
<None Update="TestFiles\Audio\Custom.4.3.audio">
33+
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
34+
</None>
3235
</ItemGroup>
3336

3437
</Project>
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
<?xml version="1.0" encoding="UTF-8" ?>
2+
<Root PresetName="Custom" MajorVersion="4" MinorVersion="3">
3+
<SoundEffectsVolume Value="83.03371429" Muted="0" />
4+
<MusicVolume Value="35.53370667" Muted="1" />
5+
<WaveScannerVolume Value="75.00000000" Muted="0" />
6+
<ShipVoiceVolume Value="75.00000000" Muted="0" />
7+
<SuitVoiceVolume Value="75.00000000" Muted="0" />
8+
<NpcVoiceCommsVolume Value="75.00000000" Muted="0" />
9+
<PlayerVoiceCommsVolume Value="75.00000000" Muted="0" />
10+
<PlaylistVolume Value="75.00000000" Muted="0" />
11+
<VoiceVolume Value="22.07865143" Muted="0" />
12+
<CombatMusicEnabled Enabled="1" />
13+
<SuperCruiseMusicEnabled Enabled="1" />
14+
<ExplorationMusicEnabled Enabled="1" />
15+
<StarportMusicEnabled Enabled="1" />
16+
<DockingMusicEnabled Enabled="1" />
17+
<StellarCartographyMusicEnabled Enabled="1" />
18+
<MenuMusicEnabled Enabled="1" />
19+
<StreamerModeEnabled Enabled="0" />
20+
<DiegeticMusicEnabled Enabled="1" />
21+
<GalnetAudioAutomaticPausingEnabled Enabled="1" />
22+
<GalnetAudioInterdictionPausingEnabled Enabled="1" />
23+
<GalnetAudioSwitchingVesselPausingEnabled Enabled="1" />
24+
<GalnetAudioTakingFirePausingEnabled Enabled="1" />
25+
<GalnetAudioOtherVoicesPausingEnabled Enabled="1" />
26+
<GalnetAudioAutomaticResumingEnabled Enabled="1" />
27+
<WaveScannerAutoDuckingEnabled Enabled="1" />
28+
<AiOrdersEnabled Enabled="1" />
29+
<BuffsEnabled Enabled="1" />
30+
<CabinPressureAlertEnabled Enabled="1" />
31+
<CallDisconnectedEnabled Enabled="1" />
32+
<CallUnsuccessfulEnabled Enabled="1" />
33+
<CannotComplyEnabled Enabled="1" />
34+
<CanopyIntegrityEnabled Enabled="1" />
35+
<CapitalShipsEnabled Enabled="1" />
36+
<CargoHoldCapacityEnabled Enabled="1" />
37+
<CargoScoopDeployedEnabled Enabled="1" />
38+
<CargoScoopRetractedEnabled Enabled="1" />
39+
<CollectorDroneEnabled Enabled="1" />
40+
<DeliveriesEnabled Enabled="1" />
41+
<DockingRequestDeniedEnabled Enabled="1" />
42+
<DockingRequestGrantedEnabled Enabled="1" />
43+
<DockingSuccessfulEnabled Enabled="1" />
44+
<DriveAssistEnabled Enabled="1" />
45+
<EjectEnabled Enabled="1" />
46+
<EngageEnabled Enabled="1" />
47+
<EnteringOrbitalCruiseEnabled Enabled="1" />
48+
<FightersEnabled Enabled="1" />
49+
<FlightAssistOffEnabled Enabled="1" />
50+
<FlightAssistOnEnabled Enabled="1" />
51+
<FrameshiftCountdownEnabled Enabled="1" />
52+
<FuelScoopActivatedEnabled Enabled="1" />
53+
<FuelScoopCompleteEnabled Enabled="1" />
54+
<FuelScoopDeactivatedEnabled Enabled="1" />
55+
<FuelTransferDroneEnabled Enabled="1" />
56+
<GravityWarningsEnabled Enabled="1" />
57+
<HandbrakeWarningEnabled Enabled="1" />
58+
<HeatsinkDeployedEnabled Enabled="1" />
59+
<HighRiskSystemWarningEnabled Enabled="1" />
60+
<HullIntegrityEnabled Enabled="1" />
61+
<HyperspaceJumpInitiatedEnabled Enabled="1" />
62+
<InboxAlertsEnabled Enabled="1" />
63+
<IncomingHailEnabled Enabled="1" />
64+
<LandingGearDeployedEnabled Enabled="1" />
65+
<LandingGearNotDeployedEnabled Enabled="1" />
66+
<LandingGearRetractedEnabled Enabled="1" />
67+
<LimpetEnabled Enabled="1" />
68+
<MainFuelCapacityEnabled Enabled="1" />
69+
<MissionAlertsEnabled Enabled="1" />
70+
<ModuleMalfunctionEnabled Enabled="1" />
71+
<ModuleRepairedEnabled Enabled="1" />
72+
<MultiCrewEnabled Enabled="1" />
73+
<NeutronStarJetConeEnabled Enabled="1" />
74+
<PassengersEnabled Enabled="1" />
75+
<PgOutOfBoundsEnabled Enabled="1" />
76+
<PgPowerUpEnabled Enabled="1" />
77+
<PowerCapacityExceededEnabled Enabled="1" />
78+
<ProgrammingDroneEnabled Enabled="1" />
79+
<ProspectorDroneEnabled Enabled="1" />
80+
<ReadyToEngageEnabled Enabled="1" />
81+
<RebootRepairEnabled Enabled="1" />
82+
<RepairDroneEnabled Enabled="1" />
83+
<RotationalCorrectionEnabled Enabled="1" />
84+
<ScanDetectedEnabled Enabled="1" />
85+
<SelfDestructEnabled Enabled="1" />
86+
<ShieldsOfflineEnabled Enabled="1" />
87+
<ShieldsOnlineEnabled Enabled="1" />
88+
<ShipRecallDismissEnabled Enabled="1" />
89+
<ShipTransferEnabled Enabled="1" />
90+
<SilentRunningOffEnabled Enabled="1" />
91+
<SilentRunningOnEnabled Enabled="1" />
92+
<SupercruiseJumpInitiatedEnabled Enabled="1" />
93+
<SynthesisEnabled Enabled="1" />
94+
<TakingDamageEnabled Enabled="1" />
95+
<TargetCargoEjectEnabled Enabled="1" />
96+
<TargetDestroyedEnabled Enabled="1" />
97+
<TargetFrameShiftEnabled Enabled="1" />
98+
<TargetShieldsOfflineEnabled Enabled="1" />
99+
<TargetShieldsOnlineEnabled Enabled="1" />
100+
<TemperatureEnabled Enabled="1" />
101+
<ThrustersOfflineEnabled Enabled="1" />
102+
<ThrustersOnlineEnabled Enabled="1" />
103+
<TrespassWarningsEnabled Enabled="1" />
104+
<UnderAttackEnabled Enabled="1" />
105+
<UndockingSuccessfulEnabled Enabled="1" />
106+
<UnknownObjectsEnabled Enabled="1" />
107+
<BoostOtherPlayersEnabled Enabled="1" />
108+
<ControllerRumbleEnabled Enabled="1" />
109+
<SubtitelsEnabled Enabled="1" />
110+
<SuitVoiceShieldsOfflineEnabled Enabled="1" />
111+
<SuitVoiceShieldsRestoredEnabled Enabled="1" />
112+
<SuitVoiceShieldTogglesEnabled Enabled="1" />
113+
<SuitVoiceVitalsLowHealthEnabled Enabled="1" />
114+
<SuitVoiceVitalsUnconsciousEnabled Enabled="1" />
115+
<SuitVoiceVitalsMedPackUsedEnabled Enabled="1" />
116+
<SuitVoiceEmergencyOxygenEnabled Enabled="1" />
117+
<SuitVoiceOxygenLowWarningsEnabled Enabled="1" />
118+
<SuitVoiceOxygenDepletedEnabled Enabled="1" />
119+
<SuitVoiceEnergyLowWarningsEnabled Enabled="1" />
120+
<SuitVoiceEnergyDepletedEnabled Enabled="1" />
121+
<SuitVoiceEnergyRechargingEnabled Enabled="1" />
122+
<SuitVoiceEnergyTransferCompleteEnabled Enabled="1" />
123+
<SuitVoiceEnergyOverloadingEnabled Enabled="0" />
124+
<SuitVoiceEnergyTransferOverloadEnabled Enabled="0" />
125+
<SuitVoiceEnergySavingModeEnabled Enabled="1" />
126+
<SuitVoiceTemperatureLethallyHighEnabled Enabled="1" />
127+
<SuitVoiceTemperatureExtremelyHighEnabled Enabled="1" />
128+
<SuitVoiceTemperatureDangerouslyHighEnabled Enabled="1" />
129+
<SuitVoiceTemperatureHighEnabled Enabled="0" />
130+
<SuitVoiceTemperatureLowEnabled Enabled="0" />
131+
<SuitVoiceTemperatureDangerouslyLowEnabled Enabled="1" />
132+
<SuitVoiceTemperatureExtremelyLowEnabled Enabled="1" />
133+
<SuitVoiceHighGravityEnabled Enabled="0" />
134+
<SuitVoiceLowGravityEnabled Enabled="0" />
135+
<SuitVoiceLineMagneticGravityEnabled Enabled="1" />
136+
<SuitVoiceEnvironmentBreathabilityEnabled Enabled="1" />
137+
<SuitVoiceTrespassZonesEnabled Enabled="1" />
138+
<SuitVoiceMissionUpdatesEnabled Enabled="1" />
139+
<SuitVoiceProfileAnalyserScanningOthersEnabled Enabled="1" />
140+
<SuitVoiceProfilerAnalyserBeingScannedEnabled Enabled="1" />
141+
<SuitVoiceDataTransferUpdatesEnabled Enabled="1" />
142+
<SuitVoiceShuttleUpdatesEnabled Enabled="1" />
143+
<SuitVoiceWingMemberJoinedEnabled Enabled="1" />
144+
<SuitVoiceWingMemberLeftEnabled Enabled="1" />
145+
<SuitVoiceWingMemberUnderAttackEnabled Enabled="1" />
146+
<SuitVoiceWingMemberDeadEnabled Enabled="1" />
147+
<SuitVoiceMyVesselDestroyedEnabled Enabled="1" />
148+
<DynamicRange Type="Default" />
149+
<StreamerMode Type="NotStreaming" />
150+
<hp3d_override Type="1" />
151+
<hp3d Type="Enabled" />
152+
<CallSign Type="CMDR_Name" />
153+
<GalnetAudioLanguage Type="0" />
154+
</Root>

EliteAPI/EliteDangerousApi.cs

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
using System.Collections.Generic;
33
using System.IO;
44
using System.Linq;
5-
using EliteAPI.Bindings;
65
using EliteAPI.Events;
6+
using EliteAPI.Options.Audio;
7+
using EliteAPI.Options.Bindings;
78
using EliteAPI.Journals;
89
using EliteAPI.Json;
910
using EliteAPI.Utils;
@@ -17,6 +18,7 @@ public class EliteDangerousApi
1718
private readonly FileWatcher _journalWatcher;
1819
private readonly List<FileWatcher> _statusWatchers;
1920
private readonly FileWatcher _bindingsPresetsWatcher;
21+
private readonly FileWatcher _audioPresetsWatcher;
2022
private readonly StatusTracker _statusTracker = new();
2123

2224
private readonly List<Action<FileInfo>> _journalChangedHandlers = [];
@@ -30,18 +32,19 @@ public class EliteDangerousApi
3032
.ToDictionary(t => t.Name.EndsWith("Event") ? t.Name.Substring(0, t.Name.Length - 5) : t.Name, t => t, StringComparer.OrdinalIgnoreCase);
3133
private readonly Dictionary<string, List<Action<(string eventName, string json)>>> _untypedEventHandlers = new(StringComparer.OrdinalIgnoreCase);
3234
private readonly List<Action<IReadOnlyCollection<Control>>> _bindingsHandlers = [];
35+
private readonly List<Action<IReadOnlyCollection<AudioSetting>>> _audioHandlers = [];
3336

3437
public Version Version => typeof(EliteDangerousApi).Assembly.GetName().Version!;
3538

36-
public EliteDangerousApi() : this(JournalUtils.GetJournalsDirectory(), BindingsUtils.GetBindingsDirectory())
39+
public EliteDangerousApi() : this(JournalUtils.GetJournalsDirectory(), BindingsUtils.GetBindingsDirectory(), AudioUtils.GetAudioDirectory())
3740
{
3841
}
3942

4043
/// <summary>
4144
/// Creates a new instance of the API with custom directories.
4245
/// Pass null for directories to skip file watcher initialization (useful for testing).
4346
/// </summary>
44-
public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? bindingsDirectory)
47+
public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? bindingsDirectory, DirectoryInfo? audioDirectory = null)
4548
{
4649
if (journalDirectory != null)
4750
{
@@ -77,6 +80,15 @@ public EliteDangerousApi(DirectoryInfo? journalDirectory, DirectoryInfo? binding
7780
{
7881
_bindingsPresetsWatcher = null!;
7982
}
83+
84+
if (audioDirectory != null)
85+
{
86+
_audioPresetsWatcher = FileWatcher.Create(audioDirectory, "*.audio", FileWatchMode.EntireFile);
87+
}
88+
else
89+
{
90+
_audioPresetsWatcher = null!;
91+
}
8092
}
8193

8294
public void Start()
@@ -107,6 +119,12 @@ public void Start()
107119
_bindingsPresetsWatcher.OnContentChanged(HandleBindingsPreset);
108120
HandleBindingsPreset(_bindingsPresetsWatcher.StartWatching());
109121
}
122+
123+
if (_audioPresetsWatcher != null)
124+
{
125+
_audioPresetsWatcher.OnContentChanged(HandleAudioContent);
126+
HandleAudioContent(_audioPresetsWatcher.StartWatching());
127+
}
110128
}
111129

112130
/// <summary>
@@ -125,6 +143,14 @@ public void OnKeybindingsChanged(Action<IReadOnlyCollection<Control>> handler)
125143
_bindingsHandlers.Add(handler);
126144
}
127145

146+
/// <summary>
147+
/// Listens for when audio settings have changed
148+
/// </summary>
149+
public void OnAudioSettingsChanged(Action<IReadOnlyCollection<AudioSetting>> handler)
150+
{
151+
_audioHandlers.Add(handler);
152+
}
153+
128154
/// <summary>
129155
/// Listens for a specific event of <typeparamref name="TEvent"/>.
130156
/// </summary>
@@ -313,4 +339,19 @@ private void HandleBindingsPreset(string presetContent)
313339
foreach (var handler in _bindingsHandlers)
314340
SafeInvoke.Invoke("handling keybindings change", handler, bindings);
315341
}
342+
343+
private void HandleAudioContent(string xml)
344+
{
345+
if (string.IsNullOrWhiteSpace(xml))
346+
return;
347+
348+
var settings = AudioParser.Parse(xml);
349+
350+
Log.Info($"Loaded {settings.Count} audio settings");
351+
foreach (var setting in settings)
352+
Log.Debug($" {setting.ToDebugString()}");
353+
354+
foreach (var handler in _audioHandlers)
355+
SafeInvoke.Invoke("handling audio settings change", handler, settings);
356+
}
316357
}

0 commit comments

Comments
 (0)