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.
PA verb
#!/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()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment