WIP V3: play track working

This commit is contained in:
Keith Edmunds 2023-11-03 15:16:27 +00:00
parent bd2fa1cab0
commit a35905dee8
4 changed files with 157 additions and 115 deletions

View File

@ -1,6 +1,6 @@
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime, timedelta from datetime import datetime, timedelta
from typing import Optional from typing import Any, Optional
from PyQt6.QtCore import pyqtSignal, QObject, QThread from PyQt6.QtCore import pyqtSignal, QObject, QThread
import numpy as np import numpy as np
@ -212,16 +212,8 @@ class AddFadeCurve(QObject):
self.finished.emit() self.finished.emit()
@helpers.singleton cnp_tracks = dict(
class CurrentTrack(PlaylistTrack): CurrentTrack = PlaylistTrack(),
pass NextTrack = PlaylistTrack(),
PreviousTrack = PlaylistTrack(),
)
@helpers.singleton
class NextTrack(PlaylistTrack):
pass
@helpers.singleton
class PreviousTrack(PlaylistTrack):
pass

View File

@ -52,12 +52,10 @@ from sqlalchemy import text
import stackprinter # type: ignore import stackprinter # type: ignore
from classes import ( from classes import (
CurrentTrack, cnp_tracks,
FadeCurve, FadeCurve,
MusicMusterSignals, MusicMusterSignals,
NextTrack,
PlaylistTrack, PlaylistTrack,
PreviousTrack,
) )
from config import Config from config import Config
from dbconfig import ( from dbconfig import (
@ -199,10 +197,6 @@ class Window(QMainWindow, Ui_MainWindow):
self.music: music.Music = music.Music() self.music: music.Music = music.Music()
self.playing: bool = False self.playing: bool = False
self.current_track = CurrentTrack()
self.next_track = NextTrack()
self.previous_track = PreviousTrack()
self.previous_track_position: Optional[float] = None self.previous_track_position: Optional[float] = None
self.selected_plrs: Optional[List[PlaylistRows]] = None self.selected_plrs: Optional[List[PlaylistRows]] = None
@ -393,7 +387,7 @@ class Window(QMainWindow, Ui_MainWindow):
Clear next track Clear next track
""" """
self.next_track = NextTrack() cnp_tracks['NextTrack'] = PlaylistTrack()
self.update_headers() self.update_headers()
def clear_selection(self) -> None: def clear_selection(self) -> None:
@ -714,12 +708,13 @@ class Window(QMainWindow, Ui_MainWindow):
# self.current_track.playlist_tab.play_ended() # self.current_track.playlist_tab.play_ended()
# Reset fade graph # Reset fade graph
self.current_track.fade_graph.clear() if cnp_tracks['CurrentTrack'].fade_graph:
cnp_tracks['CurrentTrack'].fade_graph.clear()
# Reset PlaylistTrack objects # Reset PlaylistTrack objects
if self.current_track.track_id: if cnp_tracks['CurrentTrack'].track_id:
self.previous_track = cast(PreviousTrack, self.current_track) cnp_tracks['PreviousTrack'] = cnp_tracks['CurrentTrack']
self.current_track = CurrentTrack() cnp_tracks['CurrentTrack'] = PlaylistTrack()
# Reset clocks # Reset clocks
self.frame_fade.setStyleSheet("") self.frame_fade.setStyleSheet("")
@ -790,11 +785,11 @@ class Window(QMainWindow, Ui_MainWindow):
times a second; this function has much better resolution. times a second; this function has much better resolution.
""" """
if self.current_track.track_id is None or self.current_track.start_time is None: if cnp_tracks['CurrentTrack'].track_id is None or cnp_tracks['CurrentTrack'].start_time is None:
return 0 return 0
now = datetime.now() now = datetime.now()
track_start = self.current_track.start_time track_start = cnp_tracks['CurrentTrack'].start_time
elapsed_seconds = (now - track_start).total_seconds() elapsed_seconds = (now - track_start).total_seconds()
return int(elapsed_seconds * 1000) return int(elapsed_seconds * 1000)
@ -947,7 +942,7 @@ class Window(QMainWindow, Ui_MainWindow):
plrs_to_move = [ plrs_to_move = [
plr plr
for plr in playlistrows for plr in playlistrows
if plr.id not in [self.current_track.plr_id, self.next_track.plr_id] if plr.id not in [cnp_tracks['CurrentTrack'].plr_id, cnp_tracks['NextTrack'].plr_id]
] ]
rows_to_delete = [ rows_to_delete = [
@ -1143,21 +1138,24 @@ class Window(QMainWindow, Ui_MainWindow):
- If there is no next track set, return. - If there is no next track set, return.
- If there's currently a track playing, fade it. - If there's currently a track playing, fade it.
- Move next track to current track. - Move next track to current track.
- Ensure playlist tabs are the correct colour - Clear next track
- Restore volume if -3dB active - Restore volume if -3dB active
- Play (new) current track. - Play (new) current track.
- Tell database to record it as played - Ensure 100% volume
- Tell playlist track is now playing - Show closing volume graph
- Notify model
- Note that track is now playing - Note that track is now playing
- Disable play next controls - Disable play next controls
- Update headers - Update headers
- Update clocks
""" """
# If there is no next track set, return. # If there is no next track set, return.
if not self.next_track.track_id: if not cnp_tracks['NextTrack'].track_id:
log.debug("musicmuster.play_next(): no next track selected") log.debug("musicmuster.play_next(): no next track selected")
return return
if not cnp_tracks['NextTrack'].path:
log.debug("musicmuster.play_next(): no path for next track")
return
with Session() as session: with Session() as session:
# If there's currently a track playing, fade it. # If there's currently a track playing, fade it.
@ -1166,15 +1164,10 @@ class Window(QMainWindow, Ui_MainWindow):
# Move next track to current track. # Move next track to current track.
# stop_playing() above has called end_of_track_actions() # stop_playing() above has called end_of_track_actions()
# which will have populated self.previous_track # which will have populated self.previous_track
self.current_track = cast(CurrentTrack, self.next_track) cnp_tracks['CurrentTrack'] = cnp_tracks['NextTrack']
self.clear_next()
if not self.current_track.track_id: # Clear next track
log.debug("musicmuster.play_next(): no id for next track") self.clear_next()
return
if not self.current_track.path:
log.debug("musicmuster.play_next(): no path for next track")
return
# Set current track playlist_tab colour # Set current track playlist_tab colour
# TODO Reimplement without reference to self.current_track.playlist_tab # TODO Reimplement without reference to self.current_track.playlist_tab
@ -1187,8 +1180,12 @@ class Window(QMainWindow, Ui_MainWindow):
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Play (new) current track # Play (new) current track
self.current_track.start() if not cnp_tracks['CurrentTrack'].path:
self.music.play(self.current_track.path, position) return
cnp_tracks['CurrentTrack'].start()
self.music.play(cnp_tracks['CurrentTrack'].path, position)
# Ensure 100% volume
# For as-yet unknown reasons. sometimes the volume gets # For as-yet unknown reasons. sometimes the volume gets
# reset to zero within 200mS or so of starting play. This # reset to zero within 200mS or so of starting play. This
# only happened since moving to Debian 12, which uses # only happened since moving to Debian 12, which uses
@ -1203,15 +1200,11 @@ class Window(QMainWindow, Ui_MainWindow):
sleep(0.1) sleep(0.1)
# Show closing volume graph # Show closing volume graph
self.current_track.fade_graph.plot() if cnp_tracks['CurrentTrack'].fade_graph:
cnp_tracks['CurrentTrack'].fade_graph.plot()
# Tell database to record it as played # Notify model
Playdates(session, self.current_track.track_id) self.active_model().current_track_started()
# Tell playlist track is now playing
# TODO Reimplement without reference to self.current_track.playlist_tab:
# if self.current_track.playlist_tab:
# self.current_track.playlist_tab.play_started(session)
# Note that track is now playing # Note that track is now playing
self.playing = True self.playing = True
@ -1234,7 +1227,7 @@ class Window(QMainWindow, Ui_MainWindow):
track_path = self.active_tab().get_selected_row_track_path() track_path = self.active_tab().get_selected_row_track_path()
if not track_path: if not track_path:
# Otherwise get path to next track to play # Otherwise get path to next track to play
track_path = self.next_track.path track_path = cnp_tracks['NextTrack'].path
if not track_path: if not track_path:
self.btnPreview.setChecked(False) self.btnPreview.setChecked(False)
return return
@ -1531,7 +1524,7 @@ class Window(QMainWindow, Ui_MainWindow):
Set passed plr_id as next track to play, or clear next track if None Set passed plr_id as next track to play, or clear next track if None
Actions required: Actions required:
- Update self.next_track PlaylistTrack structure - Update cnp_tracks
- Tell playlist tabs to update their 'next track' highlighting - Tell playlist tabs to update their 'next track' highlighting
- Update headers - Update headers
- Set playlist tab colours - Set playlist tab colours
@ -1610,14 +1603,14 @@ class Window(QMainWindow, Ui_MainWindow):
# Update volume fade curve # Update volume fade curve
if ( if (
self.current_track.track_id cnp_tracks['CurrentTrack'].track_id
and self.current_track.fade_graph and cnp_tracks['CurrentTrack'].fade_graph
and self.current_track.start_time and cnp_tracks['CurrentTrack'].start_time
): ):
play_time = ( play_time = (
datetime.now() - self.current_track.start_time datetime.now() - cnp_tracks['CurrentTrack'].start_time
).total_seconds() * 1000 ).total_seconds() * 1000
self.current_track.fade_graph.tick(play_time) cnp_tracks['CurrentTrack'].fade_graph.tick(play_time)
def tick_500ms(self) -> None: def tick_500ms(self) -> None:
""" """
@ -1649,22 +1642,22 @@ class Window(QMainWindow, Ui_MainWindow):
# starting play. # starting play.
if ( if (
self.music.player self.music.player
and self.current_track.start_time and cnp_tracks['CurrentTrack'].start_time
and ( and (
self.music.player.is_playing() self.music.player.is_playing()
or (datetime.now() - self.current_track.start_time) or (datetime.now() - cnp_tracks['CurrentTrack'].start_time)
< timedelta(microseconds=Config.PLAY_SETTLE) < timedelta(microseconds=Config.PLAY_SETTLE)
) )
): ):
playtime = self.get_playtime() playtime = self.get_playtime()
time_to_fade = self.current_track.fade_at - playtime time_to_fade = cnp_tracks['CurrentTrack'].fade_at - playtime
time_to_silence = self.current_track.silence_at - playtime time_to_silence = cnp_tracks['CurrentTrack'].silence_at - playtime
# Elapsed time # Elapsed time
self.label_elapsed_timer.setText( self.label_elapsed_timer.setText(
helpers.ms_to_mmss(playtime) helpers.ms_to_mmss(playtime)
+ " / " + " / "
+ helpers.ms_to_mmss(self.current_track.duration) + helpers.ms_to_mmss(cnp_tracks['CurrentTrack'].duration)
) )
# Time to fade # Time to fade
@ -1707,25 +1700,25 @@ class Window(QMainWindow, Ui_MainWindow):
Update last / current / next track headers Update last / current / next track headers
""" """
if self.previous_track.title and self.previous_track.artist: if cnp_tracks['PreviousTrack'].title and cnp_tracks['PreviousTrack'].artist:
self.hdrPreviousTrack.setText( self.hdrPreviousTrack.setText(
f"{self.previous_track.title} - {self.previous_track.artist}" f"{cnp_tracks['PreviousTrack'].title} - {cnp_tracks['PreviousTrack'].artist}"
) )
else: else:
self.hdrPreviousTrack.setText("") self.hdrPreviousTrack.setText("")
if self.current_track.title and self.current_track.artist: if cnp_tracks['CurrentTrack'].title and cnp_tracks['CurrentTrack'].artist:
self.hdrCurrentTrack.setText( self.hdrCurrentTrack.setText(
f"{self.current_track.title.replace('&', '&&')} - " f"{cnp_tracks['CurrentTrack'].title.replace('&', '&&')} - "
f"{self.current_track.artist.replace('&', '&&')}" f"{cnp_tracks['CurrentTrack'].artist.replace('&', '&&')}"
) )
else: else:
self.hdrCurrentTrack.setText("") self.hdrCurrentTrack.setText("")
if self.next_track.title and self.next_track.artist: if cnp_tracks['NextTrack'].title and cnp_tracks['NextTrack'].artist:
self.hdrNextTrack.setText( self.hdrNextTrack.setText(
f"{self.next_track.title.replace('&', '&&')} - " f"{cnp_tracks['NextTrack'].title.replace('&', '&&')} - "
f"{self.next_track.artist.replace('&', '&&')}" f"{cnp_tracks['NextTrack'].artist.replace('&', '&&')}"
) )
else: else:
self.hdrNextTrack.setText("") self.hdrNextTrack.setText("")

View File

@ -14,12 +14,12 @@ from PyQt6.QtGui import (
QFont, QFont,
) )
from classes import CurrentTrack, MusicMusterSignals, NextTrack from classes import cnp_tracks, MusicMusterSignals, PlaylistTrack
from config import Config from config import Config
from dbconfig import scoped_session, Session from dbconfig import scoped_session, Session
from helpers import file_is_unreadable from helpers import file_is_unreadable
from log import log from log import log
from models import PlaylistRows, Tracks from models import Playdates, PlaylistRows, Tracks
HEADER_NOTES_COLUMN = 1 HEADER_NOTES_COLUMN = 1
@ -104,9 +104,6 @@ class PlaylistModel(QAbstractTableModel):
self.signals.add_track_to_header_signal.connect(self.add_track_to_header) self.signals.add_track_to_header_signal.connect(self.add_track_to_header)
self.signals.add_track_to_playlist_signal.connect(self.add_track) self.signals.add_track_to_playlist_signal.connect(self.add_track)
self.current_track = CurrentTrack()
self.next_track = NextTrack()
with Session() as session: with Session() as session:
self.refresh_data(session) self.refresh_data(session)
@ -194,10 +191,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path): if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE)) return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Current track # Current track
if prd.plrid == self.current_track.plr_id: if prd.plrid == cnp_tracks['CurrentTrack'].plr_id:
return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# Next track # Next track
if prd.plrid == self.next_track.plr_id: if prd.plrid == cnp_tracks['NextTrack'].plr_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST)) return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring # Individual cell colouring
@ -219,6 +216,66 @@ class PlaylistModel(QAbstractTableModel):
return 9 return 9
def current_track_started(self) -> None:
"""
Notification from musicmuster that the current track has just
started playing
Actions required:
- sanity check
- update display
- update track times
- update Playdates in database
- update PlaylistRows in database
- find next track
"""
# Sanity check
if not cnp_tracks['CurrentTrack'].track_id:
log.error(
"playlistmodel:current_track_started called with no current track"
)
return
if cnp_tracks['CurrentTrack'].plr_rownum is None:
log.error(
"playlistmodel:current_track_started called with no row number "
f"({self.current_track=})"
)
return
# Update display
self.invalidate_row(cnp_tracks['CurrentTrack'].plr_rownum)
# Update track times
# TODO
# Update Playdates in database
with Session() as session:
Playdates(session, cnp_tracks['CurrentTrack'].track_id)
plr = session.get(PlaylistRows, cnp_tracks['CurrentTrack'].plr_id)
if plr:
plr.played = True
# Find next track
# Get all unplayed track rows
next_row = None
unplayed_rows = [
a.plr_rownum
for a in PlaylistRows.get_unplayed_rows(session, self.playlist_id)
]
if unplayed_rows:
try:
# Find next row after current track
next_row = min(
[a for a in unplayed_rows if a > cnp_tracks['CurrentTrack'].plr_rownum]
)
except ValueError:
# Find first unplayed track
next_row = min(unplayed_rows)
if next_row is not None:
self.set_next_row(next_row)
def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole): def data(self, index: QModelIndex, role: int = Qt.ItemDataRole.DisplayRole):
"""Return data to view""" """Return data to view"""
@ -523,14 +580,14 @@ class PlaylistModel(QAbstractTableModel):
return len(self.playlist_rows) return len(self.playlist_rows)
def set_next_track(self, row_number: int) -> None: def set_next_row(self, row_number: int) -> None:
""" """
Update display if necessary Set row_number as next track
""" """
# Update self.next_track PlaylistTrack structure # Update cnp_tracks
with Session() as session: with Session() as session:
self.next_track = NextTrack() cnp_tracks['NextTrack'] = PlaylistTrack()
try: try:
plrid = self.playlist_rows[row_number].plrid plrid = self.playlist_rows[row_number].plrid
except IndexError: except IndexError:
@ -547,7 +604,7 @@ class PlaylistModel(QAbstractTableModel):
# Check track is readable # Check track is readable
if file_is_unreadable(plr.track.path): if file_is_unreadable(plr.track.path):
return return
self.next_track.set_plr(session, plr) cnp_tracks['NextTrack'].set_plr(session, plr)
self.signals.next_track_changed_signal.emit() self.signals.next_track_changed_signal.emit()
self.invalidate_row(row_number) self.invalidate_row(row_number)

View File

@ -1028,7 +1028,7 @@ class PlaylistTab(QTableView):
if selected_row is None: if selected_row is None:
return return
model = cast(PlaylistModel, self.model()) model = cast(PlaylistModel, self.model())
model.set_next_track(selected_row) model.set_next_row(selected_row)
self.clearSelection() self.clearSelection()
# def tab_visible(self) -> None: # def tab_visible(self) -> None:
@ -1266,41 +1266,41 @@ class PlaylistTab(QTableView):
self._update_start_end_times(session) self._update_start_end_times(session)
def _find_next_track_row( # def _find_next_track_row(
self, session: scoped_session, starting_row: Optional[int] = None # self, session: scoped_session, starting_row: Optional[int] = None
) -> Optional[int]: # ) -> Optional[int]:
""" # """
Find next track to play. If a starting row is given, start there; # Find next track to play. If a starting row is given, start there;
otherwise, start from top. Skip rows already played. # otherwise, start from top. Skip rows already played.
If not found, return None. # If not found, return None.
If found, return row number. # If found, return row number.
""" # """
if starting_row is None: # if starting_row is None:
starting_row = 0 # starting_row = 0
track_rows = [ # track_rows = [
p.plr_rownum # p.plr_rownum
for p in PlaylistRows.get_rows_with_tracks(session, self.playlist_id) # for p in PlaylistRows.get_rows_with_tracks(session, self.playlist_id)
] # ]
played_rows = [ # played_rows = [
p.plr_rownum # p.plr_rownum
for p in PlaylistRows.get_played_rows(session, self.playlist_id) # for p in PlaylistRows.get_played_rows(session, self.playlist_id)
] # ]
for row_number in range(starting_row, self.rowCount()): # for row_number in range(starting_row, self.rowCount()):
if row_number not in track_rows or row_number in played_rows: # if row_number not in track_rows or row_number in played_rows:
continue # continue
plr = self._get_row_plr(session, row_number) # plr = self._get_row_plr(session, row_number)
if not plr: # if not plr:
continue # continue
if file_is_unreadable(plr.track.path): # if file_is_unreadable(plr.track.path):
continue # continue
else: # else:
return row_number # return row_number
return None # return None
def _get_current_track_row_number(self) -> Optional[int]: def _get_current_track_row_number(self) -> Optional[int]:
"""Return current track row or None""" """Return current track row or None"""