Skip to content

Commit c217f10

Browse files
authored
Service cleanup (#282)
Encapsulate the WebAssembly specific bits into a service abstraction, that allows the MAUI bits to be independently implemented/ignored without throwing exception, or handling try/catch.
1 parent 3f0f061 commit c217f10

File tree

8 files changed

+74
-52
lines changed

8 files changed

+74
-52
lines changed

app/SharedWebComponents/Components/VoiceDialog.razor.cs

Lines changed: 4 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public sealed partial class VoiceDialog : IDisposable
1212

1313
[Inject] public required ISpeechSynthesisService SpeechSynthesis { get; set; }
1414

15+
[Inject] public required ITextToSpeechPreferencesListener VoiceChangesListener { get; set; }
16+
1517
[Inject] public required ILocalStorageService LocalStorage { get; set; }
1618

1719
[CascadingParameter] public required MudDialogInstance Dialog { get; set; }
@@ -22,19 +24,7 @@ protected override async Task OnInitializedAsync()
2224

2325
await GetVoicesAsync();
2426

25-
try
26-
{
27-
SpeechSynthesis.OnVoicesChanged(() => GetVoicesAsync(true));
28-
}
29-
catch
30-
{
31-
// TODO: Find a better way to do this
32-
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
33-
// however the underlying code does not do this using a DI-friendly way.
34-
// The code assumes the concrete implementation for the ISpeechSynthesisService
35-
// service is the concrete Web Assembly type which is not valid.
36-
// There is no alternative API that MAUI apps can use.
37-
}
27+
VoiceChangesListener.OnAvailableVoicesChanged(() => GetVoicesAsync(true));
3828

3929
_voicePreferences = new VoicePreferences(LocalStorage);
4030

@@ -70,19 +60,7 @@ private void OnValueChanged(string selectedVoice) => _voicePreferences = _voiceP
7060

7161
public void Dispose()
7262
{
73-
try
74-
{
75-
SpeechSynthesis.UnsubscribeFromVoicesChanged();
76-
}
77-
catch
78-
{
79-
// TODO: Find a better way to do this
80-
// The Blazor.SpeechSynthesis.WebAssembly API supports listening to changes,
81-
// however the underlying code does not do this using a DI-friendly way.
82-
// The code assumes the concrete implementation for the ISpeechSynthesisService
83-
// service is the concrete Web Assembly type which is not valid.
84-
// There is no alternative API that MAUI apps can use.
85-
}
63+
VoiceChangesListener.UnsubscribeFromAvailableVoicesChanged();
8664
}
8765
}
8866

app/SharedWebComponents/Pages/VoiceChat.razor.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ private void OnSendPrompt()
7979
Name = voice
8080
};
8181
}
82+
8283
try
8384
{
8485
SpeechSynthesis.Speak(utterance, duration =>
@@ -95,9 +96,7 @@ private void OnSendPrompt()
9596
// The code assumes the concrete implementation for the ISpeechSynthesisService
9697
// service is the concrete Web Assembly type which is not valid. However, the
9798
// alternate API is something MAUI can implement.
98-
9999
SpeechSynthesis.Speak(utterance);
100-
101100
_isReadingResponse = false;
102101
StateHasChanged();
103102
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
namespace SharedWebComponents.Services;
4+
5+
public interface ITextToSpeechPreferencesListener
6+
{
7+
void OnAvailableVoicesChanged(Func<Task> onVoicesChanged);
8+
9+
void UnsubscribeFromAvailableVoicesChanged();
10+
}

app/frontend/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
builder.Services.AddSessionStorageServices();
1919
builder.Services.AddSpeechSynthesisServices();
2020
builder.Services.AddSpeechRecognitionServices();
21+
builder.Services.AddSingleton<ITextToSpeechPreferencesListener, TextToSpeechPreferencesListenerService>();
2122
builder.Services.AddMudServices();
2223
builder.Services.AddTransient<IPdfViewer, WebPdfViewer>();
2324

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) Microsoft. All rights reserved.
2+
3+
using Microsoft.JSInterop;
4+
5+
namespace ClientApp.Services;
6+
7+
public sealed class TextToSpeechPreferencesListenerService(
8+
ISpeechSynthesisService speechSynthesisService) : ITextToSpeechPreferencesListener
9+
{
10+
public void OnAvailableVoicesChanged(Func<Task> onVoicesChanged) =>
11+
speechSynthesisService.OnVoicesChanged(onVoicesChanged);
12+
13+
public void UnsubscribeFromAvailableVoicesChanged() =>
14+
speechSynthesisService.UnsubscribeFromVoicesChanged();
15+
}

app/maui-blazor/MauiProgram.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static MauiApp CreateMauiApp()
3333
builder.Services.AddSingleton<ISessionStorageService, MauiSessionStorageService>();
3434
builder.Services.AddSingleton<ISpeechRecognitionService, MauiSpeechRecognitionService>();
3535
builder.Services.AddSingleton<ISpeechSynthesisService, MauiSpeechSynthesisService>();
36+
builder.Services.AddSingleton<ITextToSpeechPreferencesListener, MauiSpeechSynthesisService>();
3637
builder.Services.AddTransient<IPdfViewer, MauiPdfViewer>();
3738

3839
builder.Services.AddMudServices();

app/maui-blazor/Services/MauiSpeechRecognitionService.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
namespace MauiBlazor.Services;
44

5-
public class MauiSpeechRecognitionService(ISpeechToText speechToText) : ISpeechRecognitionService
5+
public sealed class MauiSpeechRecognitionService(ISpeechToText speechToText) : ISpeechRecognitionService
66
{
77
private SpeechRecognitionOperation? _current;
88

99
public void CancelSpeechRecognition(bool isAborted)
1010
{
11+
_ = isAborted;
1112
_current?.Dispose();
1213
}
1314

@@ -19,6 +20,7 @@ public ValueTask DisposeAsync()
1920

2021
public Task InitializeModuleAsync(bool logModuleDetails = true)
2122
{
23+
_ = logModuleDetails;
2224
return Task.CompletedTask;
2325
}
2426

app/maui-blazor/Services/MauiSpeechSynthesisService.cs

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace MauiBlazor.Services;
44

5-
public class MauiSpeechSynthesisService : ISpeechSynthesisService
5+
public class MauiSpeechSynthesisService(ITextToSpeech textToSpeech)
6+
: ISpeechSynthesisService, ITextToSpeechPreferencesListener
67
{
78
private CancellationTokenSource? _cts;
9+
private Task? _speakTask;
810

911
public bool Paused => throw new NotImplementedException();
1012

@@ -28,6 +30,11 @@ public ValueTask<SpeechSynthesisVoice[]> GetVoicesAsync()
2830
return ValueTask.FromResult<SpeechSynthesisVoice[]>([voice]);
2931
}
3032

33+
public void OnAvailableVoicesChanged(Func<Task> onVoicesChanged)
34+
{
35+
_ = onVoicesChanged;
36+
}
37+
3138
public void Pause()
3239
{
3340
Cancel();
@@ -40,35 +47,44 @@ public void Resume()
4047
// TODO: support pause & resume
4148
}
4249

43-
public async void Speak(SpeechSynthesisUtterance utterance)
50+
public void Speak(SpeechSynthesisUtterance utterance)
4451
{
52+
_cts?.Cancel();
4553
_cts = new();
4654

47-
var current = CultureInfo.CurrentUICulture.Name;
48-
49-
var locales = await TextToSpeech.Default.GetLocalesAsync();
50-
var localeArray = locales.ToArray();
51-
var locale = localeArray.FirstOrDefault(l => current == $"{l.Language}-{l.Country}");
52-
if (locale is null)
55+
_speakTask = Task.Run(async () =>
5356
{
54-
// an exact match was not found, try just the lang
55-
var split = current.Split('-');
56-
if (split.Length == 1 || split.Length == 2)
57+
var current = CultureInfo.CurrentUICulture.Name;
58+
59+
var locales = await textToSpeech.GetLocalesAsync();
60+
var localeArray = locales.ToArray();
61+
var locale = localeArray.FirstOrDefault(l => current == $"{l.Language}-{l.Country}");
62+
if (locale is null)
5763
{
58-
// try the first part (or the whole thing if it is just lang)
59-
locale = localeArray.FirstOrDefault(l => split[0] == $"{l.Language}");
64+
// an exact match was not found, try just the lang
65+
var split = current.Split('-');
66+
if (split.Length is 1 or 2)
67+
{
68+
// try the first part (or the whole thing if it is just lang)
69+
locale = localeArray.FirstOrDefault(l => split[0] == $"{l.Language}");
70+
}
71+
else
72+
{
73+
// just go with the first one
74+
locale = localeArray.FirstOrDefault();
75+
}
6076
}
61-
else
77+
78+
var options = new SpeechOptions
6279
{
63-
// just go with the first one
64-
locale = localeArray.FirstOrDefault();
65-
}
66-
}
80+
Locale = locale
81+
};
6782

68-
var options = new SpeechOptions
69-
{
70-
Locale = locale
71-
};
72-
await TextToSpeech.Default.SpeakAsync(utterance.Text, options, _cts.Token);
83+
await textToSpeech.SpeakAsync(utterance.Text, options, _cts.Token);
84+
}, _cts.Token);
85+
}
86+
87+
public void UnsubscribeFromAvailableVoicesChanged()
88+
{
7389
}
7490
}

0 commit comments

Comments
 (0)