Created
August 27, 2025 02:47
-
-
Save twobob/d5f09c92d2e358c73e6fcdd7c68e7069 to your computer and use it in GitHub Desktop.
Revisions
-
twobob created this gist
Aug 27, 2025 .There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode charactersOriginal file line number Diff line number Diff line change @@ -0,0 +1,562 @@ #!/usr/bin/env python3 """ Public Address Effect Processor Adds vintage public address system effects to WAV files. Janky AF """ import tkinter as tk from tkinter import ttk, filedialog, messagebox import numpy as np import scipy.signal import scipy.io.wavfile import sounddevice as sd import json import os from pathlib import Path class PublicAddressProcessor: def __init__(self): self.root = tk.Tk() self.root.title("Public Address Effect Processor") self.root.geometry("800x750") self.original_audio = None self.processed_audio = None self.sample_rate = 44100 self.current_file = None self.is_playing = False self.stream = None self.playback_position = 0 self.settings = { 'reverb_time': 1.2, 'reverb_amount': 0.3, 'low_cut': 300, 'high_cut': 3500, 'slap_back_delay': 120, 'slap_back_amount': 0.25, 'echo_delay': 250, 'echo_amount': 0.15, 'echo_feedback': 0.3, 'speaker_saturation': 0.4, 'clipping_threshold': 0.7, 'overdrive_gain': 1.8, 'mix_percentage': 85, 'master_volume': 0.8 } self.setup_gui() def setup_gui(self): main_frame = ttk.Frame(self.root, padding="10") main_frame.grid(row=0, column=0, sticky=(tk.W, tk.E, tk.N, tk.S)) self.root.columnconfigure(0, weight=1) self.root.rowconfigure(0, weight=1) file_frame = ttk.LabelFrame(main_frame, text="Audio File", padding="5") file_frame.grid(row=0, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=(0, 10)) self.file_label = ttk.Label(file_frame, text="No file selected") self.file_label.grid(row=0, column=0, padx=(0, 10), sticky=tk.W) ttk.Button(file_frame, text="Browse", command=self.browse_file).grid(row=0, column=1, sticky=tk.E) self.status_label = ttk.Label(file_frame, text="Ready", foreground="green") self.status_label.grid(row=1, column=0, columnspan=2, pady=(5, 0), sticky=tk.W) self.setup_reverb_controls(main_frame, row=1) self.setup_filter_controls(main_frame, row=2) self.setup_delay_controls(main_frame, row=3) self.setup_distortion_controls(main_frame, row=4) control_frame = ttk.LabelFrame(main_frame, text="Preview & Export", padding="5") control_frame.grid(row=5, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10) self.preview_button = ttk.Button(control_frame, text="Preview", command=self.preview_audio) self.preview_button.grid(row=0, column=0, padx=5) ttk.Button(control_frame, text="Stop", command=self.stop_audio).grid(row=0, column=1, padx=5) ttk.Button(control_frame, text="Export", command=self.export_audio).grid(row=0, column=2, padx=5) settings_frame = ttk.LabelFrame(main_frame, text="Settings", padding="5") settings_frame.grid(row=6, column=0, columnspan=2, sticky=(tk.W, tk.E), pady=10) ttk.Button(settings_frame, text="Save Settings", command=self.save_settings).grid(row=0, column=0, padx=5) ttk.Button(settings_frame, text="Load Settings", command=self.load_settings).grid(row=0, column=1, padx=5) ttk.Button(settings_frame, text="Reset to Default", command=self.reset_settings).grid(row=0, column=2, padx=5) def setup_reverb_controls(self, parent, row): frame = ttk.LabelFrame(parent, text="Reverb", padding="5") frame.grid(row=row, column=0, sticky="nsew", padx=(0, 5), pady=5) self.reverb_time_var = tk.DoubleVar(value=self.settings['reverb_time']) self.reverb_time_scale = ttk.Scale(frame, from_=0.1, to=3.0, variable=self.reverb_time_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.reverb_time_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.reverb_amount_var = tk.DoubleVar(value=self.settings['reverb_amount']) self.reverb_amount_scale = ttk.Scale(frame, from_=0.0, to=1.0, variable=self.reverb_amount_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.reverb_amount_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame, text="Time:").grid(row=0, column=0, sticky=tk.W) self.reverb_time_scale.grid(row=0, column=1, sticky="ew") self.reverb_time_label = ttk.Label(frame, text=f"{self.reverb_time_var.get():.1f}") self.reverb_time_label.grid(row=0, column=2) ttk.Label(frame, text="Amount:").grid(row=1, column=0, sticky=tk.W) self.reverb_amount_scale.grid(row=1, column=1, sticky="ew") self.reverb_amount_label = ttk.Label(frame, text=f"{self.reverb_amount_var.get():.2f}") self.reverb_amount_label.grid(row=1, column=2) frame.columnconfigure(1, weight=1) def setup_filter_controls(self, parent, row): frame = ttk.LabelFrame(parent, text="Frequency Filter", padding="5") frame.grid(row=row, column=1, sticky="nsew", padx=(5, 0), pady=5) self.low_cut_var = tk.IntVar(value=self.settings['low_cut']) self.low_cut_scale = ttk.Scale(frame, from_=50, to=800, variable=self.low_cut_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.low_cut_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.high_cut_var = tk.IntVar(value=self.settings['high_cut']) self.high_cut_scale = ttk.Scale(frame, from_=1000, to=8000, variable=self.high_cut_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.high_cut_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame, text="Low Cut:").grid(row=0, column=0, sticky=tk.W) self.low_cut_scale.grid(row=0, column=1, sticky="ew") self.low_cut_label = ttk.Label(frame, text=str(self.low_cut_var.get())) self.low_cut_label.grid(row=0, column=2) ttk.Label(frame, text="High Cut:").grid(row=1, column=0, sticky=tk.W) self.high_cut_scale.grid(row=1, column=1, sticky="ew") self.high_cut_label = ttk.Label(frame, text=str(self.high_cut_var.get())) self.high_cut_label.grid(row=1, column=2) frame.columnconfigure(1, weight=1) def setup_delay_controls(self, parent, row): frame1 = ttk.LabelFrame(parent, text="Slap Back", padding="5") frame1.grid(row=row, column=0, sticky="nsew", padx=(0, 5), pady=5) self.slap_delay_var = tk.IntVar(value=self.settings['slap_back_delay']) self.slap_delay_scale = ttk.Scale(frame1, from_=50, to=300, variable=self.slap_delay_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.slap_delay_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.slap_amount_var = tk.DoubleVar(value=self.settings['slap_back_amount']) self.slap_amount_scale = ttk.Scale(frame1, from_=0.0, to=1.0, variable=self.slap_amount_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.slap_amount_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame1, text="Delay:").grid(row=0, column=0, sticky=tk.W) self.slap_delay_scale.grid(row=0, column=1, sticky="ew") self.slap_delay_label = ttk.Label(frame1, text=str(self.slap_delay_var.get())) self.slap_delay_label.grid(row=0, column=2) ttk.Label(frame1, text="Amount:").grid(row=1, column=0, sticky=tk.W) self.slap_amount_scale.grid(row=1, column=1, sticky="ew") self.slap_amount_label = ttk.Label(frame1, text=f"{self.slap_amount_var.get():.2f}") self.slap_amount_label.grid(row=1, column=2) frame1.columnconfigure(1, weight=1) frame2 = ttk.LabelFrame(parent, text="Echo", padding="5") frame2.grid(row=row, column=1, sticky="nsew", padx=(5, 0), pady=5) self.echo_delay_var = tk.IntVar(value=self.settings['echo_delay']) self.echo_delay_scale = ttk.Scale(frame2, from_=100, to=500, variable=self.echo_delay_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.echo_delay_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.echo_amount_var = tk.DoubleVar(value=self.settings['echo_amount']) self.echo_amount_scale = ttk.Scale(frame2, from_=0.0, to=0.5, variable=self.echo_amount_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.echo_amount_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.echo_feedback_var = tk.DoubleVar(value=self.settings['echo_feedback']) self.echo_feedback_scale = ttk.Scale(frame2, from_=0.0, to=0.8, variable=self.echo_feedback_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.echo_feedback_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame2, text="Delay:").grid(row=0, column=0, sticky=tk.W) self.echo_delay_scale.grid(row=0, column=1, sticky="ew") self.echo_delay_label = ttk.Label(frame2, text=str(self.echo_delay_var.get())) self.echo_delay_label.grid(row=0, column=2) ttk.Label(frame2, text="Amount:").grid(row=1, column=0, sticky=tk.W) self.echo_amount_scale.grid(row=1, column=1, sticky="ew") self.echo_amount_label = ttk.Label(frame2, text=f"{self.echo_amount_var.get():.2f}") self.echo_amount_label.grid(row=1, column=2) ttk.Label(frame2, text="Feedback:").grid(row=2, column=0, sticky=tk.W) self.echo_feedback_scale.grid(row=2, column=1, sticky="ew") self.echo_feedback_label = ttk.Label(frame2, text=f"{self.echo_feedback_var.get():.2f}") self.echo_feedback_label.grid(row=2, column=2) frame2.columnconfigure(1, weight=1) def setup_distortion_controls(self, parent, row): frame1 = ttk.LabelFrame(parent, text="Speaker Distortion", padding="5") frame1.grid(row=row, column=0, sticky="nsew", padx=(0, 5), pady=5) self.saturation_var = tk.DoubleVar(value=self.settings['speaker_saturation']) self.saturation_scale = ttk.Scale(frame1, from_=0.0, to=1.0, variable=self.saturation_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.saturation_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.overdrive_var = tk.DoubleVar(value=self.settings['overdrive_gain']) self.overdrive_scale = ttk.Scale(frame1, from_=1.0, to=3.0, variable=self.overdrive_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.overdrive_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame1, text="Saturation:").grid(row=0, column=0, sticky=tk.W) self.saturation_scale.grid(row=0, column=1, sticky="ew") self.saturation_label = ttk.Label(frame1, text=f"{self.saturation_var.get():.2f}") self.saturation_label.grid(row=0, column=2) ttk.Label(frame1, text="Overdrive:").grid(row=1, column=0, sticky=tk.W) self.overdrive_scale.grid(row=1, column=1, sticky="ew") self.overdrive_label = ttk.Label(frame1, text=f"{self.overdrive_var.get():.1f}") self.overdrive_label.grid(row=1, column=2) frame1.columnconfigure(1, weight=1) frame2 = ttk.LabelFrame(parent, text="Output", padding="5") frame2.grid(row=row, column=1, sticky="nsew", padx=(5, 0), pady=5) self.clipping_var = tk.DoubleVar(value=self.settings['clipping_threshold']) self.clipping_scale = ttk.Scale(frame2, from_=0.3, to=1.0, variable=self.clipping_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.clipping_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.mix_var = tk.IntVar(value=self.settings['mix_percentage']) self.mix_scale = ttk.Scale(frame2, from_=0, to=100, variable=self.mix_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.mix_scale.bind("<ButtonRelease-1>", self.on_slider_release) self.volume_var = tk.DoubleVar(value=self.settings['master_volume']) self.volume_scale = ttk.Scale(frame2, from_=0.0, to=1.0, variable=self.volume_var, orient=tk.HORIZONTAL, command=self._update_slider_labels) self.volume_scale.bind("<ButtonRelease-1>", self.on_slider_release) ttk.Label(frame2, text="Clipping:").grid(row=0, column=0, sticky=tk.W) self.clipping_scale.grid(row=0, column=1, sticky="ew") self.clipping_label = ttk.Label(frame2, text=f"{self.clipping_var.get():.2f}") self.clipping_label.grid(row=0, column=2) ttk.Label(frame2, text="Mix %:").grid(row=1, column=0, sticky=tk.W) self.mix_scale.grid(row=1, column=1, sticky="ew") self.mix_label = ttk.Label(frame2, text=str(self.mix_var.get())) self.mix_label.grid(row=1, column=2) ttk.Label(frame2, text="Volume:").grid(row=2, column=0, sticky=tk.W) self.volume_scale.grid(row=2, column=1, sticky="ew") self.volume_label = ttk.Label(frame2, text=f"{self.volume_var.get():.2f}") self.volume_label.grid(row=2, column=2) frame2.columnconfigure(1, weight=1) def browse_file(self): self.stop_audio() file_path = filedialog.askopenfilename( title="Select WAV file", filetypes=[("WAV files", "*.wav"), ("All files", "*.*")] ) if file_path: self.load_audio_file(file_path) def load_audio_file(self, file_path): try: self.status_label.config(text="Loading...", foreground="orange") self.root.update_idletasks() self.sample_rate, audio_data = scipy.io.wavfile.read(file_path) if audio_data.dtype == np.int16: audio_data = audio_data.astype(np.float32) / 32768.0 elif audio_data.dtype == np.int32: audio_data = audio_data.astype(np.float32) / 2147483648.0 elif audio_data.dtype == np.uint8: audio_data = (audio_data.astype(np.float32) - 128) / 128.0 else: audio_data = audio_data.astype(np.float32) if len(audio_data.shape) > 1: audio_data = np.mean(audio_data, axis=1) self.original_audio = audio_data self.current_file = file_path self.file_label.config(text=os.path.basename(file_path)) self.process_audio() self.status_label.config(text=f"Loaded: {len(audio_data)/self.sample_rate:.1f}s", foreground="green") except Exception as e: self.status_label.config(text="Error loading file", foreground="red") messagebox.showerror("Error", f"Failed to load audio file: {str(e)}") def on_slider_release(self, event): """Called when a slider is released. Triggers audio processing.""" if self.original_audio is not None: self.process_audio() def _update_slider_labels(self, *args): """Updates the numeric labels next to sliders during movement.""" self.reverb_time_label.config(text=f"{self.reverb_time_var.get():.1f}") self.reverb_amount_label.config(text=f"{self.reverb_amount_var.get():.2f}") self.low_cut_label.config(text=str(self.low_cut_var.get())) self.high_cut_label.config(text=str(self.high_cut_var.get())) self.slap_delay_label.config(text=str(self.slap_delay_var.get())) self.slap_amount_label.config(text=f"{self.slap_amount_var.get():.2f}") self.echo_delay_label.config(text=str(self.echo_delay_var.get())) self.echo_amount_label.config(text=f"{self.echo_amount_var.get():.2f}") self.echo_feedback_label.config(text=f"{self.echo_feedback_var.get():.2f}") self.saturation_label.config(text=f"{self.saturation_var.get():.2f}") self.overdrive_label.config(text=f"{self.overdrive_var.get():.1f}") self.clipping_label.config(text=f"{self.clipping_var.get():.2f}") self.mix_label.config(text=str(self.mix_var.get())) self.volume_label.config(text=f"{self.volume_var.get():.2f}") def process_audio(self): if self.original_audio is None: return try: self.status_label.config(text="Processing...", foreground="orange") self.root.update_idletasks() settings = { 'reverb_time': self.reverb_time_var.get(), 'reverb_amount': self.reverb_amount_var.get(), 'low_cut': self.low_cut_var.get(), 'high_cut': self.high_cut_var.get(), 'slap_back_delay': self.slap_delay_var.get(), 'slap_back_amount': self.slap_amount_var.get(), 'echo_delay': self.echo_delay_var.get(), 'echo_amount': self.echo_amount_var.get(), 'echo_feedback': self.echo_feedback_var.get(), 'speaker_saturation': self.saturation_var.get(), 'clipping_threshold': self.clipping_var.get(), 'overdrive_gain': self.overdrive_var.get(), 'mix_percentage': self.mix_var.get(), 'master_volume': self.volume_var.get() } audio = self.original_audio.copy() if settings['low_cut'] < settings['high_cut']: nyquist = self.sample_rate / 2 low = max(0.01, settings['low_cut'] / nyquist) high = min(0.99, settings['high_cut'] / nyquist) b, a = scipy.signal.butter(4, [low, high], btype='band') audio = scipy.signal.filtfilt(b, a, audio) if settings['overdrive_gain'] > 1.0: audio = audio * settings['overdrive_gain'] if settings['speaker_saturation'] > 0: audio = np.tanh(audio * (settings['speaker_saturation'] * 2 + 0.1)) audio = np.clip(audio, -settings['clipping_threshold'], settings['clipping_threshold']) if settings['slap_back_amount'] > 0 and settings['slap_back_delay'] > 0: delay_samples = int(settings['slap_back_delay'] * self.sample_rate / 1000) if delay_samples < len(audio): slap_back = np.zeros_like(audio) slap_back[delay_samples:] = audio[:-delay_samples] * settings['slap_back_amount'] audio = audio + slap_back if settings['echo_amount'] > 0 and settings['echo_delay'] > 0: delay_samples = int(settings['echo_delay'] * self.sample_rate / 1000) if delay_samples < len(audio): echo_audio = audio.copy() for _ in range(3): echo_delayed = np.zeros_like(echo_audio) echo_delayed[delay_samples:] = echo_audio[:-delay_samples] * settings['echo_amount'] audio = audio + echo_delayed echo_audio = echo_delayed * settings['echo_feedback'] if np.max(np.abs(echo_audio)) < 0.001: break if settings['reverb_amount'] > 0: reverb_length = int(min(settings['reverb_time'] * self.sample_rate, self.sample_rate * 2)) decay = np.exp(-np.linspace(0, 5, reverb_length)) impulse = decay * np.random.normal(0, 0.1, reverb_length) if len(impulse) > 8192: impulse = impulse[:8192] reverb = np.convolve(audio, impulse * settings['reverb_amount'], mode='same') audio = audio + reverb mix_ratio = settings['mix_percentage'] / 100.0 audio = self.original_audio * (1 - mix_ratio) + audio * mix_ratio audio = audio * settings['master_volume'] max_val = np.max(np.abs(audio)) if max_val > 0: audio = audio / max_val * 0.95 self.processed_audio = audio self.status_label.config(text="Ready", foreground="green") except Exception as e: self.status_label.config(text="Processing error", foreground="red") print(f"Processing error: {e}") def audio_callback(self, outdata, frames, time, status): """The heart of the continuous playback system.""" if status: print(status) chunk_end = self.playback_position + frames if self.processed_audio is None: outdata[:] = np.zeros((frames, 1), dtype=np.float32) return chunk = self.processed_audio[self.playback_position:chunk_end] if len(chunk) < frames: self.playback_position = 0 remaining_frames = frames - len(chunk) wrap_around_chunk = self.processed_audio[0:remaining_frames] full_chunk = np.concatenate((chunk, wrap_around_chunk)) self.playback_position = remaining_frames else: full_chunk = chunk self.playback_position = chunk_end outdata[:] = full_chunk.reshape(-1, 1) def preview_audio(self): if self.is_playing: self.stop_audio() return if self.processed_audio is None: messagebox.showwarning("Warning", "No audio to preview. Load a file first.") return try: self.playback_position = 0 self.stream = sd.OutputStream( samplerate=self.sample_rate, channels=1, callback=self.audio_callback ) self.stream.start() self.is_playing = True self.preview_button.config(text="Stop Preview") except Exception as e: messagebox.showerror("Error", f"Failed to play audio: {str(e)}") self.is_playing = False def stop_audio(self): if self.stream is not None: self.stream.stop() self.stream.close() self.stream = None self.is_playing = False self.preview_button.config(text="Preview") def export_audio(self): self.stop_audio() if self.processed_audio is None: messagebox.showwarning("Warning", "No audio to export. Load a file first.") return file_path = filedialog.asksaveasfilename( title="Save processed audio", defaultextension=".wav", filetypes=[("WAV files", "*.wav")] ) if file_path: try: audio_int16 = (self.processed_audio * 32767).astype(np.int16) scipy.io.wavfile.write(file_path, self.sample_rate, audio_int16) messagebox.showinfo("Success", f"Audio exported to {file_path}") except Exception as e: messagebox.showerror("Error", f"Failed to export audio: {str(e)}") def save_settings(self): settings = { 'reverb_time': self.reverb_time_var.get(), 'reverb_amount': self.reverb_amount_var.get(), 'low_cut': self.low_cut_var.get(), 'high_cut': self.high_cut_var.get(), 'slap_back_delay': self.slap_delay_var.get(), 'slap_back_amount': self.slap_amount_var.get(), 'echo_delay': self.echo_delay_var.get(), 'echo_amount': self.echo_amount_var.get(), 'echo_feedback': self.echo_feedback_var.get(), 'speaker_saturation': self.saturation_var.get(), 'clipping_threshold': self.clipping_var.get(), 'overdrive_gain': self.overdrive_var.get(), 'mix_percentage': self.mix_var.get(), 'master_volume': self.volume_var.get() } file_path = filedialog.asksaveasfilename( title="Save settings", defaultextension=".json", filetypes=[("JSON files", "*.json")] ) if file_path: try: with open(file_path, 'w') as f: json.dump(settings, f, indent=2) messagebox.showinfo("Success", f"Settings saved to {file_path}") except Exception as e: messagebox.showerror("Error", f"Failed to save settings: {str(e)}") def load_settings(self): file_path = filedialog.askopenfilename( title="Load settings", filetypes=[("JSON files", "*.json")] ) if file_path: try: with open(file_path, 'r') as f: settings = json.load(f) self.reverb_time_var.set(settings.get('reverb_time', 1.2)) self.reverb_amount_var.set(settings.get('reverb_amount', 0.3)) self.low_cut_var.set(settings.get('low_cut', 300)) self.high_cut_var.set(settings.get('high_cut', 3500)) self.slap_delay_var.set(settings.get('slap_back_delay', 120)) self.slap_amount_var.set(settings.get('slap_back_amount', 0.25)) self.echo_delay_var.set(settings.get('echo_delay', 250)) self.echo_amount_var.set(settings.get('echo_amount', 0.15)) self.echo_feedback_var.set(settings.get('echo_feedback', 0.3)) self.saturation_var.set(settings.get('speaker_saturation', 0.4)) self.clipping_var.set(settings.get('clipping_threshold', 0.7)) self.overdrive_var.set(settings.get('overdrive_gain', 1.8)) self.mix_var.set(settings.get('mix_percentage', 85)) self.volume_var.set(settings.get('master_volume', 0.8)) self._update_slider_labels() if self.original_audio is not None: self.process_audio() messagebox.showinfo("Success", f"Settings loaded from {file_path}") except Exception as e: messagebox.showerror("Error", f"Failed to load settings: {str(e)}") def reset_settings(self): self.reverb_time_var.set(self.settings['reverb_time']) self.reverb_amount_var.set(self.settings['reverb_amount']) self.low_cut_var.set(self.settings['low_cut']) self.high_cut_var.set(self.settings['high_cut']) self.slap_delay_var.set(self.settings['slap_back_delay']) self.slap_amount_var.set(self.settings['slap_back_amount']) self.echo_delay_var.set(self.settings['echo_delay']) self.echo_amount_var.set(self.settings['echo_amount']) self.echo_feedback_var.set(self.settings['echo_feedback']) self.saturation_var.set(self.settings['speaker_saturation']) self.clipping_var.set(self.settings['clipping_threshold']) self.overdrive_var.set(self.settings['overdrive_gain']) self.mix_var.set(self.settings['mix_percentage']) self.volume_var.set(self.settings['master_volume']) self._update_slider_labels() if self.original_audio is not None: self.process_audio() def run(self): self.root.protocol("WM_DELETE_WINDOW", self.on_closing) self.root.mainloop() def on_closing(self): """Handler for window close event.""" self.stop_audio() self.root.destroy() if __name__ == "__main__": try: import sounddevice as sd import scipy.signal import scipy.io.wavfile except ImportError as e: print(f"Missing required dependency: {e.name}") print("Please install the required packages by running:") print("pip install sounddevice scipy numpy") exit(1) app = PublicAddressProcessor() app.run()