| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109 |
- """
- 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,
- )
|