├── ignore.dict
├── Resources
├── test
├── icon.ico
└── refresh-32.png
├── banned.dict
├── replace.dict
├── Form1.cs
├── piper_tray.csproj.user
├── piper_tray.csproj
├── FileLogger.cs
├── settings.conf
├── .github
└── FUNDING.yml
├── piper_tray.generated.sln
├── Form1.Designer.cs
├── README.md
├── Program.cs
└── PiperTrayApp.cs
/ignore.dict:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/Resources/test:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/banned.dict:
--------------------------------------------------------------------------------
1 | https://
2 | http://
3 |
--------------------------------------------------------------------------------
/replace.dict:
--------------------------------------------------------------------------------
1 | LHC=Large Hadron Collider
2 |
--------------------------------------------------------------------------------
/Resources/icon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jame25/Piper-Tray/HEAD/Resources/icon.ico
--------------------------------------------------------------------------------
/Resources/refresh-32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jame25/Piper-Tray/HEAD/Resources/refresh-32.png
--------------------------------------------------------------------------------
/Form1.cs:
--------------------------------------------------------------------------------
1 | namespace piper_tray;
2 |
3 | public partial class Form1 : Form
4 | {
5 | public Form1()
6 | {
7 | InitializeComponent();
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/piper_tray.csproj.user:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Form
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/piper_tray.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Resources\icon.ico
5 | WinExe
6 | net8.0-windows
7 | enable
8 | true
9 | enable
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/FileLogger.cs:
--------------------------------------------------------------------------------
1 | using System.Text;
2 |
3 | public class FileLogger : TextWriter
4 | {
5 | private string logPath;
6 | private static object _lock = new object();
7 |
8 | public FileLogger(string path)
9 | {
10 | logPath = path;
11 | }
12 |
13 | public override void WriteLine(string value)
14 | {
15 | lock (_lock)
16 | {
17 | File.AppendAllText(logPath, value + Environment.NewLine);
18 | }
19 | }
20 |
21 | public override Encoding Encoding
22 | {
23 | get { return Encoding.UTF8; }
24 | }
25 | }
26 |
27 |
--------------------------------------------------------------------------------
/settings.conf:
--------------------------------------------------------------------------------
1 | Logging=False
2 | MonitoringModifier=0x01
3 | MonitoringKey=0x4D
4 | StopSpeechModifier=0x01
5 | StopSpeechKey=0x51
6 | ChangeVoiceModifier=0x01
7 | ChangeVoiceKey=0x56
8 | SpeedIncreaseModifier=0x01
9 | SpeedIncreaseKey=0xBB
10 | SpeedDecreaseModifier=0x01
11 | SpeedDecreaseKey=0xBD
12 | SwitchPresetModifier=0x01
13 | SwitchPresetKey=0x50
14 | VoiceModel=en_US-libritts_r-medium
15 | MonitoringEnabled=True
16 | Speed=1.1
17 | Speaker=0
18 | SentenceSilence=0.5
19 | MenuVisible_Monitoring=True
20 | MenuVisible_Export_to_WAV=False
21 | MenuVisible_Voice=True
22 | MenuVisible_Speed=True
23 | MenuVisible_Presets=True
24 | MenuVisible_Stop_Speech=False
25 | LastUsedPreset=0
26 | MonitoringHotkeyEnabled=False
27 | SwitchPresetHotkeyEnabled=False
28 | StopSpeechHotkeyEnabled=False
29 | ChangeVoiceHotkeyEnabled=False
30 | SpeedIncreaseHotkeyEnabled=False
31 | SpeedDecreaseHotkeyEnabled=False
32 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
4 | patreon: # Replace with a single Patreon username
5 | open_collective: # Replace with a single Open Collective username
6 | ko_fi: Jame25
7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | liberapay: # Replace with a single Liberapay username
10 | issuehunt: # Replace with a single IssueHunt username
11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
12 | polar: # Replace with a single Polar username
13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
14 | thanks_dev: # Replace with a single thanks.dev username
15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
16 |
--------------------------------------------------------------------------------
/piper_tray.generated.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 17
4 | VisualStudioVersion = 17.5.002.0
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "piper_tray_classic", "piper_tray_classic.csproj", "{F09F8461-69F7-4CE5-80EC-7A1BE252504C}"
7 | EndProject
8 | Global
9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
10 | Debug|Any CPU = Debug|Any CPU
11 | Release|Any CPU = Release|Any CPU
12 | EndGlobalSection
13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
14 | {F09F8461-69F7-4CE5-80EC-7A1BE252504C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
15 | {F09F8461-69F7-4CE5-80EC-7A1BE252504C}.Debug|Any CPU.Build.0 = Debug|Any CPU
16 | {F09F8461-69F7-4CE5-80EC-7A1BE252504C}.Release|Any CPU.ActiveCfg = Release|Any CPU
17 | {F09F8461-69F7-4CE5-80EC-7A1BE252504C}.Release|Any CPU.Build.0 = Release|Any CPU
18 | EndGlobalSection
19 | GlobalSection(SolutionProperties) = preSolution
20 | HideSolutionNode = FALSE
21 | EndGlobalSection
22 | GlobalSection(ExtensibilityGlobals) = postSolution
23 | SolutionGuid = {D4655AF7-8651-47A2-9ECB-16461E952061}
24 | EndGlobalSection
25 | EndGlobal
26 |
--------------------------------------------------------------------------------
/Form1.Designer.cs:
--------------------------------------------------------------------------------
1 | namespace piper_tray;
2 |
3 | partial class Form1
4 | {
5 | ///
6 | /// Required designer variable.
7 | ///
8 | private System.ComponentModel.IContainer components = null;
9 |
10 | ///
11 | /// Clean up any resources being used.
12 | ///
13 | /// true if managed resources should be disposed; otherwise, false.
14 | protected override void Dispose(bool disposing)
15 | {
16 | if (disposing && (components != null))
17 | {
18 | components.Dispose();
19 | }
20 | base.Dispose(disposing);
21 | }
22 |
23 | #region Windows Form Designer generated code
24 |
25 | ///
26 | /// Required method for Designer support - do not modify
27 | /// the contents of this method with the code editor.
28 | ///
29 | private void InitializeComponent()
30 | {
31 | this.components = new System.ComponentModel.Container();
32 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
33 | this.ClientSize = new System.Drawing.Size(800, 450);
34 | this.Text = "Form1";
35 | }
36 |
37 | #endregion
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 | Piper Tray is a small system tray utility for Windows, that utilizes [Piper](https://github.com/OHF-Voice/piper1-gpl). It will read aloud the contents of your clipboard. You can stop the speech at any time via an assigned hotkey.
5 |
6 | If you would prefer a GUI, check out my other project: [Piper Read](https://github.com/jame25/piper-read).
7 |
8 |
9 | ## Features:
10 |
11 | * Reads clipboard contents aloud
12 | * Enable / Disable clipboard monitoring
13 | * Many voices to choose from
14 | * Change Piper TTS voice model
15 | * Control Piper TTS speech rate
16 | * Presets support
17 | * Hotkeys support
18 | * Prevent keywords being read (ignore.dict)
19 | * Replace keywords with alternatives (replace.dict)
20 |
21 | ## Prerequisites:
22 |
23 | [.Net 8.0 Desktop Runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) is required to be installed.
24 |
25 | ## Install:
26 |
27 | - Download the latest version of Piper Tray from [releases](https://github.com/jame25/Piper-Tray/releases/).
28 | - Grab the latest Windows binary for Piper from [here](https://github.com/rhasspy/piper/releases). Voice models are available [here](https://huggingface.co/rhasspy/piper-voices/tree/main).
29 | - Extract all of the above into the same directory.
30 |
31 | ## Configuration:
32 |
33 | Piper Tray should support all available Piper voice models, by default **en_US-libritts_r-medium.onnx** and .json are expected to be present in directory.
34 |
35 | ## Settings:
36 |
37 | You can change the voice model being utilized by Piper Tray by editing the first line of the **settings.conf**.
38 |
39 | Speech rate can be altered using the 'speed' variable (1 is the default speed, higher values i.e 5 = faster).
40 |
41 | ## Dictionary Rules:
42 |
43 | Keywords found in the **ignore.dict** file are skipped over.
44 |
45 | If a keyword in the **banned.dict** file is detected, the entire line is skipped.
46 |
47 | **replace.dict** functions as a replacement for a keyword or phrase, i.e LHC=Large Hadron Collider
48 |
49 | ## Support:
50 |
51 | If you find this project helpful and would like to support its development, you can buy me a coffee on Ko-Fi:
52 |
53 | [](https://ko-fi.com/jame25)
54 |
--------------------------------------------------------------------------------
/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Threading;
4 | using System.Windows.Forms;
5 |
6 | namespace PiperTray
7 | {
8 | static class Program
9 | {
10 | static Mutex mutex = new Mutex(true, "{8F6F0AC4-B9A1-45fd-A8CF-72F04E6BDE8F}");
11 |
12 | [STAThread]
13 | static void Main()
14 | {
15 | Application.SetUnhandledExceptionMode(UnhandledExceptionMode.CatchException);
16 | Application.ThreadException += new ThreadExceptionEventHandler(Application_ThreadException);
17 | AppDomain.CurrentDomain.UnhandledException += new UnhandledExceptionEventHandler(CurrentDomain_UnhandledException);
18 |
19 | if (mutex.WaitOne(TimeSpan.Zero, true))
20 | {
21 | try
22 | {
23 | Application.EnableVisualStyles();
24 | Application.SetCompatibleTextRenderingDefault(false);
25 | var app = PiperTrayApp.GetInstance();
26 | app.Initialize();
27 |
28 | // Set up logging only if it's enabled in settings
29 | if (PiperTrayApp.IsLoggingEnabled)
30 | {
31 | string logPath = Path.Combine(Application.StartupPath, "system.log");
32 | Console.SetOut(new FileLogger(logPath));
33 | }
34 |
35 | Application.Run(app);
36 | }
37 | finally
38 | {
39 | mutex.ReleaseMutex();
40 | }
41 | }
42 | else
43 | {
44 | MessageBox.Show("Another instance of Piper Tray is already running.", "Piper Tray", MessageBoxButtons.OK, MessageBoxIcon.Information);
45 | }
46 | }
47 |
48 | static void Application_ThreadException(object sender, ThreadExceptionEventArgs e)
49 | {
50 | LogUnhandledException(e.Exception, "Thread Exception");
51 | }
52 |
53 | static void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
54 | {
55 | LogUnhandledException((Exception)e.ExceptionObject, "Unhandled Exception");
56 | }
57 |
58 | static void LogUnhandledException(Exception ex, string source)
59 | {
60 | string logPath = Path.Combine(Application.StartupPath, "crash.log");
61 | using (StreamWriter writer = new StreamWriter(logPath, true))
62 | {
63 | writer.WriteLine($"{DateTime.Now}: {source}");
64 | writer.WriteLine(ex.ToString());
65 | writer.WriteLine();
66 | }
67 | }
68 | }
69 | }
--------------------------------------------------------------------------------
/PiperTrayApp.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Collections.Concurrent;
4 | using System.Diagnostics;
5 | using System.Drawing;
6 | using System.IO;
7 | using System.Linq;
8 | using System.Windows.Forms;
9 | using System.Reflection;
10 | using NAudio.Wave;
11 | using System.Threading.Tasks;
12 | using NAudio.CoreAudioApi;
13 | using System.Runtime.InteropServices;
14 | using System.Runtime.CompilerServices;
15 | using System.Text;
16 | using System.Text.Json;
17 | using System.Text.Json.Serialization;
18 | using System.Text.RegularExpressions;
19 | using System.ComponentModel;
20 | using System.Globalization;
21 |
22 | namespace PiperTray
23 | {
24 | public class PiperTrayApp : Form
25 | {
26 | private static readonly Lazy lazy =
27 | new Lazy(() => new PiperTrayApp());
28 |
29 | private enum AudioPlaybackState
30 | {
31 | Idle,
32 | Initializing,
33 | Playing,
34 | Stopping,
35 | StopRequested
36 | }
37 |
38 | private AudioPlaybackState CurrentAudioState { get; set; } = AudioPlaybackState.Idle;
39 |
40 | [DllImport("user32.dll")]
41 | private static extern IntPtr SendMessage(IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
42 |
43 | [DllImport("user32.dll")]
44 | private static extern bool RegisterHotKey(IntPtr hWnd, int id, uint fsModifiers, uint vk);
45 |
46 | [DllImport("user32.dll")]
47 | private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
48 |
49 |
50 | private class VoiceModelState
51 | {
52 | public int CurrentIndex { get; set; }
53 | public List Models { get; set; }
54 | public bool IsDirty { get; set; }
55 | }
56 |
57 | private VoiceModelState voiceModelState;
58 | private ToolStripMenuItem exportMenuItem;
59 |
60 | public class CustomVoiceMenuItem : ToolStripMenuItem
61 | {
62 | public bool IsSelected { get; set; }
63 | public bool CheckMarkEnabled { get; set; } = false;
64 | public Color SelectionColor { get; set; } = Color.Green;
65 |
66 | protected override void OnPaint(PaintEventArgs e)
67 | {
68 | // Set CheckState to None to prevent the checkmark from showing
69 | this.CheckState = CheckState.Unchecked;
70 |
71 | base.OnPaint(e);
72 |
73 | if (IsSelected)
74 | {
75 | int columnWidth = 24;
76 | using (SolidBrush grayBrush = new SolidBrush(Color.LightGray))
77 | {
78 | e.Graphics.FillRectangle(grayBrush, 0, 0, columnWidth, this.Height);
79 | }
80 |
81 | using (SolidBrush greenBrush = new SolidBrush(Color.Green))
82 | {
83 | int squareSize = columnWidth - 4;
84 | int yPosition = (this.Height - squareSize) / 2;
85 | e.Graphics.FillRectangle(greenBrush, 2, yPosition, squareSize, squareSize);
86 | }
87 | }
88 | }
89 | }
90 |
91 | public class PresetSettings
92 | {
93 | public string Name { get; set; }
94 | public string VoiceModel { get; set; }
95 | public string Speaker { get; set; }
96 | public string Speed { get; set; }
97 | public string SentenceSilence { get; set; }
98 | public string Enabled { get; set; }
99 |
100 | [JsonIgnore]
101 | public int SpeakerInt => int.TryParse(Speaker, NumberStyles.Integer, CultureInfo.InvariantCulture, out var result) ? result : 0;
102 |
103 | [JsonIgnore]
104 | public double SpeedDouble => double.TryParse(Speed, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) ? result : 1.0;
105 |
106 | [JsonIgnore]
107 | public float SentenceSilenceFloat => float.TryParse(SentenceSilence, NumberStyles.Float, CultureInfo.InvariantCulture, out var result) ? result : 0.5f;
108 |
109 | [JsonIgnore]
110 | public bool EnabledBool => bool.TryParse(Enabled, out var result) ? result : false;
111 | }
112 |
113 | public const int HOTKEY_ID_STOP_SPEECH = 9000;
114 | public const int HOTKEY_ID_MONITORING = 9001;
115 | public const int HOTKEY_ID_CHANGE_VOICE = 9002;
116 | public const int HOTKEY_ID_SPEED_INCREASE = 9003;
117 | public const int HOTKEY_ID_SPEED_DECREASE = 9004;
118 | public const int HOTKEY_ID_SWITCH_PRESET = 9005;
119 |
120 | private uint switchPresetModifiers;
121 | private uint switchPresetVk;
122 | private uint monitoringModifiers;
123 | private uint monitoringVk;
124 | private uint stopSpeechModifiers;
125 | private uint stopSpeechVk;
126 | private uint changeVoiceModifiers;
127 | private uint changeVoiceVk;
128 | private uint speedIncreaseModifiers;
129 | private uint speedIncreaseVk;
130 | private uint speedDecreaseModifiers;
131 | private uint speedDecreaseVk;
132 |
133 | private Dictionary hotkeyActions;
134 |
135 | private Dictionary customCharacterMappings = new Dictionary
136 | {
137 | { "ć", "ch" } // Map 'ć' to a phonetic approximation
138 | };
139 |
140 | private Dictionary menuVisibilitySettings = new Dictionary();
141 | private Dictionary menuItems = new Dictionary();
142 |
143 | private HashSet ignoreWords;
144 | private HashSet bannedWords;
145 | private Dictionary replaceWords;
146 |
147 | private NotifyIcon trayIcon;
148 | private SettingsForm settingsForm;
149 | private ContextMenuStrip contextMenu;
150 | private ToolStripMenuItem toggleMonitoringMenuItem;
151 | private ToolStripMenuItem presetsMenuItem;
152 | private ToolStripMenuItem speedMenuItem;
153 | private ToolStripMenuItem fasterMenuItem;
154 | private ToolStripMenuItem slowerMenuItem;
155 | private ToolStripMenuItem resetSpeedMenuItem;
156 | private ToolStripMenuItem voiceMenuItem;
157 | private ToolStripMenuItem stopSpeechMenuItem;
158 | private ToolStripMenuItem settingsMenuItem;
159 | private ToolStripMenuItem exitMenuItem;
160 | private System.Windows.Forms.Timer clipboardTimer;
161 | private string lastClipboardContent = "";
162 | private bool isMonitoring = true;
163 | private bool ignoreCurrentClipboard = true;
164 | private CancellationTokenSource playbackCancellationTokenSource;
165 | private Thread playbackThread;
166 | private string piperPath;
167 | public static string LogFilePath { get; private set; }
168 | public static bool IsLoggingEnabled { get; private set; }
169 | private static readonly object logLock = new object();
170 | private bool isProcessing = false;
171 | private bool isInitializing = false;
172 | private WaveOutEvent currentWaveOut;
173 | private bool isLoggingEnabled = false;
174 | private List voiceModels;
175 | private int currentVoiceModelIndex = 0;
176 | private int currentSpeaker = 0;
177 | private readonly double[] speedOptions = {
178 | 2.0, 1.9, 1.8, 1.7, 1.6, 1.5, 1.4, 1.3, 1.2, 1.1, // -9 to 0
179 | 1.0, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1 // 1 to 10
180 | };
181 | private int currentSpeedIndex = 10; // Default to 1.0x speed
182 | private double currentSpeed = 1.0;
183 |
184 | private int currentPresetIndex = -1;
185 |
186 | private DateTime lastScanTime = DateTime.MinValue;
187 | private const int ScanCooldownSeconds = 5;
188 |
189 | private SynchronizationContext syncContext;
190 |
191 | public static PiperTrayApp Instance { get { return lazy.Value; } }
192 |
193 | private volatile bool stopRequested = false;
194 |
195 | public event EventHandler ActivePresetChanged;
196 |
197 | protected virtual void OnActivePresetChanged(int newPresetIndex)
198 | {
199 | ActivePresetChanged?.Invoke(this, newPresetIndex);
200 | }
201 |
202 | private PiperTrayApp()
203 | {
204 |
205 | }
206 |
207 | private void InitializeMenuItems()
208 | {
209 | toggleMonitoringMenuItem = new ToolStripMenuItem("Monitoring");
210 | stopSpeechMenuItem = new ToolStripMenuItem("Stop Speech", null, (s, e) => StopCurrentSpeech());
211 | voiceMenuItem = new ToolStripMenuItem("Voice");
212 | settingsMenuItem = new ToolStripMenuItem("Settings");
213 | exitMenuItem = new ToolStripMenuItem("Exit");
214 |
215 | menuItems["Monitoring"] = toggleMonitoringMenuItem;
216 | menuItems["Stop Speech"] = stopSpeechMenuItem;
217 | menuItems["Speed"] = speedMenuItem;
218 | menuItems["Voice"] = voiceMenuItem;
219 | menuItems["Presets"] = presetsMenuItem;
220 | menuItems["Export to WAV"] = exportMenuItem;
221 |
222 | // Load initial visibility settings
223 | LoadMenuVisibilitySettings();
224 | ApplyMenuVisibility();
225 | }
226 |
227 | public uint SwitchPresetModifiers
228 | {
229 | get { return switchPresetModifiers; }
230 | set { switchPresetModifiers = value; }
231 | }
232 |
233 | public uint SwitchPresetVk
234 | {
235 | get { return switchPresetVk; }
236 | set { switchPresetVk = value; }
237 | }
238 |
239 | private void LoadMenuVisibilitySettings()
240 | {
241 | var settings = ReadCurrentSettings();
242 | foreach (var menuKey in menuItems.Keys)
243 | {
244 | string settingKey = $"MenuVisible_{menuKey.Replace(" ", "_")}";
245 | if (settings.TryGetValue(settingKey, out string value))
246 | {
247 | menuVisibilitySettings[menuKey] = bool.Parse(value);
248 | }
249 | else
250 | {
251 | menuVisibilitySettings[menuKey] = true; // Default to visible
252 | }
253 | }
254 | }
255 |
256 | private void ApplyMenuVisibility()
257 | {
258 | foreach (var kvp in menuItems)
259 | {
260 | if (menuVisibilitySettings.TryGetValue(kvp.Key, out bool isVisible))
261 | {
262 | kvp.Value.Visible = isVisible;
263 | }
264 | }
265 | }
266 |
267 | public void UpdateMenuVisibility(string menuItem, bool isVisible)
268 | {
269 | if (menuItems.TryGetValue(menuItem, out ToolStripMenuItem item))
270 | {
271 | menuVisibilitySettings[menuItem] = isVisible;
272 | item.Visible = isVisible;
273 | }
274 | }
275 |
276 | protected override void CreateHandle()
277 | {
278 | base.CreateHandle();
279 | }
280 |
281 | private void InitializeComponent()
282 | {
283 | try
284 | {
285 | // Initialize voiceModelState first
286 | voiceModelState = new VoiceModelState
287 | {
288 | CurrentIndex = 0,
289 | Models = new List(),
290 | IsDirty = false
291 | };
292 |
293 | // Load voice models
294 | LoadVoiceModels();
295 |
296 | CreateNotifyIcon();
297 |
298 | // Initialize menu items
299 | toggleMonitoringMenuItem = new ToolStripMenuItem("Monitoring", null, SafeEventHandler(ToggleMonitoring))
300 | {
301 | Checked = true
302 | };
303 | Log($"[InitializeComponent] Created toggleMonitoringMenuItem with ToggleMonitoring handler");
304 | voiceMenuItem = new ToolStripMenuItem("Voice");
305 | speedMenuItem = new ToolStripMenuItem("Speed");
306 | exportMenuItem = new ToolStripMenuItem("Export to WAV", null, SafeEventHandler(ExportWav));
307 | presetsMenuItem = new ToolStripMenuItem("Presets");
308 |
309 | // Initialize the menuItems dictionary
310 | menuItems = new Dictionary();
311 |
312 | InitializeMenuItems();
313 |
314 | // Continue with the rest of your InitializeComponent code
315 | fasterMenuItem = new ToolStripMenuItem("Faster", null, (sender, e) => IncreaseSpeed());
316 | slowerMenuItem = new ToolStripMenuItem("Slower", null, (sender, e) => DecreaseSpeed());
317 | resetSpeedMenuItem = new ToolStripMenuItem("Reset", null, (sender, e) => ResetSpeed(sender, e));
318 |
319 | speedMenuItem.DropDownItems.Add(fasterMenuItem);
320 | speedMenuItem.DropDownItems.Add(slowerMenuItem);
321 | speedMenuItem.DropDownItems.Add(resetSpeedMenuItem);
322 |
323 | exitMenuItem = new ToolStripMenuItem("Exit", null, SafeEventHandler(Exit));
324 |
325 | PopulateContextMenu();
326 |
327 | this.FormBorderStyle = FormBorderStyle.None;
328 | this.ShowInTaskbar = false;
329 | this.WindowState = FormWindowState.Minimized;
330 |
331 | UpdateSpeedDisplay();
332 | }
333 | catch (Exception ex)
334 | {
335 | Log($"Error in InitializeComponent: {ex.Message}");
336 | Log($"Stack trace: {ex.StackTrace}");
337 | MessageBox.Show($"An error occurred while initializing the application: {ex.Message}", "Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
338 | Application.Exit();
339 | }
340 | }
341 |
342 | public static PiperTrayApp GetInstance()
343 | {
344 | return Instance;
345 | }
346 |
347 | public void Initialize()
348 | {
349 | try
350 | {
351 | syncContext = SynchronizationContext.Current ?? new SynchronizationContext();
352 |
353 | SetLogFilePath();
354 | InitializeLogging();
355 | SetPiperPath();
356 | Log("Starting application initialization");
357 |
358 | InitializeComponent();
359 |
360 | // Create window handle explicitly before any hotkey registration
361 | CreateHandle();
362 | Log($"Window handle created: {Handle}");
363 |
364 | LoadAndCacheDictionaries();
365 | InitializeClipboardMonitoring();
366 | InitializeHotkeyActions();
367 |
368 | LoadVoiceModels();
369 | var settings = ReadSettings();
370 |
371 | isInitializing = true;
372 | ApplySettings(
373 | settings.model,
374 | settings.speed,
375 | settings.logging,
376 | settings.monitoringHotkeyModifiers,
377 | settings.monitoringHotkeyVk,
378 | settings.changeVoiceHotkeyModifiers,
379 | settings.changeVoiceHotkeyVk,
380 | settings.monitoringEnabled,
381 | settings.speedIncreaseHotkeyModifiers,
382 | settings.speedIncreaseHotkeyVk,
383 | settings.speedDecreaseHotkeyModifiers,
384 | settings.speedDecreaseHotkeyVk,
385 | settings.switchPresetModifiers,
386 | settings.switchPresetVk,
387 | settings.speaker,
388 | settings.sentenceSilence);
389 |
390 | UpdateSpeedFromSettings(settings.speed);
391 |
392 | isInitializing = false;
393 |
394 | Application.Run(this);
395 |
396 | Log("Application initialization completed successfully");
397 | }
398 | catch (Exception ex)
399 | {
400 | Log($"Critical initialization error: {ex.Message}");
401 | Log($"Stack trace: {ex.StackTrace}");
402 | MessageBox.Show("Application failed to initialize properly. Check the log file for details.", "Initialization Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
403 | }
404 | }
405 |
406 | public void InitializeLogging()
407 | {
408 | string assemblyLocation = Assembly.GetExecutingAssembly().Location;
409 | string assemblyDirectory = Path.GetDirectoryName(assemblyLocation);
410 | string logFilePath = Path.Combine(assemblyDirectory, "system.log");
411 |
412 | // Read logging setting from config
413 | bool isLoggingEnabled = ReadLoggingSettingFromConfig();
414 |
415 | // Set up static logging properties
416 | IsLoggingEnabled = isLoggingEnabled;
417 | LogFilePath = logFilePath;
418 | }
419 |
420 | private bool ReadLoggingSettingFromConfig()
421 | {
422 | string configPath = GetConfigPath();
423 | if (File.Exists(configPath))
424 | {
425 | try
426 | {
427 | string[] lines = File.ReadAllLines(configPath);
428 | foreach (string line in lines)
429 | {
430 | string[] parts = line.Split('=');
431 | if (parts.Length == 2 && parts[0].Trim().Equals("Logging", StringComparison.OrdinalIgnoreCase))
432 | {
433 | if (bool.TryParse(parts[1].Trim(), out bool loggingEnabled))
434 | {
435 | return loggingEnabled;
436 | }
437 | else
438 | {
439 | Log($"[ReadLoggingSettingFromConfig] Invalid value for Logging: '{parts[1].Trim()}'");
440 | }
441 | }
442 | }
443 | }
444 | catch (Exception ex)
445 | {
446 | Log($"[ReadLoggingSettingFromConfig] Exception: {ex.Message}");
447 | }
448 | }
449 | else
450 | {
451 | Log("[ReadLoggingSettingFromConfig] settings.conf not found. Defaulting Logging to false.");
452 | }
453 | return false; // Default to logging disabled if setting not found or invalid
454 | }
455 |
456 | private void TestWndProc()
457 | {
458 | SendMessage(this.Handle, 0x0400, IntPtr.Zero, IntPtr.Zero);
459 | }
460 |
461 | private void CreateNotifyIcon()
462 | {
463 | if (trayIcon != null)
464 | {
465 | return;
466 | }
467 |
468 | var icon = LoadIconFromResources();
469 | if (icon == null)
470 | {
471 | throw new FileNotFoundException("Tray icon resource not found");
472 | }
473 |
474 | trayIcon = new NotifyIcon()
475 | {
476 | Icon = icon,
477 | ContextMenuStrip = new ContextMenuStrip(),
478 | Visible = true,
479 | Text = "Piper Tray"
480 | };
481 |
482 | // Add logging for menu click events
483 | trayIcon.MouseClick += (s, e) => Log($"[CreateNotifyIcon] Tray icon clicked: {e.Button}");
484 | trayIcon.ContextMenuStrip.Opening += (s, e) => Log($"[CreateNotifyIcon] Context menu opening");
485 | trayIcon.ContextMenuStrip.ItemClicked += (s, e) => Log($"[CreateNotifyIcon] Menu item clicked: {e.ClickedItem.Text}");
486 |
487 | trayIcon.ContextMenuStrip.Opening += ContextMenuStrip_Opening;
488 | }
489 |
490 | private void LogEmbeddedResources()
491 | {
492 | var assembly = Assembly.GetExecutingAssembly();
493 | var resourceNames = assembly.GetManifestResourceNames();
494 | Log("Embedded resources:");
495 | foreach (var name in resourceNames)
496 | {
497 | Log($"- {name}");
498 | }
499 | }
500 |
501 | private Icon LoadIconFromResources()
502 | {
503 | var assembly = Assembly.GetExecutingAssembly();
504 | var resourceNames = assembly.GetManifestResourceNames();
505 | var iconResourceName = resourceNames.FirstOrDefault(name => name.EndsWith("icon.ico"));
506 |
507 | if (iconResourceName == null)
508 | {
509 | Log("Icon resource not found in embedded resources");
510 | return null;
511 | }
512 |
513 | using (var stream = assembly.GetManifestResourceStream(iconResourceName))
514 | {
515 | if (stream == null)
516 | {
517 | Log($"Failed to load icon stream for resource: {iconResourceName}");
518 | return null;
519 | }
520 | return new Icon(stream);
521 | }
522 | }
523 |
524 | public static Icon GetApplicationIcon()
525 | {
526 | var icon = new PiperTrayApp().LoadIconFromResources();
527 | if (icon == null)
528 | {
529 | throw new FileNotFoundException("Application icon resource not found");
530 | }
531 | return icon;
532 | }
533 |
534 | private void ContextMenuStrip_Opening(object sender, CancelEventArgs e)
535 | {
536 | RebuildContextMenu();
537 | UpdateSpeedDisplay();
538 | UpdateVoiceMenuCheckedState();
539 | }
540 |
541 | private void ApplyFinalMenuState()
542 | {
543 | foreach (ToolStripItem item in voiceMenuItem.DropDownItems)
544 | {
545 | if (item is ToolStripMenuItem menuItem)
546 | {
547 | int index = voiceMenuItem.DropDownItems.IndexOf(item);
548 | menuItem.Checked = (index == voiceModelState.CurrentIndex);
549 | }
550 | }
551 | trayIcon.ContextMenuStrip.Refresh();
552 | }
553 |
554 | private void LoadAndCacheDictionaries()
555 | {
556 | (ignoreWords, bannedWords, replaceWords) = LoadDictionaries();
557 | Log($"Dictionaries loaded and cached. Ignore words: {ignoreWords.Count}, Banned words: {bannedWords.Count}, Replace words: {replaceWords.Count}");
558 | }
559 |
560 | private void ApplySettings(
561 | string model,
562 | float speed,
563 | bool logging,
564 | uint monitoringHotkeyModifiers,
565 | uint monitoringHotkeyVk,
566 | uint changeVoiceHotkeyModifiers,
567 | uint changeVoiceHotkeyVk,
568 | bool monitoringEnabled,
569 | uint speedIncreaseHotkeyModifiers,
570 | uint speedIncreaseHotkeyVk,
571 | uint speedDecreaseHotkeyModifiers,
572 | uint speedDecreaseHotkeyVk,
573 | uint switchPresetModifiers,
574 | uint switchPresetVk,
575 | int speaker,
576 | float sentenceSilence)
577 | {
578 | // Update instance variables with current hotkey settings
579 | this.monitoringModifiers = monitoringHotkeyModifiers;
580 | this.monitoringVk = monitoringHotkeyVk;
581 | this.changeVoiceModifiers = changeVoiceHotkeyModifiers;
582 | this.changeVoiceVk = changeVoiceHotkeyVk;
583 | this.speedIncreaseModifiers = speedIncreaseHotkeyModifiers;
584 | this.speedIncreaseVk = speedIncreaseHotkeyVk;
585 | this.speedDecreaseModifiers = speedDecreaseHotkeyModifiers;
586 | this.speedDecreaseVk = speedDecreaseHotkeyVk;
587 | this.switchPresetModifiers = switchPresetModifiers;
588 | this.switchPresetVk = switchPresetVk;
589 | this.stopSpeechModifiers = stopSpeechModifiers;
590 | this.stopSpeechVk = stopSpeechVk;
591 |
592 | // Unregister existing hotkeys first
593 | UnregisterAllHotkeys();
594 |
595 | // Read hotkey enabled states
596 | var settings = ReadCurrentSettings();
597 |
598 | // Only register hotkeys if they are enabled
599 | if (settings.TryGetValue("MonitoringHotkeyEnabled", out string monEnabled) && bool.Parse(monEnabled))
600 | {
601 | RegisterHotkey(HOTKEY_ID_MONITORING, monitoringModifiers, monitoringVk, "Monitoring");
602 | }
603 |
604 | if (settings.TryGetValue("StopSpeechHotkeyEnabled", out string stopEnabled) && bool.Parse(stopEnabled))
605 | {
606 | RegisterHotkey(HOTKEY_ID_STOP_SPEECH, stopSpeechModifiers, stopSpeechVk, "Stop Speech");
607 | }
608 |
609 | if (settings.TryGetValue("ChangeVoiceHotkeyEnabled", out string voiceEnabled) && bool.Parse(voiceEnabled))
610 | {
611 | RegisterHotkey(HOTKEY_ID_CHANGE_VOICE, changeVoiceModifiers, changeVoiceVk, "Change Voice");
612 | }
613 |
614 | if (settings.TryGetValue("SpeedIncreaseHotkeyEnabled", out string speedIncEnabled) && bool.Parse(speedIncEnabled))
615 | {
616 | RegisterHotkey(HOTKEY_ID_SPEED_INCREASE, speedIncreaseModifiers, speedIncreaseVk, "Speed Increase");
617 | }
618 |
619 | if (settings.TryGetValue("SpeedDecreaseHotkeyEnabled", out string speedDecEnabled) && bool.Parse(speedDecEnabled))
620 | {
621 | RegisterHotkey(HOTKEY_ID_SPEED_DECREASE, speedDecreaseModifiers, speedDecreaseVk, "Speed Decrease");
622 | }
623 |
624 | if (settings.TryGetValue("SwitchPresetHotkeyEnabled", out string switchEnabled) && bool.Parse(switchEnabled))
625 | {
626 | RegisterHotkey(HOTKEY_ID_SWITCH_PRESET, switchPresetModifiers, switchPresetVk, "Switch Preset");
627 | }
628 |
629 | ApplyMonitoringState(monitoringEnabled);
630 |
631 | if (settingsForm != null)
632 | {
633 | settingsForm.UpdateSentenceSilence(sentenceSilence);
634 | }
635 | }
636 |
637 |
638 | public (bool success, uint errorCode) RegisterHotkey(int hotkeyId, uint modifiers, uint vk, string hotkeyName)
639 | {
640 | string hotkeyDescription = GetHotkeyDescription(hotkeyId, hotkeyName);
641 | Log($"[RegisterHotkey] Attempting to register {hotkeyDescription}. ID: {hotkeyId}, Modifiers: 0x{modifiers:X2}, VK: 0x{vk:X2}");
642 | bool result = RegisterHotKey(this.Handle, hotkeyId, modifiers, vk);
643 | uint errorCode = result ? 0 : GetLastError();
644 | Log($"[RegisterHotkey] {hotkeyDescription} registration result: {(result ? "Success" : $"Failed (Error: {errorCode})")}");
645 | return (result, errorCode);
646 | }
647 |
648 | public string GetConfigPath()
649 | {
650 | return Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "settings.conf");
651 | }
652 |
653 | private string GetHotkeyDescription(int hotkeyId, string hotkeyName)
654 | {
655 | switch (hotkeyId)
656 | {
657 | case HOTKEY_ID_SWITCH_PRESET: return "Switch Preset hotkey";
658 | case HOTKEY_ID_MONITORING: return "Monitoring hotkey";
659 | case HOTKEY_ID_CHANGE_VOICE: return "Change Voice hotkey";
660 | case HOTKEY_ID_SPEED_INCREASE: return "Speed Increase hotkey";
661 | case HOTKEY_ID_SPEED_DECREASE: return "Speed Decrease hotkey";
662 | default: return $"{hotkeyName} hotkey";
663 | }
664 | }
665 |
666 | private void ApplyHotkeySettings(uint monitoringHotkeyModifiers, uint monitoringHotkeyVk,
667 | uint changeVoiceHotkeyModifiers, uint changeVoiceHotkeyVk,
668 | uint speedIncreaseHotkeyModifiers, uint speedIncreaseHotkeyVk,
669 | uint speedDecreaseHotkeyModifiers, uint speedDecreaseHotkeyVk,
670 | uint switchPresetModifiers, uint switchPresetVk)
671 | {
672 | Log($"[ApplyHotkeySettings] Entering method");
673 |
674 | var switchPresetResult = UpdateHotkey(HOTKEY_ID_SWITCH_PRESET, switchPresetModifiers, switchPresetVk);
675 | Log($"[ApplyHotkeySettings] Switch Preset hotkey registration result - Modifiers: 0x{switchPresetResult.modifiers:X}, VK: 0x{switchPresetResult.vk:X}");
676 |
677 | var monitoringResult = UpdateHotkey(HOTKEY_ID_MONITORING, monitoringHotkeyModifiers, monitoringHotkeyVk);
678 | Log($"[ApplyHotkeySettings] Monitoring hotkey registration result - Modifiers: 0x{monitoringResult.modifiers:X}, VK: 0x{monitoringResult.vk:X}");
679 |
680 | var changeVoiceResult = UpdateHotkey(HOTKEY_ID_CHANGE_VOICE, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk);
681 | Log($"[ApplyHotkeySettings] Change Voice hotkey registration result - Modifiers: 0x{changeVoiceResult.modifiers:X}, VK: 0x{changeVoiceResult.vk:X}");
682 |
683 | var speedIncreaseResult = UpdateHotkey(HOTKEY_ID_SPEED_INCREASE, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk);
684 | Log($"[ApplyHotkeySettings] Speed Increase hotkey registration result - Modifiers: 0x{speedIncreaseResult.modifiers:X}, VK: 0x{speedIncreaseResult.vk:X}");
685 |
686 | var speedDecreaseResult = UpdateHotkey(HOTKEY_ID_SPEED_DECREASE, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk);
687 | Log($"[ApplyHotkeySettings] Speed Decrease hotkey registration result - Modifiers: 0x{speedDecreaseResult.modifiers:X}, VK: 0x{speedDecreaseResult.vk:X}");
688 |
689 | Log($"[ApplyHotkeySettings] Hotkey settings applied");
690 | Log($"[ApplyHotkeySettings] Exiting method");
691 | }
692 |
693 |
694 |
695 | private EventHandler SafeEventHandler(EventHandler handler)
696 | {
697 | return (sender, e) =>
698 | {
699 | try
700 | {
701 | handler(sender, e);
702 | }
703 | catch (Exception ex)
704 | {
705 | Log($"Error in event handler: {ex.Message}");
706 | Log($"Stack trace: {ex.StackTrace}");
707 | MessageBox.Show($"An error occurred: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
708 | }
709 | };
710 | }
711 |
712 | private void InitializeHotkeyActions()
713 | {
714 | hotkeyActions = new Dictionary
715 | {
716 | { HOTKEY_ID_SWITCH_PRESET, SwitchPreset },
717 | { HOTKEY_ID_STOP_SPEECH, StopCurrentSpeech },
718 | { HOTKEY_ID_MONITORING, () => ToggleMonitoring(this, EventArgs.Empty) },
719 | { HOTKEY_ID_CHANGE_VOICE, ChangeVoice },
720 | { HOTKEY_ID_SPEED_INCREASE, IncreaseSpeed },
721 | { HOTKEY_ID_SPEED_DECREASE, DecreaseSpeed }
722 | };
723 |
724 | Log($"[InitializeHotkeyActions] Hotkey actions initialized. Count: {hotkeyActions.Count}");
725 | }
726 |
727 | public void SwitchPreset()
728 | {
729 | var enabledPresets = new List();
730 | for (int i = 0; i < 4; i++)
731 | {
732 | var preset = LoadPreset(i);
733 | if (preset != null && bool.Parse(preset.Enabled))
734 | {
735 | enabledPresets.Add(i);
736 | }
737 | }
738 |
739 | if (enabledPresets.Count > 0)
740 | {
741 | int currentIndex = enabledPresets.IndexOf(currentPresetIndex);
742 | int nextIndex = (currentIndex + 1) % enabledPresets.Count;
743 | ApplyPreset(enabledPresets[nextIndex]);
744 | }
745 | }
746 |
747 | private void StopCurrentSpeech()
748 | {
749 | Log($"[StopCurrentSpeech] Stopping current speech playback");
750 | if (CurrentAudioState == AudioPlaybackState.Playing)
751 | {
752 | CurrentAudioState = AudioPlaybackState.StopRequested;
753 |
754 | if (currentWaveOut != null)
755 | {
756 | try
757 | {
758 | currentWaveOut.Stop();
759 | currentWaveOut.Dispose();
760 | }
761 | catch (Exception ex)
762 | {
763 | Log($"[StopCurrentSpeech] Error stopping playback: {ex.Message}");
764 | }
765 | finally
766 | {
767 | currentWaveOut = null;
768 | }
769 | }
770 |
771 | if (playbackCancellationTokenSource != null)
772 | {
773 | playbackCancellationTokenSource.Cancel();
774 | playbackCancellationTokenSource = null;
775 | }
776 |
777 | CurrentAudioState = AudioPlaybackState.Idle;
778 | }
779 | }
780 |
781 | private void CheckVoiceFiles()
782 | {
783 | string appDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
784 | Log($"[CheckVoiceFiles] Checking for .onnx files in: {appDirectory}");
785 | string[] onnxFiles = Directory.GetFiles(appDirectory, "*.onnx");
786 | Log($"[CheckVoiceFiles] Found {onnxFiles.Length} .onnx files");
787 | foreach (var file in onnxFiles)
788 | {
789 | Log($"[CheckVoiceFiles] Found file: {Path.GetFileNameWithoutExtension(file)}");
790 | }
791 | if (onnxFiles.Length == 0)
792 | {
793 | Log($"[CheckVoiceFiles] No .onnx files detected");
794 | MessageBox.Show("No voice files (.onnx) detected in the application directory. The application cannot function correctly.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning);
795 | }
796 | }
797 |
798 | [DllImport("kernel32.dll")]
799 | static extern uint GetLastError();
800 |
801 | protected override CreateParams CreateParams
802 | {
803 | get
804 | {
805 | CreateParams cp = base.CreateParams;
806 | cp.ExStyle |= 0x80; // WS_EX_TOOLWINDOW
807 | return cp;
808 | }
809 | }
810 |
811 | protected override void SetVisibleCore(bool value)
812 | {
813 | if (!this.IsHandleCreated)
814 | {
815 | CreateHandle();
816 | Log($"[SetVisibleCore] Handle created: {this.Handle}");
817 | }
818 | base.SetVisibleCore(false);
819 | }
820 |
821 | public void RegisterHotkeys()
822 | {
823 | Log($"[RegisterHotkeys] Starting hotkey registration process");
824 | UnregisterAllHotkeys();
825 |
826 | // Register all hotkeys in a single pass
827 | RegisterHotkey(HOTKEY_ID_MONITORING, monitoringModifiers, monitoringVk, "Monitoring");
828 | RegisterHotkey(HOTKEY_ID_STOP_SPEECH, stopSpeechModifiers, stopSpeechVk, "Stop Speech");
829 | RegisterHotkey(HOTKEY_ID_CHANGE_VOICE, changeVoiceModifiers, changeVoiceVk, "Change Voice");
830 | RegisterHotkey(HOTKEY_ID_SPEED_INCREASE, speedIncreaseModifiers, speedIncreaseVk, "Speed Increase");
831 | RegisterHotkey(HOTKEY_ID_SPEED_DECREASE, speedDecreaseModifiers, speedDecreaseVk, "Speed Decrease");
832 | RegisterHotkey(HOTKEY_ID_SWITCH_PRESET, switchPresetModifiers, switchPresetVk, "Switch Preset");
833 |
834 | Log($"[RegisterHotkeys] Hotkey registration completed");
835 | }
836 |
837 | public void UnregisterAllHotkeys()
838 | {
839 | UnregisterHotKey(this.Handle, HOTKEY_ID_MONITORING);
840 | UnregisterHotKey(this.Handle, HOTKEY_ID_CHANGE_VOICE);
841 | UnregisterHotKey(this.Handle, HOTKEY_ID_SPEED_INCREASE);
842 | UnregisterHotKey(this.Handle, HOTKEY_ID_SPEED_DECREASE);
843 | UnregisterHotKey(this.Handle, HOTKEY_ID_SWITCH_PRESET);
844 | Log("All hotkeys unregistered");
845 | }
846 |
847 | private int GetHotkeyId(string hotkeyName)
848 | {
849 | switch (hotkeyName)
850 | {
851 | case "Monitoring": return HOTKEY_ID_MONITORING;
852 | case "ChangeVoice": return HOTKEY_ID_CHANGE_VOICE;
853 | case "SpeedIncrease": return HOTKEY_ID_SPEED_INCREASE;
854 | case "SpeedDecrease": return HOTKEY_ID_SPEED_DECREASE;
855 | case "SwitchPreset": return HOTKEY_ID_SWITCH_PRESET;
856 | default: throw new ArgumentException($"Unknown hotkey name: {hotkeyName}");
857 | }
858 | }
859 |
860 | public bool TryRegisterHotkey(uint modifiers, uint vk)
861 | {
862 | if (RegisterHotKey(this.Handle, HOTKEY_ID_MONITORING, modifiers, vk))
863 | {
864 | Log($"Hotkey registered successfully. Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}");
865 | return true;
866 | }
867 | else
868 | {
869 | uint errorCode = GetLastError();
870 | Log($"Failed to register hotkey. Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}, Error code: {errorCode}");
871 | return false;
872 | }
873 | }
874 |
875 | public (uint modifiers, uint vk) UpdateHotkey(int hotkeyId, uint modifiers, uint vk)
876 | {
877 | Log($"[UpdateHotkey] Entering method. HotkeyId: {hotkeyId}, Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}");
878 | Log($"[UpdateHotkey] Received values - Modifier: 0x{modifiers:X2}, Key: 0x{vk:X2}");
879 |
880 | // Unregister existing hotkey
881 | bool unregisterResult = UnregisterHotKey(this.Handle, hotkeyId);
882 | Log($"[UpdateHotkey] Unregister result: {unregisterResult}. Last error: {GetLastError()}");
883 |
884 | if (modifiers == 0 && vk == 0)
885 | {
886 | Log($"[UpdateHotkey] No hotkey combination set for ID: {hotkeyId}");
887 | return (0, 0);
888 | }
889 |
890 | // Attempt to register new hotkey
891 | bool registerResult = RegisterHotKey(this.Handle, hotkeyId, modifiers, vk);
892 | uint errorCode = GetLastError();
893 |
894 | Log($"[UpdateHotkey] Register result: {registerResult}. Error code: {errorCode}");
895 |
896 | if (registerResult)
897 | {
898 | Log($"[UpdateHotkey] Hotkey registered successfully. ID: {hotkeyId}, Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}");
899 |
900 | // Update the appropriate variables based on the hotkey ID
901 | switch (hotkeyId)
902 | {
903 | case HOTKEY_ID_SWITCH_PRESET:
904 | switchPresetModifiers = modifiers;
905 | switchPresetVk = vk;
906 | break;
907 | case HOTKEY_ID_MONITORING:
908 | monitoringModifiers = modifiers;
909 | monitoringVk = vk;
910 | break;
911 | case HOTKEY_ID_CHANGE_VOICE:
912 | changeVoiceModifiers = modifiers;
913 | changeVoiceVk = vk;
914 | break;
915 | case HOTKEY_ID_SPEED_INCREASE:
916 | speedIncreaseModifiers = modifiers;
917 | speedIncreaseVk = vk;
918 | break;
919 | case HOTKEY_ID_SPEED_DECREASE:
920 | speedDecreaseModifiers = modifiers;
921 | speedDecreaseVk = vk;
922 | break;
923 | }
924 |
925 | SaveSettings();
926 | return (modifiers, vk);
927 | }
928 | else
929 | {
930 | Log($"[UpdateHotkey] Failed to register hotkey. ID: {hotkeyId}, Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}, Error code: {errorCode}");
931 | Log($"[UpdateHotkey] Error description: {new Win32Exception((int)errorCode).Message}");
932 | return (0, 0);
933 | }
934 | }
935 |
936 | private bool IsValidHotkeyCombination(uint modifiers, uint vk)
937 | {
938 | if (modifiers == 0 || vk == 0)
939 | {
940 | Log($"Invalid hotkey combination. Modifiers: 0x{modifiers:X}, VK: 0x{vk:X}");
941 | return false;
942 | }
943 | return true;
944 | }
945 |
946 | private uint GetHotkeyModifiers(int hotkeyId)
947 | {
948 | switch (hotkeyId)
949 | {
950 | case HOTKEY_ID_MONITORING:
951 | return monitoringModifiers;
952 | case HOTKEY_ID_CHANGE_VOICE:
953 | return changeVoiceModifiers;
954 | case HOTKEY_ID_SPEED_INCREASE:
955 | return speedIncreaseModifiers;
956 | case HOTKEY_ID_SPEED_DECREASE:
957 | return speedDecreaseModifiers;
958 | default:
959 | return 0;
960 | }
961 | }
962 |
963 | private uint GetHotkeyKey(int hotkeyId)
964 | {
965 | switch (hotkeyId)
966 | {
967 | case HOTKEY_ID_MONITORING:
968 | return monitoringVk;
969 | case HOTKEY_ID_CHANGE_VOICE:
970 | return changeVoiceVk;
971 | case HOTKEY_ID_SPEED_INCREASE:
972 | return speedIncreaseVk;
973 | case HOTKEY_ID_SPEED_DECREASE:
974 | return speedDecreaseVk;
975 | default:
976 | return 0;
977 | }
978 | }
979 |
980 | public bool IsHotkeyRegistered(uint modifiers, uint vk)
981 | {
982 | // Implementation goes here
983 | // For example:
984 | return modifiers != 0 && vk != 0;
985 | }
986 |
987 | public bool IsMonitoringHotkeyRegistered()
988 | {
989 | var (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, monitoringEnabled, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVk, speaker, sentenceSilence) = ReadSettings();
990 | return IsHotkeyRegistered(monitoringHotkeyModifiers, monitoringHotkeyVk);
991 | }
992 |
993 | public void LoadAndApplyHotkeySettings()
994 | {
995 | var (_, _, _, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, _, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVk, speaker, sentenceSilence) = ReadSettings();
996 |
997 | UnregisterAllHotkeys();
998 |
999 | RegisterHotkey(HOTKEY_ID_SWITCH_PRESET, switchPresetModifiers, switchPresetVk, "Switch Preset");
1000 | RegisterHotkey(HOTKEY_ID_MONITORING, monitoringHotkeyModifiers, monitoringHotkeyVk, "Monitoring");
1001 | RegisterHotkey(HOTKEY_ID_CHANGE_VOICE, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, "Change Voice");
1002 | RegisterHotkey(HOTKEY_ID_SPEED_INCREASE, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, "Speed Increase");
1003 | RegisterHotkey(HOTKEY_ID_SPEED_DECREASE, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, "Speed Decrease");
1004 |
1005 | Log("Hotkey settings loaded and applied");
1006 | }
1007 |
1008 | protected override void WndProc(ref Message m)
1009 | {
1010 | Log($"[WndProc] Message received: 0x{m.Msg:X4}, WParam: 0x{m.WParam:X8}, LParam: 0x{m.LParam:X8}");
1011 |
1012 | switch (m.Msg)
1013 | {
1014 | case 0x0312: // WM_HOTKEY
1015 | int id = m.WParam.ToInt32();
1016 | uint modifiers = (uint)((int)m.LParam & 0xFFFF);
1017 | uint vk = (uint)((int)m.LParam >> 16);
1018 | Log($"[WndProc] WM_HOTKEY received. ID: {id}, Modifiers: 0x{modifiers:X2}, VK: 0x{vk:X2}");
1019 |
1020 | if (hotkeyActions.TryGetValue(id, out Action action))
1021 | {
1022 | Log($"[WndProc] Action found for hotkey ID: {id}. Invoking action.");
1023 | action.Invoke();
1024 | }
1025 | else
1026 | {
1027 | Log($"[WndProc] No action found for hotkey ID: {id}");
1028 | }
1029 | break;
1030 |
1031 | case 0x0001: // WM_CREATE
1032 | Log($"[WndProc] WM_CREATE message received");
1033 | break;
1034 |
1035 | case 0x0002: // WM_DESTROY
1036 | Log($"[WndProc] WM_DESTROY message received");
1037 | UnregisterAllHotkeys();
1038 | break;
1039 |
1040 | case 0x0010: // WM_CLOSE
1041 | Log($"[WndProc] WM_CLOSE message received");
1042 | break;
1043 |
1044 | default:
1045 | Log($"[WndProc] Unhandled message: 0x{m.Msg:X4}");
1046 | break;
1047 | }
1048 |
1049 | base.WndProc(ref m);
1050 | Log($"[WndProc] Message 0x{m.Msg:X4} processed");
1051 | }
1052 |
1053 | private void AddPresetsToContextMenu()
1054 | {
1055 | presetsMenuItem.DropDownItems.Clear();
1056 | bool hasEnabledPresets = false;
1057 |
1058 | string lastUsedPresetStr = ReadSettingValue("LastUsedPreset");
1059 | int lastUsedPreset = -1;
1060 | if (int.TryParse(lastUsedPresetStr, out int savedIndex))
1061 | {
1062 | lastUsedPreset = savedIndex;
1063 | }
1064 |
1065 | // If only one preset is enabled, it should be the active one
1066 | int enabledCount = 0;
1067 | int singleEnabledIndex = -1;
1068 |
1069 | // First pass to count enabled presets
1070 | for (int i = 0; i < 4; i++)
1071 | {
1072 | var settings = LoadPreset(i);
1073 | if (settings != null && bool.Parse(settings.Enabled))
1074 | {
1075 | enabledCount++;
1076 | singleEnabledIndex = i;
1077 | }
1078 | }
1079 |
1080 | // Second pass to add menu items
1081 | for (int i = 0; i < 4; i++)
1082 | {
1083 | var settings = LoadPreset(i);
1084 | if (settings != null && bool.Parse(settings.Enabled))
1085 | {
1086 | hasEnabledPresets = true;
1087 | var presetItem = new CustomVoiceMenuItem
1088 | {
1089 | Text = settings.Name,
1090 | IsSelected = (enabledCount == 1) ? (i == singleEnabledIndex) : (i == lastUsedPreset),
1091 | SelectionColor = Color.LightBlue
1092 | };
1093 | int presetIndex = i;
1094 | presetItem.Click += (s, e) =>
1095 | {
1096 | ApplyPreset(presetIndex);
1097 | currentPresetIndex = presetIndex;
1098 | UpdatePresetMenuSelection(presetIndex);
1099 | };
1100 | presetsMenuItem.DropDownItems.Add(presetItem);
1101 | }
1102 | }
1103 |
1104 | presetsMenuItem.Visible = hasEnabledPresets;
1105 | }
1106 |
1107 | private void SaveLastUsedPreset(int presetIndex)
1108 | {
1109 | string configPath = GetConfigPath();
1110 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
1111 | UpdateOrAddSetting(lines, "LastUsedPreset", presetIndex.ToString());
1112 | File.WriteAllLines(configPath, lines);
1113 | }
1114 |
1115 | private void UpdatePresetMenuSelection(int selectedIndex)
1116 | {
1117 | foreach (ToolStripItem item in presetsMenuItem.DropDownItems)
1118 | {
1119 | if (item is CustomVoiceMenuItem customItem)
1120 | {
1121 | customItem.IsSelected = (presetsMenuItem.DropDownItems.IndexOf(item) == selectedIndex);
1122 | }
1123 | }
1124 | trayIcon.ContextMenuStrip.Refresh();
1125 | }
1126 |
1127 | private void UpdatePresetMenuChecks(int selectedIndex)
1128 | {
1129 | foreach (ToolStripItem item in presetsMenuItem.DropDownItems)
1130 | {
1131 | if (item is ToolStripMenuItem menuItem)
1132 | {
1133 | menuItem.Checked = (presetsMenuItem.DropDownItems.IndexOf(item) == selectedIndex);
1134 | }
1135 | }
1136 | }
1137 |
1138 | public void ApplyPreset(int presetIndex)
1139 | {
1140 | var settings = LoadPreset(presetIndex);
1141 | if (settings != null)
1142 | {
1143 | UpdateVoiceModel(settings.VoiceModel);
1144 | UpdateCurrentSpeaker(int.Parse(settings.Speaker));
1145 | UpdateSpeedFromSettings(double.Parse(settings.Speed, CultureInfo.InvariantCulture));
1146 | SaveSettings(
1147 | speed: double.Parse(settings.Speed, CultureInfo.InvariantCulture),
1148 | voiceModel: settings.VoiceModel,
1149 | speaker: int.Parse(settings.Speaker),
1150 | sentenceSilence: float.Parse(settings.SentenceSilence, CultureInfo.InvariantCulture)
1151 | );
1152 | currentPresetIndex = presetIndex;
1153 | SaveLastUsedPreset(presetIndex);
1154 | UpdatePresetMenuSelection(presetIndex);
1155 |
1156 | // Raise the event
1157 | OnActivePresetChanged(currentPresetIndex);
1158 | }
1159 | }
1160 |
1161 | public int GetCurrentPresetIndex()
1162 | {
1163 | return currentPresetIndex;
1164 | }
1165 |
1166 | public PresetSettings LoadPreset(int index)
1167 | {
1168 | var settings = ReadCurrentSettings();
1169 | if (settings.TryGetValue($"Preset{index + 1}", out string presetJson))
1170 | {
1171 | try
1172 | {
1173 | var options = new JsonSerializerOptions
1174 | {
1175 | PropertyNameCaseInsensitive = true,
1176 | NumberHandling = JsonNumberHandling.AllowReadingFromString
1177 | };
1178 |
1179 | var preset = JsonSerializer.Deserialize(presetJson, options);
1180 | return preset;
1181 | }
1182 | catch (Exception ex)
1183 | {
1184 | Log($"[LoadPreset] Error deserializing preset {index + 1}: {ex.Message}");
1185 | }
1186 | }
1187 | return null;
1188 | }
1189 | public void SavePreset(int index, PresetSettings preset)
1190 | {
1191 | if (isInitializing)
1192 | {
1193 | return;
1194 | }
1195 |
1196 | string configPath = GetConfigPath();
1197 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
1198 |
1199 | // Include JsonSerializerOptions here
1200 | var options = new JsonSerializerOptions
1201 | {
1202 | IgnoreNullValues = false,
1203 | PropertyNameCaseInsensitive = true,
1204 | NumberHandling = JsonNumberHandling.AllowReadingFromString
1205 | };
1206 |
1207 | string presetJson = JsonSerializer.Serialize(preset, options);
1208 | UpdateOrAddSetting(lines, $"Preset{index + 1}", presetJson);
1209 | File.WriteAllLines(configPath, lines);
1210 | }
1211 |
1212 | public bool TryGetCurrentPreset(out PresetSettings preset)
1213 | {
1214 | preset = null;
1215 | if (currentPresetIndex >= 0 && currentPresetIndex < 4)
1216 | {
1217 | preset = LoadPreset(currentPresetIndex);
1218 | return preset != null;
1219 | }
1220 | return false;
1221 | }
1222 |
1223 | public void ReloadSettings()
1224 | {
1225 | var settings = ReadCurrentSettings();
1226 | if (settings.TryGetValue("VoiceModel", out string model) &&
1227 | settings.TryGetValue("Speed", out string speedStr) &&
1228 | settings.TryGetValue("Speaker", out string speakerStr) &&
1229 | settings.TryGetValue("SentenceSilence", out string silenceStr))
1230 | {
1231 | float speed = float.Parse(speedStr, CultureInfo.InvariantCulture);
1232 | int speaker = int.Parse(speakerStr);
1233 | float sentenceSilence = float.Parse(silenceStr, CultureInfo.InvariantCulture);
1234 |
1235 | SaveSettings(
1236 | speed: speed,
1237 | voiceModel: model,
1238 | speaker: speaker,
1239 | sentenceSilence: sentenceSilence
1240 | );
1241 |
1242 | // Update the speed display in the tray menu
1243 | UpdateSpeedFromSettings(speed);
1244 | UpdateSpeedDisplay();
1245 | }
1246 | }
1247 |
1248 | private void InitializeClipboardMonitoring()
1249 | {
1250 | clipboardTimer = new System.Windows.Forms.Timer();
1251 | clipboardTimer.Interval = 1000;
1252 | clipboardTimer.Tick += ClipboardTimer_Tick;
1253 | ignoreCurrentClipboard = true;
1254 | }
1255 |
1256 | private void SetPiperPath()
1257 | {
1258 | string assemblyLocation = Assembly.GetExecutingAssembly().Location;
1259 | string assemblyDirectory = Path.GetDirectoryName(assemblyLocation);
1260 | piperPath = Path.Combine(assemblyDirectory, "piper.exe");
1261 |
1262 | if (!File.Exists(piperPath))
1263 | {
1264 | Log($"Error: piper.exe not found at {piperPath}");
1265 | MessageBox.Show("piper.exe not found in the application directory.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
1266 | Application.Exit();
1267 | }
1268 | else
1269 | {
1270 | Log($"Piper executable found at {piperPath}");
1271 | }
1272 | }
1273 |
1274 | private void SetLogFilePath()
1275 | {
1276 | string assemblyLocation = Assembly.GetExecutingAssembly().Location;
1277 | string assemblyDirectory = Path.GetDirectoryName(assemblyLocation);
1278 | LogFilePath = Path.Combine(assemblyDirectory, "system.log");
1279 | }
1280 |
1281 | private void Log(string message)
1282 | {
1283 | if (!PiperTrayApp.IsLoggingEnabled)
1284 | {
1285 | return;
1286 | }
1287 |
1288 | try
1289 | {
1290 | string logMessage = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss} - {message}";
1291 | File.AppendAllText(PiperTrayApp.LogFilePath, logMessage + Environment.NewLine);
1292 | }
1293 | catch (Exception ex)
1294 | {
1295 | Console.WriteLine($"Error writing to log file: {ex.Message}");
1296 | }
1297 | }
1298 |
1299 |
1300 | private void LogAudioDevices()
1301 | {
1302 | for (int i = 0; i < WaveOut.DeviceCount; i++)
1303 | {
1304 | var capabilities = WaveOut.GetCapabilities(i);
1305 | }
1306 | }
1307 |
1308 | private void LogDefaultAudioDevice()
1309 | {
1310 | try
1311 | {
1312 | var defaultDevice = new MMDeviceEnumerator().GetDefaultAudioEndpoint(DataFlow.Render, Role.Multimedia);
1313 | Log($"Default audio device: {defaultDevice.FriendlyName}");
1314 | }
1315 | catch (Exception ex)
1316 | {
1317 | Log($"Error getting default audio device: {ex.Message}");
1318 | }
1319 | }
1320 |
1321 | private void StartMonitoring()
1322 | {
1323 | isMonitoring = true;
1324 | ignoreCurrentClipboard = true;
1325 | toggleMonitoringMenuItem.Checked = true;
1326 | toggleMonitoringMenuItem.Text = "Monitoring";
1327 | clipboardTimer.Start();
1328 | }
1329 |
1330 | private void StopMonitoring()
1331 | {
1332 | isMonitoring = false;
1333 | toggleMonitoringMenuItem.Checked = false;
1334 | toggleMonitoringMenuItem.Text = "Monitoring";
1335 | clipboardTimer.Stop();
1336 | }
1337 |
1338 | public void ToggleMonitoring(object sender, EventArgs e)
1339 | {
1340 | Log($"[ToggleMonitoring] Starting toggle operation. Current state: {isMonitoring}");
1341 | try
1342 | {
1343 | isMonitoring = !isMonitoring;
1344 | Log($"[ToggleMonitoring] State toggled to: {isMonitoring}");
1345 |
1346 | toggleMonitoringMenuItem.Checked = isMonitoring;
1347 | Log($"[ToggleMonitoring] Menu item checked state updated: {toggleMonitoringMenuItem.Checked}");
1348 |
1349 | if (isMonitoring)
1350 | {
1351 | Log($"[ToggleMonitoring] Calling StartMonitoring()");
1352 | StartMonitoring();
1353 | }
1354 | else
1355 | {
1356 | Log($"[ToggleMonitoring] Calling StopMonitoring()");
1357 | StopMonitoring();
1358 | }
1359 |
1360 | string configPath = GetConfigPath();
1361 | Log($"[ToggleMonitoring] Config path: {configPath}");
1362 | Log($"[ToggleMonitoring] Config file exists: {File.Exists(configPath)}");
1363 |
1364 | SaveSettings();
1365 | Log($"[ToggleMonitoring] SaveSettings() called");
1366 |
1367 | // Verify the save
1368 | var currentSettings = ReadCurrentSettings();
1369 | Log($"[ToggleMonitoring] Verification - MonitoringEnabled in settings: {currentSettings.GetValueOrDefault("MonitoringEnabled")}");
1370 |
1371 | toggleMonitoringMenuItem.Text = isMonitoring ? "Monitoring (On)" : "Monitoring (Off)";
1372 | Log($"[ToggleMonitoring] Menu text updated to: {toggleMonitoringMenuItem.Text}");
1373 | }
1374 | catch (Exception ex)
1375 | {
1376 | Log($"[ToggleMonitoring] Error occurred: {ex.Message}");
1377 | Log($"[ToggleMonitoring] Stack trace: {ex.StackTrace}");
1378 | }
1379 | Log($"[ToggleMonitoring] Toggle operation completed");
1380 | }
1381 |
1382 | private void SaveMonitoringState(bool enabled)
1383 | {
1384 | string configPath = GetConfigPath();
1385 | try
1386 | {
1387 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
1388 | UpdateOrAddSetting(lines, "MonitoringEnabled", enabled.ToString());
1389 | File.WriteAllLines(configPath, lines);
1390 | Log($"[SaveMonitoringState] MonitoringEnabled set to {enabled}");
1391 | }
1392 | catch (Exception ex)
1393 | {
1394 | Log($"[SaveMonitoringState] Error updating MonitoringEnabled: {ex.Message}");
1395 | MessageBox.Show($"Failed to save Monitoring state: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
1396 | }
1397 | }
1398 |
1399 | private void ChangeVoice()
1400 | {
1401 | Log($"[ChangeVoice] Method called. Current voice model index: {currentVoiceModelIndex}");
1402 |
1403 | if (voiceModels != null && voiceModels.Count > 0 && currentVoiceModelIndex < voiceModels.Count)
1404 | {
1405 | Log($"[ChangeVoice] Current voice model: {voiceModels[currentVoiceModelIndex]}");
1406 |
1407 | CycleVoiceModel();
1408 |
1409 | Log($"[ChangeVoice] Voice model cycled");
1410 | Log($"[ChangeVoice] New voice model index: {currentVoiceModelIndex}");
1411 | Log($"[ChangeVoice] New voice model: {voiceModels[currentVoiceModelIndex]}");
1412 |
1413 | UpdateVoiceModelUI();
1414 | Log($"[ChangeVoice] UI updated with new voice model");
1415 | }
1416 | else
1417 | {
1418 | Log($"[ChangeVoice] Error: Voice models not properly initialized or current index out of range");
1419 | }
1420 | }
1421 |
1422 | private void CycleVoiceModel()
1423 | {
1424 | Log($"[CycleVoiceModel] Entering method. Current voice model index: {currentVoiceModelIndex}");
1425 |
1426 | if (voiceModels == null || voiceModels.Count == 0)
1427 | {
1428 | Log("[CycleVoiceModel] Voice models not loaded. Calling LoadVoiceModels()");
1429 | LoadVoiceModels();
1430 | }
1431 |
1432 | Log($"[CycleVoiceModel] Number of voice models: {voiceModels?.Count ?? 0}");
1433 |
1434 | if (voiceModels != null && voiceModels.Count > 0)
1435 | {
1436 | int oldIndex = currentVoiceModelIndex;
1437 | currentVoiceModelIndex = (currentVoiceModelIndex + 1) % voiceModels.Count;
1438 | Log($"[CycleVoiceModel] Voice model index updated. Old: {oldIndex}, New: {currentVoiceModelIndex}");
1439 |
1440 | string newModel = Path.GetFileName(voiceModels[currentVoiceModelIndex]);
1441 | Log($"[CycleVoiceModel] New voice model selected: {newModel}");
1442 |
1443 | UpdateVoiceModelSetting(newModel);
1444 | Log("[CycleVoiceModel] UpdateVoiceModelSetting called with new model");
1445 | }
1446 | else
1447 | {
1448 | Log("[CycleVoiceModel] No voice models available to cycle through.");
1449 | }
1450 |
1451 | Log("[CycleVoiceModel] Exiting method");
1452 | }
1453 |
1454 | public void UpdateCurrentSpeaker(int speakerId)
1455 | {
1456 | currentSpeaker = speakerId;
1457 | Log($"[UpdateCurrentSpeaker] Updated current speaker to: {speakerId}");
1458 | }
1459 |
1460 | private void IncreaseSpeed()
1461 | {
1462 | if (currentSpeedIndex < speedOptions.Length - 1)
1463 | {
1464 | currentSpeedIndex++;
1465 | currentSpeed = speedOptions[currentSpeedIndex];
1466 | UpdateSpeedDisplay();
1467 | SaveSettings(currentSpeed);
1468 | }
1469 | }
1470 |
1471 | private void DecreaseSpeed()
1472 | {
1473 | if (currentSpeedIndex > 0)
1474 | {
1475 | currentSpeedIndex--;
1476 | currentSpeed = speedOptions[currentSpeedIndex];
1477 | UpdateSpeedDisplay();
1478 | SaveSettings(currentSpeed);
1479 | }
1480 | }
1481 |
1482 | private void ResetSpeed(object sender, EventArgs e)
1483 | {
1484 | currentSpeedIndex = speedOptions.Length - 10; // Reset to default speed (1.0)
1485 | currentSpeed = speedOptions[currentSpeedIndex];
1486 | UpdateSpeedDisplay();
1487 | SaveSettings(currentSpeed);
1488 | }
1489 |
1490 | public void UpdateSpeedFromSettings(double newSpeed)
1491 | {
1492 | syncContext.Post(_ =>
1493 | {
1494 | currentSpeed = newSpeed;
1495 | currentSpeedIndex = Array.FindIndex(speedOptions, s => Math.Abs(s - newSpeed) < 0.0001);
1496 |
1497 | if (currentSpeedIndex == -1)
1498 | {
1499 | // If the exact speed is not found, find the closest match
1500 | double minDifference = double.MaxValue;
1501 | for (int i = 0; i < speedOptions.Length; i++)
1502 | {
1503 | double difference = Math.Abs(speedOptions[i] - newSpeed);
1504 | if (difference < minDifference)
1505 | {
1506 | minDifference = difference;
1507 | currentSpeedIndex = i;
1508 | }
1509 | }
1510 | }
1511 |
1512 | UpdateSpeedDisplay();
1513 | }, null);
1514 | }
1515 |
1516 |
1517 | private void UpdateSpeedDisplay()
1518 | {
1519 | // Convert index to display value (-9 to 10)
1520 | // Since speedOptions array is reversed (2.0 to 0.1), we need to invert the index
1521 | int displaySpeed = -(currentSpeedIndex - 10);
1522 |
1523 | speedMenuItem.Text = $"Speed: {displaySpeed}";
1524 |
1525 | fasterMenuItem.Enabled = currentSpeedIndex < speedOptions.Length - 1;
1526 | slowerMenuItem.Enabled = currentSpeedIndex > 0;
1527 | resetSpeedMenuItem.Enabled = currentSpeedIndex != 10;
1528 |
1529 | }
1530 |
1531 | private bool IsApproximatelyEqual(double a, double b, double epsilon = 1e-6)
1532 | {
1533 | return Math.Abs(a - b) < epsilon;
1534 | }
1535 |
1536 | private void LoadVoiceModels()
1537 | {
1538 | ScanForVoiceModels();
1539 | Log($"[LoadVoiceModels] Loaded {voiceModelState.Models.Count} voice models");
1540 | foreach (var model in voiceModelState.Models)
1541 | {
1542 | Log($"[LoadVoiceModels] Loaded model: {model}");
1543 | }
1544 | }
1545 |
1546 | private void UpdateVoiceModelAndRefresh(string newModel)
1547 | {
1548 |
1549 | LoadVoiceModels();
1550 | int newIndex = voiceModelState.Models.FindIndex(m => Path.GetFileNameWithoutExtension(m).Equals(newModel, StringComparison.OrdinalIgnoreCase));
1551 |
1552 | if (newIndex != -1)
1553 | {
1554 | voiceModelState.CurrentIndex = newIndex;
1555 | UpdateVoiceModelSetting(newModel);
1556 | }
1557 | else
1558 | {
1559 | }
1560 |
1561 | RebuildContextMenu();
1562 | ForceMenuRefresh();
1563 | UpdateVoiceMenuCheckedState();
1564 | }
1565 |
1566 | public void ScanForVoiceModels()
1567 | {
1568 | if (voiceModelState == null)
1569 | {
1570 | voiceModelState = new VoiceModelState
1571 | {
1572 | CurrentIndex = 0,
1573 | Models = new List(),
1574 | IsDirty = false
1575 | };
1576 | }
1577 |
1578 | if ((DateTime.Now - lastScanTime).TotalSeconds < ScanCooldownSeconds)
1579 | {
1580 | Log($"[ScanForVoiceModels] Scan skipped, cooldown active");
1581 | return;
1582 | }
1583 |
1584 | Log($"[ScanForVoiceModels] Starting voice model scan");
1585 | string appDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
1586 | Log($"[ScanForVoiceModels] Scanning directory: {appDirectory}");
1587 |
1588 | var newModels = Directory.GetFiles(appDirectory, "*.onnx")
1589 | .Select(Path.GetFileNameWithoutExtension)
1590 | .ToList();
1591 |
1592 | Log($"[ScanForVoiceModels] Found {newModels.Count} voice models");
1593 | foreach (var model in newModels)
1594 | {
1595 | Log($"[ScanForVoiceModels] Found model: {model}");
1596 | }
1597 |
1598 | voiceModelState.Models = newModels;
1599 | voiceModels = new List(newModels);
1600 | voiceModelState.IsDirty = true;
1601 |
1602 | lastScanTime = DateTime.Now;
1603 | Log($"[ScanForVoiceModels] Voice model scan completed");
1604 | }
1605 |
1606 | public List GetVoiceModels()
1607 | {
1608 | return voiceModelState.Models;
1609 | }
1610 |
1611 | private void RefreshVoiceModels_Click(object sender, EventArgs e)
1612 | {
1613 | ScanForVoiceModels();
1614 | PopulateVoiceMenu();
1615 | ForceMenuRefresh();
1616 | }
1617 |
1618 | private void RebuildContextMenu()
1619 | {
1620 | if (trayIcon.ContextMenuStrip.InvokeRequired)
1621 | {
1622 | trayIcon.ContextMenuStrip.Invoke(new Action(RebuildContextMenu));
1623 | return;
1624 | }
1625 |
1626 | trayIcon.ContextMenuStrip.SuspendLayout();
1627 | trayIcon.ContextMenuStrip.Items.Clear();
1628 | PopulateContextMenu();
1629 | trayIcon.ContextMenuStrip.ResumeLayout(true);
1630 | trayIcon.ContextMenuStrip.Refresh();
1631 | }
1632 |
1633 | private void UpdateVoiceModelSetting(string newModel)
1634 | {
1635 | string fileName = Path.GetFileName(newModel);
1636 | string configPath = GetConfigPath();
1637 | try
1638 | {
1639 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
1640 | UpdateOrAddSetting(lines, "VoiceModel", fileName);
1641 | File.WriteAllLines(configPath, lines);
1642 | Log($"[UpdateVoiceModelSetting] VoiceModel set to {fileName}");
1643 | }
1644 | catch (Exception ex)
1645 | {
1646 | Log($"[UpdateVoiceModelSetting] Error updating VoiceModel: {ex.Message}");
1647 | MessageBox.Show($"Failed to save VoiceModel setting: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
1648 | }
1649 | }
1650 |
1651 | private string ReadSettingValue(string key)
1652 | {
1653 | string configPath = GetConfigPath();
1654 | if (File.Exists(configPath))
1655 | {
1656 | try
1657 | {
1658 | var lines = File.ReadAllLines(configPath);
1659 | var setting = lines.FirstOrDefault(l => l.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase));
1660 | if (setting != null)
1661 | {
1662 | return setting.Substring(key.Length + 1).Trim();
1663 | }
1664 | }
1665 | catch (Exception ex)
1666 | {
1667 | Log($"[ReadSettingValue] Exception reading key '{key}': {ex.Message}");
1668 | }
1669 | }
1670 | else
1671 | {
1672 | Log($"[ReadSettingValue] settings.conf not found. Key '{key}' not found.");
1673 | }
1674 | return string.Empty;
1675 | }
1676 |
1677 | private void UpdateVoiceModelUI()
1678 | {
1679 | PopulateVoiceMenu();
1680 | try
1681 | {
1682 | Log($"[UpdateVoiceModelUI] Attempting to force menu refresh");
1683 | ForceMenuRefresh();
1684 | Log($"[UpdateVoiceModelUI] Menu refresh completed successfully");
1685 | }
1686 | catch (Exception ex)
1687 | {
1688 | Log($"[UpdateVoiceModelUI] Error during menu refresh: {ex.Message}");
1689 | Log($"[UpdateVoiceModelUI] Stack trace: {ex.StackTrace}");
1690 | }
1691 | }
1692 |
1693 | private void PopulateVoiceMenu()
1694 | {
1695 | voiceMenuItem.DropDownItems.Clear();
1696 | string currentModel = ReadSettingValue("VoiceModel");
1697 |
1698 | foreach (string model in voiceModelState.Models)
1699 | {
1700 | CustomVoiceMenuItem item = new CustomVoiceMenuItem
1701 | {
1702 | Text = model,
1703 | IsSelected = (Path.GetFileNameWithoutExtension(model) == currentModel),
1704 | SelectionColor = Color.Green,
1705 | CheckMarkEnabled = false
1706 | };
1707 | item.Click += VoiceMenuItem_Click;
1708 | voiceMenuItem.DropDownItems.Add(item);
1709 | }
1710 |
1711 | voiceMenuItem.DropDownItems.Add(new ToolStripSeparator());
1712 | var refreshItem = new ToolStripMenuItem("Refresh");
1713 | refreshItem.Click += RefreshVoiceModels_Click;
1714 | voiceMenuItem.DropDownItems.Add(refreshItem);
1715 | }
1716 |
1717 |
1718 | private void VoiceMenuItem_Click(object sender, EventArgs e)
1719 | {
1720 | if (sender is CustomVoiceMenuItem clickedItem)
1721 | {
1722 | string selectedModel = clickedItem.Text;
1723 |
1724 | UpdateVoiceModelSetting(selectedModel);
1725 |
1726 | foreach (ToolStripItem item in voiceMenuItem.DropDownItems)
1727 | {
1728 | if (item is CustomVoiceMenuItem customItem)
1729 | {
1730 | customItem.IsSelected = (customItem.Text == selectedModel);
1731 | }
1732 | }
1733 |
1734 | voiceMenuItem.Invalidate(); // Force redraw
1735 | }
1736 | }
1737 |
1738 | private void SaveVoiceModelSetting(string modelName)
1739 | {
1740 | string configPath = GetConfigPath();
1741 | try
1742 | {
1743 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
1744 | UpdateOrAddSetting(lines, "VoiceModel", modelName);
1745 | File.WriteAllLines(configPath, lines);
1746 | Log($"[SaveVoiceModelSetting] VoiceModel set to {modelName}");
1747 | }
1748 | catch (Exception ex)
1749 | {
1750 | Log($"[SaveVoiceModelSetting] Error updating VoiceModel: {ex.Message}");
1751 | MessageBox.Show($"Failed to save VoiceModel setting: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
1752 | }
1753 | }
1754 |
1755 | private void RefreshVoiceModels()
1756 | {
1757 | LoadVoiceModels();
1758 | UpdateVoiceModelUI();
1759 | ForceMenuRefresh();
1760 | }
1761 |
1762 | private void RefreshVoiceModelList()
1763 | {
1764 | try
1765 | {
1766 | LoadVoiceModels();
1767 | Log($"[RefreshVoiceModelList] Loaded {voiceModelState?.Models?.Count ?? 0} voice models");
1768 |
1769 | if (voiceModelState?.Models?.Any() == true)
1770 | {
1771 | var (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceVk, monitoringEnabled, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVk, speaker, sentenceSilence) = ReadSettings();
1772 | Log($"[RefreshVoiceModelList] Current model from settings: {model}");
1773 |
1774 | if (!string.IsNullOrEmpty(model))
1775 | {
1776 | voiceModelState.CurrentIndex = voiceModelState.Models.FindIndex(m =>
1777 | Path.GetFileName(m).Equals(model, StringComparison.OrdinalIgnoreCase));
1778 | Log($"[RefreshVoiceModelList] Updated currentVoiceModelIndex: {voiceModelState.CurrentIndex}");
1779 |
1780 | if (voiceModelState.CurrentIndex == -1)
1781 | {
1782 | voiceModelState.CurrentIndex = 0;
1783 | Log($"[RefreshVoiceModelList] Voice model not found in list, reset to index 0");
1784 | }
1785 | }
1786 | else
1787 | {
1788 | Log($"[RefreshVoiceModelList] Invalid model name from settings");
1789 | }
1790 | }
1791 | else
1792 | {
1793 | Log($"[RefreshVoiceModelList] No voice models available or voiceModelState is null");
1794 | }
1795 | }
1796 | catch (Exception ex)
1797 | {
1798 | Log($"[RefreshVoiceModelList] Error: {ex.Message}");
1799 | Log($"[RefreshVoiceModelList] Stack trace: {ex.StackTrace}");
1800 | }
1801 | }
1802 |
1803 | private void UpdateVoiceMenu()
1804 | {
1805 | for (int i = 0; i < voiceMenuItem.DropDownItems.Count; i++)
1806 | {
1807 | if (voiceMenuItem.DropDownItems[i] is ToolStripMenuItem item)
1808 | {
1809 | bool oldChecked = item.Checked;
1810 | item.Checked = (i == voiceModelState.CurrentIndex);
1811 | }
1812 | }
1813 | }
1814 |
1815 | private void UpdateVoiceMenuCheckedState()
1816 | {
1817 |
1818 | for (int i = 0; i < voiceMenuItem.DropDownItems.Count; i++)
1819 | {
1820 | if (voiceMenuItem.DropDownItems[i] is ToolStripMenuItem menuItem)
1821 | {
1822 | bool oldCheckedState = menuItem.Checked;
1823 | bool newCheckedState = (i == voiceModelState.CurrentIndex);
1824 | menuItem.Checked = newCheckedState;
1825 |
1826 | }
1827 | }
1828 |
1829 | voiceMenuItem.Invalidate();
1830 | trayIcon.ContextMenuStrip.Refresh();
1831 |
1832 | }
1833 |
1834 | private void ForceMenuRefresh()
1835 | {
1836 |
1837 | trayIcon.ContextMenuStrip.SuspendLayout();
1838 | PopulateContextMenu();
1839 | trayIcon.ContextMenuStrip.ResumeLayout();
1840 |
1841 | }
1842 |
1843 | private void PopulateContextMenu()
1844 | {
1845 | Log("[PopulateContextMenu] Starting menu population");
1846 |
1847 | toggleMonitoringMenuItem = new ToolStripMenuItem("Monitoring")
1848 | {
1849 | Checked = isMonitoring
1850 | };
1851 | toggleMonitoringMenuItem.Click += (s, e) =>
1852 | {
1853 | Log("[PopulateContextMenu] Monitoring menu item clicked");
1854 | ToggleMonitoring(s, e);
1855 | };
1856 |
1857 | stopSpeechMenuItem = new ToolStripMenuItem("Stop Speech", null, (s, e) => StopCurrentSpeech());
1858 |
1859 | trayIcon.ContextMenuStrip.Items.Add(toggleMonitoringMenuItem);
1860 | trayIcon.ContextMenuStrip.Items.Add(stopSpeechMenuItem);
1861 | trayIcon.ContextMenuStrip.Items.Add(speedMenuItem);
1862 | trayIcon.ContextMenuStrip.Items.Add(voiceMenuItem);
1863 | PopulateVoiceMenu();
1864 | presetsMenuItem = new ToolStripMenuItem("Presets");
1865 | AddPresetsToContextMenu();
1866 |
1867 | exportMenuItem = new ToolStripMenuItem("Export to WAV", null, SafeEventHandler(ExportWav));
1868 |
1869 | // Apply visibility setting before adding to the menu
1870 | if (menuVisibilitySettings.TryGetValue("Export to WAV", out bool isVisible))
1871 | {
1872 | exportMenuItem.Visible = isVisible;
1873 | }
1874 |
1875 | trayIcon.ContextMenuStrip.Items.Add(exportMenuItem);
1876 | trayIcon.ContextMenuStrip.Items.Add(presetsMenuItem);
1877 | var settingsMenuItem = new ToolStripMenuItem("Settings", null, SafeEventHandler(OpenSettings));
1878 | trayIcon.ContextMenuStrip.Items.Add(settingsMenuItem);
1879 | trayIcon.ContextMenuStrip.Items.Add(exitMenuItem);
1880 |
1881 | bool stopSpeechVisible = menuVisibilitySettings.TryGetValue("Stop Speech", out bool visible) && visible;
1882 | stopSpeechMenuItem.Visible = stopSpeechVisible;
1883 | }
1884 |
1885 | private async void ExportWav(object sender, EventArgs e)
1886 | {
1887 | using (SaveFileDialog saveDialog = new SaveFileDialog())
1888 | {
1889 | saveDialog.Filter = "WAV files (*.wav)|*.wav";
1890 | saveDialog.DefaultExt = "wav";
1891 |
1892 | if (saveDialog.ShowDialog() == DialogResult.OK)
1893 | {
1894 | string clipboardText = Clipboard.GetText();
1895 | if (string.IsNullOrEmpty(clipboardText))
1896 | {
1897 | MessageBox.Show("No text in clipboard to convert.", "Export Error", MessageBoxButtons.OK, MessageBoxIcon.Warning);
1898 | return;
1899 | }
1900 |
1901 | using (var memoryStream = new MemoryStream())
1902 | {
1903 | var processedText = ProcessLine(clipboardText);
1904 | var (model, speed, _, _, _, _, _, _, _, _, _, _, _, _, speaker, sentenceSilence) = ReadSettings();
1905 |
1906 | ProcessStartInfo psi = new ProcessStartInfo
1907 | {
1908 | FileName = piperPath,
1909 | Arguments = $"--model {model}.onnx --output-raw --length-scale {speed} --speaker {speaker} --sentence-silence {sentenceSilence}",
1910 | UseShellExecute = false,
1911 | RedirectStandardInput = true,
1912 | RedirectStandardOutput = true,
1913 | CreateNoWindow = true
1914 | };
1915 |
1916 | using (Process process = new Process())
1917 | {
1918 | process.StartInfo = psi;
1919 | process.Start();
1920 |
1921 | using (var writer = new StreamWriter(process.StandardInput.BaseStream, Encoding.UTF8))
1922 | {
1923 | await writer.WriteLineAsync(processedText);
1924 | writer.Close();
1925 | }
1926 |
1927 | await process.StandardOutput.BaseStream.CopyToAsync(memoryStream);
1928 | memoryStream.Position = 0;
1929 |
1930 | using (var rawStream = new RawSourceWaveStream(memoryStream, new WaveFormat(22050, 16, 1)))
1931 | using (var waveStream = new WaveFileWriter(saveDialog.FileName, rawStream.WaveFormat))
1932 | {
1933 | await rawStream.CopyToAsync(waveStream);
1934 | }
1935 | }
1936 | }
1937 | MessageBox.Show($"Audio exported successfully to {saveDialog.FileName}", "Export Complete", MessageBoxButtons.OK, MessageBoxIcon.Information);
1938 | }
1939 | }
1940 | }
1941 |
1942 |
1943 | private void OpenSettings(object sender, EventArgs e)
1944 | {
1945 | try
1946 | {
1947 | Log($"Opening Settings form");
1948 | var (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk,
1949 | changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, monitoringEnabled,
1950 | speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk,
1951 | speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk,
1952 | switchPresetModifiers, switchPresetVk,
1953 | speaker, sentenceSilence) = ReadSettings();
1954 |
1955 | var settingsForm = SettingsForm.GetInstance();
1956 | settingsForm.ShowSettingsForm();
1957 | Log($"Settings form displayed successfully");
1958 | }
1959 | catch (Exception ex)
1960 | {
1961 | Log($"Error in OpenSettings: {ex.Message}");
1962 | Log($"Stack trace: {ex.StackTrace}");
1963 | }
1964 | }
1965 |
1966 | private void SettingsForm_VoiceModelChanged(object sender, EventArgs e)
1967 | {
1968 | var (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceVk, monitoringEnabled, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVk, speaker, sentenceSilence) = ReadSettings();
1969 |
1970 | UpdateVoiceModelAndRefresh(model);
1971 |
1972 | UpdateVoiceMenu();
1973 | ForceMenuRefresh();
1974 | UpdateVoiceMenuCheckedState();
1975 |
1976 | }
1977 |
1978 | private void SettingsForm_SpeedChanged(object sender, double newSpeed)
1979 | {
1980 | UpdateSpeedFromSettings(newSpeed);
1981 | }
1982 |
1983 | private void SettingsForm_FormClosed(object sender, FormClosedEventArgs e)
1984 | {
1985 | RefreshVoiceModels();
1986 | }
1987 |
1988 | public void UpdateVoiceModel(string newModel)
1989 | {
1990 | LoadVoiceModels(); // Refresh the list of voice models
1991 | if (voiceModelState?.Models == null)
1992 | {
1993 | Log($"[UpdateVoiceModel] voiceModelState.Models is null. Cannot update voice model.");
1994 | return;
1995 | }
1996 | string newModelName = Path.GetFileNameWithoutExtension(newModel);
1997 | voiceModelState.CurrentIndex = voiceModelState.Models.FindIndex(m => Path.GetFileNameWithoutExtension(m) == newModelName);
1998 | Log($"[UpdateVoiceModel] New currentVoiceModelIndex: {voiceModelState.CurrentIndex}");
1999 | if (voiceModelState.CurrentIndex == -1)
2000 | {
2001 | voiceModelState.CurrentIndex = 0;
2002 | Log($"[UpdateVoiceModel] Voice model not found in list, reset to index 0");
2003 | }
2004 | UpdateVoiceModelSetting(newModel);
2005 | UpdateVoiceMenu();
2006 | ForceMenuRefresh();
2007 | }
2008 |
2009 | private async void ClipboardTimer_Tick(object sender, EventArgs e)
2010 | {
2011 | if (CurrentAudioState != AudioPlaybackState.Idle)
2012 | {
2013 | if (currentWaveOut?.PlaybackState != PlaybackState.Playing)
2014 | {
2015 | Log($"[ClipboardTimer_Tick] Resetting audio state from {CurrentAudioState} to Idle.");
2016 | CurrentAudioState = AudioPlaybackState.Idle;
2017 | }
2018 | }
2019 |
2020 | if (!isMonitoring || isProcessing)
2021 | {
2022 | return;
2023 | }
2024 |
2025 | if (Clipboard.ContainsText())
2026 | {
2027 | string clipboardContent = Clipboard.GetText();
2028 | if (clipboardContent != lastClipboardContent)
2029 | {
2030 | lastClipboardContent = clipboardContent;
2031 | if (ignoreCurrentClipboard)
2032 | {
2033 | ignoreCurrentClipboard = false;
2034 | return;
2035 | }
2036 | Log($"New clipboard content detected: {clipboardContent.Substring(0, Math.Min(50, clipboardContent.Length))}...");
2037 | isProcessing = true;
2038 | await ConvertAndPlayTextToSpeechAsync(clipboardContent);
2039 | isProcessing = false;
2040 | }
2041 | }
2042 | }
2043 |
2044 | private (string model, float speed, bool logging, uint monitoringHotkeyModifiers, uint monitoringHotkeyVk, uint changeVoiceHotkeyModifiers, uint changeVoiceHotkeyVk, bool monitoringEnabled, uint speedIncreaseHotkeyModifiers, uint speedIncreaseHotkeyVk, uint speedDecreaseHotkeyModifiers, uint speedDecreaseHotkeyVk, uint switchPresetModifiers, uint switchPresetVk, int speaker, float sentenceSilence) ReadSettings()
2045 | {
2046 | string settingsPath = GetConfigPath();
2047 | string model = "";
2048 | float speed = 0f;
2049 | bool logging = false;
2050 | uint monitoringHotkeyModifiers = 0;
2051 | uint monitoringHotkeyVk = 0;
2052 | uint changeVoiceHotkeyModifiers = 0;
2053 | uint changeVoiceHotkeyVk = 0;
2054 | uint speedIncreaseHotkeyModifiers = 0;
2055 | uint speedIncreaseHotkeyVk = 0;
2056 | uint speedDecreaseHotkeyModifiers = 0;
2057 | uint speedDecreaseHotkeyVk = 0;
2058 | uint switchPresetModifiers = 0;
2059 | uint switchPresetVk = 0;
2060 | bool monitoringEnabled = true;
2061 | int speaker = 0;
2062 | float sentenceSilence = 0.2f;
2063 | bool defaultsUsed = false;
2064 |
2065 | if (!File.Exists(settingsPath))
2066 | {
2067 | defaultsUsed = true;
2068 | SaveSettings();
2069 | }
2070 |
2071 | Log($"[ReadSettings] Reading settings from: {settingsPath}");
2072 | if (File.Exists(settingsPath))
2073 | {
2074 | foreach (string line in File.ReadAllLines(settingsPath))
2075 | {
2076 | var parts = line.Split('=');
2077 | if (parts.Length == 2)
2078 | {
2079 | switch (parts[0].Trim())
2080 | {
2081 | case "VoiceModel":
2082 | model = Path.GetFileNameWithoutExtension(parts[1].Trim());
2083 | break;
2084 | case "Speed":
2085 | if (double.TryParse(parts[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out double parsedSpeed))
2086 | {
2087 | speed = (float)parsedSpeed;
2088 | }
2089 | break;
2090 | case "Logging":
2091 | bool.TryParse(parts[1].Trim(), out logging);
2092 | break;
2093 | case "MonitoringModifier":
2094 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out monitoringHotkeyModifiers);
2095 | break;
2096 | case "MonitoringKey":
2097 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out monitoringHotkeyVk);
2098 | break;
2099 | case "StopSpeechModifier":
2100 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out stopSpeechModifiers);
2101 | break;
2102 | case "StopSpeechKey":
2103 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out stopSpeechVk);
2104 | break;
2105 | case "ChangeVoiceModifier":
2106 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out changeVoiceHotkeyModifiers);
2107 | break;
2108 | case "ChangeVoiceKey":
2109 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out changeVoiceHotkeyVk);
2110 | break;
2111 | case "SpeedIncreaseModifier":
2112 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out speedIncreaseHotkeyModifiers);
2113 | break;
2114 | case "SpeedIncreaseKey":
2115 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out speedIncreaseHotkeyVk);
2116 | break;
2117 | case "SpeedDecreaseModifier":
2118 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out speedDecreaseHotkeyModifiers);
2119 | break;
2120 | case "SpeedDecreaseKey":
2121 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out speedDecreaseHotkeyVk);
2122 | break;
2123 | case "MonitoringEnabled":
2124 | bool.TryParse(parts[1].Trim(), out monitoringEnabled);
2125 | break;
2126 | case "SwitchPresetModifier":
2127 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out switchPresetModifiers);
2128 | break;
2129 | case "SwitchPresetKey":
2130 | uint.TryParse(parts[1].Trim().Replace("0x", ""), System.Globalization.NumberStyles.HexNumber, null, out switchPresetVk);
2131 | break;
2132 | case "Speaker":
2133 | int.TryParse(parts[1].Trim(), out speaker);
2134 | break;
2135 | case "SentenceSilence":
2136 | if (float.TryParse(parts[1].Trim(), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out float parsedSilence))
2137 | {
2138 | sentenceSilence = parsedSilence;
2139 | Log($"[ReadSettings] Parsed SentenceSilence value: {sentenceSilence}");
2140 | }
2141 | break;
2142 | }
2143 | }
2144 | }
2145 | }
2146 |
2147 | if (defaultsUsed)
2148 | {
2149 | SaveSettings();
2150 | }
2151 |
2152 | Log($"Voice model read from settings: {model}");
2153 | return (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, monitoringEnabled, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVk, speaker, sentenceSilence);
2154 | }
2155 |
2156 | private void ApplyMonitoringState(bool enabled)
2157 | {
2158 | if (enabled)
2159 | {
2160 | StartMonitoring();
2161 | }
2162 | else
2163 | {
2164 | StopMonitoring();
2165 | }
2166 | SaveMonitoringState(enabled);
2167 | }
2168 |
2169 | private (HashSet ignoreWords, HashSet bannedWords, Dictionary replaceWords) LoadDictionaries()
2170 | {
2171 | var ignoreWords = new HashSet(StringComparer.OrdinalIgnoreCase) { "#", "*" };
2172 | var bannedWords = new HashSet(StringComparer.OrdinalIgnoreCase);
2173 | var replaceWords = new Dictionary(StringComparer.OrdinalIgnoreCase);
2174 |
2175 | string baseDir = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
2176 |
2177 | if (File.Exists(Path.Combine(baseDir, "ignore.dict")))
2178 | ignoreWords.UnionWith(File.ReadAllLines(Path.Combine(baseDir, "ignore.dict")));
2179 |
2180 | if (File.Exists(Path.Combine(baseDir, "banned.dict")))
2181 | bannedWords = new HashSet(File.ReadAllLines(Path.Combine(baseDir, "banned.dict")), StringComparer.OrdinalIgnoreCase);
2182 |
2183 | if (File.Exists(Path.Combine(baseDir, "replace.dict")))
2184 | {
2185 | foreach (var line in File.ReadAllLines(Path.Combine(baseDir, "replace.dict")))
2186 | {
2187 | var parts = line.Split(new[] { '=' }, 2);
2188 | if (parts.Length == 2)
2189 | replaceWords[parts[0].Trim()] = parts[1].Trim();
2190 | }
2191 | }
2192 |
2193 | return (ignoreWords, bannedWords, replaceWords);
2194 | }
2195 |
2196 | private async Task ConvertAndPlayTextToSpeechAsync(string text)
2197 | {
2198 | var processedTextBuilder = new StringBuilder();
2199 | Log($"Original text: {text}");
2200 |
2201 | using (var reader = new StringReader(text))
2202 | {
2203 | string line;
2204 | while ((line = await reader.ReadLineAsync()) != null)
2205 | {
2206 | if (!bannedWords.Any(word => line.IndexOf(word, StringComparison.OrdinalIgnoreCase) >= 0))
2207 | {
2208 | var processedLine = ProcessLine(line);
2209 | Log($"Processed line before Piper: {processedLine}");
2210 | if (!string.IsNullOrWhiteSpace(processedLine))
2211 | {
2212 | processedTextBuilder.AppendLine(processedLine);
2213 | }
2214 | }
2215 | }
2216 | }
2217 |
2218 | var processedText = processedTextBuilder.ToString();
2219 | Log($"Final text sent to Piper: {processedText}");
2220 | var (model, speed, logging, monitoringHotkeyModifiers, monitoringHotkeyVk, changeVoiceHotkeyModifiers, changeVoiceHotkeyVk, monitoringEnabled, speedIncreaseHotkeyModifiers, speedIncreaseHotkeyVk, speedDecreaseHotkeyModifiers, speedDecreaseHotkeyVk, switchPresetModifiers, switchPresetVK, speaker, sentenceSilence) = ReadSettings();
2221 |
2222 | Log($"Initializing Piper process with model: {model}");
2223 | ProcessStartInfo psi = new ProcessStartInfo
2224 | {
2225 | FileName = piperPath,
2226 | Arguments = $"--model {model}.onnx --output-raw --length-scale {speed.ToString(System.Globalization.CultureInfo.InvariantCulture)} --speaker {currentSpeaker} --sentence-silence {sentenceSilence.ToString(System.Globalization.CultureInfo.InvariantCulture)}",
2227 | UseShellExecute = false,
2228 | RedirectStandardInput = true,
2229 | RedirectStandardOutput = true,
2230 | RedirectStandardError = true,
2231 | CreateNoWindow = true,
2232 | WorkingDirectory = Path.GetDirectoryName(piperPath),
2233 | StandardInputEncoding = Encoding.UTF8,
2234 | StandardOutputEncoding = Encoding.Default,
2235 | };
2236 |
2237 | try
2238 | {
2239 | if (CurrentAudioState == AudioPlaybackState.Playing)
2240 | {
2241 | CurrentAudioState = AudioPlaybackState.Stopping;
2242 | if (currentWaveOut != null)
2243 | {
2244 | currentWaveOut.Stop();
2245 | currentWaveOut.Dispose();
2246 | currentWaveOut = null;
2247 | }
2248 | }
2249 |
2250 | using (Process process = new Process())
2251 | {
2252 | process.StartInfo = psi;
2253 | process.Start();
2254 | Log($"Piper process started with model: {model}");
2255 |
2256 | process.ErrorDataReceived += (sender, e) =>
2257 | {
2258 | if (!string.IsNullOrEmpty(e.Data))
2259 | {
2260 | Log($"Piper: {e.Data}");
2261 | }
2262 | };
2263 | process.BeginErrorReadLine();
2264 |
2265 | playbackCancellationTokenSource = new CancellationTokenSource();
2266 | CurrentAudioState = AudioPlaybackState.Playing;
2267 |
2268 | using (var writer = new StreamWriter(process.StandardInput.BaseStream, Encoding.UTF8))
2269 | {
2270 | await writer.WriteLineAsync(processedText);
2271 | writer.Close();
2272 | }
2273 |
2274 | await StreamAudioPlayback(process);
2275 |
2276 | if (!process.HasExited)
2277 | {
2278 | process.Kill();
2279 | Log($"Piper process terminated after audio playback");
2280 | }
2281 |
2282 | await Task.Run(() => process.WaitForExit());
2283 | Log($"Piper process exited with code: {process.ExitCode}");
2284 | }
2285 | }
2286 | catch (Exception ex)
2287 | {
2288 | Log($"Error in ConvertAndPlayTextToSpeech: {ex.Message}");
2289 | Log($"Stack trace: {ex.StackTrace}");
2290 | MessageBox.Show($"An error occurred: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
2291 | }
2292 | finally
2293 | {
2294 | CurrentAudioState = AudioPlaybackState.Idle;
2295 | playbackCancellationTokenSource?.Dispose();
2296 | playbackCancellationTokenSource = null;
2297 | }
2298 | }
2299 |
2300 | private string ProcessLine(string line)
2301 | {
2302 |
2303 | // Currency abbreviations
2304 | line = Regex.Replace(line, @"GBP\s*(\d+)", "$1 pounds");
2305 | line = Regex.Replace(line, @"USD\s*(\d+)", "$1 dollars");
2306 | line = Regex.Replace(line, @"EUR\s*(\d+)", "$1 euros");
2307 | line = Regex.Replace(line, @"JPY\s*(\d+)", "$1 yen");
2308 | line = Regex.Replace(line, @"AUD\s*(\d+)", "$1 australian dollars");
2309 | line = Regex.Replace(line, @"CAD\s*(\d+)", "$1 canadian dollars");
2310 | line = Regex.Replace(line, @"CHF\s*(\d+)", "$1 swiss francs");
2311 | line = Regex.Replace(line, @"CNY\s*(\d+)", "$1 yuan");
2312 | line = Regex.Replace(line, @"INR\s*(\d+)", "$1 rupees");
2313 |
2314 | // Currency symbols with decimals
2315 | line = Regex.Replace(line, @"£(\d+)\.(\d{2})", "$1 pounds $2 pence");
2316 | line = Regex.Replace(line, @"£(\d+)", "$1 pounds");
2317 |
2318 | line = Regex.Replace(line, @"\$(\d+)\.(\d{2})", "$1 dollars $2 cents");
2319 | line = Regex.Replace(line, @"\$(\d+)", "$1 dollars");
2320 |
2321 | line = Regex.Replace(line, @"€(\d+)\.(\d{2})", "$1 euros $2 cents");
2322 | line = Regex.Replace(line, @"€(\d+)", "$1 euros");
2323 |
2324 | line = Regex.Replace(line, @"¥(\d+)", "$1 yen");
2325 | line = Regex.Replace(line, @"₹(\d+)", "$1 rupees");
2326 | line = Regex.Replace(line, @"₣(\d+)", "$1 francs");
2327 | line = Regex.Replace(line, @"元(\d+)", "$1 yuan");
2328 |
2329 | // Handle currency at the end of amount
2330 | line = Regex.Replace(line, @"(\d+)\s*pounds?", "$1 pounds");
2331 | line = Regex.Replace(line, @"(\d+)\s*dollars?", "$1 dollars");
2332 | line = Regex.Replace(line, @"(\d+)\s*euros?", "$1 euros");
2333 | line = Regex.Replace(line, @"(\d+)\s*yen", "$1 yen");
2334 | line = Regex.Replace(line, @"(\d+)\s*rupees?", "$1 rupees");
2335 | line = Regex.Replace(line, @"(\d+)\s*francs?", "$1 francs");
2336 | line = Regex.Replace(line, @"(\d+)\s*yuan", "$1 yuan");
2337 |
2338 | line = line.Replace("#", "").Replace("*", "");
2339 |
2340 | line = Regex.Replace(
2341 | line,
2342 | @"(\w+)([.!?])(\s*)$",
2343 | "$1, . . . . . . . $2$3",
2344 | RegexOptions.CultureInvariant
2345 | );
2346 |
2347 | char[] preservePunctuation = { '.', ',', '?', '!', ':', ';' };
2348 | var words = line.Split(new[] { ' ' }, StringSplitOptions.RemoveEmptyEntries);
2349 | var processedWords = new List();
2350 |
2351 | foreach (var word in words)
2352 | {
2353 | if (!ignoreWords.Contains(word))
2354 | {
2355 | var processedWord = ApplyReplacements(word);
2356 | foreach (char punct in preservePunctuation)
2357 | {
2358 | if (word.EndsWith(punct.ToString()) && !processedWord.EndsWith(punct.ToString()))
2359 | {
2360 | processedWord += punct;
2361 | }
2362 | }
2363 | processedWords.Add(processedWord);
2364 | }
2365 | }
2366 |
2367 | var result = string.Join(" ", processedWords);
2368 |
2369 | return result;
2370 | }
2371 |
2372 | private string ApplyReplacements(string word)
2373 | {
2374 |
2375 | // Extract quoted content if present
2376 | Match quotedMatch = Regex.Match(word, @"""(\w+)""");
2377 | if (quotedMatch.Success)
2378 | {
2379 | string quotedWord = quotedMatch.Groups[1].Value;
2380 | Log($"Found quoted word: {quotedWord}");
2381 | return $"\"{quotedWord}\""; // Return quoted word unchanged
2382 | }
2383 |
2384 | // Apply replacements only for non-quoted text
2385 | foreach (var replace in replaceWords)
2386 | {
2387 | var beforeReplace = word;
2388 | word = Regex.Replace(word, replace.Key, replace.Value, RegexOptions.IgnoreCase);
2389 | if (beforeReplace != word)
2390 | {
2391 | Log($"Replacement changed word from '{beforeReplace}' to '{word}' using pattern '{replace.Key}'");
2392 | }
2393 | }
2394 |
2395 | return word;
2396 | }
2397 |
2398 | private async Task StreamAudioPlayback(Process piperProcess)
2399 | {
2400 | if (piperProcess?.StandardOutput?.BaseStream == null)
2401 | {
2402 | Log($"[StreamAudioPlayback] Process or stream is null");
2403 | return;
2404 | }
2405 |
2406 | try
2407 | {
2408 | var waveFormat = new WaveFormat(22050, 16, 1);
2409 | var bufferedWaveProvider = new BufferedWaveProvider(waveFormat)
2410 | {
2411 | BufferDuration = TimeSpan.FromMinutes(5), // Increase duration as needed
2412 | DiscardOnBufferOverflow = false // Do not discard data on overflow
2413 | };
2414 |
2415 | using (currentWaveOut = new WaveOutEvent())
2416 | {
2417 | currentWaveOut.Init(bufferedWaveProvider);
2418 | currentWaveOut.Play();
2419 | Log($"[StreamAudioPlayback] Playback started");
2420 |
2421 | var buffer = new byte[8192];
2422 | int bytesRead;
2423 |
2424 | while ((bytesRead = await piperProcess.StandardOutput.BaseStream.ReadAsync(buffer, 0, buffer.Length)) > 0)
2425 | {
2426 | if (playbackCancellationTokenSource?.Token.IsCancellationRequested == true)
2427 | {
2428 | Log($"[StreamAudioPlayback] Cancellation requested");
2429 | break;
2430 | }
2431 |
2432 | bufferedWaveProvider.AddSamples(buffer, 0, bytesRead);
2433 | }
2434 |
2435 | // Wait for playback to finish
2436 | while (currentWaveOut.PlaybackState == PlaybackState.Playing && bufferedWaveProvider.BufferedBytes > 0)
2437 | {
2438 | await Task.Delay(50); // Check every 50 ms
2439 | }
2440 |
2441 | currentWaveOut.Stop();
2442 | }
2443 | }
2444 | catch (Exception ex)
2445 | {
2446 | Log($"[StreamAudioPlayback] Error during playback: {ex.Message}");
2447 | }
2448 | finally
2449 | {
2450 | CurrentAudioState = AudioPlaybackState.Idle;
2451 | currentWaveOut = null;
2452 | }
2453 | }
2454 |
2455 | private Task PlayAudioWithWaveOutEvent(MemoryStream audioStream)
2456 | {
2457 | playbackCancellationTokenSource = new CancellationTokenSource();
2458 | return Task.Run(() =>
2459 | {
2460 | try
2461 | {
2462 | Log($"[PlayAudioWithWaveOutEvent] Starting playback. AudioStream length: {audioStream.Length}");
2463 | audioStream.Position = 0;
2464 | using (var rawStream = new RawSourceWaveStream(audioStream, new WaveFormat(22050, 16, 1)))
2465 | using (currentWaveOut = new WaveOutEvent())
2466 | {
2467 | currentWaveOut.Init(rawStream);
2468 | CurrentAudioState = AudioPlaybackState.Playing;
2469 | currentWaveOut.Play();
2470 | Log($"[PlayAudioWithWaveOutEvent] Playback started. State: {currentWaveOut.PlaybackState}");
2471 |
2472 | while (currentWaveOut.PlaybackState == PlaybackState.Playing)
2473 | {
2474 | if (playbackCancellationTokenSource.Token.IsCancellationRequested)
2475 | {
2476 | Log($"[PlayAudioWithWaveOutEvent] Cancellation detected. Stopping playback.");
2477 | currentWaveOut.Stop();
2478 | break;
2479 | }
2480 | Thread.Sleep(1);
2481 | }
2482 | }
2483 | }
2484 | catch (Exception ex)
2485 | {
2486 | Log($"[PlayAudioWithWaveOutEvent] Exception: {ex.Message}");
2487 | }
2488 | finally
2489 | {
2490 | CurrentAudioState = AudioPlaybackState.Idle;
2491 | currentWaveOut = null;
2492 | Log($"[PlayAudioWithWaveOutEvent] Playback ended. Final state: {CurrentAudioState}");
2493 | }
2494 | });
2495 | }
2496 |
2497 | private void Exit(object sender, EventArgs e)
2498 | {
2499 | // Reload hotkey settings from configuration to ensure they are up-to-date
2500 | var currentSettings = ReadCurrentSettings();
2501 |
2502 | monitoringModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("MonitoringModifier", "0x00"), 16);
2503 | monitoringVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("MonitoringKey", "0x00"), 16);
2504 | stopSpeechModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("StopSpeechModifier", "0x00"), 16);
2505 | stopSpeechVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("StopSpeechKey", "0x00"), 16);
2506 | changeVoiceModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("ChangeVoiceModifier", "0x00"), 16);
2507 | changeVoiceVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("ChangeVoiceKey", "0x00"), 16);
2508 | speedIncreaseModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("SpeedIncreaseModifier", "0x00"), 16);
2509 | speedIncreaseVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("SpeedIncreaseKey", "0x00"), 16);
2510 | speedDecreaseModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("SpeedDecreaseModifier", "0x00"), 16);
2511 | speedDecreaseVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("SpeedDecreaseKey", "0x00"), 16);
2512 | switchPresetModifiers = Convert.ToUInt32(currentSettings.GetValueOrDefault("SwitchPresetModifier", "0x00"), 16);
2513 | switchPresetVk = Convert.ToUInt32(currentSettings.GetValueOrDefault("SwitchPresetKey", "0x00"), 16);
2514 |
2515 | // Save settings before cleanup
2516 | SaveSettings();
2517 |
2518 | // Stop all background operations
2519 | clipboardTimer?.Stop();
2520 | UnregisterAllHotkeys();
2521 |
2522 | // Clean up tray icon
2523 | if (trayIcon != null)
2524 | {
2525 | trayIcon.Visible = false;
2526 | trayIcon.Dispose();
2527 | trayIcon = null;
2528 | }
2529 |
2530 | // Exit the application directly
2531 | Environment.Exit(0);
2532 | }
2533 |
2534 | public void SaveSettings(double? speed = null, string voiceModel = null, int? speaker = null, float? sentenceSilence = null)
2535 | {
2536 | if (isInitializing)
2537 | {
2538 | Log($"[SaveSettings] Skipping save during initialization");
2539 | return;
2540 | }
2541 |
2542 | Log($"[SaveSettings] Entering method. Saving current settings to file.");
2543 | string configPath = GetConfigPath();
2544 | var lines = File.Exists(configPath) ? File.ReadAllLines(configPath).ToList() : new List();
2545 |
2546 | UpdateOrAddSetting(lines, "MonitoringEnabled", isMonitoring.ToString());
2547 |
2548 | if (speed.HasValue)
2549 | {
2550 | UpdateOrAddSetting(lines, "Speed", speed.Value.ToString("F1", System.Globalization.CultureInfo.InvariantCulture));
2551 | }
2552 | if (!string.IsNullOrEmpty(voiceModel))
2553 | {
2554 | UpdateOrAddSetting(lines, "VoiceModel", Path.GetFileName(voiceModel));
2555 | }
2556 | if (speaker.HasValue)
2557 | {
2558 | UpdateOrAddSetting(lines, "Speaker", speaker.Value.ToString());
2559 | }
2560 | if (sentenceSilence.HasValue)
2561 | {
2562 | UpdateOrAddSetting(lines, "SentenceSilence", sentenceSilence.Value.ToString("F1", System.Globalization.CultureInfo.InvariantCulture));
2563 | }
2564 |
2565 | // Only update hotkey settings if they have valid values
2566 | if (monitoringModifiers != 0 && monitoringVk != 0)
2567 | {
2568 | UpdateOrAddSetting(lines, "MonitoringModifier", $"0x{monitoringModifiers:X2}");
2569 | UpdateOrAddSetting(lines, "MonitoringKey", $"0x{monitoringVk:X2}");
2570 | }
2571 | // Repeat the above check for other hotkeys
2572 | if (stopSpeechModifiers != 0 && stopSpeechVk != 0)
2573 | {
2574 | UpdateOrAddSetting(lines, "StopSpeechModifier", $"0x{stopSpeechModifiers:X2}");
2575 | UpdateOrAddSetting(lines, "StopSpeechKey", $"0x{stopSpeechVk:X2}");
2576 | }
2577 | if (changeVoiceModifiers != 0 && changeVoiceVk != 0)
2578 | {
2579 | UpdateOrAddSetting(lines, "ChangeVoiceModifier", $"0x{changeVoiceModifiers:X2}");
2580 | UpdateOrAddSetting(lines, "ChangeVoiceKey", $"0x{changeVoiceVk:X2}");
2581 | }
2582 | if (speedIncreaseModifiers != 0 && speedIncreaseVk != 0)
2583 | {
2584 | UpdateOrAddSetting(lines, "SpeedIncreaseModifier", $"0x{speedIncreaseModifiers:X2}");
2585 | UpdateOrAddSetting(lines, "SpeedIncreaseKey", $"0x{speedIncreaseVk:X2}");
2586 | }
2587 | if (speedDecreaseModifiers != 0 && speedDecreaseVk != 0)
2588 | {
2589 | UpdateOrAddSetting(lines, "SpeedDecreaseModifier", $"0x{speedDecreaseModifiers:X2}");
2590 | UpdateOrAddSetting(lines, "SpeedDecreaseKey", $"0x{speedDecreaseVk:X2}");
2591 | }
2592 | if (switchPresetModifiers != 0 && switchPresetVk != 0)
2593 | {
2594 | UpdateOrAddSetting(lines, "SwitchPresetModifier", $"0x{switchPresetModifiers:X2}");
2595 | UpdateOrAddSetting(lines, "SwitchPresetKey", $"0x{switchPresetVk:X2}");
2596 | }
2597 |
2598 | try
2599 | {
2600 | File.WriteAllLines(configPath, lines);
2601 | Log($"Settings saved successfully.");
2602 | }
2603 | catch (Exception ex)
2604 | {
2605 | Log($"[SaveSettings] Error writing to settings.conf: {ex.Message}");
2606 | MessageBox.Show($"Failed to save settings: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
2607 | }
2608 | }
2609 |
2610 | private void UpdateOrAddSetting(List lines, string key, string value)
2611 | {
2612 | int index = lines.FindIndex(l => l.StartsWith(key + "=", StringComparison.OrdinalIgnoreCase));
2613 | if (index != -1)
2614 | {
2615 | lines[index] = $"{key}={value}";
2616 | Log($"[UpdateOrAddSetting] Updated setting '{key}' to '{value}' at line {index + 1}.");
2617 | }
2618 | else
2619 | {
2620 | lines.Add($"{key}={value}");
2621 | Log($"[UpdateOrAddSetting] Added new setting '{key}' with value '{value}'.");
2622 | }
2623 | }
2624 |
2625 | public Dictionary ReadCurrentSettings()
2626 | {
2627 | var settings = new Dictionary(StringComparer.OrdinalIgnoreCase);
2628 | string configPath = GetConfigPath();
2629 | if (File.Exists(configPath))
2630 | {
2631 | try
2632 | {
2633 | var lines = File.ReadAllLines(configPath);
2634 | foreach (string line in lines)
2635 | {
2636 | if (string.IsNullOrWhiteSpace(line))
2637 | {
2638 | continue;
2639 | }
2640 |
2641 | var parts = line.Split(new[] { '=' }, 2);
2642 | if (parts.Length == 2)
2643 | {
2644 | string key = parts[0].Trim();
2645 | string value = parts[1].Trim();
2646 | if (!settings.ContainsKey(key))
2647 | {
2648 | settings.Add(key, value);
2649 | }
2650 | }
2651 | }
2652 | }
2653 | catch (Exception ex)
2654 | {
2655 | Log($"[ReadCurrentSettings] Exception reading settings: {ex.Message}");
2656 | }
2657 | }
2658 | else
2659 | {
2660 | Log($"[ReadCurrentSettings] Config file not found at path: {configPath}");
2661 | }
2662 | return settings;
2663 | }
2664 |
2665 | protected override void Dispose(bool disposing)
2666 | {
2667 | if (disposing)
2668 | {
2669 | // Unregister all hotkeys
2670 | UnregisterAllHotkeys();
2671 |
2672 | // Dispose of the playback cancellation token source
2673 | playbackCancellationTokenSource?.Dispose();
2674 |
2675 | // Dispose of the clipboard timer
2676 | clipboardTimer?.Dispose();
2677 |
2678 | // Dispose of the current WaveOutEvent if it's active
2679 | currentWaveOut?.Dispose();
2680 |
2681 | // Dispose of the NotifyIcon
2682 | trayIcon?.Dispose();
2683 | }
2684 | base.Dispose(disposing);
2685 | }
2686 | }
2687 | }
2688 |
--------------------------------------------------------------------------------