bearbeite dieses skript und berücksichtige dabei die folgenden vorschläge zur verbesserung.



    Multithreading-Optimierung:
        Der QThreadPool nutzt automatisch die Anzahl der verfügbaren CPU-Kerne, um die Verarbeitungseffizienz zu maximieren.
        Bei Bedarf kann die maximale Anzahl der Threads angepasst werden, um die Hardware noch effektiver zu nutzen.

    Speicherverwaltung:
        Nach der Verarbeitung einer Datei werden die Audiodaten gelöscht und der Garbage Collector aufgerufen, um Speicher freizugeben.
        Dies ist besonders wichtig beim Verarbeiten großer Audiodateien.

    Erweiterte Fehlernachrichten:
        Weitere spezifische Fehlermeldungen können implementiert werden, um dem Benutzer genauere Informationen über aufgetretene Probleme zu geben.

    Zusätzliche Audioverarbeitungseffekte:
        Zur Erweiterung der Funktionalität könnten zusätzliche Effekte wie Equalizer, Reverb, Verzerrung etc. integriert werden.

    Automatische Updates und Versionskontrolle:
        Implementieren Sie Mechanismen zur automatischen Aktualisierung der Anwendung und zur Versionskontrolle, um sicherzustellen, dass Benutzer stets die neueste Version nutzen.

    User Feedback und Fortschritt pro Datei:
        Detailliertere Fortschrittsanzeige für jede einzelne Datei könnte eingeführt werden, um dem Benutzer ein genaueres Bild des Fortschritts zu geben.

    Einstellungen exportieren/importieren erweitern:
        Zusätzliche Optionen wie das Exportieren von Favoriten oder spezifischen Presets könnten die Benutzerfreundlichkeit weiter verbessern.



import sys
import os
import queue
import time
import json
import shutil
import gc
import logging
from pathlib import Path
from dataclasses import dataclass
from typing import Dict, List, Tuple, Optional

import numpy as np
import librosa
import soundfile as sf
import noisereduce as nr
import pyloudnorm as pyln
from scipy.signal import butter, lfilter

import sounddevice as sd

from PyQt6.QtWidgets import (
    QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
    QPushButton, QLabel, QFileDialog, QProgressBar, QComboBox,
    QCheckBox, QTextEdit, QGroupBox, QGridLayout, QSizePolicy, QAction,
    QMenu, QMenuBar, QMessageBox
)
from PyQt6.QtCore import (
    Qt, QThreadPool, QRunnable, pyqtSignal, pyqtSlot, QObject,
    QTimer, QPropertyAnimation, QRect, QSettings
)
from PyQt6.QtGui import (
    QDragEnterEvent, QDropEvent, QIcon, QFont, QDoubleValidator, QPalette
)
import pyqtgraph as pg
import psutil

# --- Logging Configuration ---
logging.basicConfig(
    level=logging.DEBUG,
    format='%(asctime)s - %(levelname)s - %(message)s',
    handlers=[
        logging.FileHandler('audio_processor.log'),
        logging.StreamHandler(sys.stdout)
    ]
)
logger = logging.getLogger(__name__)

# --- Constants ---
SAMPLE_RATES = [22050, 44100, 48000]
CHUNK_SIZE = 32768
MAX_FILE_SIZE = 500 * 1024 * 1024  # 500MB
SUPPORTED_FORMATS = {'.wav', '.mp3', '.ogg', '.flac', '.m4a'}  # Erweiterte Formate

# --- Data Classes ---
@dataclass
class ProcessingOptions:
    """
    Data class for processing options with validation.
    """
    sample_rate: int
    noise_reduce: bool
    noise_reduction: float
    normalize: bool
    normalize_db: float
    remove_silence: bool
    silence_threshold: float
    bandpass_filter: bool
    bandpass_low: float
    bandpass_high: float
    apply_fade: bool
    fade_length: float
    chunk_size: int = CHUNK_SIZE

    def __post_init__(self):
        """Validate options after initialization."""
        if self.sample_rate not in SAMPLE_RATES:
            raise ValueError(f"Invalid sample rate. Must be one of {SAMPLE_RATES}")
        if not 0 <= self.noise_reduction <= 1:
            raise ValueError("Noise reduction must be between 0 and 1")
        if not -100 <= self.normalize_db <= 0:
            raise ValueError("Normalize dB must be between -100 and 0")
        if not 0 <= self.silence_threshold <= 1:
            raise ValueError("Silence threshold must be between 0 and 1")
        if self.bandpass_low < 0 or self.bandpass_high > 20000:
            raise ValueError("Invalid bandpass filter frequencies")
        if self.bandpass_low >= self.bandpass_high:
            raise ValueError("Bandpass low frequency must be less than high frequency")


# --- Error Classes ---
class AudioProcessingError(Exception):
    """Base class for audio processing errors."""
    pass

class AudioLoadError(AudioProcessingError):
    """Raised when there's an error loading an audio file."""
    pass

class AudioSaveError(AudioProcessingError):
    """Raised when there's an error saving an audio file."""
    pass


# --- Signals Class ---
class WorkerSignals(QObject):
    """
    Defines the signals available from a running worker thread.

    Supported signals are:
    - progress: int, str, str for progress percentage, message, and ETA.
    - result: np.ndarray, int for processed audio data and sample rate.
    - error: str for error messages.
    - finished: bool, str for success status and message.
    """
    progress = pyqtSignal(int, str, str)  # percent, message, eta
    result = pyqtSignal(np.ndarray, int)  # processed audio, sample rate
    error = pyqtSignal(str)
    finished = pyqtSignal(bool, str)  # success, message


# --- Audio Processing Worker ---
class AudioProcessingWorker(QRunnable):
    """
    Worker thread for processing a single audio file.

    Inherits from QRunnable to handle worker thread setup, signals, and wrap-up.
    """
    def __init__(self, input_path: str, output_path: str, options: ProcessingOptions):
        super().__init__()
        self.input_path = input_path
        self.output_path = output_path
        self.options = options
        self.signals = WorkerSignals()

    @pyqtSlot()
    def run(self):
        """Execute the audio processing."""
        try:
            logger.debug(f"Started processing {self.input_path}")
            # Resource check
            resource_monitor = ResourceMonitor()
            resources_ok, msg = resource_monitor.check_resources()
            if not resources_ok:
                raise AudioProcessingError(msg)

            # Load audio
            y, sr = self._load_audio(self.input_path)

            # Resample if needed
            if sr != self.options.sample_rate:
                y = librosa.resample(y, orig_sr=sr, target_sr=self.options.sample_rate)
                sr = self.options.sample_rate
                logger.debug(f"Resampled {self.input_path} to {sr} Hz")

            # Apply processing steps
            y = self._apply_processing_chain(y, sr)

            # Save processed audio
            self._save_audio(y, sr, self.output_path)
            logger.debug(f"Finished processing {self.input_path}, saved to {self.output_path}")

            # Emit result for visualization
            self.signals.result.emit(y, sr)

            self.signals.progress.emit(100, "Processing Complete", "0:00")
            self.signals.finished.emit(True, "Processing completed successfully.")

        except AudioProcessingError as e:
            logger.error(f"Processing error: {str(e)}")
            self.signals.error.emit(str(e))
            self.signals.finished.emit(False, f"Processing failed: {str(e)}")

        except Exception as e:
            logger.exception("Unexpected error during processing")
            self.signals.error.emit(f"Unexpected error: {str(e)}")
            self.signals.finished.emit(False, f"Processing failed: {str(e)}")

        finally:
            del y
            gc.collect()

    def _load_audio(self, path: str) -> Tuple[np.ndarray, int]:
        """Loads an audio file and ensures it is mono."""
        try:
            y, sr = sf.read(path)
            # Ensure mono
            if y.ndim > 1:
                y = librosa.to_mono(y.T)
            return y, sr
        except Exception as e:
            raise AudioLoadError(f"Error loading {path}: {str(e)}")

    def _apply_processing_chain(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Applies the selected audio processing steps."""
        steps = []
        if self.options.noise_reduce:
            y = self._apply_noise_reduction(y, sr)
            steps.append("Noise Reduction")
        if self.options.normalize:
            y = self._normalize_audio(y, sr)
            steps.append("Normalization")
        if self.options.remove_silence:
            y = self._remove_silence(y, sr)
            steps.append("Silence Removal")
        if self.options.bandpass_filter:
            y = self._apply_bandpass_filter(y, sr)
            steps.append("Bandpass Filtering")
        if self.options.apply_fade:
            y = self._apply_fade_in_out(y, sr)
            steps.append("Fade In/Out")
        logger.debug(f"Applied steps: {', '.join(steps) if steps else 'None'}")
        return y

    def _apply_noise_reduction(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Applies noise reduction to the audio data."""
        try:
            # Estimate noise from the first 0.5 seconds
            noise_clip = y[:int(0.5 * sr)]
            reduced_noise = nr.reduce_noise(
                y=y,
                sr=sr,
                y_noise=noise_clip,
                prop_decrease=self.options.noise_reduction,
                stationary=True
            )
            logger.debug("Noise reduction applied")
            return reduced_noise
        except Exception as e:
            raise AudioProcessingError(f"Noise reduction failed: {str(e)}")

    def _normalize_audio(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Normalizes audio to the target dB level."""
        try:
            meter = pyln.Meter(sr)
            loudness = meter.integrated_loudness(y)
            normalized_audio = pyln.normalize.loudness(y, loudness, self.options.normalize_db)
            logger.debug("Audio normalization applied")
            return normalized_audio
        except Exception as e:
            raise AudioProcessingError(f"Normalization failed: {str(e)}")

    def _remove_silence(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Removes silence from the audio based on the silence threshold."""
        try:
            intervals = librosa.effects.split(
                y,
                top_db=self.options.silence_threshold * 60,
                frame_length=2048,
                hop_length=512
            )
            if intervals.size > 0:
                y_trimmed = np.concatenate([y[start:end] for start, end in intervals])
                logger.debug("Silence removal applied")
                return y_trimmed
            else:
                logger.debug("No silence detected to remove")
                return y
        except Exception as e:
            raise AudioProcessingError(f"Silence removal failed: {str(e)}")

    def _apply_bandpass_filter(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Applies a bandpass filter to the audio."""
        try:
            nyquist = sr / 2
            low = self.options.bandpass_low / nyquist
            high = self.options.bandpass_high / nyquist
            b, a = butter(4, [low, high], btype='band')
            filtered_audio = lfilter(b, a, y)
            logger.debug("Bandpass filter applied")
            return filtered_audio
        except Exception as e:
            raise AudioProcessingError(f"Bandpass filter failed: {str(e)}")

    def _apply_fade_in_out(self, y: np.ndarray, sr: int) -> np.ndarray:
        """Applies fade in and fade out to the audio."""
        try:
            fade_length = int(self.options.fade_length * sr)
            if fade_length <= 0:
                logger.debug("Fade length is zero or negative; skipping fade.")
                return y
            fade_in = np.linspace(0, 1, fade_length)
            fade_out = np.linspace(1, 0, fade_length)
            y[:fade_length] *= fade_in
            y[-fade_length:] *= fade_out
            logger.debug("Fade in/out applied")
            return y
        except Exception as e:
            raise AudioProcessingError(f"Fade in/out failed: {str(e)}")

    def _save_audio(self, y: np.ndarray, sr: int, output_path: str):
        """Saves the processed audio to the specified path."""
        try:
            sf.write(output_path, y, sr, subtype='PCM_24', format='WAV')
            logger.debug(f"Audio saved to {output_path}")
        except Exception as e:
            raise AudioSaveError(f"Error saving audio: {str(e)}")


# --- Resource Monitor ---
class ResourceMonitor:
    """
    Monitors system resources and provides status.

    Attributes:
        warning_threshold (int): CPU and memory usage percentage for warnings.
        critical_threshold (int): CPU and memory usage percentage for critical alerts.
    """
    def __init__(self):
        self.warning_threshold = 85  # CPU and memory usage percentage
        self.critical_threshold = 95  # CPU and memory usage percentage

    def get_system_status(self) -> Dict[str, float]:
        """
        Retrieve system resource usage.

        Returns:
            Dict[str, float]: Current usage percentages for CPU, memory, and disk.
        """
        cpu_percent = psutil.cpu_percent(interval=0.1)
        memory = psutil.virtual_memory()
        disk = psutil.disk_usage('/')
        
        return {
            'cpu_percent': cpu_percent,
            'memory_percent': memory.percent,
            'disk_percent': disk.percent
        }

    def check_resources(self) -> Tuple[bool, str]:
        """
        Checks if system resources are within acceptable limits.

        Returns:
            Tuple[bool, str]: Status and message indicating resource health.
        """
        status = self.get_system_status()
        if status['cpu_percent'] > self.critical_threshold or status['memory_percent'] > self.critical_threshold:
            return False, "System resources critically low"
        elif status['cpu_percent'] > self.warning_threshold or status['memory_percent'] > self.warning_threshold:
            return False, "System resources running high"
        return True, "Resources OK"


# --- Main Application Window ---
class MainWindow(QMainWindow):
    """
    Main application window for the Advanced Audio Processor.

    This class handles the user interface and coordinates the audio processing tasks.
    It provides options for various audio processing techniques and displays progress, logs, and visualizations.
    """
    def __init__(self):
        super().__init__()
        self.setWindowTitle("Advanced Audio Processor")
        self.setMinimumSize(1200, 900)  # Increased height for visualizations and console
        self.setStyleSheet(StyleSheet.MAIN)
        self.setWindowIcon(QIcon.fromTheme("audio-x-generic"))  # Fallback icon

        # Initialize settings
        self.settings = QSettings("AudioProcessor", "Settings")

        # Initialize UI components
        self.init_ui()
        self.load_settings()

        # Initialize Thread Pool
        self.threadpool = QThreadPool()
        logger.debug(f"Multithreading with maximum {self.threadpool.maxThreadCount()} threads.")

        # Data for preview
        self.current_processed_audio = None
        self.current_sample_rate = None

    def init_ui(self):
        """Initialize the user interface."""
        central_widget = QWidget()
        self.setCentralWidget(central_widget)
        main_layout = QVBoxLayout(central_widget)

        # Menu Bar for Settings Export/Import and About
        self.init_menu_bar()

        # Drop area
        self.drop_area = DropArea()
        self.drop_area.files_dropped.connect(self.handle_dropped_files)
        main_layout.addWidget(self.drop_area)

        # Visualizations
        self.init_visualizations(main_layout)

        # Processing settings
        self.init_processing_settings(main_layout)

        # Progress section
        self.init_progress_section(main_layout)

        # Buttons
        self.init_buttons(main_layout)

        # Console
        self.init_console(main_layout)

    def init_menu_bar(self):
        """Initialize the menu bar with export/import settings and About."""
        menu_bar = self.menuBar()
        file_menu = menu_bar.addMenu("File")
        
        import_settings_action = QAction("Import Settings", self)
        import_settings_action.triggered.connect(self.import_settings)
        file_menu.addAction(import_settings_action)
        
        export_settings_action = QAction("Export Settings", self)
        export_settings_action.triggered.connect(self.export_settings)
        file_menu.addAction(export_settings_action)
        
        exit_action = QAction("Exit", self)
        exit_action.triggered.connect(self.close)
        file_menu.addAction(exit_action)

        help_menu = menu_bar.addMenu("Help")
        about_action = QAction("About", self)
        about_action.triggered.connect(self.show_about)
        help_menu.addAction(about_action)

    def init_visualizations(self, parent_layout):
        """Initialize the visualizations for Waveform and Spectrogram."""
        visualization_group = QGroupBox("Audio Visualizations")
        visualization_layout = QHBoxLayout()
        visualization_group.setLayout(visualization_layout)

        # Waveform
        self.waveform_plot = pg.PlotWidget(title="Waveform")
        self.waveform_plot.setYRange(-1, 1)
        self.waveform_curve = self.waveform_plot.plot(pen=pg.mkPen('c', width=1))
        self.waveform_plot.setLabel('left', 'Amplitude')
        self.waveform_plot.setLabel('bottom', 'Time', units='s')
        visualization_layout.addWidget(self.waveform_plot)

        # Spektrogramm
        self.spectrogram_plot = pg.PlotWidget(title="Spectrogram")
        self.spectrogram_plot.setLogMode(False, False)
        self.spectrogram_plot.setLabel('left', 'Frequency', units='Hz')
        self.spectrogram_plot.setLabel('bottom', 'Time', units='s')
        self.img = pg.ImageItem()
        self.spectrogram_plot.addItem(self.img)
        visualization_layout.addWidget(self.spectrogram_plot)

        parent_layout.addWidget(visualization_group)

    def init_processing_settings(self, parent_layout):
        """Initialize the processing settings group."""
        settings_group = QGroupBox("Processing Settings")
        settings_layout = QGridLayout()
        settings_group.setLayout(settings_layout)

        # Sample rate selection
        self.sample_rate_combo = QComboBox()
        self.sample_rate_combo.addItems([str(sr) for sr in SAMPLE_RATES])
        self.sample_rate_combo.setToolTip("Select the sample rate for audio processing")
        settings_layout.addWidget(QLabel("Sample Rate (Hz):"), 0, 0)
        settings_layout.addWidget(self.sample_rate_combo, 0, 1)

        # Noise reduction
        self.noise_reduce_check = QCheckBox("Noise Reduction")
        self.noise_reduce_check.setToolTip("Enable noise reduction processing")
        self.noise_reduction_spin = QDoubleSpinBox()
        self.noise_reduction_spin.setRange(0, 1)
        self.noise_reduction_spin.setSingleStep(0.1)
        self.noise_reduction_spin.setValue(0.5)
        self.noise_reduction_spin.setToolTip("Set the strength of noise reduction (0-1)")
        settings_layout.addWidget(self.noise_reduce_check, 1, 0)
        settings_layout.addWidget(self.noise_reduction_spin, 1, 1)

        # Normalization
        self.normalize_check = QCheckBox("Normalize Audio")
        self.normalize_check.setToolTip("Enable audio normalization")
        self.normalize_spin = QDoubleSpinBox()
        self.normalize_spin.setRange(-100, 0)
        self.normalize_spin.setValue(-14)
        self.normalize_spin.setToolTip("Set the target dB level for normalization")
        settings_layout.addWidget(self.normalize_check, 2, 0)
        settings_layout.addWidget(self.normalize_spin, 2, 1)

        # Silence removal
        self.silence_check = QCheckBox("Remove Silence")
        self.silence_check.setToolTip("Enable silence removal")
        self.silence_threshold_spin = QDoubleSpinBox()
        self.silence_threshold_spin.setRange(0, 1)
        self.silence_threshold_spin.setSingleStep(0.1)
        self.silence_threshold_spin.setValue(0.3)
        self.silence_threshold_spin.setToolTip("Set the silence threshold (0-1)")
        settings_layout.addWidget(self.silence_check, 3, 0)
        settings_layout.addWidget(self.silence_threshold_spin, 3, 1)

        # Bandpass filter
        self.bandpass_check = QCheckBox("Bandpass Filter")
        self.bandpass_check.setToolTip("Enable bandpass filter")
        bandpass_layout = QHBoxLayout()
        self.bandpass_low_spin = QSpinBox()
        self.bandpass_low_spin.setRange(20, 20000)
        self.bandpass_low_spin.setValue(50)
        self.bandpass_low_spin.setToolTip("Set the low frequency for bandpass filter")
        self.bandpass_high_spin = QSpinBox()
        self.bandpass_high_spin.setRange(20, 20000)
        self.bandpass_high_spin.setValue(15000)
        self.bandpass_high_spin.setToolTip("Set the high frequency for bandpass filter")
        bandpass_layout.addWidget(QLabel("Low (Hz):"))
        bandpass_layout.addWidget(self.bandpass_low_spin)
        bandpass_layout.addWidget(QLabel(" - High (Hz):"))
        bandpass_layout.addWidget(self.bandpass_high_spin)
        settings_layout.addWidget(self.bandpass_check, 4, 0)
        settings_layout.addLayout(bandpass_layout, 4, 1)

        # Fade In/Out
        self.fade_check = QCheckBox("Apply Fade In/Out")
        self.fade_check.setToolTip("Enable fade in and fade out")
        self.fade_length_spin = QDoubleSpinBox()
        self.fade_length_spin.setRange(0, 5)
        self.fade_length_spin.setSingleStep(0.01)
        self.fade_length_spin.setValue(0.01)
        self.fade_length_spin.setToolTip("Set the fade length in seconds")
        settings_layout.addWidget(self.fade_check, 5, 0)
        settings_layout.addWidget(self.fade_length_spin, 5, 1)

        parent_layout.addWidget(settings_group)

    def init_progress_section(self, parent_layout):
        """Initialize the progress section."""
        progress_group = QGroupBox("Progress")
        progress_layout = QVBoxLayout()
        progress_group.setLayout(progress_layout)

        self.progress_bar = DetailedProgressBar()
        self.status_label = QLabel("Ready")
        self.status_label.setToolTip("Displays current processing status and estimated time remaining")

        progress_layout.addWidget(self.progress_bar)
        progress_layout.addWidget(self.status_label)

        parent_layout.addWidget(progress_group)

    def init_buttons(self, parent_layout):
        """Initialize the buttons for processing, preview, and settings export/import."""
        button_layout = QHBoxLayout()

        self.process_button = QPushButton("Process Files")
        self.process_button.clicked.connect(self.start_processing)
        self.process_button.setToolTip("Start processing the selected audio files")

        self.cancel_button = QPushButton("Cancel")
        self.cancel_button.clicked.connect(self.cancel_processing)
        self.cancel_button.setEnabled(False)
        self.cancel_button.setToolTip("Cancel the ongoing processing")

        self.preview_button = QPushButton("Preview Audio")
        self.preview_button.clicked.connect(self.preview_audio)
        self.preview_button.setEnabled(False)
        self.preview_button.setToolTip("Play the most recently processed audio file")

        self.export_settings_button = QPushButton("Export Settings")
        self.export_settings_button.clicked.connect(self.export_settings)
        self.export_settings_button.setToolTip("Export current processing settings to a JSON file")

        self.import_settings_button = QPushButton("Import Settings")
        self.import_settings_button.clicked.connect(self.import_settings)
        self.import_settings_button.setToolTip("Import processing settings from a JSON file")

        button_layout.addWidget(self.process_button)
        button_layout.addWidget(self.cancel_button)
        button_layout.addWidget(self.preview_button)
        button_layout.addWidget(self.export_settings_button)
        button_layout.addWidget(self.import_settings_button)

        parent_layout.addLayout(button_layout)

    def init_console(self, parent_layout):
        """Initialize the console for logs and error messages."""
        self.console = QTextEdit()
        self.console.setReadOnly(True)
        self.console.setMaximumHeight(0)  # Start minimized
        self.console.setStyleSheet("""
            QTextEdit {
                background-color: #2c3e50;
                color: #ecf0f1;
                font-family: Consolas;
                font-size: 10px;
                border: none;
            }
        """)

        # Animation for opening/closing
        self.console_animation = QPropertyAnimation(self.console, b"maximumHeight")
        self.console_animation.setDuration(300)

        # Toggle button
        self.toggle_console_button = QPushButton("\u21e9")  # Down arrow
        self.toggle_console_button.setFixedSize(20, 20)
        self.toggle_console_button.setStyleSheet("""
            QPushButton {
                background-color: #34495e;
                color: white;
                border: none;
            }
            QPushButton:hover {
                background-color: #3c5973;
            }
        """)
        self.toggle_console_button.setToolTip("Toggle console visibility")
        self.toggle_console_button.clicked.connect(self.toggle_console)

        # Layout for console and button
        console_layout = QHBoxLayout()
        console_layout.addWidget(self.console)
        console_layout.addWidget(self.toggle_console_button)

        console_widget = QWidget()
        console_widget.setLayout(console_layout)

        parent_layout.addWidget(console_widget)

    def toggle_console(self):
        """Toggle the visibility of the console with animation."""
        if self.console.maximumHeight() == 0:
            self.console_animation.setStartValue(0)
            self.console_animation.setEndValue(150)  # Expanded height
            self.toggle_console_button.setText("\u21e7")  # Up arrow
        else:
            self.console_animation.setStartValue(self.console.maximumHeight())
            self.console_animation.setEndValue(0)
            self.toggle_console_button.setText("\u21e9")  # Down arrow
        self.console_animation.start()

    def log_message(self, message: str, msg_type: str = "INFO"):
        """
        Log messages to the console with different styles.

        Args:
            message (str): The message to display.
            msg_type (str): Type of message ('INFO', 'WARNING', 'ERROR').
        """
        color = {
            "INFO": "#ecf0f1",
            "WARNING": "#f1c40f",
            "ERROR": "#e74c3c"
        }.get(msg_type, "#ecf0f1")
        formatted_message = f'<span style="color:{color};">{message}</span>'
        self.console.append(formatted_message)
        # Auto-scroll to bottom
        scrollbar = self.console.verticalScrollBar()
        scrollbar.setValue(scrollbar.maximum())

    @pyqtSlot(str)
    def show_error(self, error_message):
        """Display error message and log it."""
        QMessageBox.critical(self, "Error", error_message)
        self.log_message(f"ERROR: {error_message}", "ERROR")

    @pyqtSlot(bool, str)
    def processing_finished(self, success, message):
        """Handle completion of processing."""
        self.reset_ui()
        if success:
            QMessageBox.information(self, "Processing Complete", message)
            self.log_message(f"INFO: {message}", "INFO")
        else:
            QMessageBox.warning(self, "Processing Failed", message)
            self.log_message(f"WARNING: {message}", "WARNING")

    def display_audio_visualization(self, y: np.ndarray, sr: int):
        """Display waveform and spectrogram of the processed audio."""
        self.current_processed_audio = y
        self.current_sample_rate = sr

        # Update Waveform
        self.waveform_curve.setData(y)
        duration = librosa.get_duration(y=y, sr=sr)
        times = np.linspace(0, duration, num=len(y))
        self.waveform_plot.setXRange(0, duration)
        self.waveform_plot.setLabel('bottom', 'Time', units='s')

        # Update Spectrogram
        D = librosa.stft(y)
        S_db = librosa.amplitude_to_db(np.abs(D), ref=np.max)
        self.img.setImage(S_db, autoLevels=False)
        freqs = librosa.fft_frequencies(sr=sr)
        times_spec = librosa.frames_to_time(np.arange(S_db.shape[1]), sr=sr)
        self.spectrogram_plot.setLimits(xMin=0, yMin=0, yMax=sr / 2)
        self.spectrogram_plot.setRange(xRange=(0, duration), yRange=(0, sr / 2))
        self.spectrogram_plot.show()

        # Enable preview button
        self.preview_button.setEnabled(True)

    def start_processing(self):
        """Start the audio processing."""
        if not self.validate_inputs():
            return

        options = self.get_processing_options()
        files_to_process = []

        # Collect all files in the queue
        while not self.threadpool.activeThreadCount() and not self.processing_queue.empty():
            input_path, output_path = self.processing_queue.get()
            files_to_process.append((input_path, output_path))

        if not files_to_process:
            QMessageBox.information(self, "No Files", "No audio files to process.")
            return

        total_files = len(files_to_process)
        self.progress_bar.setMaximum(total_files)
        self.progress_bar.setValue(0)
        self.status_label.setText("Starting processing...")

        # Start processing each file
        for idx, (input_path, output_path) in enumerate(files_to_process, 1):
            worker = AudioProcessingWorker(input_path, output_path, options)
            worker.signals.progress.connect(self.update_progress_bar)
            worker.signals.result.connect(self.display_audio_visualization)
            worker.signals.error.connect(self.show_error)
            worker.signals.finished.connect(self.processing_finished)
            self.threadpool.start(worker)
            self.cancel_button.setEnabled(True)
            self.log_message(f"INFO: Started processing {input_path}", "INFO")

    def cancel_processing(self):
        """Cancel all ongoing processing."""
        if self.threadpool.activeThreadCount() > 0:
            self.threadpool.clear()
            self.log_message("INFO: Processing cancelled by user.", "INFO")
            QMessageBox.information(self, "Cancelled", "Processing has been cancelled.")
        self.reset_ui()

    def preview_audio(self):
        """Play the most recently processed audio if available."""
        if self.current_processed_audio is not None and self.current_sample_rate is not None:
            try:
                sd.stop()  # Stop any existing playback
                sd.play(self.current_processed_audio, self.current_sample_rate)
                self.log_message("INFO: Playing audio...", "INFO")
            except Exception as e:
                QMessageBox.critical(self, "Playback Error", f"Failed to play audio: {str(e)}")
                self.log_message(f"ERROR: Playback failed - {str(e)}", "ERROR")
        else:
            QMessageBox.information(self, "No Audio", "No processed audio available for preview.")
            self.log_message("WARNING: No audio to preview.", "WARNING")

    def export_settings(self):
        """Export current settings to a JSON file."""
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getSaveFileName(
            self,
            "Export Settings",
            "",
            "JSON Files (*.json)",
            options=options
        )
        if file_path:
            settings = {
                "sample_rate": self.sample_rate_combo.currentText(),
                "noise_reduce": self.noise_reduce_check.isChecked(),
                "noise_reduction": self.noise_reduction_spin.value(),
                "normalize": self.normalize_check.isChecked(),
                "normalize_db": self.normalize_spin.value(),
                "remove_silence": self.silence_check.isChecked(),
                "silence_threshold": self.silence_threshold_spin.value(),
                "bandpass_filter": self.bandpass_check.isChecked(),
                "bandpass_low": self.bandpass_low_spin.value(),
                "bandpass_high": self.bandpass_high_spin.value(),
                "apply_fade": self.fade_check.isChecked(),
                "fade_length": self.fade_length_spin.value()
            }
            try:
                with open(file_path, 'w') as f:
                    json.dump(settings, f, indent=4)
                self.log_message(f"INFO: Settings exported to {file_path}", "INFO")
            except Exception as e:
                QMessageBox.critical(self, "Export Error", f"Failed to export settings: {str(e)}")
                self.log_message(f"ERROR: Failed to export settings - {str(e)}", "ERROR")

    def import_settings(self):
        """Import settings from a JSON file."""
        options = QFileDialog.Options()
        file_path, _ = QFileDialog.getOpenFileName(
            self,
            "Import Settings",
            "",
            "JSON Files (*.json)",
            options=options
        )
        if file_path:
            try:
                with open(file_path, 'r') as f:
                    settings = json.load(f)
                # Apply settings with validation
                self.sample_rate_combo.setCurrentText(settings.get("sample_rate", "44100"))
                self.noise_reduce_check.setChecked(settings.get("noise_reduce", False))
                self.noise_reduction_spin.setValue(settings.get("noise_reduction", 0.5))
                self.normalize_check.setChecked(settings.get("normalize", False))
                self.normalize_spin.setValue(settings.get("normalize_db", -14))
                
                self.remove_silence(settings.get("remove_silence", False))
                self.silence_check.setChecked(settings.get("remove_silence", False))
                self.silence_threshold_spin.setValue(settings.get("silence_threshold", 0.3))
                
                self.bandpass_check.setChecked(settings.get("bandpass_filter", False))
                self.bandpass_low_spin.setValue(settings.get("bandpass_low", 50))
                self.bandpass_high_spin.setValue(settings.get("bandpass_high", 15000))
                
                self.fade_check.setChecked(settings.get("apply_fade", False))
                self.fade_length_spin.setValue(settings.get("fade_length", 0.01))
                
                self.log_message(f"INFO: Settings imported from {file_path}", "INFO")
            except KeyError as e:
                QMessageBox.critical(self, "Import Error", f"Missing key in settings file: {e}")
                self.log_message(f"ERROR: Missing key in settings file - {e}", "ERROR")
            except ValueError as e:
                QMessageBox.critical(self, "Import Error", f"Invalid value in settings file: {e}")
                self.log_message(f"ERROR: Invalid value in settings file - {e}", "ERROR")
            except Exception as e:
                QMessageBox.critical(self, "Import Error", f"Failed to import settings: {str(e)}")
                self.log_message(f"ERROR: Failed to import settings - {str(e)}", "ERROR")

    def show_about(self):
        """Display the About dialog."""
        about_text = """
        <h2>Advanced Audio Processor</h2>
        <p>Version 1.2</p>
        <p>Developed as the ultimate tool for audio editing, specifically for preparing audio files containing voices and training RVC voice models.</p>
        <p>Features include noise reduction, normalization, silence removal, bandpass filtering, fade in/out, audio visualizations, settings import/export, batch processing, and more.</p>
        <p>&copy; 2023 Your Company</p>
        """
        QMessageBox.about(self, "About Advanced Audio Processor", about_text)

    def update_progress_bar(self, percent: int, message: str, eta: str):
        """Update the progress bar and status label."""
        self.progress_bar.setValue(percent)
        self.progress_bar.setText(message, eta)
        self.status_label.setText(f"{message} | ETA: {eta}")

    def reset_ui(self):
        """Reset the UI to its initial state."""
        self.process_button.setEnabled(True)
        self.cancel_button.setEnabled(False)
        self.preview_button.setEnabled(True if self.current_processed_audio is not None else False)
        self.progress_bar.setValue(0)
        self.progress_bar.setText("", "")
        self.status_label.setText("Ready")

    def handle_dropped_files(self, file_paths: List[str]):
        """
        Handle files dropped into the drop area.

        Only valid audio files are enqueued for processing.

        Args:
            file_paths (List[str]): List of file paths dropped by the user.
        """
        valid_files = []
        skipped_files = 0
        for file_path in file_paths:
            is_valid, msg = AudioValidator.validate_audio(file_path)
            if is_valid:
                valid_files.append(file_path)
            else:
                skipped_files += 1
                self.log_message(f"WARNING: {file_path} skipped - {msg}", "WARNING")
        if skipped_files > 0:
            QMessageBox.warning(
                self,
                "Invalid Files Skipped",
                f"{skipped_files} file(s) were skipped due to invalid format or other issues."
            )
        if valid_files:
            self.enqueue_files(valid_files)

    def enqueue_files(self, file_paths: List[str]):
        """
        Add valid files to the processing queue.

        Args:
            file_paths (List[str]): List of valid file paths to process.
        """
        for file_path in file_paths:
            output_path = self.generate_output_path(file_path)
            self.processing_queue.put((file_path, output_path))
            self.log_message(f"INFO: Enqueued {file_path} for processing.", "INFO")
        self.process_button.setEnabled(True)
        self.preview_button.setEnabled(False)

    def generate_output_path(self, input_path: str) -> str:
        """
        Generate the output path for a processed file.

        Args:
            input_path (str): Path to the input audio file.

        Returns:
            str: Path to save the processed audio file.
        """
        directory, filename = os.path.split(input_path)
        name, ext = os.path.splitext(filename)
        return os.path.join(directory, f"{name}_processed{ext}")

    def get_processing_options(self) -> ProcessingOptions:
        """
        Retrieve processing options from the UI.

        Returns:
            ProcessingOptions: The current processing options selected by the user.
        """
        return ProcessingOptions(
            sample_rate=int(self.sample_rate_combo.currentText()),
            noise_reduce=self.noise_reduce_check.isChecked(),
            noise_reduction=self.noise_reduction_spin.value(),
            normalize=self.normalize_check.isChecked(),
            normalize_db=self.normalize_spin.value(),
            remove_silence=self.silence_check.isChecked(),
            silence_threshold=self.silence_threshold_spin.value(),
            bandpass_filter=self.bandpass_check.isChecked(),
            bandpass_low=self.bandpass_low_spin.value(),
            bandpass_high=self.bandpass_high_spin.value(),
            apply_fade=self.fade_check.isChecked(),
            fade_length=self.fade_length_spin.value()
        )

    def load_settings(self):
        """Load application settings from persistent storage."""
        self.sample_rate_combo.setCurrentText(self.settings.value("sample_rate", "44100"))
        self.noise_reduce_check.setChecked(self.settings.value("noise_reduce", False, type=bool))
        self.noise_reduction_spin.setValue(self.settings.value("noise_reduction", 0.5, type=float))
        self.normalize_check.setChecked(self.settings.value("normalize", False, type=bool))
        self.normalize_spin.setValue(self.settings.value("normalize_db", -14, type=float))
        self.silence_check.setChecked(self.settings.value("remove_silence", False, type=bool))
        self.silence_threshold_spin.setValue(self.settings.value("silence_threshold", 0.3, type=float))
        self.bandpass_check.setChecked(self.settings.value("bandpass_filter", False, type=bool))
        self.bandpass_low_spin.setValue(self.settings.value("bandpass_low", 50, type=int))
        self.bandpass_high_spin.setValue(self.settings.value("bandpass_high", 15000, type=int))
        self.fade_check.setChecked(self.settings.value("apply_fade", False, type=bool))
        self.fade_length_spin.setValue(self.settings.value("fade_length", 0.01, type=float))

    def save_settings(self):
        """Save current application settings to persistent storage."""
        self.settings.setValue("sample_rate", self.sample_rate_combo.currentText())
        self.settings.setValue("noise_reduce", self.noise_reduce_check.isChecked())
        self.settings.setValue("noise_reduction", self.noise_reduction_spin.value())
        self.settings.setValue("normalize", self.normalize_check.isChecked())
        self.settings.setValue("normalize_db", self.normalize_spin.value())
        self.settings.setValue("remove_silence", self.silence_check.isChecked())
        self.settings.setValue("silence_threshold", self.silence_threshold_spin.value())
        self.settings.setValue("bandpass_filter", self.bandpass_check.isChecked())
        self.settings.setValue("bandpass_low", self.bandpass_low_spin.value())
        self.settings.setValue("bandpass_high", self.bandpass_high_spin.value())
        self.settings.setValue("apply_fade", self.fade_check.isChecked())
        self.settings.setValue("fade_length", self.fade_length_spin.value())

    def closeEvent(self, event):
        """Handle application close event by saving settings."""
        self.save_settings()
        event.accept()

    def validate_inputs(self) -> bool:
        """
        Validate user inputs before processing.

        Returns:
            bool: True if inputs are valid, False otherwise.
        """
        if self.bandpass_check.isChecked():
            low = self.bandpass_low_spin.value()
            high = self.bandpass_high_spin.value()
            if low >= high:
                QMessageBox.warning(self, "Invalid Input", "Bandpass low frequency must be less than high frequency.")
                self.log_message("WARNING: Bandpass low frequency >= high frequency.", "WARNING")
                return False
        return True


# --- Style Sheet ---
class StyleSheet:
    """Central style definitions."""
    MAIN = """
        QMainWindow {
            background-color: #f0f0f0;
        }
        QLabel {
            font-size: 12px;
            color: #2c3e50;
        }
        QPushButton {
            background-color: #3498db;
            color: white;
            border: none;
            padding: 8px 16px;
            border-radius: 4px;
            font-weight: bold;
            transition: background-color 0.3s, transform 0.2s;
        }
        QPushButton:hover {
            background-color: #2980b9;
            transform: scale(1.05);
        }
        QPushButton:pressed {
            transform: scale(0.95);
        }
        QPushButton:disabled {
            background-color: #bdc3c7;
        }
        QProgressBar {
            border: 2px solid #bdc3c7;
            border-radius: 5px;
            text-align: center;
            transition: border-color 0.3s;
        }
        QProgressBar::chunk {
            background-color: #2ecc71;
            transition: width 0.5s;
        }
        QTextEdit {
            background-color: #2c3e50;
            color: #ecf0f1;
            font-family: Consolas;
            font-size: 10px;
            border: none;
        }
    """


# --- Drop Area Widget ---
class DropArea(QLabel):
    """
    Enhanced drag-drop area with visual feedback.
    Emits a signal when files are dropped or selected via dialog.
    """
    files_dropped = pyqtSignal(list)

    def __init__(self):
        super().__init__()
        self.setAlignment(Qt.AlignmentFlag.AlignCenter)
        self.setMinimumSize(500, 250)
        self.setStyleSheet("""
            QLabel {
                border: 2px dashed #bdc3c7;
                border-radius: 8px;
                background-color: #ecf0f1;
                padding: 20px;
            }
            QLabel:hover {
                border-color: #3498db;
                background-color: #e8f6ff;
            }
        """)
        self.setText("<h3>Drag and Drop Audio Files Here<br>or Click to Select Files</h3>")
        self.setAcceptDrops(True)

    def dragEnterEvent(self, event: QDragEnterEvent):
        """Handle drag enter event."""
        if event.mimeData().hasUrls():
            event.acceptProposedAction()
            self.setStyleSheet("""
                QLabel {
                    border: 2px dashed #3498db;
                    border-radius: 8px;
                    background-color: #e8f6ff;
                    padding: 20px;
                }
            """)
    
    def dragLeaveEvent(self, event):
        """Handle drag leave event."""
        self.setStyleSheet("""
            QLabel {
                border: 2px dashed #bdc3c7;
                border-radius: 8px;
                background-color: #ecf0f1;
                padding: 20px;
            }
        """)

    def dropEvent(self, event: QDropEvent):
        """Handle drop event."""
        files = [url.toLocalFile() for url in event.mimeData().urls()]
        self.files_dropped.emit(files)
        self.setStyleSheet("""
            QLabel {
                border: 2px dashed #bdc3c7;
                border-radius: 8px;
                background-color: #ecf0f1;
                padding: 20px;
            }
        """)

    def mousePressEvent(self, event):
        """Handle clicks to open file dialog."""
        if event.button() == Qt.MouseButton.LeftButton:
            self.open_file_dialog()

    def open_file_dialog(self):
        """Open a file dialog to select audio files."""
        file_dialog = QFileDialog()
        file_dialog.setFileMode(QFileDialog.FileMode.ExistingFiles)
        file_dialog.setNameFilters(["Audio Files (*.wav *.mp3 *.ogg *.flac *.m4a)"])
        if file_dialog.exec():
            files = file_dialog.selectedFiles()
            self.files_dropped.emit(files)


# --- Detailed Progress Bar ---
class DetailedProgressBar(QProgressBar):
    """
    Customized ProgressBar to display detailed information including progress percentage,
    number of processed/total files, and estimated time remaining (ETA).
    """
    def __init__(self, parent=None):
        super().__init__(parent)
        self.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed)
        self.setTextVisible(True)
        self.setFormat("%p% - %v/%m files - %s")
        self.setText("")

    def setText(self, message: str, eta: Optional[str] = None):
        """Set the text of the progress bar with optional ETA."""
        if eta:
            self.setFormat(f"%p% - %v/%m files - {message} | ETA: {eta}")
        else:
            self.setFormat(f"%p% - %v/%m files - {message}")