Compare commits

...

13 Commits

Author SHA1 Message Date
Keith Edmunds
6fc6fbe0d0 Merge branch 'dev' 2023-11-23 12:30:52 +00:00
Keith Edmunds
e689c9afeb Check for no title/artist tag in replace_files 2023-11-19 11:46:38 +00:00
Keith Edmunds
a0fa7e455e Change intro gap warning to 300ms 2023-11-16 22:24:37 +00:00
Keith Edmunds
bcb8a95969 Update environment 2023-11-13 21:00:15 +00:00
Keith Edmunds
8674e6d5b3 Try to fix occasional short dropouts at start of track
Move plot graph to before starting track
2023-11-13 20:55:08 +00:00
Keith Edmunds
705f3ea2f2 Fix bug with unended timed section 2023-11-08 21:10:35 +00:00
Keith Edmunds
405efee732 Fix bug that added row number to notes of imported tracks 2023-11-03 08:18:24 +00:00
Keith Edmunds
2db407edc5 Show section end time for all unplayed tracks 2023-10-30 19:27:59 +00:00
Keith Edmunds
ab8da0a312 Fix moving of timing starts and subtotals 2023-10-20 13:07:30 +01:00
Keith Edmunds
48d26d80df Fix replace_files after other updates 2023-10-19 11:20:22 +01:00
Keith Edmunds
da751ee530 Add return type in music.py 2023-10-18 08:54:15 +01:00
Keith Edmunds
282e4476a9 Clean up music.py interface 2023-10-17 22:52:30 +01:00
Keith Edmunds
ecd46b8a0a Improved fading
fade() takes an optional parameter, fade_seconds
fading is now logarithmic
2023-10-17 22:41:18 +01:00
6 changed files with 593 additions and 535 deletions

View File

@ -53,8 +53,9 @@ class Config(object):
FADE_CURVE_BACKGROUND = "lightyellow" FADE_CURVE_BACKGROUND = "lightyellow"
FADE_CURVE_FOREGROUND = "blue" FADE_CURVE_FOREGROUND = "blue"
FADE_CURVE_MS_BEFORE_FADE = 5000 FADE_CURVE_MS_BEFORE_FADE = 5000
FADE_STEPS = 20 FADEOUT_DB = -10
FADE_TIME = 3000 FADEOUT_SECONDS = 5
FADEOUT_STEPS_PER_SECOND = 5
HIDE_AFTER_PLAYING_OFFSET = 5000 HIDE_AFTER_PLAYING_OFFSET = 5000
INFO_TAB_TITLE_LENGTH = 15 INFO_TAB_TITLE_LENGTH = 15
LAST_PLAYED_TODAY_STRING = "Today" LAST_PLAYED_TODAY_STRING = "Today"

View File

@ -19,11 +19,12 @@ lock = threading.Lock()
class FadeTrack(QRunnable): class FadeTrack(QRunnable):
def __init__(self, player: vlc.MediaPlayer) -> None: def __init__(self, player: vlc.MediaPlayer, fade_seconds) -> None:
super().__init__() super().__init__()
self.player = player self.player = player
self.fade_seconds = fade_seconds
def run(self): def run(self) -> None:
""" """
Implementation of fading the player Implementation of fading the player
""" """
@ -31,24 +32,18 @@ class FadeTrack(QRunnable):
if not self.player: if not self.player:
return return
fade_time = Config.FADE_TIME / 1000 # Reduce volume logarithmically
steps = Config.FADE_STEPS total_steps = self.fade_seconds * Config.FADEOUT_STEPS_PER_SECOND
sleep_time = fade_time / steps db_reduction_per_step = Config.FADEOUT_DB / total_steps
original_volume = self.player.audio_get_volume() reduction_factor_per_step = pow(10, (db_reduction_per_step / 20))
# We reduce volume by one mesure first, then by two measures, volume = self.player.audio_get_volume()
# then three, and so on.
# The sum of the arithmetic sequence 1, 2, 3, ..n is
# (n**2 + n) / 2
total_measures_count = (steps**2 + steps) / 2
measures_to_reduce_by = 0 for i in range(1, total_steps + 1):
self.player.audio_set_volume(
for i in range(1, steps + 1): int(volume * pow(reduction_factor_per_step, i))
measures_to_reduce_by += i )
volume_factor = 1 - (measures_to_reduce_by / total_measures_count) sleep(1 / Config.FADEOUT_STEPS_PER_SECOND)
self.player.audio_set_volume(int(original_volume * volume_factor))
sleep(sleep_time)
self.player.stop() self.player.stop()
log.debug(f"Releasing player {self.player=}") log.debug(f"Releasing player {self.player=}")
@ -65,7 +60,7 @@ class Music:
self.player = None self.player = None
self.max_volume = Config.VOLUME_VLC_DEFAULT self.max_volume = Config.VOLUME_VLC_DEFAULT
def fade(self) -> None: def fade(self, fade_seconds: int = Config.FADEOUT_SECONDS) -> None:
""" """
Fade the currently playing track. Fade the currently playing track.
@ -86,7 +81,7 @@ class Music:
self.player = None self.player = None
pool = QThreadPool.globalInstance() pool = QThreadPool.globalInstance()
fader = FadeTrack(p) fader = FadeTrack(p, fade_seconds=fade_seconds)
pool.start(fader) pool.start(fader)
def get_position(self) -> Optional[float]: def get_position(self) -> Optional[float]:
@ -96,7 +91,7 @@ class Music:
return None return None
return self.player.get_position() return self.player.get_position()
def play(self, path: str, position: Optional[float] = None) -> Optional[int]: def play(self, path: str, position: Optional[float] = None) -> None:
""" """
Start playing the track at path. Start playing the track at path.
@ -107,19 +102,15 @@ class Music:
log.error(f"play({path}): path not readable") log.error(f"play({path}): path not readable")
return None return None
status = -1
media = self.VLC.media_new_path(path) media = self.VLC.media_new_path(path)
self.player = media.player_new_from_media() self.player = media.player_new_from_media()
if self.player: if self.player:
status = self.player.play() _ = self.player.play()
self.set_volume(self.max_volume) self.set_volume(self.max_volume)
if position: if position:
self.player.set_position(position) self.player.set_position(position)
return status def set_volume(self, volume=None, set_default=True) -> None:
def set_volume(self, volume=None, set_default=True):
"""Set maximum volume used for player""" """Set maximum volume used for player"""
if not self.player: if not self.player:

View File

@ -223,7 +223,7 @@ class ImportTrack(QObject):
print(e) print(e)
return return
helpers.normalise_track(track.path) helpers.normalise_track(track.path)
self.playlist.insert_track(session, track, target_row) self.playlist.insert_track(session=session, track=track, target_row=target_row)
# Insert next row under this one # Insert next row under this one
target_row += 1 target_row += 1
# We're importing potentially multiple tracks in a loop. # We're importing potentially multiple tracks in a loop.
@ -1318,6 +1318,9 @@ class Window(QMainWindow, Ui_MainWindow):
if self.btnDrop3db.isChecked(): if self.btnDrop3db.isChecked():
self.btnDrop3db.setChecked(False) self.btnDrop3db.setChecked(False)
# Show closing volume graph
self.current_track.fade_graph.plot()
# Play (new) current track # Play (new) current track
self.current_track.start() self.current_track.start()
self.music.play(self.current_track.path, position) self.music.play(self.current_track.path, position)
@ -1334,9 +1337,6 @@ class Window(QMainWindow, Ui_MainWindow):
break break
sleep(0.1) sleep(0.1)
# Show closing volume graph
self.current_track.fade_graph.plot()
# Tell database to record it as played # Tell database to record it as played
Playdates(session, self.current_track.track_id) Playdates(session, self.current_track.track_id)

View File

@ -249,17 +249,29 @@ class PlaylistTab(QTableWidget):
else: else:
rowMapping[row + len(rows)] = targetRow + idx rowMapping[row + len(rows)] = targetRow + idx
colCount = self.columnCount() colCount = self.columnCount()
with Session() as session:
for srcRow, tgtRow in sorted(rowMapping.items()): for srcRow, tgtRow in sorted(rowMapping.items()):
if self._get_row_track_id(srcRow): # Messy: will be fixed with model/view implementation.
# This is a track row # If we just move the row, the displayed text will be
# used. That is incorrect for timing starts, ends and
# subtotals, so take text from database and use that.
is_header_row = self._get_row_track_id(srcRow) == 0
if is_header_row:
# This is a header row so save original text from db
source_plr = self._get_row_plr(session, srcRow)
if not source_plr:
print("Can't get source_plr in playlists:dropEvent()")
return
note_text = source_plr.note
# Copy to new locations
for col in range(0, colCount): for col in range(0, colCount):
self.setItem(tgtRow, col, self.takeItem(srcRow, col)) self.setItem(tgtRow, col, self.takeItem(srcRow, col))
else:
self.setItem( # Fixup header text
tgtRow, if is_header_row:
HEADER_NOTES_COLUMN, target_item = self.item(tgtRow, HEADER_NOTES_COLUMN)
self.takeItem(srcRow, HEADER_NOTES_COLUMN), if target_item:
) target_item.setText(note_text)
self.setSpan(tgtRow, HEADER_NOTES_COLUMN, 1, len(columns) - 1) self.setSpan(tgtRow, HEADER_NOTES_COLUMN, 1, len(columns) - 1)
for row in reversed(sorted(rowMapping.keys())): for row in reversed(sorted(rowMapping.keys())):
self.removeRow(row) self.removeRow(row)
@ -640,8 +652,8 @@ class PlaylistTab(QTableWidget):
self, self,
session: scoped_session, session: scoped_session,
track: Tracks, track: Tracks,
note: str = "", note: Optional[str] = "",
repaint: bool = True, repaint: Optional[bool] = True,
target_row: Optional[int] = None, target_row: Optional[int] = None,
) -> None: ) -> None:
""" """
@ -1449,20 +1461,32 @@ class PlaylistTab(QTableWidget):
return userdata_item.data(role) return userdata_item.data(role)
def _get_section_timing_string( def _get_section_timing_string(
self, total_time: int, unplayed_time: int, no_end: bool = False self,
unplayed_time: int,
end_time: Optional[datetime] = None,
no_end: bool = False,
) -> str: ) -> str:
"""Return string describing section duration""" """
Return string describing section duration. If end_time specified, also
return section end time calculated as end_time + unplayed duration.
"""
total_duration = ms_to_mmss(total_time)
if unplayed_time:
unplayed_duration = ms_to_mmss(unplayed_time) unplayed_duration = ms_to_mmss(unplayed_time)
if end_time:
section_end_time = end_time + timedelta(milliseconds=unplayed_time)
end_str = (
"[End time for all unplayed tracks in section: "
+ section_end_time.strftime(Config.TRACK_TIME_FORMAT)
+ "]"
)
else: else:
unplayed_duration = "[No unplayed tracks]" end_str = ""
caveat = "" caveat = ""
if no_end: if no_end:
caveat = " (to end of playlist)" caveat = " (to end of playlist)"
return f" {unplayed_duration} ({total_duration}){caveat}" return f" {unplayed_duration} {caveat} {end_str}"
def _get_selected_row(self) -> Optional[int]: def _get_selected_row(self) -> Optional[int]:
""" """
@ -2204,7 +2228,7 @@ class PlaylistTab(QTableWidget):
if not start_gap: if not start_gap:
start_gap = 0 start_gap = 0
start_gap_item = self._set_item_text(row_number, START_GAP, str(start_gap)) start_gap_item = self._set_item_text(row_number, START_GAP, str(start_gap))
if start_gap >= 500: if start_gap >= 300:
brush = QBrush(QColor(Config.COLOUR_LONG_START)) brush = QBrush(QColor(Config.COLOUR_LONG_START))
else: else:
brush = QBrush() brush = QBrush()
@ -2386,26 +2410,24 @@ class PlaylistTab(QTableWidget):
self.save_playlist(session) self.save_playlist(session)
self._update_start_end_times(session) self._update_start_end_times(session)
def _track_time_between_rows( def _unplayed_track_time_between_rows(
self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows self, session: scoped_session, from_plr: PlaylistRows, to_plr: PlaylistRows
) -> Tuple[int, int]: ) -> int:
""" """
Returns the (total duration of all tracks in rows between Returns the total unplayed duration of all tracks in rows between
from_row and to_row inclusive, total unplayed time in those rows) from_row and to_row inclusive
""" """
plr_tracks = PlaylistRows.get_rows_with_tracks( plr_tracks = PlaylistRows.get_rows_with_tracks(
session, self.playlist_id, from_plr.plr_rownum, to_plr.plr_rownum session, self.playlist_id, from_plr.plr_rownum, to_plr.plr_rownum
) )
total_time = 0
total_time = sum([a.track.duration for a in plr_tracks if a.track.duration])
unplayed_time = 0 unplayed_time = 0
unplayed_time = sum( unplayed_time = sum(
[a.track.duration for a in plr_tracks if a.track.duration and not a.played] [a.track.duration for a in plr_tracks if a.track.duration and not a.played]
) )
return (total_time, unplayed_time) return unplayed_time
def _update_row_track_info( def _update_row_track_info(
self, session: scoped_session, row: int, track: Tracks self, session: scoped_session, row: int, track: Tracks
@ -2439,6 +2461,21 @@ class PlaylistTab(QTableWidget):
section_start_rows: List[PlaylistRows] = [] section_start_rows: List[PlaylistRows] = []
subtotal_from: Optional[PlaylistRows] = None subtotal_from: Optional[PlaylistRows] = None
active_row: Optional[int] = None
active_endtime: Optional[datetime] = None
current_row_prlid = self.musicmuster.current_track.plr_id
if current_row_prlid:
current_row = self._plrid_to_row_number(current_row_prlid)
if current_row:
active_row = current_row
active_end = self.musicmuster.current_track.end_time
else:
previous_row_plrid = self.musicmuster.previous_track.plr_id
if previous_row_plrid:
previous_row = self._plrid_to_row_number(previous_row_plrid)
if previous_row:
active_row = previous_row
active_end = self.musicmuster.previous_track.end_time
header_rows = [ header_rows = [
self._get_row_plr_id(row_number) self._get_row_plr_id(row_number)
@ -2457,12 +2494,19 @@ class PlaylistTab(QTableWidget):
try: try:
from_plr = section_start_rows.pop() from_plr = section_start_rows.pop()
to_plr = plr to_plr = plr
total_time, unplayed_time = self._track_time_between_rows( unplayed_time = self._unplayed_track_time_between_rows(
session, from_plr, to_plr session, from_plr, to_plr
) )
if (
active_row
and active_row >= from_plr.plr_rownum
and active_row <= to_plr.plr_rownum
):
time_str = self._get_section_timing_string( time_str = self._get_section_timing_string(
total_time, unplayed_time unplayed_time, active_end
) )
else:
time_str = self._get_section_timing_string(unplayed_time, None)
self._set_row_header_text( self._set_row_header_text(
session, from_plr.plr_rownum, from_plr.note + time_str session, from_plr.plr_rownum, from_plr.note + time_str
) )
@ -2491,10 +2535,19 @@ class PlaylistTab(QTableWidget):
return return
from_plr = subtotal_from from_plr = subtotal_from
to_plr = plr to_plr = plr
total_time, unplayed_time = self._track_time_between_rows( unplayed_time = self._unplayed_track_time_between_rows(
session, subtotal_from, to_plr session, subtotal_from, to_plr
) )
time_str = self._get_section_timing_string(total_time, unplayed_time) if (
active_row
and active_row >= from_plr.plr_rownum
and active_row <= to_plr.plr_rownum
):
time_str = self._get_section_timing_string(
unplayed_time, active_end
)
else:
time_str = self._get_section_timing_string(unplayed_time, None)
if to_plr.note.strip() == "=": if to_plr.note.strip() == "=":
leader_text = "Subtotal: " leader_text = "Subtotal: "
@ -2510,11 +2563,11 @@ class PlaylistTab(QTableWidget):
if possible_plr: if possible_plr:
to_plr = possible_plr to_plr = possible_plr
for from_plr in section_start_rows: for from_plr in section_start_rows:
total_time, unplayed_time = self._track_time_between_rows( unplayed_time = self._unplayed_track_time_between_rows(
session, from_plr, to_plr session, from_plr, to_plr
) )
time_str = self._get_section_timing_string( time_str = self._get_section_timing_string(
total_time, unplayed_time, no_end=True unplayed_time, no_end=True
) )
self._set_row_header_text( self._set_row_header_text(
session, from_plr.plr_rownum, from_plr.note + time_str session, from_plr.plr_rownum, from_plr.note + time_str

View File

@ -75,7 +75,13 @@ def main():
continue continue
new_tags = get_tags(new_path) new_tags = get_tags(new_path)
new_title = new_tags["title"] new_title = new_tags["title"]
if not new_title:
print(f"{new_fname} does not have a title tag")
sys.exit(1)
new_artist = new_tags["artist"] new_artist = new_tags["artist"]
if not new_artist:
print(f"{new_fname} does not have an artist tag")
sys.exit(1)
bitrate = new_tags["bitrate"] bitrate = new_tags["bitrate"]
# If same filename exists in parent direcory, check tags # If same filename exists in parent direcory, check tags
@ -255,7 +261,7 @@ def process_track(src, dst, title, artist, bitrate):
shutil.move(src, new_path) shutil.move(src, new_path)
# Update track metadata # Update track metadata
set_track_metadata(session, track) set_track_metadata(track)
main() main()

939
poetry.lock generated

File diff suppressed because it is too large Load Diff