From fbbb52df90475408cc869edd260a448ba1226cf4 Mon Sep 17 00:00:00 2001 From: Mute Date: Mon, 10 Aug 2026 12:32:07 +0000 Subject: [PATCH] v0.4: persistent config in AppData, voices in LOCALAPPDATA, remove Speak button --- Robovoice.App/AppConfig.cs | 56 +++++++++++++++++++++++++ Robovoice.App/MainForm.Designer.cs | 11 +---- Robovoice.App/MainForm.cs | 66 +++++++++++++++++++++++++----- 3 files changed, 113 insertions(+), 20 deletions(-) create mode 100644 Robovoice.App/AppConfig.cs diff --git a/Robovoice.App/AppConfig.cs b/Robovoice.App/AppConfig.cs new file mode 100644 index 0000000..f0ad4a7 --- /dev/null +++ b/Robovoice.App/AppConfig.cs @@ -0,0 +1,56 @@ +using System.Text.Json; +using System.Windows.Forms; + +namespace Robovoice.App; + +public sealed class AppConfig +{ + public string Voice { get; set; } = string.Empty; + public int PttKey { get; set; } = (int)Keys.F8; + public string OutputDevice { get; set; } = string.Empty; + public int NoiseScale { get; set; } = 667; + public int LengthScale { get; set; } = 100; + public int NoiseWScale { get; set; } = 800; + public bool MinimizeToTray { get; set; } = true; + + public static string AppDataDir => Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Hackmaster", "Robovoice"); + + public static string ConfigPath => Path.Combine(AppDataDir, "config.json"); + public static string VoicesDir => Path.Combine(AppDataDir, "Piper-voices"); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + }; + + public static AppConfig Load() + { + try + { + if (File.Exists(ConfigPath)) + { + using var stream = File.OpenRead(ConfigPath); + return JsonSerializer.Deserialize(stream) ?? new AppConfig(); + } + } + catch + { + } + return new AppConfig(); + } + + public void Save() + { + try + { + Directory.CreateDirectory(AppDataDir); + using var stream = File.Create(ConfigPath); + JsonSerializer.Serialize(stream, this, JsonOptions); + } + catch + { + } + } +} diff --git a/Robovoice.App/MainForm.Designer.cs b/Robovoice.App/MainForm.Designer.cs index da2c5e7..b11f1cd 100644 --- a/Robovoice.App/MainForm.Designer.cs +++ b/Robovoice.App/MainForm.Designer.cs @@ -22,7 +22,6 @@ partial class MainForm private Button btnClearLog = null!; private Label lblTextInput = null!; private TextBox txtTextInput = null!; - private Button btnSpeakInput = null!; private Label lblNoise = null!; private TrackBar trkNoise = null!; private Label lblNoiseVal = null!; @@ -61,7 +60,6 @@ partial class MainForm btnClearLog = new Button(); lblTextInput = new Label(); txtTextInput = new TextBox(); - btnSpeakInput = new Button(); lblNoise = new Label(); trkNoise = new TrackBar(); lblNoiseVal = new Label(); @@ -85,6 +83,7 @@ partial class MainForm txtPttKey.Size = new Size(80, 23); txtPttKey.ReadOnly = true; txtPttKey.TextAlign = HorizontalAlignment.Center; + txtPttKey.TabStop = false; // lblVoice lblVoice.Text = "Voice:"; @@ -221,13 +220,8 @@ partial class MainForm // txtTextInput txtTextInput.Location = new Point(50, 118); - txtTextInput.Size = new Size(680, 23); + txtTextInput.Size = new Size(762, 23); - // btnSpeakInput - btnSpeakInput.Text = "Speak"; - btnSpeakInput.Location = new Point(737, 117); - btnSpeakInput.Size = new Size(75, 25); - btnSpeakInput.UseVisualStyleBackColor = true; txtLog.ReadOnly = true; txtLog.Font = new Font("Consolas", 9F); txtLog.BackColor = Color.FromArgb(30, 30, 30); @@ -272,7 +266,6 @@ partial class MainForm Controls.Add(lblNoiseWVal); Controls.Add(lblTextInput); Controls.Add(txtTextInput); - Controls.Add(btnSpeakInput); Controls.Add(txtLog); Controls.Add(chkMinimizeToTray); Controls.Add(btnClearLog); diff --git a/Robovoice.App/MainForm.cs b/Robovoice.App/MainForm.cs index cb95566..218e4b7 100644 --- a/Robovoice.App/MainForm.cs +++ b/Robovoice.App/MainForm.cs @@ -10,8 +10,9 @@ namespace Robovoice.App; internal sealed partial class MainForm : Form { - private readonly string _voicesDir = @"C:\data\opencode\robovoice-voices"; + private readonly string _voicesDir = AppConfig.VoicesDir; private readonly string _espeakDataPath; + private readonly AppConfig _config; private LibPiperTtsEngine? _tts; private AudioOutput? _audioOutput; @@ -25,21 +26,40 @@ internal sealed partial class MainForm : Form { InitializeComponent(); _espeakDataPath = Path.Combine(AppContext.BaseDirectory, "espeak-ng-data"); + _config = AppConfig.Load(); + Directory.CreateDirectory(AppConfig.AppDataDir); + Directory.CreateDirectory(_voicesDir); Load += OnLoad; FormClosing += OnFormClosing; } private async void OnLoad(object? sender, EventArgs e) { + ActiveControl = txtLog; PopulateVoices(); PopulateOutputDevices(); - _pttKey = Keys.F8; + _pttKey = (Keys)_config.PttKey; + if (_pttKey == Keys.None) + _pttKey = Keys.F8; txtPttKey.Text = KeyToDisplayString(_pttKey); - if (cmbVoice.Items.Count > 0) + + if (!string.IsNullOrEmpty(_config.Voice) && cmbVoice.Items.Contains(_config.Voice)) + cmbVoice.SelectedItem = _config.Voice; + else if (cmbVoice.Items.Count > 0) cmbVoice.SelectedIndex = 0; - AutoSelectCableOutput(); + if (!string.IsNullOrEmpty(_config.OutputDevice) && cmbOutput.Items.Contains(_config.OutputDevice)) + cmbOutput.SelectedItem = _config.OutputDevice; + else + AutoSelectCableOutput(); + + trkNoise.Value = _config.NoiseScale; + trkSpeed.Value = _config.LengthScale; + trkNoiseW.Value = _config.NoiseWScale; + OnSliderScroll(null, EventArgs.Empty); + + chkMinimizeToTray.Checked = _config.MinimizeToTray; btnBrowseFile.Click += OnBrowseFile; btnTestVoice.Click += OnTestVoice; @@ -57,6 +77,10 @@ internal sealed partial class MainForm : Form trkSpeed.MouseUp += OnSliderReleased; trkNoiseW.MouseUp += OnSliderReleased; + chkMinimizeToTray.CheckedChanged += (_, _) => SaveConfig(); + + Resize += OnResize; + SetupTray(); await InitializeEngineAsync(); @@ -98,6 +122,7 @@ internal sealed partial class MainForm : Form _captureHook?.Dispose(); _captureHook = null; SetupHotkey(); + SaveConfig(); } private void CancelCapture() @@ -334,6 +359,7 @@ internal sealed partial class MainForm : Form cmbVoice.SelectedIndex = 0; } + SaveConfig(); _ = InitializeEngineAsync(); } @@ -344,6 +370,7 @@ internal sealed partial class MainForm : Form if (cmbVoice.SelectedItem is string name) { + SaveConfig(); _ = ReinitializeEngineAsync(name); } } @@ -368,6 +395,7 @@ internal sealed partial class MainForm : Form private void OnSliderReleased(object? sender, MouseEventArgs e) { + SaveConfig(); if (cmbVoice.SelectedItem is string name && VoiceCatalogue.IsVoiceInstalled(_voicesDir, name)) { _ = InitializeEngineAsync(); @@ -379,6 +407,7 @@ internal sealed partial class MainForm : Form if (_orchestrator is not null) _orchestrator.OutputDeviceName = cmbOutput.SelectedItem as string ?? string.Empty; Log($"Output device: {_orchestrator?.OutputDeviceName}"); + SaveConfig(); } private void UpdateLineStatus() @@ -415,6 +444,15 @@ internal sealed partial class MainForm : Form _trayIcon.DoubleClick += (_, _) => ShowWindow(); } + private void OnResize(object? sender, EventArgs e) + { + if (WindowState == FormWindowState.Minimized && chkMinimizeToTray.Checked) + { + Hide(); + WindowState = FormWindowState.Normal; + } + } + private void ShowWindow() { Show(); @@ -422,6 +460,18 @@ internal sealed partial class MainForm : Form Activate(); } + private void SaveConfig() + { + _config.Voice = cmbVoice.SelectedItem as string ?? string.Empty; + _config.PttKey = (int)_pttKey; + _config.OutputDevice = cmbOutput.SelectedItem as string ?? string.Empty; + _config.NoiseScale = trkNoise.Value; + _config.LengthScale = trkSpeed.Value; + _config.NoiseWScale = trkNoiseW.Value; + _config.MinimizeToTray = chkMinimizeToTray.Checked; + _config.Save(); + } + private void Log(string message) { if (IsDisposed) return; @@ -438,15 +488,9 @@ internal sealed partial class MainForm : Form private void OnFormClosing(object? sender, FormClosingEventArgs e) { - if (e.CloseReason == CloseReason.UserClosing && chkMinimizeToTray.Checked && _trayIcon?.Visible == true) - { - e.Cancel = true; - Hide(); - return; - } - _pttHotkey?.Dispose(); _trayIcon!.Visible = false; _orchestrator?.DisposeAsync().AsTask().Wait(); + SaveConfig(); } }