add cancel operation keyboard shortcut
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -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.
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user