""" player.py — VLC-baseret afspiller med PyQt6 signals. Sender signals til GUI: position_changed(float) — 0.0–1.0 progress time_changed(int, int) — (current_sec, total_sec) levels_changed(float, float) — VU-meter L/R 0.0–1.0 song_ended() — sang færdig state_changed(str) — 'playing'|'paused'|'stopped' """ from PyQt6.QtCore import QObject, pyqtSignal, QTimer import random try: import vlc VLC_AVAILABLE = True except ImportError: VLC_AVAILABLE = False print("Advarsel: python-vlc ikke installeret — afspilning deaktiveret") class Player(QObject): position_changed = pyqtSignal(float) time_changed = pyqtSignal(int, int) levels_changed = pyqtSignal(float, float) song_ended = pyqtSignal() state_changed = pyqtSignal(str) def __init__(self, parent=None): super().__init__(parent) self._path: str | None = None self._duration: int = 0 self._demo_mode = False self._demo_stop_sec = 10 self._demo_fading = False self._volume = 78 if VLC_AVAILABLE: self._instance = vlc.Instance("--no-video", "--quiet") self._media_player = self._instance.media_player_new() self._events = self._media_player.event_manager() self._events.event_attach( vlc.EventType.MediaPlayerEndReached, self._on_end_reached, ) else: self._media_player = None # Timer til polling af position + VU-simulation self._poll_timer = QTimer(self) self._poll_timer.setInterval(80) self._poll_timer.timeout.connect(self._poll) # ── Indlæsning ──────────────────────────────────────────────────────────── def load(self, path: str, duration_sec: int = 0): """Indlæs en lydfil uden at starte afspilning.""" self._path = path self._duration = duration_sec self._demo_mode = False if VLC_AVAILABLE and self._media_player: media = self._instance.media_new(path) self._media_player.set_media(media) self._media_player.audio_set_volume(self._volume) self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("stopped") # ── Transport ───────────────────────────────────────────────────────────── def play(self): self._demo_mode = False if VLC_AVAILABLE and self._media_player: self._media_player.play() self._poll_timer.start() self.state_changed.emit("playing") def play_demo(self, stop_at_sec: int = 10): """Afspil fra start og stop automatisk ved stop_at_sec med 2 sek fade-out.""" self._demo_mode = True self._demo_stop_sec = stop_at_sec self._demo_fading = False if VLC_AVAILABLE and self._media_player: self._media_player.set_time(0) self._media_player.audio_set_volume(self._volume) self._media_player.play() self._poll_timer.start() self.state_changed.emit("playing") def pause(self): if VLC_AVAILABLE and self._media_player: self._media_player.pause() self.state_changed.emit("paused") def stop(self): self._demo_mode = False self._demo_fading = False if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(self._volume) self._media_player.stop() self._poll_timer.stop() self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("stopped") def is_playing(self) -> bool: if VLC_AVAILABLE and self._media_player: return self._media_player.is_playing() return False def set_volume(self, volume: int): """0–100""" self._volume = volume if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(volume) def set_position(self, fraction: float): """Søg til position 0.0–1.0""" if VLC_AVAILABLE and self._media_player: self._media_player.set_position(fraction) # ── Intern polling ──────────────────────────────────────────────────────── def _poll(self): """Køres ~12 gange per sekund — opdaterer position og VU-meter.""" if VLC_AVAILABLE and self._media_player: pos = self._media_player.get_position() ms = self._media_player.get_time() cur = max(0, ms // 1000) else: # Simuleret tilstand (til UI-test uden VLC) pos = getattr(self, "_sim_pos", 0.0) self._sim_pos = min(1.0, pos + 0.001) cur = int(self._sim_pos * self._duration) pos = self._sim_pos if self._sim_pos >= 1.0: self._on_end_reached(None) return self.position_changed.emit(pos) self.time_changed.emit(cur, self._duration) # Demo fade-out og stop if self._demo_mode and cur >= self._demo_stop_sec: # Færdig — gendan volumen og stop if VLC_AVAILABLE and self._media_player: self._media_player.audio_set_volume(self._volume) self.stop() self._demo_mode = False self._demo_fading = False self.position_changed.emit(0.0) self.time_changed.emit(0, self._duration) self.state_changed.emit("demo_ended") return # Demo fade-out — de sidste 2 sekunder FADE_SEC = 2.0 if self._demo_mode and VLC_AVAILABLE and self._media_player: secs_left = self._demo_stop_sec - cur if secs_left <= FADE_SEC and secs_left > 0: # Fade fra fuld volumen til 0 over FADE_SEC sekunder fade_fraction = secs_left / FADE_SEC # 1.0 → 0.0 faded_vol = int(self._volume * fade_fraction) self._media_player.audio_set_volume(max(0, faded_vol)) self._demo_fading = True elif not self._demo_fading: # Ikke i fade-zone endnu — sørg for fuld volumen self._media_player.audio_set_volume(self._volume) # VU-meter: brug VLC's audio-amplitude hvis tilgængelig, ellers simulér if VLC_AVAILABLE and self._media_player and self._media_player.is_playing(): # VLC eksponerer ikke amplitude direkte — vi bruger en blød simulation # der er baseret på position så det ser organisk ud base = 0.55 + 0.3 * abs(pos - 0.5) l = min(1.0, base + random.gauss(0, 0.12)) r = min(1.0, base + random.gauss(0, 0.12)) else: l = r = 0.0 self.levels_changed.emit(max(0.0, l), max(0.0, r)) def _on_end_reached(self, event): """Kaldes fra VLC's event-tråd — må IKKE røre Qt-objekter direkte.""" # QTimer.singleShot er thread-safe og sender alt til main thread from PyQt6.QtCore import QTimer as _QTimer _QTimer.singleShot(0, self._handle_end_in_main_thread) def _handle_end_in_main_thread(self): """Kaldes i main thread — her er det sikkert at røre Qt.""" self._poll_timer.stop() self.song_ended.emit() self.state_changed.emit("stopped")