惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

N
Netflix TechBlog - Medium
S
Secure Thoughts
aimingoo的专栏
aimingoo的专栏
量子位
F
Full Disclosure
云风的 BLOG
云风的 BLOG
D
Docker
Stack Overflow Blog
Stack Overflow Blog
Last Week in AI
Last Week in AI
Security Latest
Security Latest
Project Zero
Project Zero
Cyberwarzone
Cyberwarzone
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
AWS News Blog
AWS News Blog
G
GRAHAM CLULEY
T
Threat Research - Cisco Blogs
The Hacker News
The Hacker News
The Register - Security
The Register - Security
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
MyScale Blog
MyScale Blog
博客园_首页
阮一峰的网络日志
阮一峰的网络日志
P
Privacy & Cybersecurity Law Blog
V
Visual Studio Blog
A
Arctic Wolf
L
LINUX DO - 热门话题
W
WeLiveSecurity
Google DeepMind News
Google DeepMind News
TaoSecurity Blog
TaoSecurity Blog
Recorded Future
Recorded Future
K
Kaspersky official blog
L
Lohrmann on Cybersecurity
V
V2EX
Recent Announcements
Recent Announcements
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
S
Securelist
S
Security Affairs
Forbes - Security
Forbes - Security
P
Proofpoint News Feed
Google DeepMind News
Google DeepMind News
C
Cisco Blogs
AI
AI
Vercel News
Vercel News
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
T
Tor Project blog
Scott Helme
Scott Helme
Spread Privacy
Spread Privacy

博客园 - dalgleish

C# Avalonia 23- OpenGL- 例子通用类 C# Avalonia 22- SoundAndVideo- SpeechRecognition C# Avalonia 22- SoundAndVideo- SpeechSynthesis C# Avalonia 22- SoundAndVideo- CodePlayback C# Avalonia 22- SoundAndVideo- SoundPlayerTest C# Avalonia 21- MenusAndToolbars- RibbonTest C# Avalonia 21- MenusAndToolbars- ProportionalStatusBar C# Avalonia 21- MenusAndToolbars- BasicToolbar C# Avalonia 21- MenusAndToolbars- ToolBar/Ribbon开源项目介绍和修复 C# Avalonia 21- MenusAndToolbars- SidebarMenu C# Avalonia 21- MenusAndToolbars- MixedMenus C# Avalonia 20 - WindowsMenu- 魔改Hyperlink - 使用例子 C# Avalonia 20 - WindowsMenu- 魔改Hyperlink C# Avalonia 20 - WindowsMenu- WebViewTest C# Avalonia 20 - WindowsMenu- WebView C# Avalonia 20 - WindowsMenu- ModernWindowTest C# Avalonia 20 - WindowsMenu- ModernWindow C# Avalonia 20 - WindowsMenu- TransparentWithShapes C# Avalonia 20 - WindowsMenu- TransparentBackground C# Avalonia 20 - WindowsMenu- OpenFileTest C# Avalonia 20 - WindowsMenu- SavePostion C# Avalonia 20 - WindowsMenu- CenterScreen C# Avalonia 19- DataBinding- DataGridGrouping C# Avalonia 19- DataBinding- DirectoryTreeView C# Avalonia 19- DataBinding- BoundTreeView C# Avalonia 19- DataBinding- CustomListViewTest
C# Avalonia 22- SoundAndVideo- SpeechVoiceTest
dalgleish · 2026-07-19 · via 博客园 - dalgleish

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;
}

运行效果

image