From bddb587bc5d88cb33efc503a01aa5731e2f50db7 Mon Sep 17 00:00:00 2001 From: Vladislav Antonyuk Date: Thu, 12 Jun 2025 10:07:47 +0300 Subject: [PATCH] Speech Recognition Windows Ensure unsubscribe from events on Stop Recording --- .../Platforms/Windows/Package.appxmanifest | 1 + .../Essentials/SpeechToTextViewModel.cs | 11 ++++--- ...flineSpeechToTextImplementation.windows.cs | 29 ++++++++++++------- .../SpeechToTextImplementation.windows.cs | 27 ++++++++++------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/samples/CommunityToolkit.Maui.Sample/Platforms/Windows/Package.appxmanifest b/samples/CommunityToolkit.Maui.Sample/Platforms/Windows/Package.appxmanifest index 368925607e..2225753a55 100644 --- a/samples/CommunityToolkit.Maui.Sample/Platforms/Windows/Package.appxmanifest +++ b/samples/CommunityToolkit.Maui.Sample/Platforms/Windows/Package.appxmanifest @@ -57,6 +57,7 @@ + \ No newline at end of file diff --git a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs index f9d394a864..7b40e09e5b 100644 --- a/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs +++ b/samples/CommunityToolkit.Maui.Sample/ViewModels/Essentials/SpeechToTextViewModel.cs @@ -8,11 +8,9 @@ namespace CommunityToolkit.Maui.Sample.ViewModels.Essentials; -public partial class SpeechToTextViewModel : BaseViewModel +public partial class SpeechToTextViewModel : BaseViewModel, IAsyncDisposable { const string defaultLanguage = "en-US"; - const string defaultLanguage_android = "en"; - const string defaultLanguage_tizen = "en_US"; readonly ITextToSpeech textToSpeech; readonly ISpeechToText speechToText; @@ -55,7 +53,7 @@ async Task SetLocales(CancellationToken token) Locales.Add(locale); } - CurrentLocale = Locales.FirstOrDefault(x => x.Language is defaultLanguage or defaultLanguage_android or defaultLanguage_tizen) ?? Locales.FirstOrDefault(); + CurrentLocale = Locales.FirstOrDefault(); } [RelayCommand] @@ -148,4 +146,9 @@ void HandleLocalesCollectionChanged(object? sender, NotifyCollectionChangedEvent { OnPropertyChanged(nameof(CurrentLocale)); } + + public async ValueTask DisposeAsync() + { + await speechToText.DisposeAsync(); + } } \ No newline at end of file diff --git a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.windows.cs b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.windows.cs index adba9e6e16..007eb24dd2 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.windows.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/OfflineSpeechToTextImplementation.windows.cs @@ -28,9 +28,6 @@ public SpeechToTextState CurrentState public ValueTask DisposeAsync() { InternalStopListening(); - - offlineSpeechRecognizer?.Dispose(); - offlineSpeechRecognizer = null; return ValueTask.CompletedTask; } @@ -39,7 +36,6 @@ Task InternalStartListening(SpeechToTextOptions options, CancellationToken token Initialize(options); offlineSpeechRecognizer.AudioStateChanged += OfflineSpeechRecognizer_StateChanged; - offlineSpeechRecognizer.LoadGrammar(new DictationGrammar()); offlineSpeechRecognizer.InitialSilenceTimeout = TimeSpan.MaxValue; offlineSpeechRecognizer.BabbleTimeout = TimeSpan.MaxValue; @@ -48,7 +44,12 @@ Task InternalStartListening(SpeechToTextOptions options, CancellationToken token offlineSpeechRecognizer.RecognizeCompleted += OnRecognizeCompleted; offlineSpeechRecognizer.SpeechRecognized += OnSpeechRecognized; - offlineSpeechRecognizer.RecognizeAsync(RecognizeMode.Multiple); + + if (offlineSpeechRecognizer.AudioState == AudioState.Stopped) + { + offlineSpeechRecognizer.RecognizeAsync(RecognizeMode.Multiple); + } + return Task.CompletedTask; } @@ -81,19 +82,26 @@ void InternalStopListening() { try { - if (offlineSpeechRecognizer is not null) + if (offlineSpeechRecognizer is not null && offlineSpeechRecognizer.AudioState != AudioState.Stopped) { offlineSpeechRecognizer.RecognizeAsyncStop(); - - offlineSpeechRecognizer.AudioStateChanged -= OfflineSpeechRecognizer_StateChanged; - offlineSpeechRecognizer.RecognizeCompleted -= OnRecognizeCompleted; - offlineSpeechRecognizer.SpeechRecognized -= OnSpeechRecognized; } } catch { // ignored. Recording may be already stopped } + finally + { + if (offlineSpeechRecognizer is not null) + { + offlineSpeechRecognizer.AudioStateChanged -= OfflineSpeechRecognizer_StateChanged; + offlineSpeechRecognizer.RecognizeCompleted -= OnRecognizeCompleted; + offlineSpeechRecognizer.SpeechRecognized -= OnSpeechRecognized; + offlineSpeechRecognizer?.Dispose(); + offlineSpeechRecognizer = null; + } + } } [MemberNotNull(nameof(recognitionText), nameof(offlineSpeechRecognizer), nameof(speechToTextOptions))] @@ -102,6 +110,7 @@ void Initialize(SpeechToTextOptions options) speechToTextOptions = options; recognitionText = string.Empty; offlineSpeechRecognizer = new SpeechRecognitionEngine(options.Culture); + offlineSpeechRecognizer.LoadGrammarAsync(new DictationGrammar()); } void OfflineSpeechRecognizer_StateChanged(object? sender, AudioStateChangedEventArgs e) diff --git a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.windows.cs b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.windows.cs index 26477c60ed..85bb30c672 100644 --- a/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.windows.cs +++ b/src/CommunityToolkit.Maui.Core/Essentials/SpeechToText/SpeechToTextImplementation.windows.cs @@ -1,6 +1,5 @@ +using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Speech.Recognition; using Microsoft.Maui.ApplicationModel; using Windows.Globalization; using Windows.Media.SpeechRecognition; @@ -35,8 +34,6 @@ public SpeechToTextState CurrentState public async ValueTask DisposeAsync() { await StopRecording(CancellationToken.None); - speechRecognizer?.Dispose(); - speechRecognizer = null; } async Task InternalStartListeningAsync(SpeechToTextOptions options, CancellationToken cancellationToken) @@ -48,7 +45,10 @@ async Task InternalStartListeningAsync(SpeechToTextOptions options, Cancellation speechRecognizer.ContinuousRecognitionSession.Completed += OnCompleted; try { - await speechRecognizer.ContinuousRecognitionSession.StartAsync().AsTask(cancellationToken); + if (speechRecognizer.State == SpeechRecognizerState.Idle) + { + await speechRecognizer.ContinuousRecognitionSession.StartAsync().AsTask(cancellationToken); + } } catch (Exception ex) when ((uint)ex.HResult is privacyStatementDeclinedCode) { @@ -88,12 +88,8 @@ async Task StopRecording(CancellationToken cancellationToken) { try { - if (speechRecognizer is not null) + if (speechRecognizer is not null && speechRecognizer.State != SpeechRecognizerState.Idle) { - speechRecognizer.StateChanged -= SpeechRecognizer_StateChanged; - speechRecognizer.ContinuousRecognitionSession.ResultGenerated -= ResultGenerated; - speechRecognizer.ContinuousRecognitionSession.Completed -= OnCompleted; - cancellationToken.ThrowIfCancellationRequested(); await speechRecognizer.ContinuousRecognitionSession.StopAsync().AsTask(cancellationToken); } @@ -102,6 +98,17 @@ async Task StopRecording(CancellationToken cancellationToken) { // ignored. Recording may be already stopped } + finally + { + if (speechRecognizer is not null) + { + speechRecognizer.StateChanged -= SpeechRecognizer_StateChanged; + speechRecognizer.ContinuousRecognitionSession.ResultGenerated -= ResultGenerated; + speechRecognizer.ContinuousRecognitionSession.Completed -= OnCompleted; + speechRecognizer?.Dispose(); + speechRecognizer = null; + } + } } [MemberNotNull(nameof(recognitionText), nameof(speechRecognizer), nameof(speechToTextOptions))]