借助网络配置,控制Modbus工具的日志显示,避免卡死界面
智能大石头 authored at 2021-12-11 22:09:19
7.14 KiB
XCoder
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using XCoderMac.Dialogs;

namespace XCoderMac.Views;

/// <summary>调用系统自带 /usr/bin/say,取代 XCoderAv 中只支持 Windows 的语音占位。</summary>
public partial class MacSpeechWindow : Window, INotifyPropertyChanged
{
    private Process? _speakingProcess;
    private bool _voicesLoaded;
    private string? _selectedVoice;
    private string _statusText = "就绪";
    private string _exportFilePath =
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), "XCoderSpeech.aiff");

    public new event PropertyChangedEventHandler? PropertyChanged;
    public ObservableCollection<string> Voices { get; } = new();

    public string? SelectedVoice
    {
        get => _selectedVoice;
        set { _selectedVoice = value; Changed(); }
    }

    public string StatusText
    {
        get => _statusText;
        set { _statusText = value; Changed(); }
    }

    public string ExportFilePath
    {
        get => _exportFilePath;
        set { _exportFilePath = value; Changed(); }
    }

    public MacSpeechWindow()
    {
        InitializeComponent();
        DataContext = this;
        Opened += async (_, _) => await EnsureTabVoicesAsync();
        Closed += (_, _) => StopReading();
    }

    private void Changed([CallerMemberName] string? property = null) =>
        PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(property));

    private static ProcessStartInfo NewSay(string text, int rate, string? voice, string? outputFile = null)
    {
        var info = new ProcessStartInfo("/usr/bin/say")
        {
            UseShellExecute = false,
            RedirectStandardError = true
        };
        info.ArgumentList.Add("-r");
        info.ArgumentList.Add(rate.ToString());
        if (!string.IsNullOrWhiteSpace(voice))
        {
            info.ArgumentList.Add("-v");
            info.ArgumentList.Add(voice);
        }
        if (outputFile is not null)
        {
            info.ArgumentList.Add("-o");
            info.ArgumentList.Add(outputFile);
        }
        info.ArgumentList.Add(text);
        return info;
    }

    public async Task EnsureTabVoicesAsync()
    {
        if (_voicesLoaded) return;
        _voicesLoaded = true;
        await LoadVoicesAsync();
    }

    public void StopForTab() => StopReading();

    private async Task LoadVoicesAsync()
    {
        try
        {
            var psi = new ProcessStartInfo("/usr/bin/say")
            {
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            };
            psi.ArgumentList.Add("-v");
            psi.ArgumentList.Add("?");
            using var process = Process.Start(psi) ?? throw new InvalidOperationException("无法调用 macOS say");
            var textTask = process.StandardOutput.ReadToEndAsync();
            var errorTask = process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            var output = await textTask;
            var error = await errorTask;
            if (process.ExitCode != 0) throw new InvalidOperationException(error);

            Voices.Clear();
            foreach (var line in output.Split('\n', StringSplitOptions.RemoveEmptyEntries))
            {
                var parts = Regex.Split(line.Trim(), @"\s{2,}");
                if (parts.Length > 0 && !string.IsNullOrWhiteSpace(parts[0]))
                    Voices.Add(parts[0].Trim());
            }

            SelectedVoice = Voices.Count > 0 ? Voices[0] : null;
            StatusText = $"已加载 {Voices.Count} 个系统语音";
        }
        catch (Exception ex)
        {
            StatusText = $"加载语音列表失败:{ex.Message}";
        }
    }

    private int Rate()
    {
        if (!int.TryParse(RateBox.Text, out var value) || value is < 80 or > 400)
            throw new ArgumentException("语速应为 80~400 词/分钟");
        return value;
    }

    private async void Speak_Click(object? sender, RoutedEventArgs e)
    {
        if (string.IsNullOrWhiteSpace(TextToSpeak.Text))
        {
            StatusText = "请输入要朗读的内容";
            return;
        }
        StopReading();
        try
        {
            var psi = NewSay(TextToSpeak.Text, Rate(), SelectedVoice);
            var process = Process.Start(psi) ?? throw new InvalidOperationException("语音进程启动失败");
            _speakingProcess = process;
            StatusText = "正在朗读…";
            SpeakButton.IsEnabled = false;

            var errorTask = process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            var error = await errorTask;
            StatusText = process.ExitCode == 0 ? "朗读完成" : $"朗读中断:{error}";
        }
        catch (Exception ex)
        {
            StatusText = $"朗读失败:{ex.Message}";
        }
        finally
        {
            _speakingProcess?.Dispose();
            _speakingProcess = null;
            SpeakButton.IsEnabled = true;
        }
    }

    private void Stop_Click(object? sender, RoutedEventArgs e)
    {
        StopReading();
        StatusText = "已停止朗读";
    }

    private void StopReading()
    {
        try
        {
            if (_speakingProcess is { HasExited: false } process) process.Kill();
        }
        catch (InvalidOperationException) { }
    }

    private async void Export_Click(object? sender, RoutedEventArgs e)
    {
        if (string.IsNullOrWhiteSpace(TextToSpeak.Text))
        {
            StatusText = "请输入要导出的朗读内容";
            return;
        }

        try
        {
            var file = Path.GetFullPath(ExportFilePath.Trim());
            if (!file.EndsWith(".aiff", StringComparison.OrdinalIgnoreCase))
            {
                StatusText = "请输入 .aiff 格式的文件名";
                return;
            }
            if (!Directory.Exists(Path.GetDirectoryName(file)))
            {
                StatusText = "输出目录不存在";
                return;
            }

            if (File.Exists(file) && !await ConfirmDialog.ShowAsync((TopLevel.GetTopLevel(sender as Control) as Window) ?? this,
                    "覆盖已有音频文件?", $"目标文件已存在:{file}", "确认覆盖"))
                return;

            StatusText = "正在生成音频文件…";
            var psi = NewSay(TextToSpeak.Text, Rate(), SelectedVoice, file);
            using var process = Process.Start(psi) ?? throw new InvalidOperationException("无法启动语音合成");
            var errorTask = process.StandardError.ReadToEndAsync();
            await process.WaitForExitAsync();
            var error = await errorTask;
            StatusText = process.ExitCode == 0 ? $"已导出:{file}" : $"导出失败:{error}";
        }
        catch (Exception ex)
        {
            StatusText = $"导出失败:{ex.Message}";
        }
    }
}