Compare commits

..

3 Commits

Author SHA1 Message Date
Keith Edmunds
6e754c1b3a Make music fading more solid - issue #3 2021-06-10 17:55:55 +01:00
Keith Edmunds
a80dc3f165 Select and move (un)played tracks. Fixes #4 2021-06-10 15:24:31 +01:00
Keith Edmunds
73879c6a99 Add locking to music.py
Ensure nothing interrupts the stop - release - nullify sequence. Also
don't limit how many concurrent fades there can be.
2021-06-07 20:46:05 +01:00
4 changed files with 125 additions and 48 deletions

View File

@ -8,6 +8,8 @@ from time import sleep
from log import DEBUG, ERROR from log import DEBUG, ERROR
lock = threading.Lock()
class Music: class Music:
""" """
@ -16,7 +18,7 @@ class Music:
def __init__(self): def __init__(self):
self.current_track_start_time = None self.current_track_start_time = None
self.fading = False self.fading = 0
self.VLC = vlc.Instance() self.VLC = vlc.Instance()
self.player = None self.player = None
self.track_path = None self.track_path = None
@ -32,13 +34,13 @@ class Music:
DEBUG("music.fade()", True) DEBUG("music.fade()", True)
if not self.playing(): if not self.player:
return None
# Only allow one track to fade at a time
if self.fading:
return return
self.fading = True
if not self.player.get_position() > 0 and self.player.is_playing():
return
self.fading += 1
thread = threading.Thread(target=self._fade) thread = threading.Thread(target=self._fade)
thread.start() thread.start()
@ -51,10 +53,14 @@ class Music:
# Take a copy of current player to allow another track to be # Take a copy of current player to allow another track to be
# started without interfering here # started without interfering here
p = self.player
DEBUG(f"music._fade(), {self.player=}", True) DEBUG(f"music._fade(), {self.player=}", True)
with lock:
p = self.player
self.player = None
DEBUG(f"music._fade() post-lock, {self.player=}", True)
fade_time = Config.FADE_TIME / 1000 fade_time = Config.FADE_TIME / 1000
steps = Config.FADE_STEPS steps = Config.FADE_STEPS
sleep_time = fade_time / steps sleep_time = fade_time / steps
@ -69,27 +75,38 @@ class Music:
measures_to_reduce_by = 0 measures_to_reduce_by = 0
for i in range(1, steps + 1): for i in range(1, steps + 1):
measures_to_reduce_by += i measures_to_reduce_by += i
volume_factor = 1 - (measures_to_reduce_by / total_measures_count) volume_factor = 1 - (
measures_to_reduce_by / total_measures_count)
p.audio_set_volume(int(self.max_volume * volume_factor)) p.audio_set_volume(int(self.max_volume * volume_factor))
sleep(sleep_time) sleep(sleep_time)
self.stop(p) with lock:
self.fading = False DEBUG(f"music._facde(), stopping {p=}", True)
p.stop()
DEBUG(f"Releasing player {p=}", True)
p.release()
# Ensure we don't reference player after release
p = None
self.fading -= 1
def get_playtime(self): def get_playtime(self):
"Return elapsed play time" "Return elapsed play time"
if not self.player: with lock:
return None if not self.player:
return None
return self.player.get_time() return self.player.get_time()
def get_position(self): def get_position(self):
"Return current position" "Return current position"
DEBUG("music.get_position", True) with lock:
DEBUG("music.get_position", True)
return self.player.get_position() return self.player.get_position()
def play(self, path): def play(self, path):
""" """
@ -118,46 +135,44 @@ class Music:
get_position seems more reliable. get_position seems more reliable.
""" """
if self.player: with lock:
if self.player.get_position() > 0 and self.player.is_playing(): if self.player:
return True if self.player.get_position() > 0 and self.player.is_playing():
return True
# We take a copy of the player when fading, so we could be # We take a copy of the player when fading, so we could be
# playing in a fade nowFalse # playing in a fade nowFalse
return self.fading return self.fading > 0
def set_position(self, ms): def set_position(self, ms):
"Set current play time in milliseconds from start" "Set current play time in milliseconds from start"
return self.player.set_time(ms) with lock:
return self.player.set_time(ms)
def set_volume(self, volume): def set_volume(self, volume):
"Set maximum volume used for player" "Set maximum volume used for player"
if not self.player: with lock:
return
self.max_volume = volume
self.player.audio_set_volume(volume)
def stop(self, player=None):
"Immediately stop playing"
DEBUG(f"music.stop(), {player=}", True)
if not player:
if not self.player: if not self.player:
return return
player = self.player
DEBUG(f"music.stop({player=})") self.max_volume = volume
self.player.audio_set_volume(volume)
position = player.get_position() def stop(self):
player.stop() "Immediately stop playing"
p = player
# Ensure we don't reference player after release
player = None
DEBUG(f"Releasing player {p=}", True)
p.release()
return position with lock:
DEBUG(f"music.stop(), {self.player=}", True)
if not self.player:
return
position = self.player.get_position()
self.player.stop()
DEBUG(f"Releasing player {self.player=}", True)
self.player.release()
# Ensure we don't reference player after release
self.player = None
return position

View File

@ -138,8 +138,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.actionPlay_next.triggered.connect(self.play_next) self.actionPlay_next.triggered.connect(self.play_next)
self.actionSearch_database.triggered.connect(self.search_database) self.actionSearch_database.triggered.connect(self.search_database)
self.actionSelect_next_track.triggered.connect(self.select_next_track) self.actionSelect_next_track.triggered.connect(self.select_next_track)
self.actionSelect_played_tracks.triggered.connect(self.select_played)
self.actionSelect_previous_track.triggered.connect( self.actionSelect_previous_track.triggered.connect(
self.select_previous_track) self.select_previous_track)
self.actionSelect_unplayed_tracks.triggered.connect(
self.select_unplayed)
self.actionSetNext.triggered.connect(self.set_next_track) self.actionSetNext.triggered.connect(self.set_next_track)
self.actionSkip_next.triggered.connect(self.play_next) self.actionSkip_next.triggered.connect(self.play_next)
self.actionSkipToEnd.triggered.connect(self.test_skip_to_end) self.actionSkipToEnd.triggered.connect(self.test_skip_to_end)
@ -429,6 +432,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.visible_playlist_tab().select_next_track() self.visible_playlist_tab().select_next_track()
def select_played(self):
"Select all played tracks in playlist"
self.visible_playlist_tab().select_played_tracks()
def select_previous_track(self): def select_previous_track(self):
"Select previous or first track in playlist" "Select previous or first track in playlist"
@ -449,6 +457,11 @@ class Window(QMainWindow, Ui_MainWindow):
self.next_track = Tracks.get_track(session, next_track_id) self.next_track = Tracks.get_track(session, next_track_id)
self.update_headers() self.update_headers()
def select_unplayed(self):
"Select all unplayed tracks in playlist"
self.visible_playlist_tab().select_unplayed_tracks()
def show_warning(self, title, msg): def show_warning(self, title, msg):
"Display a warning to user" "Display a warning to user"
@ -491,16 +504,19 @@ class Window(QMainWindow, Ui_MainWindow):
def stop_playing(self, fade=True): def stop_playing(self, fade=True):
"Stop playing current track" "Stop playing current track"
DEBUG("musicmuster.stop_playing()") DEBUG("musicmuster.stop_playing()", True)
if not self.music.playing(): if not self.music.playing():
DEBUG("musicmuster.stop_playing(): not playing", True)
return return
self.previous_track_position = self.music.get_position() self.previous_track_position = self.music.get_position()
if fade: if fade:
DEBUG("musicmuster.stop_playing(): fading music", True)
self.music.fade() self.music.fade()
else: else:
self.music.stop() self.music.stop()
DEBUG("musicmuster.stop_playing(): stopping music", True)
self.current_track_playlist_tab.clear_current() self.current_track_playlist_tab.clear_current()
# Shuffle tracks along # Shuffle tracks along
@ -550,7 +566,7 @@ class Window(QMainWindow, Ui_MainWindow):
if not self.even_tick: if not self.even_tick:
return return
if self.music.playing(): if self.music.player and self.music.playing():
self.playing = True self.playing = True
playtime = self.music.get_playtime() playtime = self.music.get_playtime()
time_to_fade = (self.current_track.fade_at - playtime) time_to_fade = (self.current_track.fade_at - playtime)

View File

@ -425,6 +425,21 @@ class PlaylistTab(QTableWidget):
self.selectRow(row) self.selectRow(row)
def select_played_tracks(self):
"Select all played tracks in playlist"
# Need to allow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
self.clearSelection()
for row in range(self.rowCount()):
if self._get_row_id(row) in self.played_tracks:
self.selectRow(row)
# Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def select_previous_track(self): def select_previous_track(self):
""" """
Select previous or last track. Don't select notes. Wrap at first row. Select previous or last track. Don't select notes. Wrap at first row.
@ -458,6 +473,25 @@ class PlaylistTab(QTableWidget):
self.selectRow(row) self.selectRow(row)
def select_unplayed_tracks(self):
"Select all unplayed tracks in playlist"
# Need to allow multiple rows to be selected
self.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection)
notes_rows = self._meta_get_notes()
self.clearSelection()
for row in range(self.rowCount()):
if row in notes_rows:
continue
if self._get_row_id(row) in self.played_tracks:
continue
self.selectRow(row)
# Reset extended selection
self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
def set_selected_as_next(self): def set_selected_as_next(self):
""" """
Sets the selected track as the next track. Sets the selected track as the next track.

View File

@ -757,6 +757,8 @@ border: 1px solid rgb(85, 87, 83);</string>
<addaction name="actionAdd_file"/> <addaction name="actionAdd_file"/>
<addaction name="action_Clear_selection"/> <addaction name="action_Clear_selection"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionSelect_unplayed_tracks"/>
<addaction name="actionSelect_played_tracks"/>
<addaction name="actionMoveSelected"/> <addaction name="actionMoveSelected"/>
<addaction name="separator"/> <addaction name="separator"/>
<addaction name="actionExport_playlist"/> <addaction name="actionExport_playlist"/>
@ -977,6 +979,16 @@ border: 1px solid rgb(85, 87, 83);</string>
<string>K</string> <string>K</string>
</property> </property>
</action> </action>
<action name="actionSelect_played_tracks">
<property name="text">
<string>Select played tracks</string>
</property>
</action>
<action name="actionSelect_unplayed_tracks">
<property name="text">
<string>Select unplayed tracks</string>
</property>
</action>
</widget> </widget>
<resources> <resources>
<include location="icons.qrc"/> <include location="icons.qrc"/>