├── 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 | [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](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 | --------------------------------------------------------------------------------