This commit is contained in:
2026-04-11 00:38:04 +02:00
parent 99f6a265c0
commit b678787236
48 changed files with 7884 additions and 0 deletions

View File

View File

@@ -0,0 +1,200 @@
"""
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:
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, 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")