diff options
-rw-r--r-- | MainWindow.py | 81 | ||||
-rw-r--r-- | mainwindow.ui | 112 | ||||
-rw-r--r-- | requirements.txt | 2 | ||||
-rw-r--r-- | solo-tool.py | 196 |
4 files changed, 391 insertions, 0 deletions
diff --git a/MainWindow.py b/MainWindow.py new file mode 100644 index 0000000..fb97749 --- /dev/null +++ b/MainWindow.py @@ -0,0 +1,81 @@ +# -*- coding: utf-8 -*- + +# Form implementation generated from reading ui file 'mainwindow.ui' +# +# Created by: PyQt5 UI code generator 5.15.6 +# +# WARNING: Any manual changes made to this file will be lost when pyuic5 is +# run again. Do not edit this file unless you know what you are doing. + + +from PyQt5 import QtCore, QtGui, QtWidgets + + +class Ui_MainWindow(object): + def setupUi(self, MainWindow): + MainWindow.setObjectName("MainWindow") + MainWindow.resize(602, 424) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.centralWidgetLayout = QtWidgets.QHBoxLayout(self.centralwidget) + self.centralWidgetLayout.setObjectName("centralWidgetLayout") + self.songListView = QtWidgets.QListView(self.centralwidget) + self.songListView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.songListView.setObjectName("songListView") + self.centralWidgetLayout.addWidget(self.songListView) + self.verticalLayout = QtWidgets.QVBoxLayout() + self.verticalLayout.setObjectName("verticalLayout") + self.songSlider = QtWidgets.QSlider(self.centralwidget) + self.songSlider.setMinimumSize(QtCore.QSize(0, 0)) + self.songSlider.setOrientation(QtCore.Qt.Horizontal) + self.songSlider.setObjectName("songSlider") + self.verticalLayout.addWidget(self.songSlider) + self.aSlider = QtWidgets.QSlider(self.centralwidget) + self.aSlider.setOrientation(QtCore.Qt.Horizontal) + self.aSlider.setObjectName("aSlider") + self.verticalLayout.addWidget(self.aSlider) + self.bSlider = QtWidgets.QSlider(self.centralwidget) + self.bSlider.setOrientation(QtCore.Qt.Horizontal) + self.bSlider.setObjectName("bSlider") + self.verticalLayout.addWidget(self.bSlider) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.abListView = QtWidgets.QListView(self.centralwidget) + self.abListView.setObjectName("abListView") + self.horizontalLayout.addWidget(self.abListView) + self.formLayout = QtWidgets.QFormLayout() + self.formLayout.setObjectName("formLayout") + self.playPauseButton = QtWidgets.QPushButton(self.centralwidget) + self.playPauseButton.setObjectName("playPauseButton") + self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.playPauseButton) + self.saveAbButton = QtWidgets.QPushButton(self.centralwidget) + self.saveAbButton.setObjectName("saveAbButton") + self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.saveAbButton) + self.saveSessionButton = QtWidgets.QPushButton(self.centralwidget) + self.saveSessionButton.setObjectName("saveSessionButton") + self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.saveSessionButton) + self.loadSessionButton = QtWidgets.QPushButton(self.centralwidget) + self.loadSessionButton.setObjectName("loadSessionButton") + self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.loadSessionButton) + self.abRepeatButton = QtWidgets.QCheckBox(self.centralwidget) + self.abRepeatButton.setObjectName("abRepeatButton") + self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.abRepeatButton) + self.horizontalLayout.addLayout(self.formLayout) + self.verticalLayout.addLayout(self.horizontalLayout) + self.centralWidgetLayout.addLayout(self.verticalLayout) + MainWindow.setCentralWidget(self.centralwidget) + self.addSongAction = QtWidgets.QAction(MainWindow) + self.addSongAction.setObjectName("addSongAction") + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.playPauseButton.setText(_translate("MainWindow", "Play/pause")) + self.saveAbButton.setText(_translate("MainWindow", "Save AB")) + self.saveSessionButton.setText(_translate("MainWindow", "Save session")) + self.loadSessionButton.setText(_translate("MainWindow", "Load session")) + self.abRepeatButton.setText(_translate("MainWindow", "AB repeat")) + self.addSongAction.setText(_translate("MainWindow", "Add song")) diff --git a/mainwindow.ui b/mainwindow.ui new file mode 100644 index 0000000..fc63434 --- /dev/null +++ b/mainwindow.ui @@ -0,0 +1,112 @@ +<?xml version="1.0" encoding="UTF-8"?> +<ui version="4.0"> + <class>MainWindow</class> + <widget class="QMainWindow" name="MainWindow"> + <property name="geometry"> + <rect> + <x>0</x> + <y>0</y> + <width>602</width> + <height>424</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QHBoxLayout" name="centralWidgetLayout"> + <item> + <widget class="QListView" name="songListView"> + <property name="contextMenuPolicy"> + <enum>Qt::CustomContextMenu</enum> + </property> + </widget> + </item> + <item> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <widget class="QSlider" name="songSlider"> + <property name="minimumSize"> + <size> + <width>0</width> + <height>0</height> + </size> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="aSlider"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <widget class="QSlider" name="bSlider"> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QListView" name="abListView"/> + </item> + <item> + <layout class="QFormLayout" name="formLayout"> + <item row="0" column="1"> + <widget class="QPushButton" name="playPauseButton"> + <property name="text"> + <string>Play/pause</string> + </property> + </widget> + </item> + <item row="1" column="1"> + <widget class="QPushButton" name="saveAbButton"> + <property name="text"> + <string>Save AB</string> + </property> + </widget> + </item> + <item row="3" column="1"> + <widget class="QPushButton" name="saveSessionButton"> + <property name="text"> + <string>Save session</string> + </property> + </widget> + </item> + <item row="4" column="1"> + <widget class="QPushButton" name="loadSessionButton"> + <property name="text"> + <string>Load session</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="abRepeatButton"> + <property name="text"> + <string>AB repeat</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + <action name="addSongAction"> + <property name="text"> + <string>Add song</string> + </property> + </action> + </widget> + <resources/> + <connections/> +</ui> diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..8cb824a --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyQt5>=5.6 +sip diff --git a/solo-tool.py b/solo-tool.py new file mode 100644 index 0000000..90616a8 --- /dev/null +++ b/solo-tool.py @@ -0,0 +1,196 @@ +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from PyQt5.QtMultimedia import * +from PyQt5.QtMultimediaWidgets import * + +from MainWindow import Ui_MainWindow + +class PlaylistModel(QAbstractListModel): + def __init__(self, playlist, *args, **kwargs): + super(PlaylistModel, self).__init__(*args, **kwargs) + self.playlist = playlist + + def data(self, index, role): + if role == Qt.DisplayRole: + media = self.playlist.media(index.row()) + return media.canonicalUrl().fileName() + + def rowCount(self, index): + return self.playlist.mediaCount() + +class AbListModel(QAbstractListModel): + def __init__(self, *args, **kwargs): + super(AbListModel, self).__init__(*args, **kwargs) + self.abList = list() + + def data(self, index, role): + if role == Qt.DisplayRole: + ab = self.abList[index.row()] + return f"{hhmmss(ab[0])} - {hhmmss(ab[1])}" + + def rowCount(self, index): + return len(self.abList) + +def hhmmss(ms): + # s = 1000 + # m = 60000 + # h = 360000 + h, r = divmod(ms, 36000) + m, r = divmod(r, 60000) + s, _ = divmod(r, 1000) + return ("%d:%02d:%02d" % (h,m,s)) if h else ("%d:%02d" % (m,s)) + +class MainWindow(QMainWindow, Ui_MainWindow): + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + self.setupUi(self) + + self.player = QMediaPlayer() + + # Setup the playlist. + self.playlist = QMediaPlaylist() + self.player.setPlaylist(self.playlist) + + self.playlistModel = PlaylistModel(self.playlist) + self.songListView.setModel(self.playlistModel) + self.playlist.currentIndexChanged.connect(self.playlistPositionChanged) + selection_model = self.songListView.selectionModel() + selection_model.selectionChanged.connect(self.playlistSelectionChanged) + + # set button context menu policy + self.songListView.customContextMenuRequested.connect(self.onContextMenu) + + # create context menu + self.songListMenu = QMenu(self) + self.addSongAction.triggered.connect(self.openSoundFile) + self.songListMenu.addAction(self.addSongAction) + + self.playPauseButton.pressed.connect(self.playPause) + + self.player.durationChanged.connect(self.updateDuration) + self.player.positionChanged.connect(self.updatePosition) + self.songSlider.valueChanged.connect(self.player.setPosition) + + self.player.positionChanged.connect(self.positionChanged) + + self.abListModel = AbListModel() + self.abListView.setModel(self.abListModel) + self.abListView.selectionModel().selectionChanged.connect(self.abListSelectionChanged) + + self.saveAbButton.pressed.connect(self.addAb) + self.internalState = dict() + + self.saveSessionButton.pressed.connect(self.saveSession) + self.loadSessionButton.pressed.connect(self.loadSession) + + self.show() + + def playlistSelectionChanged(self, ix): + i = ix.indexes()[0].row() + self.playlist.setCurrentIndex(i) + path = self.playlist.currentMedia().canonicalUrl().path() + self.abListModel.abList = self.internalState[path] + self.abListView.selectionModel().clearSelection() + self.abListModel.layoutChanged.emit() + self.aSlider.setValue(0) + self.bSlider.setValue(0) + + def playlistPositionChanged(self, i): + if i > -1: + ix = self.playlistModel.index(i) + self.songListView.setCurrentIndex(ix) + + def abListSelectionChanged(self, ix): + if len(ix.indexes()) > 0: + i = ix.indexes()[0].row() + ab = self.abListModel.abList[i] + self.aSlider.setValue(ab[0]) + self.bSlider.setValue(ab[1]) + + def addAb(self, song=None, a=None, b=None): + currentSong = song or self.playlist.currentMedia().canonicalUrl().path() + abState = [a or self.aSlider.value(), b or self.bSlider.value()] + self.abListModel.abList.append(abState) + self.abListModel.layoutChanged.emit() + + def playPause(self): + if self.player.state() == QMediaPlayer.PlayingState: + self.player.pause() + else: + self.player.play() + + def updateDuration(self, duration): + self.songSlider.setMaximum(duration) + self.aSlider.setMaximum(duration) + self.bSlider.setMaximum(duration) + + def updatePosition(self, position): + # Disable the events to prevent updating triggering a setPosition event (can cause stuttering). + self.songSlider.blockSignals(True) + self.songSlider.setValue(position) + self.songSlider.blockSignals(False) + + def positionChanged(self, position): + if self.abRepeatButton.isChecked() and position > self.bSlider.value(): + self.player.setPosition(self.aSlider.value()) + + def onContextMenu(self, point): + self.songListMenu.exec_(self.songListView.mapToGlobal(point)) + + def openSoundFile(self): + path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "mp3 Audio (*.mp3);FLAC audio (*.flac);All files (*.*)") + if path: + self.addSong(path) + + def addSong(self, path): + self.playlist.addMedia(QMediaContent(QUrl.fromLocalFile(path))) + self.internalState[path] = list() + self.playlistModel.layoutChanged.emit() + + def loadSession(self): + path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "session (*.json)") + if path: + import json + with open(path, "r") as f: + session = json.load(f) + + for song in session: + self.addSong(song) + self.abListModel.abList = self.internalState[song] + + for ab in session[song]: + self.addAb(song=song, a=ab[0], b=ab[1]) + + def saveSession(self): + path, _ = QFileDialog.getSaveFileName(self, "Save file", "", "session (*.json)") + if path: + import json + with open(path, "w") as f: + json.dump(self.internalState, f) + +if __name__ == '__main__': + app = QApplication([]) + app.setApplicationName("Solo Tool") + app.setStyle("Fusion") + + # Fusion dark palette from https://gist.github.com/QuantumCD/6245215. + palette = QPalette() + palette.setColor(QPalette.Window, QColor(53, 53, 53)) + palette.setColor(QPalette.WindowText, Qt.white) + palette.setColor(QPalette.Base, QColor(25, 25, 25)) + palette.setColor(QPalette.AlternateBase, QColor(53, 53, 53)) + palette.setColor(QPalette.ToolTipBase, Qt.white) + palette.setColor(QPalette.ToolTipText, Qt.white) + palette.setColor(QPalette.Text, Qt.white) + palette.setColor(QPalette.Button, QColor(53, 53, 53)) + palette.setColor(QPalette.ButtonText, Qt.white) + palette.setColor(QPalette.BrightText, Qt.red) + palette.setColor(QPalette.Link, QColor(42, 130, 218)) + palette.setColor(QPalette.Highlight, QColor(42, 130, 218)) + palette.setColor(QPalette.HighlightedText, Qt.black) + app.setPalette(palette) + app.setStyleSheet("QToolTip { color: #ffffff; background-color: #2a82da; border: 1px solid white; }") + + window = MainWindow() + app.exec_() |