flac.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. # -*- coding: utf-8 -*-
  2. """
  3. FLAC-Decoder.
  4. Dekodiert FLAC-Dateien (verlustfrei) zu PCM.
  5. """
  6. import asyncio
  7. import io
  8. import logging
  9. import struct
  10. from pathlib import Path
  11. from typing import AsyncIterator
  12. from trixy_core.music.formats.base import (
  13. AudioDecoder,
  14. AudioFormat,
  15. AudioInfo,
  16. DecodedAudio,
  17. DecoderError,
  18. )
  19. class FLACDecoder(AudioDecoder):
  20. """
  21. FLAC-Decoder.
  22. FLAC ist ein verlustfreies Format, perfekt für hochwertige Audio-Wiedergabe.
  23. Verwendet pydub/ffmpeg als Backend.
  24. """
  25. FORMAT = AudioFormat.FLAC
  26. EXTENSIONS = {"flac", "fla"}
  27. MIME_TYPES = {"audio/flac", "audio/x-flac"}
  28. def __init__(self) -> None:
  29. super().__init__()
  30. self.logger = logging.getLogger(__name__)
  31. self._use_pydub = False
  32. self._use_ffmpeg = False
  33. def _check_dependencies(self) -> bool:
  34. """Prüft verfügbare Decoder-Backends."""
  35. # Versuche pydub
  36. try:
  37. from pydub import AudioSegment
  38. self._use_pydub = True
  39. return True
  40. except ImportError:
  41. pass
  42. # Versuche ffmpeg
  43. import shutil
  44. if shutil.which("ffmpeg"):
  45. self._use_ffmpeg = True
  46. return True
  47. return False
  48. async def get_info(self, source: Path | bytes) -> AudioInfo:
  49. """Liest FLAC-Informationen."""
  50. if isinstance(source, bytes):
  51. return await self._get_info_from_bytes(source)
  52. return await self._get_info_from_file(source)
  53. async def _get_info_from_file(self, path: Path) -> AudioInfo:
  54. """Liest Informationen aus FLAC-Datei."""
  55. if not path.exists():
  56. raise DecoderError(f"Datei nicht gefunden: {path}")
  57. file_size = path.stat().st_size
  58. # Versuche Mutagen
  59. try:
  60. import mutagen.flac
  61. audio = mutagen.flac.FLAC(path)
  62. return AudioInfo(
  63. format=AudioFormat.FLAC,
  64. duration_ms=int(audio.info.length * 1000),
  65. sample_rate=audio.info.sample_rate,
  66. channels=audio.info.channels,
  67. bit_depth=audio.info.bits_per_sample,
  68. bit_rate=int(file_size * 8 / audio.info.length) if audio.info.length > 0 else 0,
  69. file_size=file_size,
  70. )
  71. except ImportError:
  72. pass
  73. except Exception as e:
  74. self.logger.debug(f"Mutagen FLAC Fehler: {e}")
  75. # Fallback: FLAC-Header parsen
  76. try:
  77. return await asyncio.to_thread(self._parse_flac_header, path)
  78. except Exception as e:
  79. self.logger.debug(f"FLAC-Header Fehler: {e}")
  80. return AudioInfo(format=AudioFormat.FLAC, file_size=file_size)
  81. async def _get_info_from_bytes(self, data: bytes) -> AudioInfo:
  82. """Liest Informationen aus FLAC-Bytes."""
  83. info = self._parse_flac_header_bytes(data)
  84. info.file_size = len(data)
  85. return info
  86. def _parse_flac_header(self, path: Path) -> AudioInfo:
  87. """Parst FLAC-Header aus Datei."""
  88. with open(path, "rb") as f:
  89. data = f.read(1024)
  90. info = self._parse_flac_header_bytes(data)
  91. info.file_size = path.stat().st_size
  92. return info
  93. def _parse_flac_header_bytes(self, data: bytes) -> AudioInfo:
  94. """
  95. Parst FLAC-Header.
  96. FLAC Format:
  97. - 4 bytes: "fLaC" Marker
  98. - Metadata Blocks (STREAMINFO ist immer erster Block)
  99. """
  100. info = AudioInfo(format=AudioFormat.FLAC)
  101. # Prüfe Magic
  102. if data[:4] != b"fLaC":
  103. raise DecoderError("Keine gültige FLAC-Datei")
  104. # STREAMINFO Block (ab Byte 4)
  105. # Format: 1 bit (last-block) + 7 bit (type) + 24 bit (length)
  106. block_header = data[4]
  107. is_last = bool(block_header & 0x80)
  108. block_type = block_header & 0x7F
  109. if block_type != 0: # 0 = STREAMINFO
  110. return info
  111. # Block-Länge (3 Bytes, Big-Endian)
  112. block_length = (data[5] << 16) | (data[6] << 8) | data[7]
  113. if block_length < 34:
  114. return info
  115. # STREAMINFO Daten (34 Bytes)
  116. streaminfo = data[8:8+34]
  117. # Minimum Block Size (2 bytes)
  118. min_block_size = (streaminfo[0] << 8) | streaminfo[1]
  119. # Maximum Block Size (2 bytes)
  120. max_block_size = (streaminfo[2] << 8) | streaminfo[3]
  121. # Minimum Frame Size (3 bytes)
  122. # Maximum Frame Size (3 bytes)
  123. # -> Bytes 4-9
  124. # Sample Rate (20 bits) + Channels (3 bits) + Bit Depth (5 bits) + Total Samples (36 bits)
  125. # Bytes 10-17
  126. # Sample Rate (20 bits): Bytes 10-11 + obere 4 bits von Byte 12
  127. sample_rate = (streaminfo[10] << 12) | (streaminfo[11] << 4) | (streaminfo[12] >> 4)
  128. # Channels - 1 (3 bits): untere 4 bits von Byte 12 + oberstes Bit
  129. channels = ((streaminfo[12] & 0x0E) >> 1) + 1
  130. # Bit Depth - 1 (5 bits)
  131. bit_depth = ((streaminfo[12] & 0x01) << 4) | (streaminfo[13] >> 4) + 1
  132. # Total Samples (36 bits)
  133. total_samples = (
  134. ((streaminfo[13] & 0x0F) << 32) |
  135. (streaminfo[14] << 24) |
  136. (streaminfo[15] << 16) |
  137. (streaminfo[16] << 8) |
  138. streaminfo[17]
  139. )
  140. # Dauer berechnen
  141. if sample_rate > 0:
  142. duration_ms = int((total_samples / sample_rate) * 1000)
  143. else:
  144. duration_ms = 0
  145. return AudioInfo(
  146. format=AudioFormat.FLAC,
  147. duration_ms=duration_ms,
  148. sample_rate=sample_rate,
  149. channels=channels,
  150. bit_depth=bit_depth,
  151. )
  152. async def decode(
  153. self,
  154. source: Path | bytes,
  155. target_sample_rate: int | None = None,
  156. target_channels: int | None = None,
  157. ) -> DecodedAudio:
  158. """Dekodiert FLAC vollständig zu PCM."""
  159. target_sr = target_sample_rate or self.OUTPUT_SAMPLE_RATE
  160. target_ch = target_channels or self.OUTPUT_CHANNELS
  161. if self._use_pydub:
  162. return await self._decode_pydub(source, target_sr, target_ch)
  163. elif self._use_ffmpeg:
  164. return await self._decode_ffmpeg(source, target_sr, target_ch)
  165. else:
  166. raise DecoderError("Kein FLAC-Decoder verfügbar")
  167. async def _decode_pydub(
  168. self,
  169. source: Path | bytes,
  170. target_sr: int,
  171. target_ch: int,
  172. ) -> DecodedAudio:
  173. """Dekodiert mit pydub."""
  174. try:
  175. from pydub import AudioSegment
  176. def decode():
  177. if isinstance(source, bytes):
  178. audio = AudioSegment.from_file(io.BytesIO(source), format="flac")
  179. else:
  180. audio = AudioSegment.from_file(str(source), format="flac")
  181. # Resampling falls nötig
  182. if audio.frame_rate != target_sr:
  183. audio = audio.set_frame_rate(target_sr)
  184. # Channel-Anpassung
  185. if audio.channels != target_ch:
  186. audio = audio.set_channels(target_ch)
  187. # Auf 16-bit normalisieren
  188. audio = audio.set_sample_width(2)
  189. return DecodedAudio(
  190. pcm_data=audio.raw_data,
  191. sample_rate=audio.frame_rate,
  192. channels=audio.channels,
  193. bit_depth=16,
  194. )
  195. return await asyncio.to_thread(decode)
  196. except Exception as e:
  197. raise DecoderError(f"pydub FLAC-Dekodierung fehlgeschlagen: {e}")
  198. async def _decode_ffmpeg(
  199. self,
  200. source: Path | bytes,
  201. target_sr: int,
  202. target_ch: int,
  203. ) -> DecodedAudio:
  204. """Dekodiert mit ffmpeg."""
  205. try:
  206. if isinstance(source, bytes):
  207. cmd = [
  208. "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
  209. "-i", "pipe:0",
  210. "-f", "s16le",
  211. "-acodec", "pcm_s16le",
  212. "-ar", str(target_sr),
  213. "-ac", str(target_ch),
  214. "pipe:1"
  215. ]
  216. process = await asyncio.create_subprocess_exec(
  217. *cmd,
  218. stdin=asyncio.subprocess.PIPE,
  219. stdout=asyncio.subprocess.PIPE,
  220. stderr=asyncio.subprocess.PIPE,
  221. )
  222. stdout, stderr = await process.communicate(input=source)
  223. else:
  224. cmd = [
  225. "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
  226. "-i", str(source),
  227. "-f", "s16le",
  228. "-acodec", "pcm_s16le",
  229. "-ar", str(target_sr),
  230. "-ac", str(target_ch),
  231. "pipe:1"
  232. ]
  233. process = await asyncio.create_subprocess_exec(
  234. *cmd,
  235. stdout=asyncio.subprocess.PIPE,
  236. stderr=asyncio.subprocess.PIPE,
  237. )
  238. stdout, stderr = await process.communicate()
  239. if process.returncode != 0:
  240. raise DecoderError(f"ffmpeg Fehler: {stderr.decode()}")
  241. return DecodedAudio(
  242. pcm_data=stdout,
  243. sample_rate=target_sr,
  244. channels=target_ch,
  245. bit_depth=16,
  246. )
  247. except DecoderError:
  248. raise
  249. except Exception as e:
  250. raise DecoderError(f"ffmpeg FLAC-Dekodierung fehlgeschlagen: {e}")
  251. async def decode_stream(
  252. self,
  253. source: Path | bytes,
  254. chunk_duration_ms: int = 100,
  255. ) -> AsyncIterator[DecodedAudio]:
  256. """Dekodiert FLAC als Stream."""
  257. bytes_per_sample = 2
  258. samples_per_chunk = int(self.OUTPUT_SAMPLE_RATE * chunk_duration_ms / 1000)
  259. chunk_size = samples_per_chunk * bytes_per_sample * self.OUTPUT_CHANNELS
  260. if self._use_pydub:
  261. decoded = await self.decode(source)
  262. data = decoded.pcm_data
  263. for i in range(0, len(data), chunk_size):
  264. yield DecodedAudio(
  265. pcm_data=data[i:i + chunk_size],
  266. sample_rate=decoded.sample_rate,
  267. channels=decoded.channels,
  268. bit_depth=decoded.bit_depth,
  269. )
  270. elif self._use_ffmpeg:
  271. async for chunk in self._stream_ffmpeg(source, chunk_size):
  272. yield chunk
  273. async def _stream_ffmpeg(
  274. self,
  275. source: Path | bytes,
  276. chunk_size: int,
  277. ) -> AsyncIterator[DecodedAudio]:
  278. """Streaming mit ffmpeg."""
  279. try:
  280. if isinstance(source, bytes):
  281. cmd = [
  282. "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
  283. "-i", "pipe:0",
  284. "-f", "s16le",
  285. "-acodec", "pcm_s16le",
  286. "-ar", str(self.OUTPUT_SAMPLE_RATE),
  287. "-ac", str(self.OUTPUT_CHANNELS),
  288. "pipe:1"
  289. ]
  290. process = await asyncio.create_subprocess_exec(
  291. *cmd,
  292. stdin=asyncio.subprocess.PIPE,
  293. stdout=asyncio.subprocess.PIPE,
  294. stderr=asyncio.subprocess.PIPE,
  295. )
  296. if process.stdin:
  297. process.stdin.write(source)
  298. await process.stdin.drain()
  299. process.stdin.close()
  300. else:
  301. cmd = [
  302. "ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
  303. "-i", str(source),
  304. "-f", "s16le",
  305. "-acodec", "pcm_s16le",
  306. "-ar", str(self.OUTPUT_SAMPLE_RATE),
  307. "-ac", str(self.OUTPUT_CHANNELS),
  308. "pipe:1"
  309. ]
  310. process = await asyncio.create_subprocess_exec(
  311. *cmd,
  312. stdout=asyncio.subprocess.PIPE,
  313. stderr=asyncio.subprocess.PIPE,
  314. )
  315. while True:
  316. chunk = await process.stdout.read(chunk_size)
  317. if not chunk:
  318. break
  319. yield DecodedAudio(
  320. pcm_data=chunk,
  321. sample_rate=self.OUTPUT_SAMPLE_RATE,
  322. channels=self.OUTPUT_CHANNELS,
  323. bit_depth=16,
  324. )
  325. await process.wait()
  326. except Exception as e:
  327. raise DecoderError(f"FLAC Streaming fehlgeschlagen: {e}")