From 9656bac49f1ef29f6f7e0605380fe4a5fec94153 Mon Sep 17 00:00:00 2001 From: Keith Edmunds Date: Wed, 3 Jul 2024 15:41:14 +0100 Subject: [PATCH] WIP: preview via pygame working --- .envrc | 2 + app/musicmuster.py | 171 ++++++++++++++++++++++++++------------------- app/playlists.py | 20 ------ poetry.lock | 68 +++++++++++++++++- pyproject.toml | 1 + 5 files changed, 170 insertions(+), 92 deletions(-) diff --git a/.envrc b/.envrc index 4462d1f..672c57b 100644 --- a/.envrc +++ b/.envrc @@ -4,6 +4,8 @@ export MAIL_PORT=587 export MAIL_SERVER="smtp.fastmail.com" export MAIL_USERNAME="kae@midnighthax.com" export MAIL_USE_TLS=True +export PYGAME_HIDE_SUPPORT_PROMPT=1 + branch=$(git branch --show-current) # Always treat running from /home/kae/mm as production diff --git a/app/musicmuster.py b/app/musicmuster.py index 659e2ab..ffea7fd 100755 --- a/app/musicmuster.py +++ b/app/musicmuster.py @@ -39,6 +39,7 @@ from PyQt6.QtWidgets import ( # Third party imports import pipeclient +from pygame import mixer from sqlalchemy.exc import IntegrityError from sqlalchemy.orm.session import Session import stackprinter # type: ignore @@ -142,6 +143,46 @@ class ImportTrack(QObject): self.import_finished.emit() +class PreviewManager: + """ + Manage track preview player + """ + + def __init__(self) -> None: + mixer.init() + self.path: str = "" + self.start_time: Optional[dt.datetime] = None + + def get_playtime(self) -> int: + """ + Return time since track started in milliseconds, 0 if not playing + """ + + if not mixer.music.get_busy(): + return 0 + + if not self.start_time: + return 0 + + return int((dt.datetime.now() - self.start_time).total_seconds() * 1000) + + def is_playing(self) -> bool: + return mixer.music.get_busy() + + def play(self) -> None: + mixer.music.play() + self.start_time = dt.datetime.now() + + def set_path(self, path) -> None: + self.path = path + mixer.music.load(path) + + def stop(self) -> None: + mixer.music.stop() + self.path = "" + self.start_time = None + + class Window(QMainWindow, Ui_MainWindow): def __init__(self, parent=None, *args, **kwargs) -> None: super().__init__(parent) @@ -159,6 +200,7 @@ class Window(QMainWindow, Ui_MainWindow): self.txtSearch.setHidden(True) self.statusbar.addWidget(self.txtSearch) self.hide_played_tracks = False + self.preview_manager = PreviewManager() self.widgetFadeVolume.hideAxis("bottom") self.widgetFadeVolume.hideAxis("left") @@ -288,7 +330,9 @@ class Window(QMainWindow, Ui_MainWindow): current_track_playlist_id = track_sequence.current.playlist_id if current_track_playlist_id: if closing_tab_playlist_id == current_track_playlist_id: - helpers.show_OK(self, "Current track", "Can't close current track playlist") + helpers.show_OK( + self, "Current track", "Can't close current track playlist" + ) return False # Don't close next track playlist @@ -296,7 +340,9 @@ class Window(QMainWindow, Ui_MainWindow): next_track_playlist_id = track_sequence.next.playlist_id if next_track_playlist_id: if closing_tab_playlist_id == next_track_playlist_id: - helpers.show_OK(self, "Next track", "Can't close next track playlist") + helpers.show_OK( + self, "Next track", "Can't close next track playlist" + ) return False # Record playlist as closed and update remaining playlist tabs @@ -1043,42 +1089,23 @@ class Window(QMainWindow, Ui_MainWindow): def preview(self) -> None: """ - Preview selected or next track. + Preview selected or next track. We use a different mechanism to + normal track playing so that the user can route the output audio + differently (eg, to headphones). """ if self.btnPreview.isChecked(): - # Get track_id for first selected track if there is one - track_id = None - row_number_and_track_id = self.active_tab().get_selected_row_and_track_id() - if row_number_and_track_id: - row_number, track_id = row_number_and_track_id - else: + # Get track path for first selected track if there is one + track_path = self.active_tab().get_selected_row_track_path() + if not track_path: # Otherwise get track_id to next track to play if track_sequence.next: - track_id = track_sequence.next.track_id - row_number = track_sequence.next.row_number - if not track_id or row_number is None: - self.btnPreview.setChecked(False) - return - - try: - with db.Session() as session: - self.preview_track_manager = PreviewTrackManager( - session=session, track_id=track_id, row_number=row_number - ) - self.preview_track_manager.play() - except ValueError as e: - log.error(f"Error creating PreviewTrackManager({str(e)})") - return - + track_path = track_sequence.next.path + if track_path: + self.preview_manager.set_path(track_path) + self.preview_manager.play() else: - if self.preview_track_manager: - self.preview_track_manager.stop() - self.preview_track_manager = None - self.label_intro_timer.setText("0.0") - self.label_intro_timer.setStyleSheet("") - self.btnPreviewMark.setEnabled(False) - self.btnPreviewArm.setChecked(False) + self.preview_manager.stop() def preview_arm(self): """Manager arm button for setting intro length""" @@ -1088,45 +1115,47 @@ class Window(QMainWindow, Ui_MainWindow): def preview_back(self) -> None: """Wind back preview file""" - if self.preview_track_manager: - self.preview_track_manager.move_back() + # TODO + pass def preview_end(self) -> None: """Advance preview file to Config.PREVIEW_END_BUFFER_MS before end of intro""" - if self.preview_track_manager: - self.preview_track_manager.move_to_intro_end() + # TODO + pass def preview_fwd(self) -> None: """Advance preview file""" - if self.preview_track_manager: - self.preview_track_manager.move_forward() + # TODO + pass def preview_mark(self) -> None: """Set intro time""" - if self.preview_track_manager: - track_id = self.preview_track_manager.track_id - row_number = self.preview_track_manager.row_number - with db.Session() as session: - track = session.get(Tracks, track_id) - if track: - # Save intro as millisends rounded to nearest 0.1 - # second because editor spinbox only resolves to 0.1 - # seconds - intro = round(self.preview_track_manager.time_playing() / 100) * 100 - track.intro = intro - session.commit() - self.preview_track_manager.intro = intro - self.active_tab().source_model.refresh_row(session, row_number) - self.active_tab().source_model.invalidate_row(row_number) + # TODO + pass + # if self.preview_track_manager: + # track_id = self.preview_track_manager.track_id + # row_number = self.preview_track_manager.row_number + # with db.Session() as session: + # track = session.get(Tracks, track_id) + # if track: + # # Save intro as millisends rounded to nearest 0.1 + # # second because editor spinbox only resolves to 0.1 + # # seconds + # intro = round(self.preview_track_manager.time_playing() / 100) * 100 + # track.intro = intro + # session.commit() + # self.preview_track_manager.intro = intro + # self.active_tab().source_model.refresh_row(session, row_number) + # self.active_tab().source_model.invalidate_row(row_number) def preview_start(self) -> None: """Restart preview""" - if self.preview_track_manager: - self.preview_track_manager.restart() + # TODO + pass def rename_playlist(self) -> None: """ @@ -1477,23 +1506,23 @@ class Window(QMainWindow, Ui_MainWindow): pass # Ensure preview button is reset if preview finishes playing - if self.preview_track_manager: - self.btnPreview.setChecked(self.preview_track_manager.is_playing()) - - # Update preview timer - if self.preview_track_manager.is_playing(): - playtime = self.preview_track_manager.time_playing() + # Update preview timer + if self.btnPreview.isChecked(): + if self.preview_manager.is_playing(): + self.btnPreview.setChecked(True) + playtime = self.preview_manager.get_playtime() self.label_intro_timer.setText(f"{playtime / 1000:.1f}") - if self.preview_track_manager.time_remaining_intro() <= 50: - self.label_intro_timer.setStyleSheet( - f"background: {Config.COLOUR_WARNING_TIMER}" - ) - else: - self.label_intro_timer.setStyleSheet("") - else: - self.label_intro_timer.setText("0.0") - self.label_intro_timer.setStyleSheet("") - self.btnPreview.setChecked(False) + # if self.preview_track_manager.time_remaining_intro() <= 50: + # self.label_intro_timer.setStyleSheet( + # f"background: {Config.COLOUR_WARNING_TIMER}" + # ) + # else: + # self.label_intro_timer.setStyleSheet("") + else: + self.btnPreview.setChecked(False) + self.label_intro_timer.setText("0.0") + self.label_intro_timer.setStyleSheet("") + self.btnPreview.setChecked(False) def tick_1000ms(self) -> None: """ diff --git a/app/playlists.py b/app/playlists.py index d42252f..53dd2c9 100644 --- a/app/playlists.py +++ b/app/playlists.py @@ -644,26 +644,6 @@ class PlaylistTab(QTableView): self.source_model.delete_rows(self.selected_model_row_numbers()) self.clear_selection() - def get_selected_row_and_track_id(self) -> Optional[tuple[int, int]]: - """ - Return the (row_number, track_id) of the selected row. If no row selected or selected - row does not have a track, return None. - """ - - row_number = self.source_model_selected_row_number() - if row_number is None: - result = None - else: - track_id = self.source_model.get_row_track_id(row_number) - if not track_id: - result = None - else: - result = (row_number, track_id) - - log.debug(f"get_selected_row_and_track_id() returned: {result=}") - - return result - def get_selected_row_track_path(self) -> str: """ Return the path of the selected row. If no row selected or selected diff --git a/poetry.lock b/poetry.lock index 41a6040..35c7c09 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1253,6 +1253,72 @@ files = [ {file = "pyfzf-0.3.1.tar.gz", hash = "sha256:dd902e34cffeca9c3082f96131593dd20b4b3a9bba5b9dde1b0688e424b46bd2"}, ] +[[package]] +name = "pygame" +version = "2.6.0" +description = "Python Game Development" +optional = false +python-versions = ">=3.6" +files = [ + {file = "pygame-2.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e5707aa9d029752495b3eddc1edff62e0e390a02f699b0f1ce77fe0b8c70ea4f"}, + {file = "pygame-2.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3ed0547368733b854c0d9981c982a3cdfabfa01b477d095c57bf47f2199da44"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6050f3e95f1f16602153d616b52619c6a2041cee7040eb529f65689e9633fc3e"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89be55b7e9e22e0eea08af9d6cfb97aed5da780f0b3a035803437d481a16d972"}, + {file = "pygame-2.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d65fb222eea1294cfc8206d9e5754d476a1673eb2783c03c4f70e0455320274"}, + {file = "pygame-2.6.0-cp310-cp310-win32.whl", hash = "sha256:71eebb9803cb350298de188fb7cdd3ebf13299f78d59a71c7e81efc649aae348"}, + {file = "pygame-2.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:1551852a2cd5b4139a752888f6cbeeb4a96fc0fe6e6f3f8b9d9784eb8fceab13"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f6e5e6c010b1bf429388acf4d41d7ab2f7ad8fbf241d0db822102d35c9a2eb84"}, + {file = "pygame-2.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:99902f4a2f6a338057200d99b5120a600c27a9f629ca012a9b0087c045508d08"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a284664978a1989c1e31a0888b2f70cfbcbafdfa3bb310e750b0d3366416225"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:829623cee298b3dbaa1dd9f52c3051ae82f04cad7708c8c67cb9a1a4b8fd3c0b"}, + {file = "pygame-2.6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acf7949ed764487d51123f4f3606e8f76b0df167fef12ef73ef423c35fdea39"}, + {file = "pygame-2.6.0-cp311-cp311-win32.whl", hash = "sha256:3f809560c99bd1fb4716610eca0cd36412528f03da1a63841a347b71d0c604ee"}, + {file = "pygame-2.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:6897ab87f9193510a774a3483e00debfe166f340ca159f544ef99807e2a44ec4"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b834711ebc8b9d0c2a5f9bfae4403dd277b2c61bcb689e1aa630d01a1ebcf40a"}, + {file = "pygame-2.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b5ac288655e8a31a303cc286e79cc57979ed2ba19c3a14042d4b6391c1d3bed2"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d666667b7826b0a7921b8ce0a282ba5281dfa106976c1a3b24e32a0af65ad3b1"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd8848a37a7cee37854c7efb8d451334477c9f8ce7ac339c079e724dc1334a76"}, + {file = "pygame-2.6.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:315e7b3c1c573984f549ac5da9778ac4709b3b4e3a4061050d94eab63fa4fe31"}, + {file = "pygame-2.6.0-cp312-cp312-win32.whl", hash = "sha256:e44bde0840cc21a91c9d368846ac538d106cf0668be1a6030f48df139609d1e8"}, + {file = "pygame-2.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:1c429824b1f881a7a5ce3b5c2014d3d182aa45a22cea33c8347a3971a5446907"}, + {file = "pygame-2.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b832200bd8b6fc485e087bf3ef7ec1a21437258536413a5386088f5dcd3a9870"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098029d01a46ea4e30620dfb7c28a577070b456c8fc96350dde05f85c0bf51b5"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a858bbdeac5ec473ec9e726c55fb8fbdc2f4aad7c55110e899883738071c7c9b"}, + {file = "pygame-2.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f908762941fd99e1f66d1211d26383184f6045c45673443138b214bf48a89aa"}, + {file = "pygame-2.6.0-cp36-cp36m-win32.whl", hash = "sha256:4a63daee99d050f47d6ec7fa7dbd1c6597b8f082cdd58b6918d382d2bc31262d"}, + {file = "pygame-2.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:ace471b3849d68968e5427fc01166ef5afaf552a5c442fc2c28d3b7226786f55"}, + {file = "pygame-2.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fea019713d0c89dfd5909225aa933010100035d1cd30e6c936e8b6f00529fb80"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:249dbf2d51d9f0266009a380ccf0532e1a57614a1528bb2f89a802b01d61f93e"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cb51533ee3204e8160600b0de34eaad70eb913a182c94a7777b6051e8fc52f1"}, + {file = "pygame-2.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f637636a44712e94e5601ec69160a080214626471983dfb0b5b68aa0c61563d"}, + {file = "pygame-2.6.0-cp37-cp37m-win32.whl", hash = "sha256:e432156b6f346f4cc6cab03ce9657600093390f4c9b10bf458716b25beebfe33"}, + {file = "pygame-2.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a0194652db7874bdde7dfc69d659ca954544c012e04ae527151325bfb970f423"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eae3ee62cc172e268121d5bd9dc406a67094d33517de3a91de3323d6ae23eb02"}, + {file = "pygame-2.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f6a58b0a5a8740a3c2cf6fc5366888bd4514561253437f093c12a9ab4fb3ecae"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c71da36997dc7b9b4ee973fa3a5d4a6cfb2149161b5b1c08b712d2f13a63ccfe"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b86771801a7fc10d9a62218f27f1d5c13341c3a27394aa25578443a9cd199830"}, + {file = "pygame-2.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4928f3acf5a9ce5fbab384c21f1245304535ffd5fb167ae92a6b4d3cdb55a3b6"}, + {file = "pygame-2.6.0-cp38-cp38-win32.whl", hash = "sha256:4faab2df9926c4d31215986536b112f0d76f711cf02f395805f1ff5df8fd55fc"}, + {file = "pygame-2.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:afbb8d97aed93dfb116fe105603dacb68f8dab05b978a40a9e4ab1b6c1f683fd"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d11f3646b53819892f4a731e80b8589a9140343d0d4b86b826802191b241228c"}, + {file = "pygame-2.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5ef92ed93c354eabff4b85e457d4d6980115004ec7ff52a19fd38b929c3b80fb"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bc1795f2e36302882546faacd5a0191463c4f4ae2b90e7c334a7733aa4190d2"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e92294fcc85c4955fe5bc6a0404e4cc870808005dc8f359e881544e3cc214108"}, + {file = "pygame-2.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0cb7bdf3ee0233a3ac02ef777c01dfe315e6d4670f1312c83b91c1ef124359a"}, + {file = "pygame-2.6.0-cp39-cp39-win32.whl", hash = "sha256:ac906478ae489bb837bf6d2ae1eb9261d658aa2c34fa5b283027a04149bda81a"}, + {file = "pygame-2.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:92cf12a9722f6f0bdc5520d8925a8f085cff9c054a2ea462fc409cba3781be27"}, + {file = "pygame-2.6.0-pp36-pypy36_pp73-win32.whl", hash = "sha256:a6636f452fdaddf604a060849feb84c056930b6a3c036214f607741f16aac942"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3dc242dc15d067d10f25c5b12a1da48ca9436d8e2d72353eaf757e83612fba2f"}, + {file = "pygame-2.6.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f82df23598a281c8c342d3c90be213c8fe762a26c15815511f60d0aac6e03a70"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:2ed2539bb6bd211fc570b1169dc4a64a74ec5cd95741e62a0ab46bd18fe08e0d"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:904aaf29710c6b03a7e1a65b198f5467ed6525e8e60bdcc5e90ff8584c1d54ea"}, + {file = "pygame-2.6.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fcd28f96f0fffd28e71a98773843074597e10d7f55a098e2e5bcb2bef1bdcbf5"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4fad1ab33443ecd4f958dbbb67fc09fcdc7a37e26c34054e3296fb7e26ad641e"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e909186d4d512add39b662904f0f79b73028fbfc4fbfdaf6f9412aed4e500e9c"}, + {file = "pygame-2.6.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79abcbf6d12fce51a955a0652ccd50b6d0a355baa27799535eaf21efb43433dd"}, + {file = "pygame-2.6.0.tar.gz", hash = "sha256:722d33ae676aa8533c1f955eded966411298831346b8d51a77dad22e46ba3e35"}, +] + [[package]] name = "pygments" version = "2.17.2" @@ -1970,4 +2036,4 @@ test = ["websockets"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "3b2e747f93972b78a9a35454810c99c4ec81e14fc9780e65a6a4434a97d1a713" +content-hash = "4400e265162fa56d70d4ef5501896dec6f22b743414364ef99bdfef0be979785" diff --git a/pyproject.toml b/pyproject.toml index e45b6b4..eefe72d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ pyqtgraph = "^0.13.3" colorlog = "^6.8.2" alchemical = "^1.0.2" obs-websocket-py = "^1.0" +pygame = "^2.6.0" [tool.poetry.dev-dependencies] ipdb = "^0.13.9"