wav.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296
  1. # -*- coding: utf-8 -*-
  2. """
  3. WAV-Decoder.
  4. Dekodiert WAV-Dateien zu PCM.
  5. """
  6. import asyncio
  7. import io
  8. import logging
  9. import struct
  10. import wave
  11. from pathlib import Path
  12. from typing import AsyncIterator
  13. from trixy_core.music.formats.base import (
  14. AudioDecoder,
  15. AudioFormat,
  16. AudioInfo,
  17. DecodedAudio,
  18. DecoderError,
  19. )
  20. class WAVDecoder(AudioDecoder):
  21. """
  22. WAV-Decoder.
  23. WAV ist bereits PCM, daher ist die Dekodierung einfach.
  24. Unterstützt Resampling und Channel-Konvertierung.
  25. """
  26. FORMAT = AudioFormat.WAV
  27. EXTENSIONS = {"wav", "wave"}
  28. MIME_TYPES = {"audio/wav", "audio/x-wav", "audio/wave"}
  29. def __init__(self) -> None:
  30. super().__init__()
  31. self.logger = logging.getLogger(__name__)
  32. def _check_dependencies(self) -> bool:
  33. """WAV wird von Python nativ unterstützt."""
  34. return True
  35. async def get_info(self, source: Path | bytes) -> AudioInfo:
  36. """Liest WAV-Informationen."""
  37. try:
  38. if isinstance(source, bytes):
  39. wav_file = wave.open(io.BytesIO(source), "rb")
  40. else:
  41. if not source.exists():
  42. raise DecoderError(f"Datei nicht gefunden: {source}")
  43. wav_file = wave.open(str(source), "rb")
  44. try:
  45. frames = wav_file.getnframes()
  46. sample_rate = wav_file.getframerate()
  47. channels = wav_file.getnchannels()
  48. sample_width = wav_file.getsampwidth()
  49. duration_ms = int((frames / sample_rate) * 1000)
  50. bit_rate = sample_rate * channels * sample_width * 8
  51. return AudioInfo(
  52. format=AudioFormat.WAV,
  53. duration_ms=duration_ms,
  54. sample_rate=sample_rate,
  55. channels=channels,
  56. bit_depth=sample_width * 8,
  57. bit_rate=bit_rate,
  58. file_size=source.stat().st_size if isinstance(source, Path) else len(source),
  59. )
  60. finally:
  61. wav_file.close()
  62. except wave.Error as e:
  63. raise DecoderError(f"Ungültige WAV-Datei: {e}")
  64. except Exception as e:
  65. raise DecoderError(f"Kann WAV-Info nicht lesen: {e}")
  66. async def decode(
  67. self,
  68. source: Path | bytes,
  69. target_sample_rate: int | None = None,
  70. target_channels: int | None = None,
  71. ) -> DecodedAudio:
  72. """Dekodiert WAV zu PCM."""
  73. target_sr = target_sample_rate or self.OUTPUT_SAMPLE_RATE
  74. target_ch = target_channels or self.OUTPUT_CHANNELS
  75. def do_decode():
  76. if isinstance(source, bytes):
  77. wav_file = wave.open(io.BytesIO(source), "rb")
  78. else:
  79. wav_file = wave.open(str(source), "rb")
  80. try:
  81. frames = wav_file.getnframes()
  82. sample_rate = wav_file.getframerate()
  83. channels = wav_file.getnchannels()
  84. sample_width = wav_file.getsampwidth()
  85. # Rohdaten lesen
  86. raw_data = wav_file.readframes(frames)
  87. # Konvertierung wenn nötig
  88. pcm_data = self._convert_pcm(
  89. raw_data,
  90. sample_rate=sample_rate,
  91. channels=channels,
  92. sample_width=sample_width,
  93. target_sr=target_sr,
  94. target_ch=target_ch,
  95. )
  96. return DecodedAudio(
  97. pcm_data=pcm_data,
  98. sample_rate=target_sr,
  99. channels=target_ch,
  100. bit_depth=16,
  101. )
  102. finally:
  103. wav_file.close()
  104. try:
  105. return await asyncio.to_thread(do_decode)
  106. except Exception as e:
  107. raise DecoderError(f"WAV Dekodierung fehlgeschlagen: {e}")
  108. def _convert_pcm(
  109. self,
  110. data: bytes,
  111. sample_rate: int,
  112. channels: int,
  113. sample_width: int,
  114. target_sr: int,
  115. target_ch: int,
  116. ) -> bytes:
  117. """
  118. Konvertiert PCM-Daten.
  119. Args:
  120. data: Rohe PCM-Daten
  121. sample_rate: Ursprüngliche Sample-Rate
  122. channels: Ursprüngliche Kanalzahl
  123. sample_width: Bytes pro Sample (1, 2, 3, 4)
  124. target_sr: Ziel-Sample-Rate
  125. target_ch: Ziel-Kanalzahl
  126. Returns:
  127. Konvertierte PCM-Daten (16-bit)
  128. """
  129. # Schritt 1: Zu 16-bit konvertieren falls nötig
  130. if sample_width == 1:
  131. # 8-bit unsigned → 16-bit signed
  132. samples = struct.unpack(f"{len(data)}B", data)
  133. samples = [(s - 128) << 8 for s in samples]
  134. data = struct.pack(f"{len(samples)}h", *samples)
  135. sample_width = 2
  136. elif sample_width == 3:
  137. # 24-bit → 16-bit (einfach obere 16 bit nehmen)
  138. samples = []
  139. for i in range(0, len(data), 3):
  140. # Little-endian 24-bit
  141. val = data[i] | (data[i+1] << 8) | (data[i+2] << 16)
  142. if val & 0x800000:
  143. val -= 0x1000000
  144. samples.append(val >> 8)
  145. data = struct.pack(f"{len(samples)}h", *samples)
  146. sample_width = 2
  147. elif sample_width == 4:
  148. # 32-bit → 16-bit
  149. samples = struct.unpack(f"{len(data)//4}i", data)
  150. samples = [s >> 16 for s in samples]
  151. data = struct.pack(f"{len(samples)}h", *samples)
  152. sample_width = 2
  153. # Schritt 2: Channel-Konvertierung
  154. if channels != target_ch:
  155. samples = struct.unpack(f"{len(data)//2}h", data)
  156. if channels == 2 and target_ch == 1:
  157. # Stereo → Mono (Mittelwert)
  158. mono = []
  159. for i in range(0, len(samples), 2):
  160. mono.append((samples[i] + samples[i+1]) // 2)
  161. samples = mono
  162. channels = 1
  163. elif channels == 1 and target_ch == 2:
  164. # Mono → Stereo (duplizieren)
  165. stereo = []
  166. for s in samples:
  167. stereo.extend([s, s])
  168. samples = stereo
  169. channels = 2
  170. data = struct.pack(f"{len(samples)}h", *samples)
  171. # Schritt 3: Resampling falls nötig
  172. if sample_rate != target_sr:
  173. data = self._resample(data, channels, sample_rate, target_sr)
  174. return data
  175. def _resample(
  176. self,
  177. data: bytes,
  178. channels: int,
  179. src_rate: int,
  180. dst_rate: int,
  181. ) -> bytes:
  182. """
  183. Einfaches lineares Resampling.
  184. Für Produktionsqualität sollte scipy.signal.resample verwendet werden.
  185. """
  186. samples = struct.unpack(f"{len(data)//2}h", data)
  187. frame_count = len(samples) // channels
  188. # Ratio
  189. ratio = dst_rate / src_rate
  190. new_frame_count = int(frame_count * ratio)
  191. resampled = []
  192. for i in range(new_frame_count):
  193. # Position im Original
  194. src_pos = i / ratio
  195. src_idx = int(src_pos)
  196. frac = src_pos - src_idx
  197. for ch in range(channels):
  198. idx1 = src_idx * channels + ch
  199. idx2 = min(idx1 + channels, len(samples) - 1)
  200. if idx1 < len(samples):
  201. # Lineare Interpolation
  202. val1 = samples[idx1]
  203. val2 = samples[idx2] if idx2 < len(samples) else val1
  204. val = int(val1 * (1 - frac) + val2 * frac)
  205. resampled.append(max(-32768, min(32767, val)))
  206. return struct.pack(f"{len(resampled)}h", *resampled)
  207. async def decode_stream(
  208. self,
  209. source: Path | bytes,
  210. chunk_duration_ms: int = 100,
  211. ) -> AsyncIterator[DecodedAudio]:
  212. """Dekodiert WAV als Stream."""
  213. # Berechne Chunk-Größe
  214. bytes_per_sample = 2 # 16-bit
  215. samples_per_chunk = int(self.OUTPUT_SAMPLE_RATE * chunk_duration_ms / 1000)
  216. chunk_size = samples_per_chunk * bytes_per_sample * self.OUTPUT_CHANNELS
  217. try:
  218. if isinstance(source, bytes):
  219. wav_file = wave.open(io.BytesIO(source), "rb")
  220. else:
  221. wav_file = wave.open(str(source), "rb")
  222. try:
  223. source_sr = wav_file.getframerate()
  224. source_ch = wav_file.getnchannels()
  225. source_sw = wav_file.getsampwidth()
  226. # Frames pro Chunk (in Quell-Rate)
  227. source_chunk_frames = int(samples_per_chunk * source_sr / self.OUTPUT_SAMPLE_RATE)
  228. while True:
  229. raw_data = wav_file.readframes(source_chunk_frames)
  230. if not raw_data:
  231. break
  232. # Konvertieren
  233. pcm_data = self._convert_pcm(
  234. raw_data,
  235. sample_rate=source_sr,
  236. channels=source_ch,
  237. sample_width=source_sw,
  238. target_sr=self.OUTPUT_SAMPLE_RATE,
  239. target_ch=self.OUTPUT_CHANNELS,
  240. )
  241. yield DecodedAudio(
  242. pcm_data=pcm_data,
  243. sample_rate=self.OUTPUT_SAMPLE_RATE,
  244. channels=self.OUTPUT_CHANNELS,
  245. bit_depth=16,
  246. )
  247. finally:
  248. wav_file.close()
  249. except Exception as e:
  250. raise DecoderError(f"WAV Streaming fehlgeschlagen: {e}")