player.py 34 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951
  1. # -*- coding: utf-8 -*-
  2. """
  3. Music-Player.
  4. Zentraler Musik-Player mit Source-, Decoder- und Streaming-Integration.
  5. """
  6. import asyncio
  7. import logging
  8. import time
  9. import uuid
  10. from collections import deque
  11. from dataclasses import dataclass, field
  12. from datetime import datetime
  13. from enum import Enum
  14. from typing import Any, Callable, Awaitable
  15. from trixy_core.music.track import Track, TrackState
  16. from trixy_core.music.playlist import Playlist, PlaylistManager, RepeatMode, ShuffleMode
  17. from trixy_core.music.queue import PlayQueue, QueueState
  18. from trixy_core.music.sources import SourceManager, MusicSource
  19. from trixy_core.music.formats import DecoderRegistry, get_default_registry
  20. from trixy_core.music.formats.base import DecodedAudio
  21. from trixy_core.music.stream import AudioStreamer, StreamConfig, StreamState
  22. from trixy_core.audio.processing import AudioProcessingPipeline, AudioProcessingContext, AudioType
  23. from trixy_core.utils.debug import pdebug
  24. class PlayerState(Enum):
  25. """Zustand des Players."""
  26. STOPPED = "stopped"
  27. LOADING = "loading"
  28. PLAYING = "playing"
  29. PAUSED = "paused"
  30. BUFFERING = "buffering"
  31. ERROR = "error"
  32. class PlaybackEvent(Enum):
  33. """Wiedergabe-Events."""
  34. TRACK_STARTED = "track_started"
  35. TRACK_ENDED = "track_ended"
  36. TRACK_CHANGED = "track_changed"
  37. POSITION_CHANGED = "position_changed"
  38. STATE_CHANGED = "state_changed"
  39. VOLUME_CHANGED = "volume_changed"
  40. QUEUE_CHANGED = "queue_changed"
  41. ERROR = "error"
  42. @dataclass
  43. class PlayerConfig:
  44. """Player-Konfiguration."""
  45. # Audio (44.1kHz - Standard für die meisten Geräte)
  46. sample_rate: int = 44100
  47. channels: int = 2
  48. bit_depth: int = 16
  49. # Pufferung
  50. buffer_ms: int = 500
  51. prebuffer_ms: int = 200
  52. # Verhalten
  53. auto_play_next: bool = True
  54. crossfade_ms: int = 0 # 0 = kein Crossfade
  55. gapless: bool = True
  56. # Lautstärke
  57. default_volume: float = 1.0 # 0.0 - 1.0
  58. # Limits
  59. max_queue_size: int = 1000
  60. # Zusätzlich
  61. extra: dict[str, Any] = field(default_factory=dict)
  62. class MusicPlayer:
  63. """
  64. Zentraler Musik-Player.
  65. Integriert:
  66. - Musikquellen (lokal, USB, URL, etc.)
  67. - Audio-Decoder (MP3, WAV, OGG, FLAC)
  68. - Playlists und Queue
  69. - Streaming an Satellites
  70. """
  71. def __init__(
  72. self,
  73. config: PlayerConfig | None = None,
  74. source_manager: SourceManager | None = None,
  75. decoder_registry: DecoderRegistry | None = None,
  76. playlist_manager: PlaylistManager | None = None,
  77. ) -> None:
  78. """
  79. Initialisiert den MusicPlayer.
  80. Args:
  81. config: Player-Konfiguration
  82. source_manager: Optionaler Source-Manager
  83. decoder_registry: Optionale Decoder-Registry
  84. playlist_manager: Optionaler Playlist-Manager
  85. """
  86. self.id = f"player_{uuid.uuid4().hex[:8]}"
  87. self.config = config or PlayerConfig()
  88. self.logger = logging.getLogger(__name__)
  89. # Komponenten
  90. self.sources = source_manager or SourceManager()
  91. self.decoders = decoder_registry or get_default_registry()
  92. self.playlists = playlist_manager or PlaylistManager()
  93. # Queue und aktuelle Playlist
  94. self.queue = PlayQueue()
  95. self._current_playlist: Playlist | None = None
  96. # Streaming
  97. self._streamer = AudioStreamer(
  98. config=StreamConfig(
  99. sample_rate=self.config.sample_rate,
  100. channels=self.config.channels,
  101. bit_depth=self.config.bit_depth,
  102. prebuffer_ms=self.config.prebuffer_ms,
  103. )
  104. )
  105. # Audio-Processing-Pipeline (für Ducking, Crossfade, etc.)
  106. self._processing_pipeline = AudioProcessingPipeline()
  107. # Zustand
  108. self._state = PlayerState.STOPPED
  109. self._volume = self.config.default_volume
  110. self._muted = False
  111. self._position_ms = 0
  112. self._duration_ms = 0
  113. # Crossfade
  114. self._crossfade_buffer: deque = deque(maxlen=50) # Rolling buffer für Crossfade-Erkennung
  115. self._crossfade_in_progress = False # Flag ob gerade ein Crossfade läuft
  116. self._crossfade_to_next = False # Flag für manuelles Crossfade (Ctrl+N)
  117. # Playback-Task
  118. self._playback_task: asyncio.Task | None = None
  119. self._stop_event = asyncio.Event()
  120. # Event-Callbacks
  121. self._event_callbacks: list[Callable[[PlaybackEvent, dict], Awaitable[None]]] = []
  122. # ==========================================================================
  123. # Properties
  124. # ==========================================================================
  125. @property
  126. def state(self) -> PlayerState:
  127. """Aktueller Zustand."""
  128. return self._state
  129. @property
  130. def is_playing(self) -> bool:
  131. """Prüft ob abgespielt wird."""
  132. return self._state == PlayerState.PLAYING
  133. @property
  134. def is_paused(self) -> bool:
  135. """Prüft ob pausiert."""
  136. return self._state == PlayerState.PAUSED
  137. @property
  138. def current_track(self) -> Track | None:
  139. """Aktueller Track."""
  140. return self.queue.current_track
  141. @property
  142. def position_ms(self) -> int:
  143. """Aktuelle Position in Millisekunden."""
  144. return self._position_ms
  145. @property
  146. def duration_ms(self) -> int:
  147. """Dauer des aktuellen Tracks in Millisekunden."""
  148. return self._duration_ms
  149. @property
  150. def progress(self) -> float:
  151. """Fortschritt (0.0 - 1.0)."""
  152. if self._duration_ms > 0:
  153. return min(1.0, self._position_ms / self._duration_ms)
  154. return 0.0
  155. @property
  156. def volume(self) -> float:
  157. """Aktuelle Lautstärke (0.0 - 1.0)."""
  158. return self._volume
  159. @property
  160. def is_muted(self) -> bool:
  161. """Prüft ob stummgeschaltet."""
  162. return self._muted
  163. @property
  164. def current_playlist(self) -> Playlist | None:
  165. """Aktuelle Playlist."""
  166. return self._current_playlist
  167. @property
  168. def repeat_mode(self) -> RepeatMode:
  169. """Wiederholungsmodus."""
  170. if self._current_playlist:
  171. return self._current_playlist.repeat_mode
  172. return RepeatMode.OFF
  173. @property
  174. def shuffle_mode(self) -> ShuffleMode:
  175. """Zufallsmodus."""
  176. if self._current_playlist:
  177. return self._current_playlist.shuffle_mode
  178. return ShuffleMode.OFF
  179. @property
  180. def processing_pipeline(self) -> AudioProcessingPipeline:
  181. """Audio-Processing-Pipeline für Plugins (Ducking, Crossfade, etc.)."""
  182. return self._processing_pipeline
  183. # ==========================================================================
  184. # Playback-Steuerung
  185. # ==========================================================================
  186. async def play(
  187. self,
  188. track: Track | None = None,
  189. playlist: Playlist | None = None,
  190. satellite_ids: list[str] | None = None,
  191. ) -> bool:
  192. """
  193. Startet Wiedergabe.
  194. Args:
  195. track: Optionaler Track zum Abspielen
  196. playlist: Optionale Playlist
  197. satellite_ids: Ziel-Satellites (None = alle)
  198. Returns:
  199. True wenn erfolgreich
  200. """
  201. # Bestehende Wiedergabe stoppen
  202. if self._playback_task and not self._playback_task.done():
  203. await self.stop()
  204. # Track bestimmen
  205. if track:
  206. self.queue.add(track, play_next=True)
  207. self.queue.next()
  208. elif playlist:
  209. self._current_playlist = playlist
  210. first_track = playlist.start()
  211. if first_track:
  212. self.queue.add(first_track, play_next=True)
  213. self.queue.next()
  214. elif self.queue.current_item:
  215. pass # Aktuellen Track weiterspielen
  216. elif self.queue.has_next:
  217. self.queue.next()
  218. else:
  219. self.logger.warning("Kein Track zum Abspielen")
  220. return False
  221. current = self.queue.current_track
  222. if not current:
  223. return False
  224. self._stop_event.clear()
  225. self._state = PlayerState.LOADING
  226. await self._emit_event(PlaybackEvent.STATE_CHANGED, {"state": self._state.value})
  227. try:
  228. # Playback-Task starten
  229. self._playback_task = asyncio.create_task(
  230. self._playback_loop(satellite_ids)
  231. )
  232. self.logger.info(f"Wiedergabe gestartet: {current.title}")
  233. return True
  234. except Exception as e:
  235. self.logger.error(f"Wiedergabe fehlgeschlagen: {e}")
  236. self._state = PlayerState.ERROR
  237. await self._emit_event(PlaybackEvent.ERROR, {"error": str(e)})
  238. return False
  239. async def pause(self) -> None:
  240. """Pausiert Wiedergabe."""
  241. if self._state == PlayerState.PLAYING:
  242. self._state = PlayerState.PAUSED
  243. await self._streamer.pause()
  244. await self._emit_event(PlaybackEvent.STATE_CHANGED, {"state": self._state.value})
  245. self.logger.debug("Wiedergabe pausiert")
  246. async def resume(self) -> None:
  247. """Setzt Wiedergabe fort."""
  248. if self._state == PlayerState.PAUSED:
  249. self._state = PlayerState.PLAYING
  250. await self._streamer.resume()
  251. await self._emit_event(PlaybackEvent.STATE_CHANGED, {"state": self._state.value})
  252. self.logger.debug("Wiedergabe fortgesetzt")
  253. async def toggle_pause(self) -> None:
  254. """Wechselt zwischen Play und Pause."""
  255. if self._state == PlayerState.PLAYING:
  256. await self.pause()
  257. elif self._state == PlayerState.PAUSED:
  258. await self.resume()
  259. async def stop(self) -> None:
  260. """Stoppt Wiedergabe."""
  261. self._stop_event.set()
  262. if self._playback_task and not self._playback_task.done():
  263. self._playback_task.cancel()
  264. try:
  265. await self._playback_task
  266. except asyncio.CancelledError:
  267. pass
  268. await self._streamer.stop()
  269. self._state = PlayerState.STOPPED
  270. self._position_ms = 0
  271. await self._emit_event(PlaybackEvent.STATE_CHANGED, {"state": self._state.value})
  272. self.logger.info("Wiedergabe gestoppt")
  273. async def next(self, use_crossfade: bool = True) -> Track | None:
  274. """
  275. Springt zum nächsten Track.
  276. Args:
  277. use_crossfade: Crossfade verwenden wenn möglich
  278. Returns:
  279. Nächster Track oder None
  280. """
  281. # Prüfen ob Crossfade möglich ist
  282. crossfade_possible = (
  283. use_crossfade and
  284. self.config.crossfade_ms > 0 and
  285. len(self._crossfade_buffer) > 0 and
  286. self._state == PlayerState.PLAYING and
  287. not self._crossfade_in_progress
  288. )
  289. if crossfade_possible:
  290. # Crossfade-Flag setzen - die _playback_loop wird das Crossfade ausführen
  291. pdebug(f"MusicPlayer: Manuelles Crossfade angefordert (buffer={len(self._crossfade_buffer)})")
  292. self._crossfade_to_next = True
  293. # Warte kurz damit die _playback_loop das Flag sehen kann
  294. await asyncio.sleep(0.05)
  295. return self.queue.peek_next().track if self.queue.peek_next() else None
  296. # Normaler Wechsel ohne Crossfade
  297. # Aus Playlist
  298. if self._current_playlist and self._current_playlist.has_next:
  299. track = self._current_playlist.next()
  300. if track:
  301. self.queue.add(track, play_next=True)
  302. # Aus Queue
  303. item = self.queue.next()
  304. if item:
  305. await self._emit_event(PlaybackEvent.TRACK_CHANGED, {"track": item.track.to_dict()})
  306. if self._state != PlayerState.STOPPED:
  307. await self.play()
  308. return item.track
  309. # Nichts mehr
  310. await self.stop()
  311. return None
  312. async def previous(self) -> Track | None:
  313. """
  314. Springt zum vorherigen Track.
  315. Returns:
  316. Vorheriger Track oder None
  317. """
  318. # Wenn Position > 3s, zum Anfang springen
  319. if self._position_ms > 3000:
  320. await self.seek(0)
  321. return self.current_track
  322. # Aus Playlist
  323. if self._current_playlist and self._current_playlist.has_previous:
  324. track = self._current_playlist.previous()
  325. if track:
  326. self.queue.add(track, play_next=True)
  327. self.queue.next()
  328. await self._emit_event(PlaybackEvent.TRACK_CHANGED, {"track": track.to_dict()})
  329. if self._state != PlayerState.STOPPED:
  330. await self.play()
  331. return track
  332. # Aus Queue-History
  333. item = self.queue.previous()
  334. if item:
  335. await self._emit_event(PlaybackEvent.TRACK_CHANGED, {"track": item.track.to_dict()})
  336. if self._state != PlayerState.STOPPED:
  337. await self.play()
  338. return item.track
  339. return None
  340. async def seek(self, position_ms: int) -> bool:
  341. """
  342. Springt zu Position.
  343. Args:
  344. position_ms: Ziel-Position in Millisekunden
  345. Returns:
  346. True wenn erfolgreich
  347. """
  348. if position_ms < 0:
  349. position_ms = 0
  350. if position_ms > self._duration_ms:
  351. position_ms = self._duration_ms
  352. self._position_ms = position_ms
  353. # Streamer informieren
  354. await self._streamer.seek(position_ms)
  355. await self._emit_event(PlaybackEvent.POSITION_CHANGED, {"position_ms": position_ms})
  356. return True
  357. # ==========================================================================
  358. # Lautstärke
  359. # ==========================================================================
  360. async def set_volume(self, volume: float) -> None:
  361. """
  362. Setzt Lautstärke.
  363. Args:
  364. volume: Lautstärke (0.0 - 1.0)
  365. """
  366. self._volume = max(0.0, min(1.0, volume))
  367. await self._emit_event(PlaybackEvent.VOLUME_CHANGED, {"volume": self._volume})
  368. async def mute(self) -> None:
  369. """Schaltet stumm."""
  370. self._muted = True
  371. await self._emit_event(PlaybackEvent.VOLUME_CHANGED, {"volume": 0, "muted": True})
  372. async def unmute(self) -> None:
  373. """Hebt Stummschaltung auf."""
  374. self._muted = False
  375. await self._emit_event(PlaybackEvent.VOLUME_CHANGED, {"volume": self._volume, "muted": False})
  376. async def toggle_mute(self) -> None:
  377. """Wechselt Stummschaltung."""
  378. if self._muted:
  379. await self.unmute()
  380. else:
  381. await self.mute()
  382. # ==========================================================================
  383. # Repeat/Shuffle
  384. # ==========================================================================
  385. async def set_repeat(self, mode: RepeatMode) -> None:
  386. """Setzt Wiederholungsmodus."""
  387. if self._current_playlist:
  388. self._current_playlist.repeat_mode = mode
  389. async def set_shuffle(self, mode: ShuffleMode) -> None:
  390. """Setzt Zufallsmodus."""
  391. if self._current_playlist:
  392. self._current_playlist.set_shuffle(mode)
  393. async def toggle_repeat(self) -> RepeatMode:
  394. """Wechselt Wiederholungsmodus."""
  395. if self._current_playlist:
  396. current = self._current_playlist.repeat_mode
  397. modes = list(RepeatMode)
  398. next_idx = (modes.index(current) + 1) % len(modes)
  399. new_mode = modes[next_idx]
  400. self._current_playlist.repeat_mode = new_mode
  401. return new_mode
  402. return RepeatMode.OFF
  403. async def toggle_shuffle(self) -> ShuffleMode:
  404. """Wechselt Zufallsmodus."""
  405. if self._current_playlist:
  406. if self._current_playlist.shuffle_mode == ShuffleMode.OFF:
  407. self._current_playlist.set_shuffle(ShuffleMode.ON)
  408. return ShuffleMode.ON
  409. else:
  410. self._current_playlist.set_shuffle(ShuffleMode.OFF)
  411. return ShuffleMode.OFF
  412. return ShuffleMode.OFF
  413. # ==========================================================================
  414. # Queue-Management
  415. # ==========================================================================
  416. async def add_to_queue(
  417. self,
  418. track: Track,
  419. play_next: bool = False,
  420. ) -> None:
  421. """
  422. Fügt Track zur Queue hinzu.
  423. Args:
  424. track: Track
  425. play_next: Als nächstes abspielen
  426. """
  427. self.queue.add(track, play_next=play_next)
  428. await self._emit_event(PlaybackEvent.QUEUE_CHANGED, {"action": "add"})
  429. async def remove_from_queue(self, index: int) -> None:
  430. """
  431. Entfernt Track aus Queue.
  432. Args:
  433. index: Position
  434. """
  435. self.queue.remove(index)
  436. await self._emit_event(PlaybackEvent.QUEUE_CHANGED, {"action": "remove"})
  437. async def clear_queue(self) -> None:
  438. """Leert die Queue."""
  439. self.queue.clear()
  440. await self._emit_event(PlaybackEvent.QUEUE_CHANGED, {"action": "clear"})
  441. # ==========================================================================
  442. # Satellite-Management
  443. # ==========================================================================
  444. def add_satellite(
  445. self,
  446. satellite_id: str,
  447. send_callback: Callable[[bytes], Awaitable[bool]],
  448. ) -> None:
  449. """
  450. Fügt Satellite als Streaming-Ziel hinzu.
  451. Args:
  452. satellite_id: ID des Satellites
  453. send_callback: Callback zum Senden
  454. """
  455. self._streamer.add_satellite(satellite_id, send_callback)
  456. async def remove_satellite(self, satellite_id: str) -> None:
  457. """
  458. Entfernt Satellite.
  459. Args:
  460. satellite_id: ID des Satellites
  461. """
  462. await self._streamer.remove_satellite(satellite_id)
  463. # ==========================================================================
  464. # Playback-Loop
  465. # ==========================================================================
  466. async def _playback_loop(self, satellite_ids: list[str] | None) -> None:
  467. """Haupt-Playback-Loop."""
  468. track = self.queue.current_track
  469. if not track:
  470. return
  471. self._duration_ms = track.duration_ms
  472. self._position_ms = 0
  473. # Nächsten Track für Pipeline-Kontext ermitteln
  474. next_track = None
  475. if self.queue.has_next:
  476. next_item = self.queue.peek_next()
  477. next_track = next_item.track if next_item else None
  478. pdebug(f"MusicPlayer: next_track aus Queue: {next_track.title if next_track else 'None'}")
  479. elif self._current_playlist and self._current_playlist.has_next:
  480. next_track = self._current_playlist.peek_next()
  481. pdebug(f"MusicPlayer: next_track aus Playlist: {next_track.title if next_track else 'None'}")
  482. else:
  483. pdebug(f"MusicPlayer: Kein next_track (queue.has_next={self.queue.has_next}, playlist={self._current_playlist is not None})")
  484. # Pipeline über Track-Start informieren
  485. self._processing_pipeline.set_current_track(track)
  486. self._processing_pipeline.set_next_track(next_track)
  487. start_context = AudioProcessingContext(
  488. audio_type=AudioType.MUSIC,
  489. track=track,
  490. next_track=next_track,
  491. duration_ms=self._duration_ms,
  492. sample_rate=self.config.sample_rate,
  493. channels=self.config.channels,
  494. volume=self._volume,
  495. is_muted=self._muted,
  496. satellite_ids=satellite_ids or [],
  497. )
  498. self._processing_pipeline.notify_track_start(start_context)
  499. await self._emit_event(PlaybackEvent.TRACK_STARTED, {"track": track.to_dict()})
  500. try:
  501. # Streaming starten
  502. await self._streamer.start(track, satellite_ids)
  503. # Decoder finden
  504. decoder = self.decoders.get_best_decoder(
  505. path=track.path,
  506. extension=track.metadata.format,
  507. )
  508. if not decoder:
  509. raise Exception(f"Kein Decoder für Format: {track.metadata.format}")
  510. self._state = PlayerState.PLAYING
  511. await self._emit_event(PlaybackEvent.STATE_CHANGED, {"state": self._state.value})
  512. # Audio-Stream dekodieren und senden
  513. chunk_count = 0
  514. crossfade_ms = self.config.crossfade_ms
  515. # Crossfade-Buffer: Speichert die letzten N Sekunden für Fade-Out (Instanzvariable)
  516. crossfade_chunks_needed = crossfade_ms // 100 if crossfade_ms > 0 else 0
  517. self._crossfade_buffer = deque(maxlen=crossfade_chunks_needed) if crossfade_chunks_needed > 0 else deque()
  518. self._current_satellite_ids = satellite_ids # Für Crossfade bei manuellem next()
  519. pdebug(f"MusicPlayer: Starte '{track.title}' ({self._duration_ms}ms), crossfade_ms={crossfade_ms}, chunks_needed={crossfade_chunks_needed}")
  520. # Wall-Clock Timing: verhindert ALSA-Underrun durch akkurate Chunk-Taktung
  521. chunk_interval = 0.1 # 100ms pro Chunk
  522. next_chunk_time = time.monotonic()
  523. if track.path:
  524. # Generator für aktuellen Track
  525. current_gen = decoder.decode_stream(track.path, chunk_duration_ms=100)
  526. next_gen = None # Wird bei Crossfade gesetzt
  527. cf_track = None # Crossfade-Ziel-Track
  528. cf_chunk_index = 0
  529. cf_total_chunks = crossfade_ms // 100
  530. while True:
  531. if self._stop_event.is_set():
  532. pdebug(f"MusicPlayer: stop_event bei Chunk {chunk_count}")
  533. break
  534. # Auto-Crossfade: N Sekunden vor Track-Ende automatisch starten
  535. if (next_gen is None and crossfade_ms > 0 and self._duration_ms > 0
  536. and self._position_ms >= self._duration_ms - crossfade_ms
  537. and not self._stop_event.is_set()):
  538. pdebug(f"MusicPlayer: Auto-Crossfade bei {self._position_ms}/{self._duration_ms}ms")
  539. self._crossfade_to_next = True
  540. # Crossfade angefordert (manuell oder auto)?
  541. if self._crossfade_to_next and next_gen is None:
  542. pdebug(f"MusicPlayer: Crossfade bei Chunk {chunk_count}")
  543. self._crossfade_to_next = False
  544. # Nächsten Track ermitteln
  545. if self._current_playlist and self._current_playlist.has_next:
  546. cf_track = self._current_playlist.peek_next()
  547. elif self.queue.has_next:
  548. cf_next_item = self.queue.peek_next()
  549. cf_track = cf_next_item.track if cf_next_item else None
  550. if cf_track and cf_track.path:
  551. cf_decoder = self.decoders.get_best_decoder(
  552. path=cf_track.path,
  553. extension=cf_track.metadata.format,
  554. )
  555. if cf_decoder:
  556. next_gen = cf_decoder.decode_stream(cf_track.path, chunk_duration_ms=100)
  557. cf_chunk_index = 0
  558. pdebug(f"MusicPlayer: Crossfade '{track.title}' -> '{cf_track.title}'")
  559. # Chunks holen
  560. current_chunk = await anext(current_gen, None)
  561. next_chunk = None
  562. if next_gen is not None:
  563. next_chunk = await anext(next_gen, None)
  564. # Beide Tracks fertig?
  565. if current_chunk is None and next_chunk is None:
  566. break
  567. # Crossfade-Logik
  568. if next_gen is not None and next_chunk is not None:
  569. if cf_chunk_index < cf_total_chunks and current_chunk is not None:
  570. # Mischen
  571. mix_factor = cf_chunk_index / cf_total_chunks
  572. output_data = self._mix_chunks(current_chunk.pcm_data, next_chunk.pcm_data, mix_factor)
  573. if cf_chunk_index % 10 == 0:
  574. pdebug(f"MusicPlayer: Crossfade {cf_chunk_index}/{cf_total_chunks}, mix={mix_factor:.2f}")
  575. else:
  576. # Nur neuer Track
  577. output_data = next_chunk.pcm_data
  578. # Crossfade abgeschlossen
  579. if cf_chunk_index == cf_total_chunks:
  580. pdebug("MusicPlayer: Crossfade abgeschlossen")
  581. await self._emit_event(PlaybackEvent.TRACK_ENDED, {"track": track.to_dict()})
  582. await self._emit_event(PlaybackEvent.TRACK_CHANGED, {
  583. "previous_track": track.to_dict(),
  584. "current_track": cf_track.to_dict(),
  585. })
  586. await self._emit_event(PlaybackEvent.TRACK_STARTED, {"track": cf_track.to_dict()})
  587. # Playlist-Position konsumieren
  588. if self._current_playlist and self._current_playlist.has_next:
  589. self._current_playlist.next()
  590. # Track wechseln
  591. track = cf_track
  592. next_track = None
  593. if self._current_playlist and self._current_playlist.has_next:
  594. next_track = self._current_playlist.peek_next()
  595. self._duration_ms = track.duration_ms
  596. self._position_ms = 0
  597. # Alten Generator vergessen, neuer ist jetzt current
  598. current_gen = next_gen
  599. next_gen = None
  600. cf_track = None
  601. cf_chunk_index += 1
  602. elif current_chunk is not None:
  603. # Normaler Modus (kein Crossfade)
  604. # Durch Processing-Pipeline (Ducking, Equalizer, etc.)
  605. processing_context = AudioProcessingContext(
  606. audio_type=AudioType.MUSIC,
  607. position_ms=self._position_ms,
  608. duration_ms=self._duration_ms,
  609. chunk_duration_ms=100,
  610. track=track,
  611. next_track=next_track,
  612. sample_rate=self.config.sample_rate,
  613. channels=self.config.channels,
  614. volume=self._volume,
  615. is_muted=self._muted,
  616. satellite_ids=satellite_ids or [],
  617. )
  618. output_data = self._processing_pipeline.process(
  619. current_chunk.pcm_data,
  620. processing_context
  621. )
  622. # Chunk in Crossfade-Buffer speichern
  623. if crossfade_ms > 0:
  624. self._crossfade_buffer.append(output_data)
  625. else:
  626. # Aktueller Track fertig, aber Crossfade läuft noch
  627. if next_chunk is not None:
  628. output_data = next_chunk.pcm_data
  629. cf_chunk_index += 1
  630. else:
  631. break
  632. # Pause-Handling
  633. while self._state == PlayerState.PAUSED:
  634. await asyncio.sleep(0.1)
  635. if self._stop_event.is_set():
  636. break
  637. if self._stop_event.is_set():
  638. break
  639. # Lautstärke
  640. if not self._muted and self._volume < 1.0:
  641. output_data = self._apply_volume(output_data, self._volume)
  642. # Senden
  643. send_chunk = DecodedAudio(
  644. pcm_data=output_data,
  645. sample_rate=self.config.sample_rate,
  646. channels=self.config.channels,
  647. )
  648. await self._streamer.feed_decoded(send_chunk, satellite_ids)
  649. # Position
  650. self._position_ms += 100
  651. chunk_count += 1
  652. if chunk_count % 100 == 0:
  653. pdebug(f"MusicPlayer: Chunk {chunk_count}, buffer={len(self._crossfade_buffer)}/{crossfade_chunks_needed}")
  654. # Wall-Clock Sleep: nur die verbleibende Zeit schlafen
  655. next_chunk_time += chunk_interval
  656. sleep_time = next_chunk_time - time.monotonic()
  657. if sleep_time > 0:
  658. await asyncio.sleep(sleep_time)
  659. elif sleep_time < -0.5:
  660. # Mehr als 500ms hinterher - Timing zurücksetzen
  661. next_chunk_time = time.monotonic()
  662. pdebug(f"MusicPlayer: Track '{track.title}' beendet nach {self._position_ms}ms")
  663. # Pipeline über Track-Ende informieren
  664. end_context = AudioProcessingContext(
  665. audio_type=AudioType.MUSIC,
  666. position_ms=self._position_ms,
  667. duration_ms=self._duration_ms,
  668. track=track,
  669. next_track=next_track,
  670. sample_rate=self.config.sample_rate,
  671. channels=self.config.channels,
  672. )
  673. self._processing_pipeline.notify_track_end(end_context)
  674. # Track beendet
  675. # WICHTIG: Task als beendet markieren, BEVOR next() aufgerufen wird,
  676. # damit play() nicht versucht den eigenen Task zu canceln
  677. self._playback_task = None
  678. await self._emit_event(PlaybackEvent.TRACK_ENDED, {"track": track.to_dict()})
  679. if self.config.auto_play_next and not self._stop_event.is_set():
  680. await self.next(use_crossfade=False)
  681. except asyncio.CancelledError:
  682. pdebug("MusicPlayer: Playback-Task wurde abgebrochen (CancelledError)")
  683. except Exception as e:
  684. pdebug(f"MusicPlayer: Exception in _playback_loop: {e}")
  685. self.logger.error(f"Playback-Fehler: {e}")
  686. self._state = PlayerState.ERROR
  687. await self._emit_event(PlaybackEvent.ERROR, {"error": str(e)})
  688. def _apply_volume(self, pcm_data: bytes, volume: float) -> bytes:
  689. """
  690. Wendet Lautstärke auf PCM-Daten an.
  691. Args:
  692. pcm_data: 16-bit PCM-Daten
  693. volume: Lautstärke (0.0 - 1.0)
  694. Returns:
  695. Modifizierte PCM-Daten
  696. """
  697. import struct
  698. if volume >= 1.0:
  699. return pcm_data
  700. samples = struct.unpack(f"{len(pcm_data)//2}h", pcm_data)
  701. adjusted = [int(s * volume) for s in samples]
  702. # Clipping vermeiden
  703. adjusted = [max(-32768, min(32767, s)) for s in adjusted]
  704. return struct.pack(f"{len(adjusted)}h", *adjusted)
  705. def _mix_chunks(self, chunk_a: bytes, chunk_b: bytes, mix_factor: float) -> bytes:
  706. """
  707. Mischt zwei Audio-Chunks für Crossfade.
  708. Args:
  709. chunk_a: Erster Chunk (wird ausgeblendet)
  710. chunk_b: Zweiter Chunk (wird eingeblendet)
  711. mix_factor: 0.0 = nur A, 1.0 = nur B
  712. Returns:
  713. Gemischter Chunk
  714. """
  715. import struct
  716. # Chunks müssen gleich lang sein
  717. if len(chunk_a) != len(chunk_b):
  718. # Kürzer auf länger anpassen
  719. min_len = min(len(chunk_a), len(chunk_b))
  720. chunk_a = chunk_a[:min_len]
  721. chunk_b = chunk_b[:min_len]
  722. num_samples = len(chunk_a) // 2
  723. samples_a = struct.unpack(f"{num_samples}h", chunk_a)
  724. samples_b = struct.unpack(f"{num_samples}h", chunk_b)
  725. # Linear crossfade
  726. factor_a = 1.0 - mix_factor
  727. factor_b = mix_factor
  728. mixed = []
  729. for sa, sb in zip(samples_a, samples_b):
  730. sample = int(sa * factor_a + sb * factor_b)
  731. # Clipping vermeiden
  732. sample = max(-32768, min(32767, sample))
  733. mixed.append(sample)
  734. return struct.pack(f"{len(mixed)}h", *mixed)
  735. # ==========================================================================
  736. # Events
  737. # ==========================================================================
  738. def on_event(self, callback: Callable[[PlaybackEvent, dict], Awaitable[None]]) -> None:
  739. """Registriert Event-Callback."""
  740. self._event_callbacks.append(callback)
  741. def remove_event_callback(self, callback: Callable) -> None:
  742. """Entfernt Event-Callback."""
  743. if callback in self._event_callbacks:
  744. self._event_callbacks.remove(callback)
  745. async def _emit_event(self, event: PlaybackEvent, data: dict) -> None:
  746. """Emittiert Event an alle Callbacks."""
  747. for callback in self._event_callbacks:
  748. try:
  749. await callback(event, data)
  750. except Exception as e:
  751. self.logger.error(f"Event-Callback-Fehler: {e}")
  752. # ==========================================================================
  753. # Status
  754. # ==========================================================================
  755. def get_status(self) -> dict[str, Any]:
  756. """Liefert Player-Status."""
  757. return {
  758. "id": self.id,
  759. "state": self._state.value,
  760. "current_track": self.current_track.to_dict() if self.current_track else None,
  761. "position_ms": self._position_ms,
  762. "duration_ms": self._duration_ms,
  763. "progress": self.progress,
  764. "volume": self._volume,
  765. "muted": self._muted,
  766. "repeat_mode": self.repeat_mode.value,
  767. "shuffle_mode": self.shuffle_mode.value,
  768. "queue_count": self.queue.count,
  769. "playlist": self._current_playlist.name if self._current_playlist else None,
  770. "streaming": {
  771. "session_count": self._streamer.session_count,
  772. "is_streaming": self._streamer.is_streaming,
  773. },
  774. }
  775. def __repr__(self) -> str:
  776. return (
  777. f"MusicPlayer(id={self.id!r}, "
  778. f"state={self._state.value!r}, "
  779. f"track={self.current_track.title if self.current_track else None!r})"
  780. )