230 lines
9.1 KiB
Python
230 lines
9.1 KiB
Python
"""
|
||
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
|
||
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):
|
||
"""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 _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")
|