WIP: implemented trackmanager, tracks play, clocks work

This commit is contained in:
Keith Edmunds 2024-06-02 11:57:45 +01:00
parent fbcedb6c3b
commit 5278b124ca
5 changed files with 216 additions and 189 deletions

View File

@ -48,6 +48,7 @@ class MusicMusterSignals(QObject):
show_warning_signal = pyqtSignal(str, str)
span_cells_signal = pyqtSignal(int, int, int, int, int)
status_message_signal = pyqtSignal(str, int)
track_ended_signal = pyqtSignal()
def __post_init__(self):
super().__init__()

View File

@ -47,7 +47,6 @@ from PyQt6.QtWidgets import (
)
# Third party imports
# from pygame import mixer
import pipeclient
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm.session import Session
@ -55,11 +54,7 @@ import stackprinter # type: ignore
# App imports
from classes import (
track_sequence,
FadeCurve,
MusicMusterSignals,
PlaylistTrack,
PreviewTrackPlayer,
TrackFileData,
)
from config import Config
@ -68,6 +63,11 @@ from log import log
from models import db, Carts, Playdates, PlaylistRows, Playlists, Settings, Tracks
from playlistmodel import PlaylistModel, PlaylistProxyModel
from playlists import PlaylistTab
from trackmanager import (
MainTrackManager,
PreviewTrackManager,
track_sequence,
)
from ui import icons_rc # noqa F401
from ui.dlg_cart_ui import Ui_DialogCartEdit # type: ignore
from ui.dlg_SelectPlaylist_ui import Ui_dlgSelectPlaylist # type: ignore
@ -75,7 +75,6 @@ from ui.downloadcsv_ui import Ui_DateSelect # type: ignore
from ui.main_window_ui import Ui_MainWindow # type: ignore
from utilities import check_db, update_bitrates
import helpers
import music
class CartButton(QPushButton):
@ -231,10 +230,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.timer500: QTimer = QTimer()
self.timer1000: QTimer = QTimer()
self.music: music.Music = music.Music(name=Config.VLC_MAIN_PLAYER_NAME)
self.preview_track_player: Optional[PreviewTrackPlayer] = None
self.playing: bool = False
self.preview_track_player: Optional[PreviewTrackManager] = None
self.set_main_window_size()
self.lblSumPlaytime = QLabel("")
@ -420,6 +416,7 @@ class Window(QMainWindow, Ui_MainWindow):
btn.setEnabled(True)
# Setting to position 0 doesn't seem to work
btn.player = self.music.VLC.media_player_new(btn.path)
MainTrackManager,
btn.player.audio_set_volume(Config.VLC_VOLUME_DEFAULT)
colour = Config.COLOUR_CART_READY
btn.setStyleSheet("background-color: " + colour + ";\n")
@ -449,7 +446,7 @@ class Window(QMainWindow, Ui_MainWindow):
return
# Don't allow window to close when a track is playing
if self.playing:
if track_sequence.current and track_sequence.current.is_playing():
event.ignore()
helpers.show_warning(
self, "Track playing", "Can't close application while track is playing"
@ -604,6 +601,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.signals.next_track_changed_signal.connect(self.update_headers)
self.signals.status_message_signal.connect(self.show_status_message)
self.signals.show_warning_signal.connect(self.show_warning)
self.signals.track_ended_signal.connect(self.end_of_track_actions)
self.timer10.timeout.connect(self.tick_10ms)
self.timer500.timeout.connect(self.tick_500ms)
@ -724,10 +722,8 @@ class Window(QMainWindow, Ui_MainWindow):
def drop3db(self) -> None:
"""Drop music level by 3db if button checked"""
if self.btnDrop3db.isChecked():
self.music.set_volume(Config.VLC_VOLUME_DROP3db, set_default=False)
else:
self.music.set_volume(Config.VLC_VOLUME_DEFAULT, set_default=False)
if track_sequence.current:
track_sequence.current.drop3db(self.btnDrop3db.isChecked())
def enable_escape(self, enabled: bool) -> None:
"""
@ -741,6 +737,38 @@ class Window(QMainWindow, Ui_MainWindow):
self.action_Clear_selection.setEnabled(enabled)
def end_of_track_actions(self) -> None:
"""
Actions required:
- Reset track_sequence objects
- Tell model track has finished
- Reset clocks
- Update headers
- Enable controls
"""
# Reset track_sequence objects
track_sequence.previous = track_sequence.current
track_sequence.current = None
# Tell model previous track has finished
self.active_proxy_model().previous_track_ended()
# Reset clocks
self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00 / 00:00")
self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00")
# Update headers
self.update_headers()
# Enable controls
self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0)
def export_playlist_tab(self) -> None:
"""Export the current playlist to an m3u file"""
@ -788,7 +816,8 @@ class Window(QMainWindow, Ui_MainWindow):
def fade(self) -> None:
"""Fade currently playing track"""
self.stop_playing(fade=True)
if track_sequence.current:
track_sequence.current.fade()
def hide_played(self):
"""Toggle hide played tracks"""
@ -1152,8 +1181,8 @@ class Window(QMainWindow, Ui_MainWindow):
# Suppress inadvertent double press
if (
track_sequence.current
and track_sequence.current.track_player.start_time
and track_sequence.current.track_player.start_time
and track_sequence.current.start_time
and track_sequence.current.start_time
+ dt.timedelta(milliseconds=Config.RETURN_KEY_DEBOUNCE_MS)
> dt.datetime.now()
):
@ -1161,16 +1190,10 @@ class Window(QMainWindow, Ui_MainWindow):
# If return is pressed during first PLAY_NEXT_GUARD_MS then
# default to NOT playing the next track, else default to
# playing it.
default_yes: bool = (
track_sequence.current.track_player.start_time is not None
and (
(
dt.datetime.now()
- track_sequence.current.track_player.start_time
).total_seconds()
* 1000
> Config.PLAY_NEXT_GUARD_MS
)
default_yes: bool = track_sequence.current.start_time is not None and (
(dt.datetime.now() - track_sequence.current.start_time).total_seconds()
* 1000
> Config.PLAY_NEXT_GUARD_MS
)
if not helpers.ask_yes_no(
"Track playing",
@ -1194,14 +1217,15 @@ class Window(QMainWindow, Ui_MainWindow):
# seconds of playback. Re-enabled tick_1000ms
self.timer10.stop()
self.show_status_message("10ms timer disabled", 0)
log.debug("10ms timer disabled", 0)
# If there's currently a track playing, fade it.
self.stop_playing(fade=True)
if track_sequence.current:
track_sequence.current.fade()
# Move next track to current track.
# stop_playing() above has called end_of_track_actions()
# which will have populated self.previous_track
# end_of_track_actions() will have saved current track to
# previous_track
track_sequence.current = track_sequence.next
# Clear next track
@ -1213,7 +1237,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False)
# Play (new) current track
track_sequence.current.track_player.play(position)
track_sequence.current.play(position)
# Disable play next controls
self.catch_return_key = True
@ -1251,7 +1275,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnPreview.setChecked(False)
return
with db.Session() as session:
self.preview_track_player = PreviewTrackPlayer(session, track_id)
self.preview_track_player = PreviewTrackManager(session, track_id)
self.preview_track_player.play()
else:
@ -1586,7 +1610,7 @@ class Window(QMainWindow, Ui_MainWindow):
self.statusbar.showMessage(message, timing)
def show_track(self, playlist_track: PlaylistTrack) -> None:
def show_track(self, playlist_track: MainTrackManager) -> None:
"""Scroll to show track in plt"""
# Switch to the correct tab
@ -1604,7 +1628,9 @@ class Window(QMainWindow, Ui_MainWindow):
display_row = (
self.active_proxy_model()
.mapFromSource(
self.active_proxy_model().source_model.index(playlist_track.row_number, 0)
self.active_proxy_model().source_model.index(
playlist_track.row_number, 0
)
)
.row()
)
@ -1640,66 +1666,8 @@ class Window(QMainWindow, Ui_MainWindow):
def stop(self) -> None:
"""Stop playing immediately"""
self.stop_playing(fade=False)
def stop_playing(self, fade: bool = True) -> None:
"""
Stop playing current track
Actions required:
- Set flag to say we're not playing a track
- Return if not playing
- Stop/fade track
- Reset playlist_tab colour
- Tell playlist_tab track has finished
- Reset PlaylistTrack objects
- Reset clocks
- Reset fade graph
- Update headers
- Enable controls
"""
# Set flag to say we're not playing a track so that timer ticks
# don't see player=None and kick off end-of-track actions
if self.playing:
self.playing = False
else:
# Return if not playing
log.info("stop_playing() called but not playing")
return
# Stop/fade track
track_sequence.now.resume_marker = self.music.get_position()
if fade:
self.music.fade()
else:
self.music.stop()
# Reset fade graph
if track_sequence.now.fade_graph:
track_sequence.now.fade_graph.clear()
# Reset track_sequence objects
if track_sequence.now.track_id:
track_sequence.previous = track_sequence.now
track_sequence.now = PlaylistTrack()
# Tell model previous track has finished
self.active_proxy_model().previous_track_ended()
# Reset clocks
self.frame_fade.setStyleSheet("")
self.frame_silent.setStyleSheet("")
self.label_elapsed_timer.setText("00:00 / 00:00")
self.label_fade_timer.setText("00:00")
self.label_silent_timer.setText("00:00")
# Update headers
self.update_headers()
# Enable controls
self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0)
if track_sequence.current:
track_sequence.current.stop()
def tab_change(self):
"""Called when active tab changed"""
@ -1743,11 +1711,15 @@ class Window(QMainWindow, Ui_MainWindow):
Called every 100ms
"""
if track_sequence.current:
track_sequence.current.check_for_end_of_track()
return
# Update intro counter if applicable and, if updated, return
# because playing an intro takes precedence over timing a
# preview.
if self.music.is_playing() and track_sequence.now.intro:
remaining_ms = track_sequence.now.intro - self.music.get_playtime()
if self.music.is_playing() and track_sequence.current.intro:
remaining_ms = track_sequence.current.intro - self.music.get_playtime()
if remaining_ms > 0:
self.label_intro_timer.setText(f"{remaining_ms / 1000:.1f}")
if remaining_ms <= Config.INTRO_SECONDS_WARNING_MS:
@ -1784,28 +1756,34 @@ class Window(QMainWindow, Ui_MainWindow):
# Only update play clocks once a second so that their updates
# are synchronised (otherwise it looks odd)
if not self.playing:
return
self.update_clocks()
def update_clocks(self) -> None:
"""
Update track clocks.
"""
# If track is playing, update track clocks time and colours
if self.music.player and self.music.player.is_playing():
playtime = self.music.get_playtime()
time_to_fade = track_sequence.now.fade_at - playtime
time_to_silence = track_sequence.now.silence_at - playtime
# see play_next() and issue #223
if playtime > 10000 and not self.timer10.isActive():
if track_sequence.current and track_sequence.current.is_playing():
# see play_next() and issue #223.
# TODO: find a better way of handling this
if (
track_sequence.current.time_playing() > 10000
and not self.timer10.isActive()
):
self.timer10.start(10)
self.show_status_message("10ms timer enabled", 0)
log.debug("10ms timer enabled")
# Elapsed time
self.label_elapsed_timer.setText(
helpers.ms_to_mmss(playtime)
helpers.ms_to_mmss(track_sequence.current.time_playing())
+ " / "
+ helpers.ms_to_mmss(track_sequence.now.duration)
+ helpers.ms_to_mmss(track_sequence.current.duration)
)
# Time to fade
time_to_fade = track_sequence.current.time_to_fade()
time_to_silence = track_sequence.current.time_to_silence()
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# If silent in the next 5 seconds, put warning colour on
@ -1816,11 +1794,13 @@ class Window(QMainWindow, Ui_MainWindow):
self.frame_silent.setStyleSheet(css_silence)
self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0)
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
css_fade = f"background: {Config.COLOUR_WARNING_TIMER}"
if self.frame_silent.styleSheet() != css_fade:
self.frame_silent.setStyleSheet(css_fade)
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE:
@ -1835,38 +1815,30 @@ class Window(QMainWindow, Ui_MainWindow):
self.label_silent_timer.setText(helpers.ms_to_mmss(time_to_silence))
# Autoplay next track
# if time_to_silence <= 1500:
# self.play_next()
else:
if self.playing:
self.stop_playing()
def update_headers(self) -> None:
"""
Update last / current / next track headers
"""
if track_sequence.previous:
player = track_sequence.previous.track_player
self.hdrPreviousTrack.setText(f"{player.title} - {player.artist}")
self.hdrPreviousTrack.setText(
f"{track_sequence.previous.title} - {track_sequence.previous.artist}"
)
else:
self.hdrPreviousTrack.setText("")
if track_sequence.current:
player = track_sequence.current.track_player
self.hdrCurrentTrack.setText(
f"{player.title.replace('&', '&&')} - "
f"{player.artist.replace('&', '&&')}"
f"{track_sequence.current.title.replace('&', '&&')} - "
f"{track_sequence.current.artist.replace('&', '&&')}"
)
else:
self.hdrCurrentTrack.setText("")
if track_sequence.next:
player = track_sequence.next.track_player
self.hdrNextTrack.setText(
f"{player.title.replace('&', '&&')} - "
f"{player.artist.replace('&', '&&')}"
f"{track_sequence.next.title.replace('&', '&&')} - "
f"{track_sequence.next.artist.replace('&', '&&')}"
)
else:
self.hdrNextTrack.setText("")

View File

@ -30,7 +30,7 @@ import obswebsocket # type: ignore
# import snoop # type: ignore
# App imports
from classes import Col, track_sequence, MusicMusterSignals, PlaylistTrack
from classes import Col, MusicMusterSignals
from config import Config
from helpers import (
file_is_unreadable,
@ -41,6 +41,10 @@ from helpers import (
)
from log import log
from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from trackmanager import (
MainTrackManager,
track_sequence,
)
HEADER_NOTES_COLUMN = 1
@ -684,7 +688,7 @@ class PlaylistModel(QAbstractTableModel):
end_time_str = ""
if (
track_sequence.current
and track_sequence.current.track_player.end_time
and track_sequence.current.end_time
and (
row_number
< track_sequence.current.row_number
@ -692,7 +696,7 @@ class PlaylistModel(QAbstractTableModel):
)
):
section_end_time = (
track_sequence.current.track_player.end_time
track_sequence.current.end_time
+ dt.timedelta(milliseconds=duration)
)
end_time_str = (
@ -1257,12 +1261,13 @@ class PlaylistModel(QAbstractTableModel):
)
return
try:
track_sequence.next = PlaylistTrack(prd.plrid)
self.invalidate_row(row_number)
except ValueError as e:
log.error(f"Error creating PlaylistTrack({prd=}): ({str(e)})")
return
with db.Session() as session:
try:
track_sequence.next = MainTrackManager(session, prd.plrid)
self.invalidate_row(row_number)
except ValueError as e:
log.error(f"Error creating PlaylistTrack({prd=}): ({str(e)})")
return
self.signals.search_wikipedia_signal.emit(
self.playlist_rows[row_number].title
@ -1555,9 +1560,9 @@ class PlaylistProxyModel(QSortFilterProxyModel):
and previous_plr.playlist_id
== self.source_model.playlist_id
):
if track_sequence.current.track_player.start_time:
if track_sequence.current.start_time:
if dt.datetime.now() > (
track_sequence.current.track_player.start_time
track_sequence.current.start_time
+ dt.timedelta(
milliseconds=Config.HIDE_AFTER_PLAYING_OFFSET
)

View File

@ -35,7 +35,7 @@ from PyQt6.QtWidgets import (
# Third party imports
# App imports
from classes import Col, MusicMusterSignals, track_sequence
from classes import Col, MusicMusterSignals
from config import Config
from dialogs import TrackSelectDialog
from helpers import (
@ -47,6 +47,7 @@ from helpers import (
from log import log
from models import db, Settings
from playlistmodel import PlaylistModel, PlaylistProxyModel
from trackmanager import track_sequence
if TYPE_CHECKING:
from musicmuster import Window

View File

@ -21,6 +21,7 @@ from PyQt6.QtCore import (
)
# App imports
from classes import MusicMusterSignals
from config import Config
from log import log
from models import db, PlaylistRows, Tracks
@ -185,6 +186,25 @@ class _Music:
else:
self.start_dt -= dt.timedelta(milliseconds=ms)
def stop(self) -> None:
"""Immediately stop playing"""
log.debug(f"Music[{self.name}].stop()")
self.start_dt = None
if not self.player:
return
p = self.player
self.player = None
self.start_dt = None
with lock:
p.stop()
p.release()
p = None
def fade(self, fade_seconds: int) -> None:
"""
Fade the currently playing track.
@ -193,8 +213,6 @@ class _Music:
to hold up the UI during the fade.
"""
log.info(f"Music[{self.name}].stop()")
if not self.player:
return
@ -202,7 +220,7 @@ class _Music:
return
if fade_seconds <= 0:
self._stop()
self.stop()
return
# Take a copy of current player to allow another track to be
@ -249,13 +267,10 @@ class _Music:
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
return (
self.start_dt is not None
and (
self.player.is_playing()
or (dt.datetime.now() - self.start_dt)
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
return self.start_dt is not None and (
self.player.is_playing()
or (dt.datetime.now() - self.start_dt)
< dt.timedelta(microseconds=Config.PLAY_SETTLE)
)
def move_back(self, ms: int) -> None:
@ -342,25 +357,6 @@ class _Music:
log.debug(f"Reset from {volume=}")
sleep(0.1)
def _stop(self) -> None:
"""Immediately stop playing"""
log.info(f"Music[{self.name}].stop()")
self.start_dt = None
if not self.player:
return
p = self.player
self.player = None
self.start_dt = None
with lock:
p.stop()
p.release()
p = None
class _TrackManager:
"""
@ -397,9 +393,10 @@ class _TrackManager:
self.resume_marker: Optional[float]
self.start_time: Optional[dt.datetime] = None
self.player = _Music(name=player_name)
self.signals = MusicMusterSignals()
# Initialise player
self.track_player = MainTrackManager(session=session, track_id=self.track_id)
self.player = _Music(name=player_name)
# Initialise and add FadeCurve in a thread as it's slow
self.fadecurve_thread = QThread()
@ -416,23 +413,50 @@ class _TrackManager:
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start()
def check_for_end_of_track(self) -> None:
"""
Check whether track has ended. If so, emit track_ended_signal
"""
if self.start_time is None:
return
if not self.player.is_playing():
self.start_time = None
self.signals.track_ended_signal.emit()
def drop3db(self, enable: bool) -> None:
"""
If enable is true, drop output by 3db else restore to full volume
"""
if enable:
self.player.set_volume(volume=Config.VLC_VOLUME_DROP3db, set_default=False)
else:
self.player.set_volume(volume=Config.VLC_VOLUME_DEFAULT, set_default=False)
def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
"""Fade music"""
self.player.fade(fade_seconds)
@property
def is_playing(self) -> bool:
return self.track_player.is_playing()
"""
Return True if we're currently playing else False
"""
if self.start_time is None:
return False
return self.player.is_playing()
def play(self, position: Optional[float] = None) -> None:
"""Play track"""
now = dt.datetime.now()
self.start_time = now
self.player.play(self.path, position)
now = dt.datetime.now()
self.start_time = now
self.end_time = self.start_time + dt.timedelta(milliseconds=self.duration)
# Calculate time fade_graph should start updating
@ -444,7 +468,7 @@ class _TrackManager:
milliseconds=update_graph_at_ms
)
def stop_playing(self, fade_seconds: int = 0) -> None:
def stop(self, fade_seconds: int = 0) -> None:
"""
Stop this track playing
"""
@ -456,41 +480,64 @@ class _TrackManager:
if self.fade_graph:
self.fade_graph.clear()
def time_playing(self) -> int:
"""
Return time track has been playing in milliseconds, zero if not playing
"""
if self.start_time is None:
return 0
return self.player.get_playtime()
def time_to_fade(self) -> int:
"""
Return milliseconds until fade time. Return zero if we're not playing.
"""
if not self.player.is_playing:
if self.start_time is None:
return 0
return self.fade_at - self.time_playing()
def time_to_silence(self) -> int:
"""
Return milliseconds until silent. Return zero if we're not playing.
"""
if self.start_time is None:
return 0
return self.silence_at - self.time_playing()
class MainTrackManager(_TrackManager):
"""
Manage playing tracks from the playlist with associated data
"""
def __init__(self, plr_id: int) -> None:
def __init__(self, session: db.Session, plr_id: int) -> None:
"""
Set up manager for playlist tracks
"""
with db.Session() as session:
# Ensure we have a track
plr = session.get(PlaylistRows, plr_id)
if not plr:
raise ValueError(f"PlaylistTrack: unable to retreive plr {plr_id=}")
# Ensure we have a track
plr = session.get(PlaylistRows, plr_id)
if not plr:
raise ValueError(f"PlaylistTrack: unable to retreive plr {plr_id=}")
self.track_id: int = plr.track_id
self.track_id: int = plr.track_id
super().__init__(
session=session, player_name=Config.VLC_MAIN_PLAYER_NAME, track_id=self.track_id
)
super().__init__(
session=session,
player_name=Config.VLC_MAIN_PLAYER_NAME,
track_id=self.track_id,
)
# Save non-track plr info
self.plr_id: int = plr.id
self.playlist_id: int = plr.playlist_id
self.row_number: int = plr.plr_rownum
# Save non-track plr info
self.plr_id: int = plr.id
self.playlist_id: int = plr.playlist_id
self.row_number: int = plr.plr_rownum
def __repr__(self) -> str:
return (
@ -504,8 +551,9 @@ class PreviewTrackManager(_TrackManager):
Manage previewing tracks
"""
def __init__(self, track_id: int) -> None:
def __init__(self, session: db.Session, track_id: int) -> None:
super().__init__(
session=session,
player_name=Config.VLC_PREVIEW_PLAYER_NAME,
track_id=track_id,
)