""" File Loader — Load and decode WAV/MP3 audio files. Supports WAV via soundfile and MP3 via pydub (requires ffmpeg). All audio is returned as normalized float32 numpy arrays. """ import numpy as np import soundfile as sf from pathlib import Path from dataclasses import dataclass from typing import Optional @dataclass class AudioData: """Container for loaded audio data.""" samples: np.ndarray # float32, shape: (num_samples, channels) sample_rate: int channels: int duration: float # seconds file_path: Optional[str] = None format_name: Optional[str] = None @property def num_samples(self) -> int: return self.samples.shape[0] def to_mono(self) -> "AudioData": """Convert to mono by averaging channels.""" if self.channels == 1: return self mono = np.mean(self.samples, axis=1, keepdims=True) return AudioData( samples=mono, sample_rate=self.sample_rate, channels=1, duration=self.duration, file_path=self.file_path, format_name=self.format_name, ) def resample(self, target_rate: int) -> "AudioData": """Resample audio to a different sample rate using linear interpolation.""" if self.sample_rate == target_rate: return self ratio = target_rate / self.sample_rate new_length = int(self.num_samples * ratio) # Use scipy for high-quality resampling from scipy.signal import resample as scipy_resample resampled = scipy_resample(self.samples, new_length, axis=0).astype(np.float32) return AudioData( samples=resampled, sample_rate=target_rate, channels=self.channels, duration=new_length / target_rate, file_path=self.file_path, format_name=self.format_name, ) def get_audio_duration(file_path: str) -> float: """Get the duration of an audio file in seconds without fully loading it.""" try: info = sf.info(file_path) return info.duration except Exception: return 0.0 def load_audio(file_path: str) -> AudioData: """ Load an audio file using soundfile. Supported: WAV, FLAC, OGG, AIFF, MP3, OPUS. """ path = Path(file_path) if not path.exists(): raise FileNotFoundError(f"File not found: {file_path}") # soundfile natively supports MP3 in newer versions data, sample_rate = sf.read(file_path, dtype="float32", always_2d=True) return AudioData( samples=data, sample_rate=sample_rate, channels=data.shape[1], duration=len(data) / sample_rate, file_path=str(path.resolve()), format_name=path.suffix.upper().strip("."), ) def normalize_audio(audio: AudioData, target_peak: float = 0.95) -> AudioData: """Normalize audio to a target peak level.""" peak = np.max(np.abs(audio.samples)) if peak == 0: return audio scale = target_peak / peak return AudioData( samples=(audio.samples * scale).astype(np.float32), sample_rate=audio.sample_rate, channels=audio.channels, duration=audio.duration, file_path=audio.file_path, format_name=audio.format_name, )