Created
August 27, 2025 02:47
-
-
Save twobob/d5f09c92d2e358c73e6fcdd7c68e7069 to your computer and use it in GitHub Desktop.
PA verb
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 characters
| #!/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