Added volume fade graph.

This commit is contained in:
Keith Edmunds 2023-06-18 09:20:55 +01:00
parent af0d715423
commit 4eb3a98c95
6 changed files with 18569 additions and 18498 deletions

View File

@ -48,6 +48,9 @@ class Config(object):
DISPLAY_SQL = False
ERRORS_FROM = ['noreply@midnighthax.com']
ERRORS_TO = ['kae@midnighthax.com']
FADE_CURVE_BACKGROUND = "lightyellow"
FADE_CURVE_FOREGROUND = "blue"
FADE_CURVE_MS_BEFORE_FADE = 5000
FADE_STEPS = 20
FADE_TIME = 3000
HIDE_AFTER_PLAYING_OFFSET = 5000
@ -77,7 +80,7 @@ class Config(object):
SCROLL_TOP_MARGIN = 3
TEXT_NO_TRACK_NO_NOTE = "[Section header]"
TOD_TIME_FORMAT = "%H:%M:%S"
TIMER_MS = 500
TIMER_MS = 100
TRACK_TIME_FORMAT = "%H:%M:%S"
VOLUME_VLC_DEFAULT = 75
VOLUME_VLC_DROP3db = 65

View File

@ -1,3 +1,4 @@
import numpy as np
import os
import psutil
import shutil

View File

@ -3,17 +3,17 @@
from log import log
from os.path import basename
import argparse
import matplotlib # type: ignore
import os
import numpy as np
import pyqtgraph as pg # type: ignore
import stackprinter # type: ignore
import subprocess
import sys
import threading
import icons_rc
from datetime import datetime, timedelta
from pygame import mixer
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg # type: ignore
from matplotlib.figure import Figure # type: ignore
from time import sleep
from typing import (
Callable,
@ -143,78 +143,63 @@ class CartButton(QPushButton):
self.pgb.setGeometry(0, 0, self.width(), 10)
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
class FadeCurve:
GraphWidget = None
def __init__(self) -> None:
def __init__(self, track):
"""
Only initialises data structure. Call set_plr to populate.
Do NOT store row_number here - that changes if tracks are reordered
in playlist (add, remove, drag/drop) and we shouldn't care about row
number: that's the playlist's problem.
Set up fade graph array
"""
self.artist: Optional[str] = None
self.duration: Optional[int] = None
self.end_time: Optional[datetime] = None
self.fade_at: Optional[int] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.playlist_tab: Optional[PlaylistTab] = None
self.plr_id: Optional[int] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
audio = helpers.get_audio_segment(track.path)
if not audio:
return None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"playlist_id={self.playlist_id}>"
# 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.audio_segment = audio[self.start_ms:self.end_ms]
self.graph_array = np.array(self.audio_segment.get_array_of_samples())
# Calculate the factor to map milliseconds of track to array
self.ms_to_array_factor = (
len(self.graph_array) / (self.end_ms - self.start_ms)
)
def set_plr(self, session: scoped_session, plr: PlaylistRows,
tab: PlaylistTab) -> None:
"""
Update with new plr information
"""
self.region = None
self.playlist_tab = tab
def clear(self) -> None:
"""Clear the current graph"""
session.add(plr)
track = plr.track
if self.GraphWidget:
self.GraphWidget.clear()
self.artist = track.artist
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
def plot(self):
self.curve = self.GraphWidget.plot(self.graph_array)
self.curve.setPen(Config.FADE_CURVE_FOREGROUND)
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
def tick(self, play_position) -> None:
"""Update volume fade curve"""
def start(self) -> None:
"""
Called when track starts playing
"""
if not self.GraphWidget:
return
self.start_time = datetime.now()
if self.duration:
self.end_time = (
self.start_time + timedelta(milliseconds=self.duration))
ms_of_graph = play_position - self.start_ms
if ms_of_graph < 0:
return
if self.region is None:
# Create the region now that we're into fade
self.region = pg.LinearRegionItem(
[0, 0],
bounds=[0, len(self.graph_array)]
)
self.GraphWidget.addItem(self.region)
# Update region position
self.region.setRegion([0, ms_of_graph * self.ms_to_array_factor])
class ImportTrack(QObject):
@ -253,17 +238,6 @@ class ImportTrack(QObject):
self.finished.emit(self.playlist)
class MplCanvas(FigureCanvasQTAgg):
"""
From https://www.pythonguis.com/tutorials/plotting-matplotlib/
"""
def __init__(self, parent=None, width=5, height=4, dpi=100):
fig = Figure(figsize=(width, height), dpi=dpi)
# self.axes = fig.add_subplot(111)
super(MplCanvas, self).__init__(fig)
class MusicMusterSignals(QObject):
"""
Class for all MusicMuster signals. See:
@ -275,6 +249,84 @@ class MusicMusterSignals(QObject):
set_next_track_signal = pyqtSignal(int, int)
class PlaylistTrack:
"""
Used to provide a single reference point for specific playlist tracks,
typically the previous, current and next track.
"""
def __init__(self) -> None:
"""
Only initialises data structure. Call set_plr to populate.
Do NOT store row_number here - that changes if tracks are reordered
in playlist (add, remove, drag/drop) and we shouldn't care about row
number: that's the playlist's problem.
"""
self.artist: Optional[str] = None
self.duration: Optional[int] = None
self.end_time: Optional[datetime] = None
self.fade_at: Optional[int] = None
self.fade_curve: Optional[FadeCurve] = None
self.fade_length: Optional[int] = None
self.path: Optional[str] = None
self.playlist_id: Optional[int] = None
self.playlist_tab: Optional[PlaylistTab] = None
self.plr_id: Optional[int] = None
self.silence_at: Optional[int] = None
self.start_gap: Optional[int] = None
self.start_time: Optional[datetime] = None
self.title: Optional[str] = None
self.track_id: Optional[int] = None
def __repr__(self) -> str:
return (
f"<PlaylistTrack(title={self.title}, artist={self.artist}, "
f"playlist_id={self.playlist_id}>"
)
def set_plr(self, session: scoped_session, plr: PlaylistRows,
tab: PlaylistTab) -> None:
"""
Update with new plr information
"""
if not plr.track:
return
self.playlist_tab = tab
session.add(plr)
track = plr.track
self.artist = track.artist
self.duration = track.duration
self.end_time = None
self.fade_at = track.fade_at
self.fade_graph = FadeCurve(track)
self.path = track.path
self.playlist_id = plr.playlist_id
self.plr_id = plr.id
self.silence_at = track.silence_at
self.start_gap = track.start_gap
self.start_time = None
self.title = track.title
self.track_id = track.id
if track.silence_at and track.fade_at:
self.fade_length = track.silence_at - track.fade_at
def start(self) -> None:
"""
Called when track starts playing
"""
self.start_time = datetime.now()
if self.duration:
self.end_time = (
self.start_time + timedelta(milliseconds=self.duration))
class Window(QMainWindow, Ui_MainWindow):
def __init__(self, parent=None, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
@ -303,7 +355,10 @@ class Window(QMainWindow, Ui_MainWindow):
self.txtSearch.setHidden(True)
self.hide_played_tracks = False
mixer.init()
matplotlib.use('QtAgg')
self.widgetFadeVolume.hideAxis('bottom')
self.widgetFadeVolume.hideAxis('left')
self.widgetFadeVolume.setBackground(Config.FADE_CURVE_BACKGROUND)
FadeCurve.GraphWidget = self.widgetFadeVolume
self.visible_playlist_tab: Callable[[], PlaylistTab] = \
self.tabPlaylist.currentWidget
@ -315,6 +370,7 @@ class Window(QMainWindow, Ui_MainWindow):
else:
self.carts_init()
self.enable_play_next_controls()
self.clock_counter = 0
self.timer.start(Config.TIMER_MS)
self.connect_signals_slots()
@ -766,6 +822,7 @@ class Window(QMainWindow, Ui_MainWindow):
- Tell playlist_tab track has finished
- Reset PlaylistTrack objects
- Reset clocks
- Reset fade graph
- Update headers
- Enable controls
"""
@ -778,6 +835,9 @@ class Window(QMainWindow, Ui_MainWindow):
if self.current_track.playlist_tab:
self.current_track.playlist_tab.play_ended()
# Reset fade graph
self.current_track.fade_graph.clear()
# Reset PlaylistTrack objects
if self.current_track.track_id:
self.previous_track = self.current_track
@ -1250,9 +1310,7 @@ class Window(QMainWindow, Ui_MainWindow):
sleep(0.1)
# Show closing volume graph
sc = MplCanvas(self, width=2, height=2, dpi=100)
# sc.axes.plot([0, 1, 2, 3, 4], [10, 1, 20, 3, 40])
self.horizontalLayout_graph.addWidget(sc)
self.current_track.fade_graph.plot()
# Tell database to record it as played
Playdates(session, self.current_track.track_id)
@ -1538,7 +1596,7 @@ class Window(QMainWindow, Ui_MainWindow):
def set_next_plr_id(self, next_plr_id: Optional[int],
playlist_tab: PlaylistTab) -> None:
"""
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:
- Update self.next_track PlaylistTrack structure
@ -1615,6 +1673,11 @@ class Window(QMainWindow, Ui_MainWindow):
"""
Carry out clock tick actions.
self.clock_counter is incrememted at each tick (100ms), and this
value is used to determine the actions to take.
The Fade Volume graph is updated every 100ms.
The Time of Day clock and any cart progress bars are updated
every tick (500ms).
@ -1624,6 +1687,7 @@ class Window(QMainWindow, Ui_MainWindow):
updating. That looks odd.
Actions required:
- Update Fade Volume graph
- Update TOD clock
- Call cart_tick
- If track is playing:
@ -1632,74 +1696,78 @@ class Window(QMainWindow, Ui_MainWindow):
run stop_track
"""
# Update TOD clock
self.lblTOD.setText(datetime.now().strftime(Config.TOD_TIME_FORMAT))
# Update carts
self.cart_tick()
# Update volume fade curve
if self.current_track.track_id and self.current_track.fade_graph:
self.current_track.fade_graph.tick(self.music.get_playtime())
self.even_tick = not self.even_tick
if not self.even_tick:
return
if not self.playing:
return
if self.clock_counter % 2 == 0:
# Update TOD clock
self.lblTOD.setText(datetime.now().strftime(
Config.TOD_TIME_FORMAT))
# Update carts
self.cart_tick()
# If track is playing, update track clocks time and colours
# There is a discrete time between starting playing a track and
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
if self.music.player and self.current_track.start_time and (
self.music.player.is_playing() or
(datetime.now() - self.current_track.start_time)
< timedelta(microseconds=Config.PLAY_SETTLE)):
playtime = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime)
time_to_silence = (
self.current_track.silence_at - playtime)
time_to_end = (self.current_track.duration - playtime)
if self.clock_counter % 5 == 0:
if not self.playing:
return
# Elapsed time
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
# If track is playing, update track clocks time and colours
# There is a discrete time between starting playing a track and
# player.is_playing() returning True, so assume playing if less
# than Config.PLAY_SETTLE microseconds have passed since
# starting play.
if self.music.player and self.current_track.start_time and (
self.music.player.is_playing() or
(datetime.now() - self.current_track.start_time)
< timedelta(microseconds=Config.PLAY_SETTLE)):
playtime = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime)
time_to_silence = (
self.current_track.silence_at - playtime)
time_to_end = (self.current_track.duration - playtime)
# Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# Elapsed time
self.label_elapsed_timer.setText(helpers.ms_to_mmss(playtime))
# If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls
if time_to_silence <= 5500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
# Time to fade
self.label_fade_timer.setText(helpers.ms_to_mmss(time_to_fade))
# If silent in the next 5 seconds, put warning colour on
# time to silence box and enable play controls
if time_to_silence <= 5500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_ENDING_TIMER}"
)
self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
self.enable_play_next_controls()
else:
self.frame_silent.setStyleSheet("")
self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText(
helpers.ms_to_mmss(time_to_silence)
)
self.enable_play_next_controls()
# Set warning colour on time to silence box when fade starts
elif time_to_fade <= 500:
self.frame_silent.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
# Five seconds before fade starts, set warning colour on
# time to silence box and enable play controls
elif time_to_fade <= 5500:
self.frame_fade.setStyleSheet(
f"background: {Config.COLOUR_WARNING_TIMER}"
)
self.enable_play_next_controls()
# Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
# Autoplay next track
# if time_to_silence <= 1500:
# self.play_next()
else:
self.frame_silent.setStyleSheet("")
self.frame_fade.setStyleSheet("")
self.label_silent_timer.setText(
helpers.ms_to_mmss(time_to_silence)
)
# Time to end
self.label_end_timer.setText(helpers.ms_to_mmss(time_to_end))
# Autoplay next track
# if time_to_silence <= 1500:
# self.play_next()
else:
if self.playing:
self.stop_playing()
if self.playing:
self.stop_playing()
def update_headers(self) -> None:
"""

File diff suppressed because it is too large Load Diff

View File

@ -349,6 +349,12 @@ padding-left: 8px;</string>
</item>
<item row="5" column="0">
<widget class="QFrame" name="InfoFooterFrame">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>16777215</height>
</size>
</property>
<property name="styleSheet">
<string notr="true">background-color: rgb(192, 191, 188)</string>
</property>
@ -440,18 +446,11 @@ padding-left: 8px;</string>
</layout>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_graph">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
</layout>
</item>
<item>
<widget class="QFrame" name="frame_elapsed">
<property name="minimumSize">
<size>
<width>0</width>
<width>152</width>
<height>112</height>
</size>
</property>
@ -503,7 +502,7 @@ padding-left: 8px;</string>
<widget class="QFrame" name="frame_fade">
<property name="minimumSize">
<size>
<width>0</width>
<width>152</width>
<height>112</height>
</size>
</property>
@ -549,10 +548,26 @@ padding-left: 8px;</string>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_silent">
<widget class="PlotWidget" name="widgetFadeVolume" native="true">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</widget>
</item>
<item>
<widget class="QFrame" name="frame_silent">
<property name="minimumSize">
<size>
<width>152</width>
<height>112</height>
</size>
</property>
@ -601,7 +616,7 @@ padding-left: 8px;</string>
<widget class="QFrame" name="frame_end">
<property name="minimumSize">
<size>
<width>0</width>
<width>152</width>
<height>112</height>
</size>
</property>
@ -1155,6 +1170,12 @@ padding-left: 8px;</string>
<header>infotabs</header>
<container>1</container>
</customwidget>
<customwidget>
<class>PlotWidget</class>
<extends>QWidget</extends>
<header>pyqtgraph</header>
<container>1</container>
</customwidget>
</customwidgets>
<resources>
<include location="icons.qrc"/>

View File

@ -179,6 +179,7 @@ class Ui_MainWindow(object):
self.tabInfolist.setObjectName("tabInfolist")
self.gridLayout_4.addWidget(self.splitter, 4, 0, 1, 1)
self.InfoFooterFrame = QtWidgets.QFrame(parent=self.centralwidget)
self.InfoFooterFrame.setMaximumSize(QtCore.QSize(16777215, 16777215))
self.InfoFooterFrame.setStyleSheet("background-color: rgb(192, 191, 188)")
self.InfoFooterFrame.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.InfoFooterFrame.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
@ -214,11 +215,8 @@ class Ui_MainWindow(object):
self.btnPreview.setObjectName("btnPreview")
self.gridLayout.addWidget(self.btnPreview, 1, 0, 1, 2)
self.horizontalLayout.addWidget(self.FadeStopInfoFrame)
self.horizontalLayout_graph = QtWidgets.QHBoxLayout()
self.horizontalLayout_graph.setObjectName("horizontalLayout_graph")
self.horizontalLayout.addLayout(self.horizontalLayout_graph)
self.frame_elapsed = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_elapsed.setMinimumSize(QtCore.QSize(0, 112))
self.frame_elapsed.setMinimumSize(QtCore.QSize(152, 112))
self.frame_elapsed.setStyleSheet("")
self.frame_elapsed.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_elapsed.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
@ -242,7 +240,7 @@ class Ui_MainWindow(object):
self.verticalLayout_4.addWidget(self.label_elapsed_timer)
self.horizontalLayout.addWidget(self.frame_elapsed)
self.frame_fade = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_fade.setMinimumSize(QtCore.QSize(0, 112))
self.frame_fade.setMinimumSize(QtCore.QSize(152, 112))
self.frame_fade.setStyleSheet("")
self.frame_fade.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_fade.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
@ -264,8 +262,17 @@ class Ui_MainWindow(object):
self.label_fade_timer.setObjectName("label_fade_timer")
self.verticalLayout_2.addWidget(self.label_fade_timer)
self.horizontalLayout.addWidget(self.frame_fade)
self.widgetFadeVolume = PlotWidget(parent=self.InfoFooterFrame)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Policy.Preferred, QtWidgets.QSizePolicy.Policy.Preferred)
sizePolicy.setHorizontalStretch(1)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.widgetFadeVolume.sizePolicy().hasHeightForWidth())
self.widgetFadeVolume.setSizePolicy(sizePolicy)
self.widgetFadeVolume.setMinimumSize(QtCore.QSize(0, 0))
self.widgetFadeVolume.setObjectName("widgetFadeVolume")
self.horizontalLayout.addWidget(self.widgetFadeVolume)
self.frame_silent = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_silent.setMinimumSize(QtCore.QSize(0, 112))
self.frame_silent.setMinimumSize(QtCore.QSize(152, 112))
self.frame_silent.setStyleSheet("")
self.frame_silent.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_silent.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
@ -288,7 +295,7 @@ class Ui_MainWindow(object):
self.verticalLayout_5.addWidget(self.label_silent_timer)
self.horizontalLayout.addWidget(self.frame_silent)
self.frame_end = QtWidgets.QFrame(parent=self.InfoFooterFrame)
self.frame_end.setMinimumSize(QtCore.QSize(0, 112))
self.frame_end.setMinimumSize(QtCore.QSize(152, 112))
self.frame_end.setStyleSheet("")
self.frame_end.setFrameShape(QtWidgets.QFrame.Shape.StyledPanel)
self.frame_end.setFrameShadow(QtWidgets.QFrame.Shadow.Raised)
@ -615,3 +622,4 @@ class Ui_MainWindow(object):
self.actionSearch_title_in_Songfacts.setText(_translate("MainWindow", "Search title in Songfacts"))
self.actionSearch_title_in_Songfacts.setShortcut(_translate("MainWindow", "Ctrl+S"))
from infotabs import InfoTabs
from pyqtgraph import PlotWidget