Compare commits

...

4 Commits

Author SHA1 Message Date
Keith Edmunds
8956642e05 Remove unused import. 2026-01-04 13:58:57 +00:00
Keith Edmunds
791fad680a Clean up old error logging; log excessive jitter. 2026-01-04 13:54:14 +00:00
Keith Edmunds
8c60d6a03d Jitter monitor phase 0 2026-01-03 21:37:50 +00:00
Keith Edmunds
7391b4e61c Start header in column zero 2026-01-03 18:44:36 +00:00
5 changed files with 73 additions and 53 deletions

56
app/jittermonitor.py Normal file
View File

@ -0,0 +1,56 @@
from PyQt6.QtCore import QObject, QTimer, QElapsedTimer
import logging
import time
from config import Config
class EventLoopJitterMonitor(QObject):
def __init__(
self,
parent=None,
interval_ms: int = 20,
jitter_threshold_ms: int = 100,
log_cooldown_s: float = 1.0,
):
super().__init__(parent)
self._interval = interval_ms
self._jitter_threshold = jitter_threshold_ms
self._log_cooldown_s = log_cooldown_s
self._timer = QTimer(self)
self._timer.setInterval(self._interval)
self._timer.timeout.connect(self._on_timeout)
self._elapsed = QElapsedTimer()
self._elapsed.start()
self._last = self._elapsed.elapsed()
# child logger: e.g. "musicmuster.jitter"
self._log = logging.getLogger(f"{Config.LOG_NAME}.jitter")
self._last_log_time = 0.0
def start(self) -> None:
self._timer.start()
def _on_timeout(self) -> None:
now_ms = self._elapsed.elapsed()
delta = now_ms - self._last
self._last = now_ms
if delta > (self._interval + self._jitter_threshold):
self._log_jitter(now_ms, delta)
def _log_jitter(self, now_ms: int, gap_ms: int) -> None:
now = time.monotonic()
# simple rate limit: only one log every log_cooldown_s
if now - self._last_log_time < self._log_cooldown_s:
return
self._last_log_time = now
self._log.warning(
"Event loop gap detected: t=%d ms, gap=%d ms (interval=%d ms)",
now_ms,
gap_ms,
self._interval,
)

View File

@ -23,8 +23,8 @@ filters:
# - function-name-1 # - function-name-1
# - function-name-2 # - function-name-2
musicmuster: musicmuster:
- update_clocks
- play_next - play_next
jittermonitor: []
handlers: handlers:
stderr: stderr:

View File

@ -30,6 +30,7 @@ from log import log
from models import PlaylistRows from models import PlaylistRows
from vlcmanager import VLCManager from vlcmanager import VLCManager
# Define the VLC callback function type # Define the VLC callback function type
# import ctypes # import ctypes
# import platform # import platform
@ -353,21 +354,6 @@ class _Music:
self.player.set_position(position) self.player.set_position(position)
self.start_dt = start_time self.start_dt = start_time
# 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
# Pipewire for sound (which may be irrelevant).
# It has been known for the volume to need correcting more
# than once in the first 200mS.
# Update August 2024: This no longer seems to be an issue
# for _ in range(3):
# if self.player:
# volume = self.player.audio_get_volume()
# if volume < Config.VLC_VOLUME_DEFAULT:
# self.set_volume(Config.VLC_VOLUME_DEFAULT)
# log.error(f"Reset from {volume=}")
# sleep(0.1)
def set_position(self, position: float) -> None: def set_position(self, position: float) -> None:
""" """
Set player position Set player position
@ -391,17 +377,6 @@ class _Music:
volume = Config.VLC_VOLUME_DEFAULT volume = Config.VLC_VOLUME_DEFAULT
self.player.audio_set_volume(volume) self.player.audio_set_volume(volume)
# Ensure volume correct
# 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
# Pipewire for sound (which may be irrelevant).
for _ in range(3):
current_volume = self.player.audio_get_volume()
if current_volume < volume:
self.player.audio_set_volume(volume)
log.debug(f"Reset from {volume=}")
sleep(0.1)
def stop(self) -> None: def stop(self) -> None:
"""Immediately stop playing""" """Immediately stop playing"""

View File

@ -90,6 +90,7 @@ from ui.main_window_footer_ui import Ui_FooterSection # type: ignore
from utilities import check_db, update_bitrates from utilities import check_db, update_bitrates
import helpers import helpers
from jittermonitor import EventLoopJitterMonitor
class Current: class Current:
base_model: PlaylistModel base_model: PlaylistModel
@ -1206,6 +1207,16 @@ class Window(QMainWindow):
self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self) self.action_quicklog = QShortcut(QKeySequence("Ctrl+L"), self)
self.action_quicklog.activated.connect(self.quicklog) self.action_quicklog.activated.connect(self.quicklog)
# Jitter monitor - log delays in main event loop
self.jitter_monitor = EventLoopJitterMonitor(
parent=self,
interval_ms=20,
jitter_threshold_ms=100, # only care about >~100 ms
log_cooldown_s=1.0, # at most 1 warning per second
)
self.jitter_monitor.start()
self.load_last_playlists() self.load_last_playlists()
self.stop_autoplay = False self.stop_autoplay = False
@ -2206,14 +2217,6 @@ class Window(QMainWindow):
if self.return_pressed_in_error(): if self.return_pressed_in_error():
return return
# Issue #223 concerns a very short pause (maybe 0.1s) sometimes
# when starting to play at track. Resolution appears to be to
# disable timer10 for a short time. Timer is re-enabled in
# update_clocks.
self.timer10.stop()
log.debug("issue223: play_next: 10ms timer disabled")
# If there's currently a track playing, fade it. # If there's currently a track playing, fade it.
if track_sequence.current: if track_sequence.current:
track_sequence.current.fade() track_sequence.current.fade()
@ -2725,8 +2728,7 @@ class Window(QMainWindow):
# WARNING_MS_BEFORE_FADE milliseconds before fade starts, set # WARNING_MS_BEFORE_FADE milliseconds before fade starts, set
# warning colour on time to silence box and enable play # warning colour on time to silence box and enable play
# controls. This is also a good time to re-enable the 10ms # controls.
# timer (see play_next() and issue #223).
elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE: elif time_to_fade <= Config.WARNING_MS_BEFORE_FADE:
self.footer_section.frame_fade.setStyleSheet( self.footer_section.frame_fade.setStyleSheet(
@ -2734,11 +2736,6 @@ class Window(QMainWindow):
) )
self.catch_return_key = False self.catch_return_key = False
self.show_status_message("Play controls: Enabled", 0) self.show_status_message("Play controls: Enabled", 0)
# Re-enable 10ms timer (see above)
log.debug(f"issue287: {self.timer10.isActive()=}")
if not self.timer10.isActive():
self.timer10.start(10)
log.debug("issue223: update_clocks: 10ms timer enabled")
else: else:
self.footer_section.frame_silent.setStyleSheet("") self.footer_section.frame_silent.setStyleSheet("")

View File

@ -51,7 +51,7 @@ from models import db, NoteColours, Playdates, PlaylistRows, Tracks
from music_manager import RowAndTrack, track_sequence from music_manager import RowAndTrack, track_sequence
HEADER_NOTES_COLUMN = 1 HEADER_NOTES_COLUMN = 0
scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]") scene_change_re = re.compile(r"SetScene=\[([^[\]]*)\]")
@ -417,7 +417,7 @@ class PlaylistModel(QAbstractTableModel):
if column == HEADER_NOTES_COLUMN: if column == HEADER_NOTES_COLUMN:
column_span = 1 column_span = 1
if header_row: if header_row:
column_span = self.columnCount() - 1 column_span = self.columnCount() - HEADER_NOTES_COLUMN
self.signals.span_cells_signal.emit( self.signals.span_cells_signal.emit(
self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span self.playlist_id, row, HEADER_NOTES_COLUMN, 1, column_span
) )
@ -534,7 +534,7 @@ class PlaylistModel(QAbstractTableModel):
Col.ARTIST.value, Col.ARTIST.value,
Col.NOTE.value, Col.NOTE.value,
Col.INTRO.value, Col.INTRO.value,
]: ] or self.is_header_row(index.row()) and index.column() == HEADER_NOTES_COLUMN:
return default | Qt.ItemFlag.ItemIsEditable return default | Qt.ItemFlag.ItemIsEditable
return default return default
@ -765,8 +765,6 @@ class PlaylistModel(QAbstractTableModel):
Signal to view to refresh invalidated row Signal to view to refresh invalidated row
""" """
log.debug(f"issue285: {self}: invalidate_row({modified_row=})")
self.dataChanged.emit( self.dataChanged.emit(
self.index(modified_row, 0), self.index(modified_row, 0),
self.index(modified_row, self.columnCount() - 1), self.index(modified_row, self.columnCount() - 1),
@ -778,8 +776,6 @@ class PlaylistModel(QAbstractTableModel):
Signal to view to refresh invlidated rows Signal to view to refresh invlidated rows
""" """
log.debug(f"issue285: {self}: invalidate_rows({modified_rows=})")
for modified_row in modified_rows: for modified_row in modified_rows:
# only invalidate required roles # only invalidate required roles
self.invalidate_row(modified_row, roles) self.invalidate_row(modified_row, roles)
@ -1201,8 +1197,6 @@ class PlaylistModel(QAbstractTableModel):
looking up the playlistrow_id and retrieving the row number from the database. looking up the playlistrow_id and retrieving the row number from the database.
""" """
log.debug(f"issue285: {self}: reset_track_sequence_row_numbers()")
# Check the track_sequence.next, current and previous plrs and # Check the track_sequence.next, current and previous plrs and
# update the row number # update the row number
with db.Session() as session: with db.Session() as session:
@ -1667,8 +1661,6 @@ class PlaylistModel(QAbstractTableModel):
Update track start/end times in self.playlist_rows Update track start/end times in self.playlist_rows
""" """
log.debug(f"issue285: {self}: update_track_times()")
next_start_time: Optional[dt.datetime] = None next_start_time: Optional[dt.datetime] = None
update_rows: list[int] = [] update_rows: list[int] = []
row_count = len(self.playlist_rows) row_count = len(self.playlist_rows)