file_loader.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """
  2. File Loader — Load and decode WAV/MP3 audio files.
  3. Supports WAV via soundfile and MP3 via pydub (requires ffmpeg).
  4. All audio is returned as normalized float32 numpy arrays.
  5. """
  6. import numpy as np
  7. import soundfile as sf
  8. from pathlib import Path
  9. from dataclasses import dataclass
  10. from typing import Optional
  11. @dataclass
  12. class AudioData:
  13. """Container for loaded audio data."""
  14. samples: np.ndarray # float32, shape: (num_samples, channels)
  15. sample_rate: int
  16. channels: int
  17. duration: float # seconds
  18. file_path: Optional[str] = None
  19. format_name: Optional[str] = None
  20. @property
  21. def num_samples(self) -> int:
  22. return self.samples.shape[0]
  23. def to_mono(self) -> "AudioData":
  24. """Convert to mono by averaging channels."""
  25. if self.channels == 1:
  26. return self
  27. mono = np.mean(self.samples, axis=1, keepdims=True)
  28. return AudioData(
  29. samples=mono,
  30. sample_rate=self.sample_rate,
  31. channels=1,
  32. duration=self.duration,
  33. file_path=self.file_path,
  34. format_name=self.format_name,
  35. )
  36. def resample(self, target_rate: int) -> "AudioData":
  37. """Resample audio to a different sample rate using linear interpolation."""
  38. if self.sample_rate == target_rate:
  39. return self
  40. ratio = target_rate / self.sample_rate
  41. new_length = int(self.num_samples * ratio)
  42. # Use scipy for high-quality resampling
  43. from scipy.signal import resample as scipy_resample
  44. resampled = scipy_resample(self.samples, new_length, axis=0).astype(np.float32)
  45. return AudioData(
  46. samples=resampled,
  47. sample_rate=target_rate,
  48. channels=self.channels,
  49. duration=new_length / target_rate,
  50. file_path=self.file_path,
  51. format_name=self.format_name,
  52. )
  53. def get_audio_duration(file_path: str) -> float:
  54. """Get the duration of an audio file in seconds without fully loading it."""
  55. try:
  56. info = sf.info(file_path)
  57. return info.duration
  58. except Exception:
  59. return 0.0
  60. def load_audio(file_path: str) -> AudioData:
  61. """
  62. Load an audio file using soundfile.
  63. Supported: WAV, FLAC, OGG, AIFF, MP3, OPUS.
  64. """
  65. path = Path(file_path)
  66. if not path.exists():
  67. raise FileNotFoundError(f"File not found: {file_path}")
  68. # soundfile natively supports MP3 in newer versions
  69. data, sample_rate = sf.read(file_path, dtype="float32", always_2d=True)
  70. return AudioData(
  71. samples=data,
  72. sample_rate=sample_rate,
  73. channels=data.shape[1],
  74. duration=len(data) / sample_rate,
  75. file_path=str(path.resolve()),
  76. format_name=path.suffix.upper().strip("."),
  77. )
  78. def normalize_audio(audio: AudioData, target_peak: float = 0.95) -> AudioData:
  79. """Normalize audio to a target peak level."""
  80. peak = np.max(np.abs(audio.samples))
  81. if peak == 0:
  82. return audio
  83. scale = target_peak / peak
  84. return AudioData(
  85. samples=(audio.samples * scale).astype(np.float32),
  86. sample_rate=audio.sample_rate,
  87. channels=audio.channels,
  88. duration=audio.duration,
  89. file_path=audio.file_path,
  90. format_name=audio.format_name,
  91. )