base.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235
  1. # -*- coding: utf-8 -*-
  2. """
  3. Basis-Klassen für Audio-Decoder.
  4. Definiert die abstrakte Schnittstelle für alle Audio-Formate.
  5. """
  6. from abc import ABC, abstractmethod
  7. from dataclasses import dataclass
  8. from enum import Enum
  9. from pathlib import Path
  10. from typing import AsyncIterator
  11. class AudioFormat(Enum):
  12. """Unterstützte Audio-Formate."""
  13. MP3 = "mp3"
  14. WAV = "wav"
  15. OGG = "ogg"
  16. FLAC = "flac"
  17. AAC = "aac"
  18. M4A = "m4a"
  19. OPUS = "opus"
  20. WMA = "wma"
  21. AIFF = "aiff"
  22. UNKNOWN = "unknown"
  23. @classmethod
  24. def from_extension(cls, ext: str) -> "AudioFormat":
  25. """Bestimmt Format aus Dateiendung."""
  26. ext = ext.lower().lstrip(".")
  27. mapping = {
  28. "mp3": cls.MP3,
  29. "wav": cls.WAV,
  30. "wave": cls.WAV,
  31. "ogg": cls.OGG,
  32. "oga": cls.OGG,
  33. "flac": cls.FLAC,
  34. "aac": cls.AAC,
  35. "m4a": cls.M4A,
  36. "mp4": cls.M4A,
  37. "opus": cls.OPUS,
  38. "wma": cls.WMA,
  39. "aiff": cls.AIFF,
  40. "aif": cls.AIFF,
  41. }
  42. return mapping.get(ext, cls.UNKNOWN)
  43. @classmethod
  44. def from_mime(cls, mime: str) -> "AudioFormat":
  45. """Bestimmt Format aus MIME-Type."""
  46. mime = mime.lower().split(";")[0].strip()
  47. mapping = {
  48. "audio/mpeg": cls.MP3,
  49. "audio/mp3": cls.MP3,
  50. "audio/wav": cls.WAV,
  51. "audio/x-wav": cls.WAV,
  52. "audio/wave": cls.WAV,
  53. "audio/ogg": cls.OGG,
  54. "audio/vorbis": cls.OGG,
  55. "application/ogg": cls.OGG,
  56. "audio/flac": cls.FLAC,
  57. "audio/x-flac": cls.FLAC,
  58. "audio/aac": cls.AAC,
  59. "audio/mp4": cls.M4A,
  60. "audio/x-m4a": cls.M4A,
  61. "audio/opus": cls.OPUS,
  62. "audio/x-ms-wma": cls.WMA,
  63. "audio/aiff": cls.AIFF,
  64. "audio/x-aiff": cls.AIFF,
  65. }
  66. return mapping.get(mime, cls.UNKNOWN)
  67. class DecoderError(Exception):
  68. """Fehler beim Decodieren."""
  69. pass
  70. @dataclass
  71. class AudioInfo:
  72. """Informationen über eine Audio-Datei."""
  73. format: AudioFormat
  74. duration_ms: int = 0
  75. sample_rate: int = 44100
  76. channels: int = 2
  77. bit_depth: int = 16
  78. bit_rate: int = 0
  79. file_size: int = 0
  80. @property
  81. def duration_seconds(self) -> float:
  82. """Dauer in Sekunden."""
  83. return self.duration_ms / 1000.0
  84. @property
  85. def is_stereo(self) -> bool:
  86. """Ist Stereo."""
  87. return self.channels == 2
  88. @dataclass
  89. class DecodedAudio:
  90. """Dekodierte Audio-Daten."""
  91. # PCM-Daten (16-bit signed, little-endian)
  92. pcm_data: bytes
  93. # Audio-Eigenschaften
  94. sample_rate: int = 44100
  95. channels: int = 2
  96. bit_depth: int = 16
  97. @property
  98. def duration_ms(self) -> int:
  99. """Dauer in Millisekunden."""
  100. bytes_per_sample = self.bit_depth // 8
  101. samples = len(self.pcm_data) // (bytes_per_sample * self.channels)
  102. return int((samples / self.sample_rate) * 1000)
  103. @property
  104. def sample_count(self) -> int:
  105. """Anzahl der Samples."""
  106. bytes_per_sample = self.bit_depth // 8
  107. return len(self.pcm_data) // (bytes_per_sample * self.channels)
  108. class AudioDecoder(ABC):
  109. """
  110. Abstrakte Basis-Klasse für Audio-Decoder.
  111. Dekodiert komprimierte Audio-Formate zu PCM.
  112. """
  113. # Format das dieser Decoder unterstützt
  114. FORMAT: AudioFormat = AudioFormat.UNKNOWN
  115. # Unterstützte Dateiendungen
  116. EXTENSIONS: set[str] = set()
  117. # Unterstützte MIME-Types
  118. MIME_TYPES: set[str] = set()
  119. # Standard-Output-Eigenschaften
  120. OUTPUT_SAMPLE_RATE: int = 44100 # Hz
  121. OUTPUT_CHANNELS: int = 2 # Stereo
  122. OUTPUT_BIT_DEPTH: int = 16 # 16-bit
  123. def __init__(self) -> None:
  124. """Initialisiert den Decoder."""
  125. self._is_initialized = False
  126. @property
  127. def is_available(self) -> bool:
  128. """Prüft ob Decoder verfügbar ist (Dependencies installiert)."""
  129. return self._check_dependencies()
  130. @abstractmethod
  131. def _check_dependencies(self) -> bool:
  132. """Prüft ob alle Dependencies vorhanden sind."""
  133. pass
  134. @abstractmethod
  135. async def get_info(self, source: Path | bytes) -> AudioInfo:
  136. """
  137. Liest Audio-Informationen ohne vollständiges Dekodieren.
  138. Args:
  139. source: Dateipfad oder Rohdaten
  140. Returns:
  141. Audio-Informationen
  142. Raises:
  143. DecoderError: Bei Fehlern
  144. """
  145. pass
  146. @abstractmethod
  147. async def decode(
  148. self,
  149. source: Path | bytes,
  150. target_sample_rate: int | None = None,
  151. target_channels: int | None = None,
  152. ) -> DecodedAudio:
  153. """
  154. Dekodiert Audio vollständig zu PCM.
  155. Args:
  156. source: Dateipfad oder Rohdaten
  157. target_sample_rate: Ziel-Sample-Rate (None = Standard)
  158. target_channels: Ziel-Kanalzahl (None = Standard)
  159. Returns:
  160. Dekodierte Audio-Daten
  161. Raises:
  162. DecoderError: Bei Fehlern
  163. """
  164. pass
  165. @abstractmethod
  166. async def decode_stream(
  167. self,
  168. source: Path | bytes,
  169. chunk_duration_ms: int = 100,
  170. ) -> AsyncIterator[DecodedAudio]:
  171. """
  172. Dekodiert Audio als Stream (chunk-weise).
  173. Args:
  174. source: Dateipfad oder Rohdaten
  175. chunk_duration_ms: Chunk-Dauer in Millisekunden
  176. Yields:
  177. Dekodierte Audio-Chunks
  178. Raises:
  179. DecoderError: Bei Fehlern
  180. """
  181. pass
  182. def supports_extension(self, ext: str) -> bool:
  183. """Prüft ob Dateiendung unterstützt wird."""
  184. return ext.lower().lstrip(".") in self.EXTENSIONS
  185. def supports_mime(self, mime: str) -> bool:
  186. """Prüft ob MIME-Type unterstützt wird."""
  187. return mime.lower().split(";")[0].strip() in self.MIME_TYPES
  188. def __repr__(self) -> str:
  189. return (
  190. f"{self.__class__.__name__}("
  191. f"format={self.FORMAT.value}, "
  192. f"available={self.is_available})"
  193. )