Skip to content

Instantly share code, notes, and snippets.

@twobob
Created August 27, 2025 02:47
Show Gist options
  • Save twobob/d5f09c92d2e358c73e6fcdd7c68e7069 to your computer and use it in GitHub Desktop.
Save twobob/d5f09c92d2e358c73e6fcdd7c68e7069 to your computer and use it in GitHub Desktop.

Revisions

  1. twobob created this gist Aug 27, 2025.
    562 changes: 562 additions & 0 deletions PAverb.py
    Original 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()