v0.4: persistent config in AppData, voices in LOCALAPPDATA, remove Speak button

This commit is contained in:
2026-08-10 12:32:07 +00:00
parent 9b0dc11ee6
commit fbbb52df90
3 changed files with 113 additions and 20 deletions
+56
View File
@@ -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<AppConfig>(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
{
}
}
}
+2 -9
View File
@@ -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);
+55 -11
View File
@@ -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();
}
}