Compare commits

..

No commits in common. "a35905dee8861917dbce73da2cbd8514642b615e" and "4d3dc1fd00f0db9ff7d601e3e79cf1baa36562f1" have entirely different histories.

4 changed files with 122 additions and 213 deletions

View File

@ -1,35 +1,33 @@
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Optional
from typing import Optional
from PyQt6.QtCore import pyqtSignal, QObject, QThread
from PyQt6.QtCore import pyqtSignal, QObject
import numpy as np
import pyqtgraph as pg # type: ignore
from config import Config
from dbconfig import scoped_session, Session
from models import PlaylistRows, Tracks
from dbconfig import scoped_session
from models import PlaylistRows
import helpers
class FadeCurve:
GraphWidget = None
def __init__(
self, track_path: str, track_fade_at: int, track_silence_at: int
) -> None:
def __init__(self, track):
"""
Set up fade graph array
"""
audio = helpers.get_audio_segment(track_path)
audio = helpers.get_audio_segment(track.path)
if not audio:
return None
# Start point of curve is Config.FADE_CURVE_MS_BEFORE_FADE
# milliseconds before fade starts to silence
self.start_ms = max(0, track_fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track_silence_at
self.start_ms = max(0, track.fade_at - Config.FADE_CURVE_MS_BEFORE_FADE - 1)
self.end_ms = track.silence_at
self.audio_segment = audio[self.start_ms : self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
@ -108,7 +106,7 @@ class PlaylistTrack:
self.duration: Optional[int] = None
self.end_time: Optional[datetime] = None
self.fade_at: Optional[int] = None
self.fade_graph: Optional[FadeCurve] = None
self.fade_curve: Optional[FadeCurve] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
@ -141,6 +139,7 @@ class PlaylistTrack:
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.fade_graph = FadeCurve(track) # TODO: speed this line up
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
@ -154,22 +153,6 @@ class PlaylistTrack:
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
# Initialise and add FadeCurve in a thread as it's slow
# Import in separate thread
self.fadecurve_thread = QThread()
self.worker = AddFadeCurve(
self,
track_path=track.path,
track_fade_at=track.fade_at,
track_silence_at=track.silence_at,
)
self.worker.moveToThread(self.fadecurve_thread)
self.fadecurve_thread.started.connect(self.worker.run)
self.worker.finished.connect(self.fadecurve_thread.quit)
self.worker.finished.connect(self.worker.deleteLater)
self.fadecurve_thread.finished.connect(self.fadecurve_thread.deleteLater)
self.fadecurve_thread.start()
def start(self) -> None:
"""
Called when track starts playing
@ -180,40 +163,16 @@ class PlaylistTrack:
self.end_time = self.start_time + timedelta(milliseconds=self.duration)
class AddFadeCurve(QObject):
"""
Initialising a fade curve introduces a noticeable delay so carry out in
a thread.
"""
finished = pyqtSignal()
def __init__(
self,
playlist_track: PlaylistTrack,
track_path: str,
track_fade_at: int,
track_silence_at: int,
):
super().__init__()
self.playlist_track = playlist_track
self.track_path = track_path
self.track_fade_at = track_fade_at
self.track_silence_at = track_silence_at
def run(self):
"""
Create fade curve and add to PlaylistTrack object
"""
self.playlist_track.fade_graph = FadeCurve(
self.track_path, self.track_fade_at, self.track_silence_at
)
self.finished.emit()
@helpers.singleton
class CurrentTrack(PlaylistTrack):
pass
cnp_tracks = dict(
CurrentTrack = PlaylistTrack(),
NextTrack = PlaylistTrack(),
PreviousTrack = PlaylistTrack(),
)
@helpers.singleton
class NextTrack(PlaylistTrack):
pass
@helpers.singleton
class PreviousTrack(PlaylistTrack):
pass

View File

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

View File

@ -14,12 +14,12 @@ from PyQt6.QtGui import (
QFont,
)
from classes import cnp_tracks, MusicMusterSignals, PlaylistTrack
from classes import CurrentTrack, MusicMusterSignals, NextTrack
from config import Config
from dbconfig import scoped_session, Session
from helpers import file_is_unreadable
from log import log
from models import Playdates, PlaylistRows, Tracks
from models import PlaylistRows, Tracks
HEADER_NOTES_COLUMN = 1
@ -104,6 +104,9 @@ 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)
@ -191,10 +194,10 @@ class PlaylistModel(QAbstractTableModel):
if file_is_unreadable(prd.path):
return QBrush(QColor(Config.COLOUR_UNREADABLE))
# Current track
if prd.plrid == cnp_tracks['CurrentTrack'].plr_id:
if prd.plrid == self.current_track.plr_id:
return QBrush(QColor(Config.COLOUR_CURRENT_PLAYLIST))
# Next track
if prd.plrid == cnp_tracks['NextTrack'].plr_id:
if prd.plrid == self.next_track.plr_id:
return QBrush(QColor(Config.COLOUR_NEXT_PLAYLIST))
# Individual cell colouring
@ -216,66 +219,6 @@ 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"""
@ -580,14 +523,14 @@ class PlaylistModel(QAbstractTableModel):
return len(self.playlist_rows)
def set_next_row(self, row_number: int) -> None:
def set_next_track(self, row_number: int) -> None:
"""
Set row_number as next track
Update display if necessary
"""
# Update cnp_tracks
# Update self.next_track PlaylistTrack structure
with Session() as session:
cnp_tracks['NextTrack'] = PlaylistTrack()
self.next_track = NextTrack()
try:
plrid = self.playlist_rows[row_number].plrid
except IndexError:
@ -604,7 +547,7 @@ class PlaylistModel(QAbstractTableModel):
# Check track is readable
if file_is_unreadable(plr.track.path):
return
cnp_tracks['NextTrack'].set_plr(session, plr)
self.next_track.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_row(selected_row)
model.set_next_track(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"""