Files
LinedanceAfspiller/linedance-app/player/player.py
2026-04-12 10:25:41 +02:00

230 lines
9.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
player.py — VLC-baseret afspiller med PyQt6 signals.
Sender signals til GUI:
position_changed(float) — 0.01.0 progress
time_changed(int, int) — (current_sec, total_sec)
levels_changed(float, float) — VU-meter L/R 0.01.0
song_ended() — sang færdig
state_changed(str) — 'playing'|'paused'|'stopped'
"""
from PyQt6.QtCore import QObject, pyqtSignal, QTimer
import random
import math
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_fade_sec = 5
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:
# Konverter GVFS SMB-sti til SMB URL som VLC kan tilgå direkte
vlc_path = self._resolve_path(path)
media = self._instance.media_new(vlc_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")
def _resolve_path(self, path: str) -> str:
"""Konverter platform-specifikke netværksstier til URL'er VLC kan bruge."""
import re, sys
# Linux GVFS SMB: /run/user/1000/gvfs/smb-share:server=X,share=Y/sti/fil.mp3
m = re.match(r".*/gvfs/smb-share:server=([^,]+),share=([^/]+)(/.+)$", path)
if m:
server, share, rest = m.group(1), m.group(2), m.group(3)
return f"smb://{server}/{share}{rest}"
# Windows UNC: \\server\share\sti\fil.mp3
if path.startswith("\\\\"):
# \\server\share\rest → smb://server/share/rest
parts = path.replace("\\", "/").lstrip("/").split("/", 2)
if len(parts) >= 2:
server = parts[0]
share = parts[1]
rest = "/" + parts[2] if len(parts) > 2 else ""
return f"smb://{server}/{share}{rest}"
# Lokale stier og drevbogstaver (C:\...) — VLC håndterer dem fint
return path
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, fade_sec: int = 5):
"""
Afspil fra start, fade ud over fade_sec sekunder og stop.
Total afspilningstid = stop_at_sec + fade_sec.
fade_sec=0 giver ingen fade.
"""
self._demo_mode = True
self._demo_stop_sec = stop_at_sec + fade_sec # total tid inkl. fade
self._demo_fade_sec = fade_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):
"""0100"""
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.01.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 _demo_fade_sec sekunder (0 = ingen fade)
if self._demo_mode and VLC_AVAILABLE and self._media_player and self._demo_fade_sec > 0:
secs_left = self._demo_stop_sec - cur
if secs_left <= self._demo_fade_sec and secs_left > 0:
fade_fraction = secs_left / self._demo_fade_sec # 1.0 → 0.0
log_fraction = math.log10(1 + fade_fraction * 9) / math.log10(10)
faded_vol = int(self._volume * log_fraction)
self._media_player.audio_set_volume(max(0, faded_vol))
self._demo_fading = True
elif not self._demo_fading:
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")