




























SpeechVoiceTest.axaml代码
<Window xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" Width="600" Height="600" x:Class="AvaloniaUI.SpeechVoiceTest" Title="SpeechVoiceTest"> <Grid Margin="10" RowDefinitions="auto,auto,auto,auto"> <StackPanel Grid.Row="0" Orientation="Horizontal" Spacing="8"> <Button x:Name="btnSpeak" Content="Speak" Click="BtnSpeakClick"/> <Button x:Name="btnListen" Content="Listen Once" Click="BtnListenClick"/> <Button x:Name="btnStart" Content="Start Continuous" Click="BtnStartClick"/> <Button x:Name="btnStop" Content="Stop" Click="BtnStopClick"/> </StackPanel> <Grid Grid.Row="1" ColumnDefinitions="auto,*" Margin="0,10,0,0"> <TextBlock Grid.Column="0" Text="Text:" VerticalAlignment="Center" Margin="0,0,8,0"/> <TextBox Grid.Column="1" x:Name="txtSpeak" Text="Hello Avalonia" Watermark="Text to speak"/> </Grid> <Border Grid.Row="2" Margin="0,10,0,0" Padding="10"> <StackPanel Spacing="6"> <TextBlock Text="Status:" FontWeight="Bold"/> <TextBlock x:Name="txtStatus" TextWrapping="Wrap"/> <TextBlock Text="Recognized:" FontWeight="Bold" Margin="0,6,0,0"/> <TextBox x:Name="txtRecognized" AcceptsReturn="True" TextWrapping="Wrap" MinHeight="140"/> <TextBlock x:Name="txtMetrics" Opacity="0.8"/> </StackPanel> </Border> <TextBlock Grid.Row="3" Margin="0,8,0,0" Opacity="0.7" Text="Tip: In noisy environments, raise MinAudioLevel and StableCount; increase SilenceDelay if it pauses too aggressively."/> </Grid> </Window>
SpeechVoiceTest.axaml.cs代码
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using Shares;
using System;
namespace AvaloniaUI;
public partial class SpeechVoiceTest : Window
{
private readonly SpeechVoice voice = new();
public SpeechVoiceTest()
{
InitializeComponent();
voice.StatusChanged += (_, msg) =>
Dispatcher.UIThread.Post(() => txtStatus.Text = msg);
voice.SpeechRecognized += (_, e) =>
{
Dispatcher.UIThread.Post(() =>
{
var line = $"[{DateTime.Now:HH:mm:ss}] {e.Text}";
txtRecognized.Text = string.IsNullOrWhiteSpace(txtRecognized.Text)
? line
: txtRecognized.Text + Environment.NewLine + line;
txtMetrics.Text = $"Confidence={e.Confidence:0.00} AudioLevel={e.AudioLevel}";
});
};
voice.SpeechSynthesized += (_, _) =>
Dispatcher.UIThread.Post(() => txtStatus.Text = "Speech synthesized.");
}
private void BtnSpeakClick(object? sender, RoutedEventArgs e)
{
voice.Speak(txtSpeak.Text!);
}
private void BtnListenClick(object? sender, RoutedEventArgs e)
{
voice.Listen();
}
private void BtnStartClick(object? sender, RoutedEventArgs e)
{
voice.Start();
}
private void BtnStopClick(object? sender, RoutedEventArgs e)
{
voice.Stop();
}
}
SpeechVoice代码
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
using System.Speech.Recognition;
using System.Speech.Synthesis;
using System.Threading;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
namespace Shares;
public sealed partial class SpeechVoice : ObservableObject, IDisposable
{
private readonly Thread staThread;
private readonly BlockingCollection<Action> staQueue = [];
private readonly CancellationTokenSource staCts = new();
private SpeechSynthesizer? synthesizer;
private SpeechRecognitionEngine? recognizer;
private int disposing;
private int disposed;
private VoiceState state;
private long lastVoiceTick;
private long lastRestartTick;
private int lastAudioLevel;
private Timer? timeoutTimer;
private Timer? statusTimer;
private string lastStatus = string.Empty;
private int modeGeneration;
private enum VoiceState
{
Stopped,
Single,
MultipleRunning,
MultiplePaused
}
[ObservableProperty]
private TimeSpan timeout = TimeSpan.FromSeconds(10);
[ObservableProperty]
private double confidenceThreshold = 0.15;
[ObservableProperty]
private int silenceAudioLevelThresholdLow = 12;
[ObservableProperty]
private int silenceAudioLevelThresholdHigh = 14;
[ObservableProperty]
private TimeSpan silencePauseDelay = TimeSpan.FromSeconds(3);
[ObservableProperty]
private TimeSpan restartMinInterval = TimeSpan.FromMilliseconds(350);
[ObservableProperty]
private TimeSpan statusThrottleWindow = TimeSpan.FromMilliseconds(60);
public event EventHandler<SpeechRecognizedEvent>? SpeechRecognized;
public event EventHandler? SpeechSynthesized;
public event EventHandler<string>? StatusChanged;
public SpeechVoice()
{
staThread = new Thread(StaLoop) { IsBackground = true };
staThread.SetApartmentState(ApartmentState.STA);
staThread.Start();
}
public void Start()
{
PostSta(() =>
{
if (IsDisposed())
return;
ApplyMode(VoiceState.MultipleRunning, null);
});
}
public void Stop()
{
PostSta(() =>
{
if (IsDisposed())
return;
ApplyMode(VoiceState.Stopped, null);
});
}
public void Listen(TimeSpan? timeoutOverride = null)
{
PostSta(() =>
{
if (IsDisposed())
return;
ApplyMode(VoiceState.Single, timeoutOverride ?? Timeout);
});
}
public void Speak(string text)
{
if (string.IsNullOrWhiteSpace(text))
return;
PostSta(() =>
{
if (IsDisposed())
return;
synthesizer ??= new SpeechSynthesizer();
synthesizer.SpeakCompleted -= OnSpeakCompleted;
synthesizer.SpeakCompleted += OnSpeakCompleted;
synthesizer.SpeakAsync(text);
});
}
private void ApplyMode(VoiceState newState, TimeSpan? timeoutValue)
{
modeGeneration++;
state = newState;
if (newState == VoiceState.Stopped)
{
StopTimeout();
if (recognizer is not null)
{
recognizer.RecognizeAsyncCancel();
recognizer.RecognizeAsyncStop();
}
RaiseStatus("Stopped");
return;
}
var engine = EnsureRecognizer();
if (engine is null)
return;
engine.RecognizeAsyncCancel();
engine.RecognizeAsyncStop();
engine.SetInputToDefaultAudioDevice();
lastVoiceTick = Environment.TickCount64;
lastRestartTick = 0;
if (newState == VoiceState.Single)
{
StartTimeout(timeoutValue ?? Timeout);
RaiseStatus("Listen Single");
engine.RecognizeAsync(RecognizeMode.Single);
return;
}
StopTimeout();
RaiseStatus("Start Multiple");
engine.RecognizeAsync(RecognizeMode.Multiple);
}
private SpeechRecognitionEngine? EnsureRecognizer()
{
if (recognizer is not null)
return recognizer;
var info = SpeechRecognitionEngine
.InstalledRecognizers()
.FirstOrDefault(x =>
x.Culture.Name.Equals(
CultureInfo.CurrentCulture.Name,
StringComparison.OrdinalIgnoreCase))
?? SpeechRecognitionEngine.InstalledRecognizers().FirstOrDefault();
if (info is null)
{
RaiseStatus("No recognizer installed");
return null;
}
var engine = new SpeechRecognitionEngine(info);
engine.LoadGrammar(new DictationGrammar());
engine.AudioLevelUpdated += OnAudioLevel;
engine.RecognizeCompleted += OnRecognizeCompleted;
engine.SpeechRecognized += OnRecognized;
recognizer = engine;
return engine;
}
private void OnAudioLevel(object? sender, AudioLevelUpdatedEventArgs e)
{
PostSta(() =>
{
if (IsDisposed())
return;
lastAudioLevel = e.AudioLevel;
if (state == VoiceState.MultipleRunning)
{
if (e.AudioLevel >= SilenceAudioLevelThresholdLow)
lastVoiceTick = Environment.TickCount64;
if (Environment.TickCount64 - lastVoiceTick >
(long)SilencePauseDelay.TotalMilliseconds)
{
state = VoiceState.MultiplePaused;
RaiseStatus("Paused by silence");
}
}
else if (state == VoiceState.MultiplePaused)
{
if (e.AudioLevel >= SilenceAudioLevelThresholdHigh)
{
state = VoiceState.MultipleRunning;
RaiseStatus("Resume by voice");
}
}
});
}
private void OnRecognized(object? sender, SpeechRecognizedEventArgs e)
{
PostSta(() =>
{
if (IsDisposed())
return;
if (state == VoiceState.MultiplePaused)
return;
var text = e.Result?.Text ?? string.Empty;
var confidence = e.Result?.Confidence ?? 0;
if (string.IsNullOrWhiteSpace(text))
return;
if (confidence < ConfidenceThreshold)
return;
var audioLevel = lastAudioLevel;
RaiseToUi(() =>
{
if (IsDisposed())
return;
SpeechRecognized?.Invoke(this,
new SpeechRecognizedEvent(text, confidence, audioLevel));
});
});
}
private void OnRecognizeCompleted(object? sender, RecognizeCompletedEventArgs e)
{
var generationSnapshot = Volatile.Read(ref modeGeneration);
PostSta(() =>
{
if (IsDisposed())
return;
if (generationSnapshot != modeGeneration)
return;
if (state == VoiceState.Stopped || state == VoiceState.Single)
return;
var now = Environment.TickCount64;
if (now - lastRestartTick < (long)RestartMinInterval.TotalMilliseconds)
return;
lastRestartTick = now;
if (recognizer is not null &&
(state == VoiceState.MultipleRunning ||
state == VoiceState.MultiplePaused))
{
recognizer.RecognizeAsync(RecognizeMode.Multiple);
}
});
}
private void OnSpeakCompleted(object? sender, SpeakCompletedEventArgs e)
=> RaiseToUi(() =>
{
if (IsDisposed())
return;
SpeechSynthesized?.Invoke(this, EventArgs.Empty);
});
private void RaiseStatus(string msg)
{
lastStatus = msg;
statusTimer ??= new Timer(_ =>
{
RaiseToUi(() =>
{
if (IsDisposed())
return;
StatusChanged?.Invoke(this, lastStatus);
});
});
statusTimer.Change(StatusThrottleWindow, System.Threading.Timeout.InfiniteTimeSpan);
}
private void StartTimeout(TimeSpan value)
{
StopTimeout();
timeoutTimer = new Timer(_ =>
{
PostSta(() =>
{
if (IsDisposed())
return;
recognizer?.RecognizeAsyncCancel();
recognizer?.RecognizeAsyncStop();
RaiseStatus("Timeout");
});
}, null, value, System.Threading.Timeout.InfiniteTimeSpan);
}
private void StopTimeout()
{
timeoutTimer?.Dispose();
timeoutTimer = null;
}
private void StaLoop()
{
try
{
foreach (var a in staQueue.GetConsumingEnumerable(staCts.Token))
{
try { a(); }
catch (Exception ex) { Debug.WriteLine(ex); }
}
}
catch (OperationCanceledException)
{
}
}
private void PostSta(Action a)
{
if (IsDisposed())
return;
try
{
staQueue.TryAdd(a);
}
catch (InvalidOperationException)
{
}
}
private static void RaiseToUi(Action a)
=> Dispatcher.UIThread.Post(a);
private bool IsDisposed()
=> Volatile.Read(ref disposing) != 0 || Volatile.Read(ref disposed) != 0;
public void Dispose()
{
if (Interlocked.Exchange(ref disposing, 1) != 0)
return;
StopTimeout();
statusTimer?.Dispose();
statusTimer = null;
PostSta(() =>
{
if (recognizer is not null)
{
recognizer.RecognizeAsyncCancel();
recognizer.RecognizeAsyncStop();
recognizer.AudioLevelUpdated -= OnAudioLevel;
recognizer.RecognizeCompleted -= OnRecognizeCompleted;
recognizer.SpeechRecognized -= OnRecognized;
recognizer.Dispose();
recognizer = null;
}
if (synthesizer is not null)
{
synthesizer.SpeakCompleted -= OnSpeakCompleted;
synthesizer.Dispose();
synthesizer = null;
}
});
staQueue.CompleteAdding();
staCts.Cancel();
try { staThread.Join(2000); }
catch { }
Volatile.Write(ref disposed, 1);
}
}
public sealed class SpeechRecognizedEvent(string text, double confidence, int audioLevel) : EventArgs
{
public string Text { get; } = text;
public double Confidence { get; } = confidence;
public int AudioLevel { get; } = audioLevel;
}
运行效果

此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。