Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions BuildInstaller/BuildInstaller.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@
:: This is a local build rather than a continuous integration... we will proceed.

cd "$(SolutionDir)"
if exist $(SolutionDir)postBuildTests.bat (
if exist "$(SolutionDir)postBuildTests.bat" (
@echo Post-build script exists at: $(SolutionDir)postBuildTests.bat - executing...
call $(SolutionDir)postBuildTests.bat "$(Configuration)" "$(SolutionDir)" "bin\$(Configuration)\"
call "$(SolutionDir)postBuildTests.bat" "$(Configuration)" "$(SolutionDir)" "bin\$(Configuration)\"
)

if "$(Configuration)" == "Release" (
Expand Down
20 changes: 19 additions & 1 deletion ConfigService/Configurations/SpeechServiceConfiguration.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using Newtonsoft.Json;
using Newtonsoft.Json;

namespace EddiConfigService.Configurations
{
Expand All @@ -15,6 +15,7 @@ public class SpeechServiceConfiguration : Config
private int _effectsLevel = 50;
private int _volume = 80;
private string _standardVoice;
private string _audioDevice;

[ JsonProperty( "standardVoice" ) ]
public string StandardVoice
Expand All @@ -32,6 +33,22 @@ public string StandardVoice
}
}

[ JsonProperty( "audioDevice" ) ]
public string AudioDevice
{
get => _audioDevice;
set
{
if ( value == _audioDevice )
{
return;
}

_audioDevice = value;
OnPropertyChanged();
}
}

[ JsonProperty( "volume" ) ]
public int Volume
{
Expand Down Expand Up @@ -134,6 +151,7 @@ public bool EnableIcao
public void Clear()
{
StandardVoice = null;
AudioDevice = null;
Volume = 100;
EffectsLevel = 50;
DistortOnDamage = true;
Expand Down
11 changes: 10 additions & 1 deletion EddiUI/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion EddiUI/Properties/Resources.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -373,6 +373,9 @@ Please close the other instance and try again.</value>
<data name="voice_test_ship" xml:space="preserve">
<value>This is how I will sound in your {0}.</value>
</data>
<data name="tab_tts_audio_device_label" xml:space="preserve">
<value>Audio device:</value>
</data>
<data name="wiki_hyperlink" xml:space="preserve">
<value>project wiki</value>
</data>
Expand Down
45 changes: 24 additions & 21 deletions EddiUI/TextToSpeechTab.xaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<UserControl x:Class="EddiUI.TextToSpeechTab"
<UserControl x:Class="EddiUI.TextToSpeechTab"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
Expand Down Expand Up @@ -29,41 +29,44 @@
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
</Grid.RowDefinitions>
<Label x:Name="ttsVoiceLabel" Grid.Column="0" Grid.Row="0" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_voice_label}" />
<ComboBox x:Name="ttsVoiceDropDown" Grid.Column="1" Grid.Row="0" Margin="5" VerticalContentAlignment="Center" SelectionChanged="ttsVoiceDropDownUpdated"/>
<Label x:Name="ttsVolumeLabel" Grid.Column="0" Grid.Row="1" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_volume_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="1" Margin="5" VerticalAlignment="Center">
<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}" />
<ComboBox x:Name="ttsAudioDeviceDropDown" Grid.Column="1" Grid.Row="0" Margin="5" VerticalContentAlignment="Center" DisplayMemberPath="Name" SelectedValuePath="Id" SelectionChanged="ttsAudioDeviceDropDownUpdated"/>
<Label x:Name="ttsVoiceLabel" Grid.Column="0" Grid.Row="1" Margin="0, 5" VerticalContentAlignment="Center" Content="{x:Static resx:Resources.tab_tts_voice_label}" />
<ComboBox x:Name="ttsVoiceDropDown" Grid.Column="1" Grid.Row="1" Margin="5" VerticalContentAlignment="Center" SelectionChanged="ttsVoiceDropDownUpdated"/>
<Label x:Name="ttsVolumeLabel" Grid.Column="0" Grid.Row="2" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_volume_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="2" Margin="5" VerticalAlignment="Center">
<TextBox x:Name="ttsVolumeText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsVolumeSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40" Margin="5,0,0,0"/>
<Slider x:Name="ttsVolumeSlider" Minimum="0" Maximum="100" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsVolumeUpdated"/>
</DockPanel>
<Label x:Name="ttsRateLabel" Grid.Column="0" Grid.Row="2" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_rate_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="2" Margin="5" VerticalAlignment="Center">
<Label x:Name="ttsRateLabel" Grid.Column="0" Grid.Row="3" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_rate_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="3" Margin="5" VerticalAlignment="Center">
<TextBox x:Name="ttsRateText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsRateSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40"/>
<Slider x:Name="ttsRateSlider" Minimum="-10" Maximum="10" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsRateUpdated"/>
</DockPanel>
<Label x:Name="ttsEffectsLevelLabel" Grid.Column="0" Grid.Row="3" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_level_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="3" Margin="5" VerticalAlignment="Center">
<Label x:Name="ttsEffectsLevelLabel" Grid.Column="0" Grid.Row="4" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_level_label}" />
<DockPanel LastChildFill="True" Grid.Column="1" Grid.Row="4" Margin="5" VerticalAlignment="Center">
<TextBox x:Name="ttsEffectsLevelText" DockPanel.Dock="Right" Text="{Binding ElementName=ttsEffectsLevelSlider, Path=Value, UpdateSourceTrigger=PropertyChanged}" TextAlignment="Right" Width="40"/>
<Slider x:Name="ttsEffectsLevelSlider" Minimum="0" Maximum="100" IsSnapToTickEnabled="True" TickFrequency="1" ValueChanged="ttsEffectsLevelUpdated"/>
</DockPanel>
<Label x:Name="ttsDistortLabel" Grid.Column="0" Grid.Row="4" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_distort_label}" />
<CheckBox x:Name="ttsDistortCheckbox" Grid.Column="1" Grid.Row="4" Margin="5" VerticalAlignment="Center" Checked="ttsDistortionLevelUpdated" Unchecked="ttsDistortionLevelUpdated"/>
<TextBlock Grid.Column="0" Grid.Row="5" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_test_desc}" />
<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}" />
<ComboBox x:Name="ttsTestShipDropDown" Grid.Column="1" Grid.Row="6" Margin="5" VerticalContentAlignment="Center"/>
<UniformGrid Grid.Column="0" Grid.Row="7" Grid.ColumnSpan="2" Margin="5" Columns="2">
<Label x:Name="ttsDistortLabel" Grid.Column="0" Grid.Row="5" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_distort_label}" />
<CheckBox x:Name="ttsDistortCheckbox" Grid.Column="1" Grid.Row="5" Margin="5" VerticalAlignment="Center" Checked="ttsDistortionLevelUpdated" Unchecked="ttsDistortionLevelUpdated"/>
<TextBlock Grid.Column="0" Grid.Row="6" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_test_desc}" />
<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}" />
<ComboBox x:Name="ttsTestShipDropDown" Grid.Column="1" Grid.Row="7" Margin="5" VerticalContentAlignment="Center"/>
<UniformGrid Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="2" Margin="5" Columns="2">
<Button x:Name="ttsTestButton" Margin="0,0,5,0" Content="{x:Static resx:Resources.tab_tts_test_button}" Click="ttsTestVoiceButtonClickedAsync" />
<Button x:Name="ttsTestDamagedButton" Margin="5,0,0,0" Content="{x:Static resx:Resources.tab_tts_test_damaged_button}" Click="ttsTestDamagedVoiceButtonClickedAsync" />
</UniformGrid>
<TextBlock Grid.Column="0" Grid.Row="8" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_phonetic_speech_desc}" />
<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}" />
<DockPanel Grid.Column="1" Grid.Row="9" Margin="0, 5">
<TextBlock Grid.Column="0" Grid.Row="9" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_phonetic_speech_desc}" />
<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}" />
<DockPanel Grid.Column="1" Grid.Row="10" Margin="0, 5">
<CheckBox x:Name="DisableIpaCheckbox" Margin="5" VerticalAlignment="Top" Checked="disableIpaUpdated" Unchecked="disableIpaUpdated"/>
</DockPanel>
<TextBlock Grid.Column="0" Grid.Row="10" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_icao_desc}" />
<Label x:Name="enableIcaoLabel" Grid.Column="0" Grid.Row="11" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_icao_label}" />
<CheckBox x:Name="enableIcaoCheckbox" Grid.Column="1" Grid.Row="11" Margin="5" VerticalAlignment="Center" Checked="enableICAOUpdated" Unchecked="enableICAOUpdated"/>
<TextBlock Grid.Column="0" Grid.Row="11" Grid.ColumnSpan="2" Margin="5" TextWrapping="Wrap" Text="{x:Static resx:Resources.tab_tts_icao_desc}" />
<Label x:Name="enableIcaoLabel" Grid.Column="0" Grid.Row="12" Margin="0, 5" Content="{x:Static resx:Resources.tab_tts_icao_label}" />
<CheckBox x:Name="enableIcaoCheckbox" Grid.Column="1" Grid.Row="12" Margin="5" VerticalAlignment="Center" Checked="enableICAOUpdated" Unchecked="enableICAOUpdated"/>
</Grid>
</DockPanel>
</UserControl>
111 changes: 110 additions & 1 deletion EddiUI/TextToSpeechTab.xaml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using EddiConfigService;
using EddiConfigService;
using EddiConfigService.Configurations;
using EddiDataDefinitions;
using EddiSpeechService;
Expand All @@ -7,6 +7,7 @@
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Interop;
using Utilities;

namespace EddiUI
Expand All @@ -17,11 +18,110 @@ public TextToSpeechTab ()
{
InitializeComponent();
ConfigureTTS();
this.Loaded += TextToSpeechTab_Loaded;
this.Unloaded += TextToSpeechTab_Unloaded;
}

private void TextToSpeechTab_Loaded(object sender, RoutedEventArgs e)
{
var window = Window.GetWindow(this);
if (window != null)
{
var helper = new WindowInteropHelper(window);
var source = HwndSource.FromHwnd(helper.Handle);
source?.AddHook(HwndMessageHook);
}
}

private void TextToSpeechTab_Unloaded(object sender, RoutedEventArgs e)
{
var window = Window.GetWindow(this);
if (window != null)
{
var helper = new WindowInteropHelper(window);
var source = HwndSource.FromHwnd(helper.Handle);
source?.RemoveHook(HwndMessageHook);
}
}

private IntPtr HwndMessageHook(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
const int WM_DEVICECHANGE = 0x0219;
if (msg == WM_DEVICECHANGE)
{
RefreshAudioDevices();
}
return IntPtr.Zero;
}

private void RefreshAudioDevices()
{
var activeDevices = AudioDeviceService.GetAudioDevices();
var currentOptions = ttsAudioDeviceDropDown.ItemsSource as List<AudioDevice>;
if (currentOptions == null) return;

var currentIds = currentOptions.Skip(1).Select(d => d.Id).ToList();
var activeIds = activeDevices.Select(d => d.Id).ToList();

bool changed = currentIds.Count != activeIds.Count || !currentIds.SequenceEqual(activeIds);

if (changed)
{
var speechServiceConfiguration = ConfigService.Instance.speechServiceConfiguration;
var configuredDevice = speechServiceConfiguration?.AudioDevice;

var audioDeviceOptions = new List<AudioDevice>
{
new AudioDevice { Name = "Default Device", Id = null }
};
audioDeviceOptions.AddRange(activeDevices);

ttsAudioDeviceDropDown.SelectionChanged -= ttsAudioDeviceDropDownUpdated;
ttsAudioDeviceDropDown.ItemsSource = audioDeviceOptions;

if (configuredDevice != null && audioDeviceOptions.Any(d => d.Id == configuredDevice))
{
ttsAudioDeviceDropDown.SelectedValue = configuredDevice;
}
else
{
ttsAudioDeviceDropDown.SelectedIndex = 0;
if (configuredDevice != null)
{
speechServiceConfiguration.AudioDevice = null;
ConfigService.Instance.speechServiceConfiguration = speechServiceConfiguration;
}
}
ttsAudioDeviceDropDown.SelectionChanged += ttsAudioDeviceDropDownUpdated;
}
}

public void ConfigureTTS()
{
var speechServiceConfiguration = ConfigService.Instance.speechServiceConfiguration;

// Populate audio devices
var audioDeviceOptions = new List<AudioDevice>
{
new AudioDevice { Name = "Default Device", Id = null }
};
audioDeviceOptions.AddRange(AudioDeviceService.GetAudioDevices());
ttsAudioDeviceDropDown.ItemsSource = audioDeviceOptions;
var configuredDevice = speechServiceConfiguration.AudioDevice;
if (configuredDevice != null && audioDeviceOptions.Any(d => d.Id == configuredDevice))
{
ttsAudioDeviceDropDown.SelectedValue = configuredDevice;
}
else
{
ttsAudioDeviceDropDown.SelectedIndex = 0;
if (configuredDevice != null)
{
speechServiceConfiguration.AudioDevice = null;
ConfigService.Instance.speechServiceConfiguration = speechServiceConfiguration;
}
}

var speechOptions = new List<string>
{
"Windows TTS default"
Expand Down Expand Up @@ -65,6 +165,14 @@ public void ConfigureTTS()
ttsTestShipDropDown.Text = "Adder";
}

private void ttsAudioDeviceDropDownUpdated(object sender, SelectionChangedEventArgs e)
{
if (sender is FrameworkElement element && element.IsLoaded )
{
ttsUpdated();
}
}

private void ttsVoiceDropDownUpdated(object sender, SelectionChangedEventArgs e)
{
if (sender is FrameworkElement element && element.IsLoaded )
Expand Down Expand Up @@ -167,6 +275,7 @@ private void ttsUpdated()
{
var speechConfiguration = new SpeechServiceConfiguration
{
AudioDevice = ttsAudioDeviceDropDown.SelectedValue?.ToString(),
StandardVoice = ttsVoiceDropDown.SelectedItem == null ||
ttsVoiceDropDown.SelectedItem.ToString() == "Windows TTS default"
? null
Expand Down
39 changes: 39 additions & 0 deletions SpeechService/AudioDevice.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
using NAudio.CoreAudioApi;
using System;
using System.Collections.Generic;
using Utilities;

namespace EddiSpeechService
{
public class AudioDevice
{
public string Name { get; set; }
public string Id { get; set; }
}

public static class AudioDeviceService
{
public static List<AudioDevice> GetAudioDevices()
{
var list = new List<AudioDevice>();
try
{
var enumerator = new MMDeviceEnumerator();
var devices = enumerator.EnumerateAudioEndPoints(DataFlow.Render, DeviceState.Active);
foreach (var device in devices)
{
list.Add(new AudioDevice
{
Name = device.FriendlyName,
Id = device.ID
});
}
}
catch (Exception ex)
{
Logging.Error("Failed to list audio devices", ex);
}
return list;
}
}
}
26 changes: 24 additions & 2 deletions SpeechService/SoundManager.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using NAudio.Wave;
using NAudio.Wave;
using NAudio.CoreAudioApi;
using EddiConfigService;
using System;
using System.Runtime.InteropServices;
using Utilities;
Expand All @@ -12,7 +14,27 @@ internal static IWavePlayer GetSoundOut ( IWaveProvider provider )
// Try WASAPI first
try
{
var wasapiOut = new WasapiOut();
WasapiOut wasapiOut;
var deviceId = ConfigService.Instance.speechServiceConfiguration?.AudioDevice;
if ( !string.IsNullOrEmpty( deviceId ) )
{
try
{
var enumerator = new MMDeviceEnumerator();
var device = enumerator.GetDevice( deviceId );
wasapiOut = new WasapiOut( device, AudioClientShareMode.Shared, true, 200 );
}
catch ( Exception ex )
{
Logging.Warn( $"Failed to initialize WASAPI with selected device {deviceId}, falling back to default device.", ex );
wasapiOut = new WasapiOut();
}
}
else
{
wasapiOut = new WasapiOut();
}

if ( TryInitializeSoundOut( wasapiOut, provider ) )
{
return wasapiOut;
Expand Down