Skip to content

Commit 8a1fb2b

Browse files
Tiberius-SicaeTkael
authored andcommitted
Feature: Audio Device Selection
1 parent 617bc3d commit 8a1fb2b

8 files changed

Lines changed: 232 additions & 29 deletions

File tree

BuildInstaller/BuildInstaller.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818
:: This is a local build rather than a continuous integration... we will proceed.
1919

2020
cd "$(SolutionDir)"
21-
if exist $(SolutionDir)postBuildTests.bat (
21+
if exist "$(SolutionDir)postBuildTests.bat" (
2222
@echo Post-build script exists at: $(SolutionDir)postBuildTests.bat - executing...
23-
call $(SolutionDir)postBuildTests.bat "$(Configuration)" "$(SolutionDir)" "bin\$(Configuration)\"
23+
call "$(SolutionDir)postBuildTests.bat" "$(Configuration)" "$(SolutionDir)" "bin\$(Configuration)\"
2424
)
2525

2626
if "$(Configuration)" == "Release" (

ConfigService/Configurations/SpeechServiceConfiguration.cs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Newtonsoft.Json;
1+
using Newtonsoft.Json;
22

33
namespace EddiConfigService.Configurations
44
{
@@ -15,6 +15,7 @@ public class SpeechServiceConfiguration : Config
1515
private int _effectsLevel = 50;
1616
private int _volume = 80;
1717
private string _standardVoice;
18+
private string _audioDevice;
1819

1920
[ JsonProperty( "standardVoice" ) ]
2021
public string StandardVoice
@@ -32,6 +33,22 @@ public string StandardVoice
3233
}
3334
}
3435

36+
[ JsonProperty( "audioDevice" ) ]
37+
public string AudioDevice
38+
{
39+
get => _audioDevice;
40+
set
41+
{
42+
if ( value == _audioDevice )
43+
{
44+
return;
45+
}
46+
47+
_audioDevice = value;
48+
OnPropertyChanged();
49+
}
50+
}
51+
3552
[ JsonProperty( "volume" ) ]
3653
public int Volume
3754
{
@@ -134,6 +151,7 @@ public bool EnableIcao
134151
public void Clear()
135152
{
136153
StandardVoice = null;
154+
AudioDevice = null;
137155
Volume = 100;
138156
EffectsLevel = 50;
139157
DistortOnDamage = true;

EddiUI/Properties/Resources.Designer.cs

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

EddiUI/Properties/Resources.resx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<?xml version="1.0" encoding="utf-8"?>
1+
<?xml version="1.0" encoding="utf-8"?>
22
<root>
33
<!--
44
Microsoft ResX Schema
@@ -373,6 +373,9 @@ Please close the other instance and try again.</value>
373373
<data name="voice_test_ship" xml:space="preserve">
374374
<value>This is how I will sound in your {0}.</value>
375375
</data>
376+
<data name="tab_tts_audio_device_label" xml:space="preserve">
377+
<value>Audio device:</value>
378+
</data>
376379
<data name="wiki_hyperlink" xml:space="preserve">
377380
<value>project wiki</value>
378381
</data>

EddiUI/TextToSpeechTab.xaml

Lines changed: 24 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<UserControl x:Class="EddiUI.TextToSpeechTab"
1+
<UserControl x:Class="EddiUI.TextToSpeechTab"
22
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
44
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
@@ -29,41 +29,44 @@
2929
<RowDefinition Height="auto" />
3030
<RowDefinition Height="auto" />
3131
<RowDefinition Height="auto" />
32+
<RowDefinition Height="auto" />
3233
</Grid.RowDefinitions>
33-
<Label x:Name="ttsVoiceLabel" Grid.Column="0" Grid.Row="0" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_voice_label}" />
34-
<ComboBox x:Name="ttsVoiceDropDown" Grid.Column="1" Grid.Row="0" Margin="5" VerticalContentAlignment="Center" SelectionChanged="ttsVoiceDropDownUpdated"/>
35-
<Label x:Name="ttsVolumeLabel" Grid.Column="0" Grid.Row="1" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_volume_label}" />
36-
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="1" Margin="5" VerticalAlignment="Center">
34+
<Label x:Name="ttsAudioDeviceLabel" Grid.Column="0" Grid.Row="0" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_audio_device_label}" />
35+
<ComboBox x:Name="ttsAudioDeviceDropDown" Grid.Column="1" Grid.Row="0" Margin="5" VerticalContentAlignment="Center" DisplayMemberPath="Name" SelectedValuePath="Id" SelectionChanged="ttsAudioDeviceDropDownUpdated"/>
36+
<Label x:Name="ttsVoiceLabel" Grid.Column="0" Grid.Row="1" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_voice_label}" />
37+
<ComboBox x:Name="ttsVoiceDropDown" Grid.Column="1" Grid.Row="1" Margin="5" VerticalContentAlignment="Center" SelectionChanged="ttsVoiceDropDownUpdated"/>
38+
<Label x:Name="ttsVolumeLabel" Grid.Column="0" Grid.Row="2" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_volume_label}" />
39+
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="2" Margin="5" VerticalAlignment="Center">
3740
<TextBox x:Name="ttsVolumeText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsVolumeSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40" Margin="5,0,0,0"/>
3841
<Slider x:Name="ttsVolumeSlider" Minimum="0" Maximum="100" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsVolumeUpdated"/>
3942
</DockPanel>
40-
<Label x:Name="ttsRateLabel" Grid.Column="0" Grid.Row="2" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_rate_label}" />
41-
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="2" Margin="5" VerticalAlignment="Center">
43+
<Label x:Name="ttsRateLabel" Grid.Column="0" Grid.Row="3" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_rate_label}" />
44+
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="3" Margin="5" VerticalAlignment="Center">
4245
<TextBox x:Name="ttsRateText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsRateSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40"/>
4346
<Slider x:Name="ttsRateSlider" Minimum="-10" Maximum="10" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsRateUpdated"/>
4447
</DockPanel>
45-
<Label x:Name="ttsEffectsLevelLabel" Grid.Column="0" Grid.Row="3" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_level_label}" />
46-
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="3" Margin="5" VerticalAlignment="Center">
48+
<Label x:Name="ttsEffectsLevelLabel" Grid.Column="0" Grid.Row="4" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_level_label}" />
49+
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="4" Margin="5" VerticalAlignment="Center">
4750
<TextBox x:Name="ttsEffectsLevelText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsEffectsLevelSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40"/>
4851
<Slider x:Name="ttsEffectsLevelSlider" Minimum="0" Maximum="100" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsEffectsLevelUpdated"/>
4952
</DockPanel>
50-
<Label x:Name="ttsDistortLabel" Grid.Column="0" Grid.Row="4" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_distort_label}" />
51-
<CheckBox x:Name="ttsDistortCheckbox" Grid.Column="1" Grid.Row="4" Margin="5" VerticalAlignment="Center" Checked="ttsDistortionLevelUpdated" Unchecked="ttsDistortionLevelUpdated"/>
52-
<TextBlock Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_test_desc}" />
53-
<Label x:Name="ttsTestShipLabel" Grid.Column="0" Grid.Row="6" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_test_ship_label}" />
54-
<ComboBox x:Name="ttsTestShipDropDown" Grid.Column="1" Grid.Row="6" Margin="5" VerticalContentAlignment="Center"/>
55-
<UniformGrid Grid.Column="0" Grid.Row="7" Grid.ColumnSpan="2" Margin="5" Columns="2">
53+
<Label x:Name="ttsDistortLabel" Grid.Column="0" Grid.Row="5" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_distort_label}" />
54+
<CheckBox x:Name="ttsDistortCheckbox" Grid.Column="1" Grid.Row="5" Margin="5" VerticalAlignment="Center" Checked="ttsDistortionLevelUpdated" Unchecked="ttsDistortionLevelUpdated"/>
55+
<TextBlock Grid.Column="0" Grid.Row="6" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_test_desc}" />
56+
<Label x:Name="ttsTestShipLabel" Grid.Column="0" Grid.Row="7" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_test_ship_label}" />
57+
<ComboBox x:Name="ttsTestShipDropDown" Grid.Column="1" Grid.Row="7" Margin="5" VerticalContentAlignment="Center"/>
58+
<UniformGrid Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="2" Margin="5" Columns="2">
5659
<Button x:Name="ttsTestButton" Margin="0,0,5,0" Content="{x:Static resx:Resources.tab_tts_test_button}" Click="ttsTestVoiceButtonClickedAsync" />
5760
<Button x:Name="ttsTestDamagedButton" Margin="5,0,0,0" Content="{x:Static resx:Resources.tab_tts_test_damaged_button}" Click="ttsTestDamagedVoiceButtonClickedAsync" />
5861
</UniformGrid>
59-
<TextBlock Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_phonetic_speech_desc}" />
60-
<Label x:Name="disableIpaLabel" VerticalAlignment="Top" Grid.Column="0" Grid.Row="9" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_disable_phonetic_speech_label}" />
61-
<DockPanel Grid.Column="1" Grid.Row="9" Margin="0, 5">
62+
<TextBlock Grid.Column="0" Grid.Row="9" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_phonetic_speech_desc}" />
63+
<Label x:Name="disableIpaLabel" VerticalAlignment="Top" Grid.Column="0" Grid.Row="10" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_disable_phonetic_speech_label}" />
64+
<DockPanel Grid.Column="1" Grid.Row="10" Margin="0, 5">
6265
<CheckBox x:Name="DisableIpaCheckbox" Margin="5" VerticalAlignment="Top" Checked="disableIpaUpdated" Unchecked="disableIpaUpdated"/>
6366
</DockPanel>
64-
<TextBlock Grid.Column="0" Grid.Row="10" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_icao_desc}" />
65-
<Label x:Name="enableIcaoLabel" Grid.Column="0" Grid.Row="11" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_icao_label}" />
66-
<CheckBox x:Name="enableIcaoCheckbox" Grid.Column="1" Grid.Row="11" Margin="5" VerticalAlignment="Center" Checked="enableICAOUpdated" Unchecked="enableICAOUpdated"/>
67+
<TextBlock Grid.Column="0" Grid.Row="11" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_icao_desc}" />
68+
<Label x:Name="enableIcaoLabel" Grid.Column="0" Grid.Row="12" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_icao_label}" />
69+
<CheckBox x:Name="enableIcaoCheckbox" Grid.Column="1" Grid.Row="12" Margin="5" VerticalAlignment="Center" Checked="enableICAOUpdated" Unchecked="enableICAOUpdated"/>
6770
</Grid>
6871
</DockPanel>
6972
</UserControl>

EddiUI/TextToSpeechTab.xaml.cs

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using EddiConfigService;
1+
using EddiConfigService;
22
using EddiConfigService.Configurations;
33
using EddiDataDefinitions;
44
using EddiSpeechService;
@@ -7,6 +7,7 @@
77
using System.Linq;
88
using System.Windows;
99
using System.Windows.Controls;
10+
using System.Windows.Interop;
1011
using Utilities;
1112

1213
namespace EddiUI
@@ -17,11 +18,110 @@ public TextToSpeechTab ()
1718
{
1819
InitializeComponent();
1920
ConfigureTTS();
21+
this.Loaded += TextToSpeechTab_Loaded;
22+
this.Unloaded += TextToSpeechTab_Unloaded;
23+
}
24+
25+
private void TextToSpeechTab_Loaded(object sender, RoutedEventArgs e)
26+
{
27+
var window = Window.GetWindow(this);
28+
if (window != null)
29+
{
30+
var helper = new WindowInteropHelper(window);
31+
var source = HwndSource.FromHwnd(helper.Handle);
32+
source?.AddHook(HwndMessageHook);
33+
}
34+
}
35+
36+
private void TextToSpeechTab_Unloaded(object sender, RoutedEventArgs e)
37+
{
38+
var window = Window.GetWindow(this);
39+
if (window != null)
40+
{
41+
var helper = new WindowInteropHelper(window);
42+
var source = HwndSource.FromHwnd(helper.Handle);
43+
source?.RemoveHook(HwndMessageHook);
44+
}
45+
}
46+
47+
private IntPtr HwndMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
48+
{
49+
const int WM_DEVICECHANGE = 0x0219;
50+
if (msg == WM_DEVICECHANGE)
51+
{
52+
RefreshAudioDevices();
53+
}
54+
return IntPtr.Zero;
55+
}
56+
57+
private void RefreshAudioDevices()
58+
{
59+
var activeDevices = AudioDeviceService.GetAudioDevices();
60+
var currentOptions = ttsAudioDeviceDropDown.ItemsSource as List<AudioDevice>;
61+
if (currentOptions == null) return;
62+
63+
var currentIds = currentOptions.Skip(1).Select(d => d.Id).ToList();
64+
var activeIds = activeDevices.Select(d => d.Id).ToList();
65+
66+
bool changed = currentIds.Count != activeIds.Count || !currentIds.SequenceEqual(activeIds);
67+
68+
if (changed)
69+
{
70+
var speechServiceConfiguration = ConfigService.Instance.speechServiceConfiguration;
71+
var configuredDevice = speechServiceConfiguration?.AudioDevice;
72+
73+
var audioDeviceOptions = new List<AudioDevice>
74+
{
75+
new AudioDevice { Name = "Default Device", Id = null }
76+
};
77+
audioDeviceOptions.AddRange(activeDevices);
78+
79+
ttsAudioDeviceDropDown.SelectionChanged -= ttsAudioDeviceDropDownUpdated;
80+
ttsAudioDeviceDropDown.ItemsSource = audioDeviceOptions;
81+
82+
if (configuredDevice != null && audioDeviceOptions.Any(d => d.Id == configuredDevice))
83+
{
84+
ttsAudioDeviceDropDown.SelectedValue = configuredDevice;
85+
}
86+
else
87+
{
88+
ttsAudioDeviceDropDown.SelectedIndex = 0;
89+
if (configuredDevice != null)
90+
{
91+
speechServiceConfiguration.AudioDevice = null;
92+
ConfigService.Instance.speechServiceConfiguration = speechServiceConfiguration;
93+
}
94+
}
95+
ttsAudioDeviceDropDown.SelectionChanged += ttsAudioDeviceDropDownUpdated;
96+
}
2097
}
2198

2299
public void ConfigureTTS()
23100
{
24101
var speechServiceConfiguration = ConfigService.Instance.speechServiceConfiguration;
102+
103+
// Populate audio devices
104+
var audioDeviceOptions = new List<AudioDevice>
105+
{
106+
new AudioDevice { Name = "Default Device", Id = null }
107+
};
108+
audioDeviceOptions.AddRange(AudioDeviceService.GetAudioDevices());
109+
ttsAudioDeviceDropDown.ItemsSource = audioDeviceOptions;
110+
var configuredDevice = speechServiceConfiguration.AudioDevice;
111+
if (configuredDevice != null && audioDeviceOptions.Any(d => d.Id == configuredDevice))
112+
{
113+
ttsAudioDeviceDropDown.SelectedValue = configuredDevice;
114+
}
115+
else
116+
{
117+
ttsAudioDeviceDropDown.SelectedIndex = 0;
118+
if (configuredDevice != null)
119+
{
120+
speechServiceConfiguration.AudioDevice = null;
121+
ConfigService.Instance.speechServiceConfiguration = speechServiceConfiguration;
122+
}
123+
}
124+
25125
var speechOptions = new List<string>
26126
{
27127
"Windows TTS default"
@@ -65,6 +165,14 @@ public void ConfigureTTS()
65165
ttsTestShipDropDown.Text = "Adder";
66166
}
67167

168+
private void ttsAudioDeviceDropDownUpdated(object sender, SelectionChangedEventArgs e)
169+
{
170+
if (sender is FrameworkElement element && element.IsLoaded )
171+
{
172+
ttsUpdated();
173+
}
174+
}
175+
68176
private void ttsVoiceDropDownUpdated(object sender, SelectionChangedEventArgs e)
69177
{
70178
if (sender is FrameworkElement element && element.IsLoaded )
@@ -167,6 +275,7 @@ private void ttsUpdated()
167275
{
168276
var speechConfiguration = new SpeechServiceConfiguration
169277
{
278+
AudioDevice = ttsAudioDeviceDropDown.SelectedValue?.ToString(),
170279
StandardVoice = ttsVoiceDropDown.SelectedItem == null ||
171280
ttsVoiceDropDown.SelectedItem.ToString() == "Windows TTS default"
172281
? null

SpeechService/AudioDevice.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using NAudio.CoreAudioApi;
2+
using System;
3+
using System.Collections.Generic;
4+
using Utilities;
5+
6+
namespace EddiSpeechService
7+
{
8+
public class AudioDevice
9+
{
10+
public string Name { get; set; }
11+
public string Id { get; set; }
12+
}
13+
14+
public static class AudioDeviceService
15+
{
16+
public static List<AudioDevice> GetAudioDevices()
17+
{
18+
var list = new List<AudioDevice>();
19+
try
20+
{
21+
var enumerator = new MMDeviceEnumerator();
22+
var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
23+
foreach (var device in devices)
24+
{
25+
list.Add(new AudioDevice
26+
{
27+
Name = device.FriendlyName,
28+
Id = device.ID
29+
});
30+
}
31+
}
32+
catch (Exception ex)
33+
{
34+
Logging.Error("Failed to list audio devices", ex);
35+
}
36+
return list;
37+
}
38+
}
39+
}

SpeechService/SoundManager.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using NAudio.Wave;
1+
using NAudio.Wave;
2+
using NAudio.CoreAudioApi;
3+
using EddiConfigService;
24
using System;
35
using System.Runtime.InteropServices;
46
using Utilities;
@@ -12,7 +14,27 @@ internal static IWavePlayer GetSoundOut ( IWaveProvider provider )
1214
// Try WASAPI first
1315
try
1416
{
15-
var wasapiOut = new WasapiOut();
17+
WasapiOut wasapiOut;
18+
var deviceId = ConfigService.Instance.speechServiceConfiguration?.AudioDevice;
19+
if ( !string.IsNullOrEmpty( deviceId ) )
20+
{
21+
try
22+
{
23+
var enumerator = new MMDeviceEnumerator();
24+
var device = enumerator.GetDevice( deviceId );
25+
wasapiOut = new WasapiOut( device, AudioClientShareMode.Shared, true, 200 );
26+
}
27+
catch ( Exception ex )
28+
{
29+
Logging.Warn( $"Failed to initialize WASAPI with selected device {deviceId}, falling back to default device.", ex );
30+
wasapiOut = new WasapiOut();
31+
}
32+
}
33+
else
34+
{
35+
wasapiOut = new WasapiOut();
36+
}
37+
1638
if ( TryInitializeSoundOut( wasapiOut, provider ) )
1739
{
1840
return wasapiOut;

0 commit comments

Comments
 (0)