From e6b89736859858830bf65ca2bd63e0a5c05378aa Mon Sep 17 00:00:00 2001 From: Andrew Ward Date: Fri, 21 Mar 2025 19:19:31 +0000 Subject: [PATCH] add cancel operation keyboard shortcut --- .../hotkey_manager.cpython-312.pyc | Bin 35651 -> 38967 bytes utils/__pycache__/text_to_mic.cpython-312.pyc | Bin 62971 -> 67678 bytes utils/hotkey_manager.py | 55 +++++- utils/text_to_mic.py | 176 +++++++++++++++--- 4 files changed, 200 insertions(+), 31 deletions(-) diff --git a/utils/__pycache__/hotkey_manager.cpython-312.pyc b/utils/__pycache__/hotkey_manager.cpython-312.pyc index 2fe0ce515838b6e81cec67365b33a83a6b880adc..af38859aa4f974c88396d1a4392c8d7f18a5b787 100644 GIT binary patch delta 5820 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 aDiJ^Xag(-9Kr065BitbOF-3HV#PNSX3Ak7Q delta 3961 zcma)9d3aM*7Qg4cmwju~E#0?tNn4<$EJdnilR|4j1QDWGLSKq)6Ix!vVo4}|A8Hw4 zXy*>59Z{+!byP~BjRB-8pNIkz5CmCd?tN_$nfbn%{PBCgd(OG% z-gC~~@?{fu;|Es%iB6|R_UyH0G7*vdS!&=0L*r9qMZeAazVri%dS@T2(7#N7d9(Fv; z#rxQ~kdF=AbX7iYS_{@_J|Ek-?9zNbiL{dWWa2J`AH}DTRw|!LT4vHRlU5o(nolF& zbka^I-wg82Am2mJS_#%svyo+tm?nE)2PxudV*>|`A|KVo zzy@(dOuqtJ#gLSJ47`Io%^k2`{6pFpfP-RZ`f`9SaYCj7pc_xKoPlFvZq}JN@QJ2k z4Ztg+d77VvFT`7=x7Dx>-d0t0s#@UNYi5v3T|C01L&R8h%zlwHSQ< z1`FHqxfg1fb{Sr?ECq*5P^!J`V&vsi$SuR!8=?*22tC0dlADqayy8h3PRj{@6vhtr|Uyc&1xkRh-1 zs9!=aGed@e?$W6|n&0X?>NSi<<5hdq9!yb%?8eAHH<|7j5akm^_6yub{ zIM=HjFpl7O88znRchyXZi<+Z{A+K_^vWZ!5;w82MR-l#ae6#|{z~;LKo^Z#Pme+aY z#FzdaUy*pPR4R4ud~y~Jy5;|e+teYqc}70Wqr5ksf!^yc{HK?J6ybGyG)(vmyc)N{ z931t>HDv9N5zf7oFvN*}P%;B!8bJ$nf z`J}o*!~2pfu|g}^dBUIYtW61WdiV=#am}V-s}b`utf&-1Nef$E9FrK(+bY~u%k6`HTmg=^xoxfxVwgmJn18V#|8)|+ ztB1fEbXps>apt<2J=*w2@EgM$6+vzasA5iM4{xvE_ds8ENq0y_ld7c<$N6HF$)WK< zIZDmI_xtMMG(O;qYDnBNw{>oNUSIa4?(9h)j676ypr~i^oSy7Q{liA=F5gk!GwcDs zWt2aA#EF9O`yT2km>yK21oMxGODKh=sliATYVzru^=LRvkB#4Qf9w72;XSc9yUjSa-;~g2%IY>{9W~|n zO$l4Ht=i3czbR&mvQ@cR?KdTE(YNZ`<$IVm&r#DTzbU;vx!aW8st<;t)T|(j!jjwa zdW_~^0@CR>maZ$^P+4U(iGPuE>*N#k1Jqfka9o$w%4cXFx{ovc$gxwAsMxbS65a3o72>W^)f*IUQn*Fo8Uc$e5Q+N_M&jA;ZHF@P z#(Rq(0+FRE00o8uOM%0}{z5Q|yZ0w^%x%C&4w_-Ec=e#0)k?0ZFX3mvD-LBdHvw-u zw4J#IxU8cJjQFh%2RtsCJ8PKyv&76r&H-t22#JIUUl37!^c?$2_ng|Ys*2%@?e22k zzPzf!US74NvUZL5;U{K*MttUvJFrxOqYc-yfYu;A`v$#4$;c+4rM zQx(3)mAxsDh1c~aLL%PLn*hyXNAFaboIKM=zH#`8yxcTO<<$BliN|6P$g>U!K?;%; zdGQUFX0T|>PHbR0S&=&#$ATFr_osj#PwAfkIr!OrbA*MuJ4O4@lPw^#Rl4m0ez)J8 zPYZod@p|6OoM#V9<#?8t@#JO2%U=V63JI030k7K3^6JeBFN>Fa9f@y!rf#5@ zq=%i2hHg2FX2x}$%kFlQ5M0BnUA0Su$4Tf@g*`MYon$}@8{jJ3f+HYvJylid4nVGj z6{gd~l@JKPxZgGlNb@%7ChVdPwowNP;m(dw4s8j%_51sxCQ=8Sqt?a?c4|G1_R}-? z^7k{!!^p~Yy+#CUklz&7XUgw3Yewg7)XIiUk(E?(bZH` zL*SlG<@A*^5YIGmZ>?rv9KdMbKJvGrfvy|jGF=m|Qg)lNKT{?hAR}lqg?db+AgwgI zI|aJO2BMS#lrp}Rc3VJU9@VDOzE4uNkU}+8I0*~r%F8PpHkYfy?r`|dbC4iUB05)$ zFH{CC3UXV;T$;!D4#?m^S%l1VX}C`#hawp1E0sfZ{4nZEx=*C*Wi}l|sxs?(+V$3R oa(E2X&k`FVM=po&i~>@9IwcfF%rQby6XTOL%dQ~Gg=Av!Z{C1Rr2qf` diff --git a/utils/hotkey_manager.py b/utils/hotkey_manager.py index 357bdc5..c16ff35 100644 --- a/utils/hotkey_manager.py +++ b/utils/hotkey_manager.py @@ -40,6 +40,12 @@ class HotkeyManager: lambda: self.hotkey_play_last_audio_trigger() )) + # Add cancel operation hotkey + self.hotkeys.append(keyboard.add_hotkey( + self.format_shortcut(hotkey_settings["cancel_operation"]), + lambda: self.hotkey_cancel_operation_trigger() + )) + return True except Exception as e: print(f"Error registering hotkeys: {e}") @@ -181,6 +187,48 @@ class HotkeyManager: else: self.app.stop_recording(auto_play=True) + def hotkey_cancel_operation_trigger(self): + """Cancel current operation (recording or playback).""" + print("Cancel operation hotkey triggered") + + # Play feedback sound first + self.app.play_sound('assets/pop.wav') + + # For recording cancellation + if self.app.recording: + print("Canceling recording operation") + # Schedule the stop_recording call on the main thread to avoid threading issues + self.app.after(100, lambda: self._safe_cancel_recording()) + return + + # For playback cancellation + if hasattr(self.app, 'is_playing') and self.app.is_playing: + print("Canceling playback operation") + # Schedule the stop_playback call on the main thread to avoid threading issues + self.app.after(100, lambda: self._safe_cancel_playback()) + return + + print("No active operation to cancel") + + def _safe_cancel_recording(self): + """Safely cancel recording on the main thread.""" + try: + if self.app.recording: + self.app.stop_recording(cancel_save=True) + self.app.recording = False + print("Recording cancelled successfully") + except Exception as e: + print(f"Error canceling recording: {e}") + + def _safe_cancel_playback(self): + """Safely cancel playback on the main thread.""" + try: + if hasattr(self.app, 'stop_playback') and callable(self.app.stop_playback): + self.app.stop_playback() + print("Playback cancelled successfully") + except Exception as e: + print(f"Error canceling playback: {e}") + @staticmethod def hotkey_settings_dialog(app): """Show the hotkey settings dialog with interactive key capture.""" @@ -712,7 +760,8 @@ class HotkeyManager: default_shortcuts = { "record_start_stop": ["ctrl", "shift", "0"], "stop_recording": ["ctrl", "shift", "9"], - "play_last_audio": ["ctrl", "shift", "8"] + "play_last_audio": ["ctrl", "shift", "8"], + "cancel_operation": ["ctrl", "shift", "1"] } # Update settings @@ -763,6 +812,7 @@ class HotkeyManager: record_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["record_start_stop"]) play_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["play_last_audio"]) stop_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["stop_recording"]) + cancel_shortcut = hotkey_manager.format_shortcut(settings["hotkeys"]["cancel_operation"]) instructions = f"""Available Hotkeys: @@ -775,6 +825,9 @@ class HotkeyManager: 3. {stop_shortcut} - Stop Recording Immediately stops any active recording. +4. {cancel_shortcut} - Cancel Operation + Cancels the current operation (recording or playback) without saving or processing. + These hotkeys work globally across your system, even when the app is minimized. You can customize these hotkeys in Settings → Hotkey Settings. diff --git a/utils/text_to_mic.py b/utils/text_to_mic.py index d429c08..4118fb4 100644 --- a/utils/text_to_mic.py +++ b/utils/text_to_mic.py @@ -7,6 +7,7 @@ import wave import webbrowser import json import sys +import time from pystray import Icon as icon, MenuItem as item, Menu as menu from PIL import Image, ImageDraw, ImageTk @@ -684,14 +685,30 @@ Please also make sure you read the Terms of use and licence statement before usi return resampled_file_path def play_audio_multiplexed(self, file_paths, device_indices): - - p = pyaudio.PyAudio() - streams = [] + """Play audio files to multiple devices with better cancellation handling.""" + # Stop any existing playback first + if hasattr(self, 'is_playing') and self.is_playing: + self.stop_playback() + # Add a small delay to ensure previous resources are cleaned up + time.sleep(0.2) + + # Make p and streams accessible for stop_playback + try: + self.current_playback_p = pyaudio.PyAudio() + self.current_playback_streams = [] + self.is_playing = True + except Exception as e: + print(f"Error initializing PyAudio: {e}") + messagebox.showerror("Audio Error", f"Failed to initialize audio system: {e}") + return try: # Open all files and start all streams for file_path, device_index in zip(file_paths, device_indices): - + if not self.is_playing: + print("Playback canceled during initialization") + break + try: # Ensure the file_path is a string when opening the file wf = wave.open(str(file_path), 'rb') @@ -703,7 +720,6 @@ Please also make sure you read the Terms of use and licence statement before usi continue try: - # Ensure output audio sample rate matches that of the selected device device_info = self.get_device_info(device_index) sample_rate = int(device_info['defaultSampleRate']) # Fetch default sample rate from device info @@ -718,46 +734,139 @@ Please also make sure you read the Terms of use and licence statement before usi # Make the audio file sample rate match the device output sample rate # if there is a mismatch (prevents playback speed issues or crashes) if sample_rate != wf_frame_rate: - #if mismatch, make a new resampled version that matches the output device + # If mismatch, make a new resampled version that matches the output device resampled_file_path = self.resample_audio(str(file_path), sample_rate) - #update the playback file to the new resampled file + # Update the playback file to the new resampled file file_path = resampled_file_path - #re-open the new file for processing + # Re-open the new file for processing + wf.close() # Close the original file first wf = wave.open(str(file_path), 'rb') - #Create a stream from our file - stream = p.open(format=p.get_format_from_width(wf.getsampwidth()), - channels=wf.getnchannels(), - rate=sample_rate, - output=True, - output_device_index=int(device_index)) + # Create a stream from our file + stream = self.current_playback_p.open( + format=self.current_playback_p.get_format_from_width(wf.getsampwidth()), + channels=wf.getnchannels(), + rate=sample_rate, + output=True, + output_device_index=int(device_index) + ) except Exception as e: messagebox.showerror("Stream Creation Error", f"Failed to create audio stream for device index {device_index}: {str(e)}") wf.close() continue - streams.append((stream, wf)) + self.current_playback_streams.append((stream, wf)) - # Play interleaved - active_streams = len(streams) - while active_streams > 0: - for stream, wf in streams: + # Play interleaved using a more robust approach + self._play_audio_streams() + + except Exception as e: + print(f"Playback setup error: {e}") + messagebox.showerror("Playback Error", f"Error setting up playback: {e}") + self.stop_playback() + + def _play_audio_streams(self): + """Handle the actual audio playback in chunks, with better error handling.""" + if not hasattr(self, 'current_playback_streams') or not self.current_playback_streams: + print("No streams to play") + self.stop_playback() + return + + try: + finished_streams = [] + + # Process one chunk from each stream + for stream, wf in self.current_playback_streams: + if not self.is_playing: + print("Playback canceled during streaming") + break + + try: data = wf.readframes(1024) if data: stream.write(data) else: - stream.stop_stream() - stream.close() - wf.close() - streams.remove((stream, wf)) - active_streams -= 1 - + # Mark this stream as finished + finished_streams.append((stream, wf)) + except Exception as e: + print(f"Error during stream playback: {e}") + # Add to finished streams if there's an error + finished_streams.append((stream, wf)) + + # Remove finished streams + for stream_pair in finished_streams: + try: + stream, wf = stream_pair + stream.stop_stream() + stream.close() + wf.close() + self.current_playback_streams.remove(stream_pair) + except Exception as e: + print(f"Error cleaning up finished stream: {e}") + + # If we still have streams and playback is active, schedule the next chunk + if self.current_playback_streams and self.is_playing: + self.after(1, self._play_audio_streams) # Schedule next chunk processing + else: + # All done or canceled, clean up + self.stop_playback() + except Exception as e: - messagebox.showerror("Playback Error", f"Error during multiplexed playback: {e}") - finally: - p.terminate() - + print(f"Error in _play_audio_streams: {e}") + self.stop_playback() + + def stop_playback(self): + """Stop any active audio playback.""" + print("Attempting to stop playback") + + # Set flag first to exit any playback loops + self.is_playing = False + + try: + # Close any active streams + if hasattr(self, 'current_playback_streams'): + # Make a copy of the list to safely iterate while potentially modifying + streams_to_close = list(self.current_playback_streams) + + for stream, wf in streams_to_close: + try: + # Check if stream exists and is active before attempting to stop it + if stream and stream.is_active(): + stream.stop_stream() + if stream: + stream.close() + if wf: + wf.close() + except Exception as e: + print(f"Error closing stream: {e}") + + # Clear the list after processing all streams + self.current_playback_streams = [] + + # Terminate PyAudio instance - do this last and carefully + if hasattr(self, 'current_playback_p') and self.current_playback_p: + try: + # Add a small delay to ensure streams are properly closed before terminating + self.after(100, self._complete_playback_termination) + except Exception as e: + print(f"Error scheduling PyAudio termination: {e}") + + except Exception as e: + print(f"Error in stop_playback: {e}") + # Don't reraise - we want to prevent crashes + + def _complete_playback_termination(self): + """Complete the termination of PyAudio in a separate step to avoid crashes.""" + try: + if hasattr(self, 'current_playback_p') and self.current_playback_p: + self.current_playback_p.terminate() + self.current_playback_p = None + print("PyAudio terminated successfully") + except Exception as e: + print(f"Error terminating PyAudio: {e}") + # Still clear the reference even if termination fails + self.current_playback_p = None def play_last_audio(self): @@ -1045,6 +1154,12 @@ Please also make sure you read the Terms of use and licence statement before usi try: with open(settings_file, "r") as f: settings = json.load(f) + + # Check if the new cancel_operation hotkey exists + if "hotkeys" in settings and "cancel_operation" not in settings["hotkeys"]: + settings["hotkeys"]["cancel_operation"] = ["ctrl", "shift", "1"] + self.save_settings_to_JSON(settings) + except FileNotFoundError: # Default settings settings = { @@ -1057,7 +1172,8 @@ Please also make sure you read the Terms of use and licence statement before usi "hotkeys": { "record_start_stop": ["ctrl", "shift", "0"], "stop_recording": ["ctrl", "shift", "9"], - "play_last_audio": ["ctrl", "shift", "8"] + "play_last_audio": ["ctrl", "shift", "8"], + "cancel_operation": ["ctrl", "shift", "1"] } } self.save_settings_to_JSON(settings)