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 datetime import datetime, timedelta
from typing import Optional
from typing import Any, Optional
from PyQt6.QtCore import pyqtSignal, QObject, QThread
import numpy as np
@ -212,16 +212,8 @@ class AddFadeCurve(QObject):
self.finished.emit()
@helpers.singleton
class CurrentTrack(PlaylistTrack):
pass
@helpers.singleton
class NextTrack(PlaylistTrack):
pass
@helpers.singleton
class PreviousTrack(PlaylistTrack):
pass
cnp_tracks = dict(
CurrentTrack = PlaylistTrack(),
NextTrack = PlaylistTrack(),
PreviousTrack = PlaylistTrack(),
)

View File

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

View File

@ -14,12 +14,12 @@ from PyQt6.QtGui import (
QFont,
)
from classes import CurrentTrack, MusicMusterSignals, NextTrack
from classes import cnp_tracks, MusicMusterSignals, PlaylistTrack
from config import Config
from dbconfig import scoped_session, Session
from helpers import file_is_unreadable
from log import log
from models import PlaylistRows, Tracks
from models import Playdates, PlaylistRows, Tracks
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_playlist_signal.connect(self.add_track)
self.current_track = CurrentTrack()
self.next_track = NextTrack()
with Session() as session:
self.refresh_data(session)
@ -194,10 +191,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
# 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))
# 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))
# Individual cell colouring
@ -219,6 +216,66 @@ class PlaylistModel(QAbstractTableModel):
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):
"""Return data to view"""
@ -523,14 +580,14 @@ class PlaylistModel(QAbstractTableModel):
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:
self.next_track = NextTrack()
cnp_tracks['NextTrack'] = PlaylistTrack()
try:
plrid = self.playlist_rows[row_number].plrid
except IndexError:
@ -547,7 +604,7 @@ class PlaylistModel(QAbstractTableModel):
# Check track is readable
if file_is_unreadable(plr.track.path):
return
self.next_track.set_plr(session, plr)
cnp_tracks['NextTrack'].set_plr(session, plr)
self.signals.next_track_changed_signal.emit()
self.invalidate_row(row_number)

View File

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