Use vlc events to trigger end-of-track actions

This commit is contained in:
Keith Edmunds 2025-04-19 12:26:44 +01:00
parent edd8c36c53
commit 223c7cd3ab
5 changed files with 76 additions and 15 deletions

View File

@ -270,15 +270,39 @@ def leading_silence(
return min(trim_ms, len(audio_segment))
def ms_to_mmss(ms: int | None, none: str = "-") -> str:
def ms_to_mmss(
ms: Optional[int],
decimals: int = 0,
negative: bool = False,
none: Optional[str] = None,
) -> str:
"""Convert milliseconds to mm:ss"""
if ms is None:
return none
minutes: int
remainder: int
seconds: float
minutes, seconds = divmod(ms // 1000, 60)
if not ms:
if none:
return none
else:
return "-"
sign = ""
if ms < 0:
if negative:
sign = "-"
else:
ms = 0
return f"{minutes}:{seconds:02d}"
minutes, remainder = divmod(ms, 60 * 1000)
seconds = remainder / 1000
# if seconds >= 59.5, it will be represented as 60, which looks odd.
# So, fake it under those circumstances
if seconds >= 59.5:
seconds = 59.0
return f"{sign}{minutes:.0f}:{seconds:02.{decimals}f}"
def normalise_track(path: str) -> None:

View File

@ -53,6 +53,7 @@ class _FadeTrack(QThread):
)
sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.stop()
self.finished.emit()
@ -80,9 +81,11 @@ class Music:
self.vlc_instance = vlc_manager.get_instance()
self.vlc_instance.set_user_agent(name, name)
self.player: vlc.MediaPlayer | None = None
self.vlc_event_manager: vlc.EventManager | None = None
self.max_volume: int = Config.VLC_VOLUME_DEFAULT
self.start_dt: dt.datetime | None = None
self.signals = MusicMusterSignals()
self.end_of_track_signalled = False
def fade(self, fade_seconds: int) -> None:
"""
@ -98,6 +101,8 @@ class Music:
if not self.player.get_position() > 0 and self.player.is_playing():
return
self.signal_track_ended()
self.fader_worker = _FadeTrack(self.player, fade_seconds=fade_seconds)
self.fader_worker.finished.connect(self.player.release)
self.fader_worker.start()
@ -142,10 +147,12 @@ class Music:
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
# @log_call
def play(
self,
path: str,
start_time: dt.datetime,
playlist_id: int,
position: float | None = None,
) -> None:
"""
@ -157,7 +164,7 @@ class Music:
the start time is the same
"""
log.debug(f"Music[{self.name}].play({path=}, {position=}")
self.playlist_id = playlist_id
if helpers.file_is_unreadable(path):
log.error(f"play({path}): path not readable")
@ -171,6 +178,10 @@ class Music:
)
return
self.events = self.player.event_manager()
self.events.event_attach(vlc.EventType.MediaPlayerEndReached, self.track_end_event_handler)
self.events.event_attach(vlc.EventType.MediaPlayerStopped, self.track_end_event_handler)
_ = self.player.play()
self.set_volume(self.max_volume)
@ -213,6 +224,21 @@ class Music:
log.debug(f"Reset from {volume=}")
sleep(0.1)
def signal_track_ended(self) -> None:
"""
Multiple parts of the Music class can signal that the track has
ended. Handle them all here to ensure that only one such signal
is raised. Make this thead safe.
"""
lock = threading.Lock()
with lock:
if self.end_of_track_signalled:
return
self.signals.track_ended_signal.emit(self.playlist_id)
self.end_of_track_signalled = True
def stop(self) -> None:
"""Immediately stop playing"""
@ -227,3 +253,11 @@ class Music:
self.player.stop()
self.player.release()
self.player = None
def track_end_event_handler(self, event: vlc.Event) -> None:
"""
Handler for MediaPlayerEndReached
"""
log.debug("track_end_event_handler() called")
self.signal_track_ended()

View File

@ -2563,8 +2563,6 @@ class Window(QMainWindow):
if self.track_sequence.current:
try:
self.track_sequence.current.check_for_end_of_track()
# Update intro counter if applicable and, if updated,
# return because playing an intro uses the intro field to
# show timing and this takes precedence over timing a

View File

@ -1057,7 +1057,7 @@ class PlaylistModel(QAbstractTableModel):
Remove track from row, retaining row as a header row
"""
self.playlist_rows[row_number].track_id = None
self.playlist_rows[row_number].track_id = 0
# only invalidate required roles
roles = [
@ -1349,13 +1349,13 @@ class PlaylistModel(QAbstractTableModel):
plr = self.playlist_rows[row_number]
if column == Col.NOTE.value:
plr.note = value
plr.note = str(value)
elif column == Col.TITLE.value:
plr.title = value
plr.title = str(value)
elif column == Col.ARTIST.value:
plr.artist = value
plr.artist = str(value)
elif column == Col.INTRO.value:
intro = int(round(float(value), 1) * 1000)

View File

@ -103,7 +103,7 @@ class PlaylistRow:
@property
def intro(self) -> int:
if self.dto.track:
return self.dto.track.intro
return self.dto.track.intro or 0
else:
return 0
@ -531,12 +531,17 @@ class TrackSequence:
"""
if self.current is None:
raise ApplicationError("Tried to move non-existent track from current to previous")
raise ApplicationError(
"Tried to move non-existent track from current to previous"
)
# Dereference the fade curve so it can be garbage collected
self.current.fade_graph = None
if self.current.fade_graph:
self.current.fade_graph.clear()
self.current.fade_graph = None
self.previous = self.current
self.current = None
self.start_time = None
def move_previous_to_next(self) -> None:
"""