diff options
Diffstat (limited to 'gui-project')
-rw-r--r-- | gui-project/pyproject.toml | 24 | ||||
-rw-r--r-- | gui-project/src/MainWindow.py | 111 | ||||
-rw-r--r-- | gui-project/src/mainwindow.ui | 161 | ||||
-rw-r--r-- | gui-project/src/solo_tool_gui.py | 261 |
4 files changed, 557 insertions, 0 deletions
diff --git a/gui-project/pyproject.toml b/gui-project/pyproject.toml new file mode 100644 index 0000000..1e6fcf4 --- /dev/null +++ b/gui-project/pyproject.toml @@ -0,0 +1,24 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "solo_tool_gui" +authors = [ + { name = "Eddy Pedroni", email = "epedroni@pm.me" }, +] +description = "A Qt5-based GUI frontend for the solo_tool library" +requires-python = ">=3.12" +dependencies = [ + "PyQt5>=5.6", + "solo_tool" +] +dynamic = ["version"] + +[project.optional-dependencies] +dev = [ +] + +[project.gui-scripts] +solo-tool-gui = "solo_tool_gui:main" + diff --git a/gui-project/src/MainWindow.py b/gui-project/src/MainWindow.py new file mode 100644 index 0000000..137bd33 --- /dev/null +++ b/gui-project/src/MainWindow.py @@ -0,0 +1,111 @@ +# -*- 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(971, 767) + self.centralwidget = QtWidgets.QWidget(MainWindow) + self.centralwidget.setObjectName("centralwidget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) + self.verticalLayout.setObjectName("verticalLayout") + self.listsLayout = QtWidgets.QHBoxLayout() + self.listsLayout.setSizeConstraint(QtWidgets.QLayout.SetDefaultConstraint) + self.listsLayout.setSpacing(6) + self.listsLayout.setObjectName("listsLayout") + self.songListView = QtWidgets.QListView(self.centralwidget) + self.songListView.setObjectName("songListView") + self.listsLayout.addWidget(self.songListView) + self.abListView = QtWidgets.QListView(self.centralwidget) + self.abListView.setObjectName("abListView") + self.listsLayout.addWidget(self.abListView) + self.verticalLayout.addLayout(self.listsLayout) + self.slidersLayout = QtWidgets.QVBoxLayout() + self.slidersLayout.setObjectName("slidersLayout") + self.songSlider = QtWidgets.QSlider(self.centralwidget) + self.songSlider.setMinimumSize(QtCore.QSize(0, 0)) + self.songSlider.setOrientation(QtCore.Qt.Horizontal) + self.songSlider.setObjectName("songSlider") + self.slidersLayout.addWidget(self.songSlider) + self.aSlider = QtWidgets.QSlider(self.centralwidget) + self.aSlider.setOrientation(QtCore.Qt.Horizontal) + self.aSlider.setObjectName("aSlider") + self.slidersLayout.addWidget(self.aSlider) + self.bSlider = QtWidgets.QSlider(self.centralwidget) + self.bSlider.setOrientation(QtCore.Qt.Horizontal) + self.bSlider.setObjectName("bSlider") + self.slidersLayout.addWidget(self.bSlider) + self.verticalLayout.addLayout(self.slidersLayout) + self.buttonsLayout = QtWidgets.QGridLayout() + self.buttonsLayout.setObjectName("buttonsLayout") + self.pauseButton = QtWidgets.QPushButton(self.centralwidget) + self.pauseButton.setObjectName("pauseButton") + self.buttonsLayout.addWidget(self.pauseButton, 0, 1, 1, 1) + self.initMidiButton = QtWidgets.QPushButton(self.centralwidget) + self.initMidiButton.setObjectName("initMidiButton") + self.buttonsLayout.addWidget(self.initMidiButton, 0, 4, 1, 1) + self.rateSlider = QtWidgets.QSlider(self.centralwidget) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.rateSlider.sizePolicy().hasHeightForWidth()) + self.rateSlider.setSizePolicy(sizePolicy) + self.rateSlider.setOrientation(QtCore.Qt.Horizontal) + self.rateSlider.setObjectName("rateSlider") + self.buttonsLayout.addWidget(self.rateSlider, 2, 0, 1, 1) + self.playButton = QtWidgets.QPushButton(self.centralwidget) + self.playButton.setObjectName("playButton") + self.buttonsLayout.addWidget(self.playButton, 0, 0, 1, 1) + self.saveSessionButton = QtWidgets.QPushButton(self.centralwidget) + self.saveSessionButton.setObjectName("saveSessionButton") + self.buttonsLayout.addWidget(self.saveSessionButton, 0, 2, 1, 1) + self.storeAbButton = QtWidgets.QPushButton(self.centralwidget) + self.storeAbButton.setObjectName("storeAbButton") + self.buttonsLayout.addWidget(self.storeAbButton, 2, 3, 1, 1) + self.loadSessionButton = QtWidgets.QPushButton(self.centralwidget) + self.loadSessionButton.setObjectName("loadSessionButton") + self.buttonsLayout.addWidget(self.loadSessionButton, 0, 3, 1, 1) + self.abRepeatCheckBox = QtWidgets.QCheckBox(self.centralwidget) + self.abRepeatCheckBox.setObjectName("abRepeatCheckBox") + self.buttonsLayout.addWidget(self.abRepeatCheckBox, 2, 1, 1, 1) + self.addSongButton = QtWidgets.QPushButton(self.centralwidget) + self.addSongButton.setObjectName("addSongButton") + self.buttonsLayout.addWidget(self.addSongButton, 2, 4, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") + self.setAButton = QtWidgets.QPushButton(self.centralwidget) + self.setAButton.setObjectName("setAButton") + self.horizontalLayout.addWidget(self.setAButton) + self.setBButton = QtWidgets.QPushButton(self.centralwidget) + self.setBButton.setObjectName("setBButton") + self.horizontalLayout.addWidget(self.setBButton) + self.buttonsLayout.addLayout(self.horizontalLayout, 2, 2, 1, 1) + self.verticalLayout.addLayout(self.buttonsLayout) + MainWindow.setCentralWidget(self.centralwidget) + + self.retranslateUi(MainWindow) + QtCore.QMetaObject.connectSlotsByName(MainWindow) + + def retranslateUi(self, MainWindow): + _translate = QtCore.QCoreApplication.translate + MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow")) + self.pauseButton.setText(_translate("MainWindow", "Pause")) + self.initMidiButton.setText(_translate("MainWindow", "Connect MIDI")) + self.playButton.setText(_translate("MainWindow", "Play")) + self.saveSessionButton.setText(_translate("MainWindow", "Save session")) + self.storeAbButton.setText(_translate("MainWindow", "Store AB")) + self.loadSessionButton.setText(_translate("MainWindow", "Load session")) + self.abRepeatCheckBox.setText(_translate("MainWindow", "AB repeat")) + self.addSongButton.setText(_translate("MainWindow", "Add song")) + self.setAButton.setText(_translate("MainWindow", "Set A")) + self.setBButton.setText(_translate("MainWindow", "Set B")) diff --git a/gui-project/src/mainwindow.ui b/gui-project/src/mainwindow.ui new file mode 100644 index 0000000..ac4d97b --- /dev/null +++ b/gui-project/src/mainwindow.ui @@ -0,0 +1,161 @@ +<?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>971</width> + <height>767</height> + </rect> + </property> + <property name="windowTitle"> + <string>MainWindow</string> + </property> + <widget class="QWidget" name="centralwidget"> + <layout class="QVBoxLayout" name="verticalLayout"> + <item> + <layout class="QHBoxLayout" name="listsLayout"> + <property name="spacing"> + <number>6</number> + </property> + <property name="sizeConstraint"> + <enum>QLayout::SetDefaultConstraint</enum> + </property> + <item> + <widget class="QListView" name="songListView"/> + </item> + <item> + <widget class="QListView" name="abListView"/> + </item> + </layout> + </item> + <item> + <layout class="QVBoxLayout" name="slidersLayout"> + <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> + </layout> + </item> + <item> + <layout class="QGridLayout" name="buttonsLayout"> + <item row="0" column="1"> + <widget class="QPushButton" name="pauseButton"> + <property name="text"> + <string>Pause</string> + </property> + </widget> + </item> + <item row="0" column="4"> + <widget class="QPushButton" name="initMidiButton"> + <property name="text"> + <string>Connect MIDI</string> + </property> + </widget> + </item> + <item row="2" column="0"> + <widget class="QSlider" name="rateSlider"> + <property name="sizePolicy"> + <sizepolicy hsizetype="Minimum" vsizetype="Fixed"> + <horstretch>0</horstretch> + <verstretch>0</verstretch> + </sizepolicy> + </property> + <property name="orientation"> + <enum>Qt::Horizontal</enum> + </property> + </widget> + </item> + <item row="0" column="0"> + <widget class="QPushButton" name="playButton"> + <property name="text"> + <string>Play</string> + </property> + </widget> + </item> + <item row="0" column="2"> + <widget class="QPushButton" name="saveSessionButton"> + <property name="text"> + <string>Save session</string> + </property> + </widget> + </item> + <item row="2" column="3"> + <widget class="QPushButton" name="storeAbButton"> + <property name="text"> + <string>Store AB</string> + </property> + </widget> + </item> + <item row="0" column="3"> + <widget class="QPushButton" name="loadSessionButton"> + <property name="text"> + <string>Load session</string> + </property> + </widget> + </item> + <item row="2" column="1"> + <widget class="QCheckBox" name="abRepeatCheckBox"> + <property name="text"> + <string>AB repeat</string> + </property> + </widget> + </item> + <item row="2" column="4"> + <widget class="QPushButton" name="addSongButton"> + <property name="text"> + <string>Add song</string> + </property> + </widget> + </item> + <item row="2" column="2"> + <layout class="QHBoxLayout" name="horizontalLayout"> + <item> + <widget class="QPushButton" name="setAButton"> + <property name="text"> + <string>Set A</string> + </property> + </widget> + </item> + <item> + <widget class="QPushButton" name="setBButton"> + <property name="text"> + <string>Set B</string> + </property> + </widget> + </item> + </layout> + </item> + </layout> + </item> + </layout> + </widget> + </widget> + <resources/> + <connections/> +</ui> diff --git a/gui-project/src/solo_tool_gui.py b/gui-project/src/solo_tool_gui.py new file mode 100644 index 0000000..0488f84 --- /dev/null +++ b/gui-project/src/solo_tool_gui.py @@ -0,0 +1,261 @@ +import sys + +from PyQt5.QtGui import * +from PyQt5.QtWidgets import * +from PyQt5.QtCore import * +from MainWindow import Ui_MainWindow + +from solo_tool import SoloTool +from solo_tool.midi_controller_launchpad_mini import MidiController + +POSITION_FACTOR = 100000 +RATE_FACTOR = 10 +UI_REFRESH_PERIOD_MS = 500 + +CHANGE_GUI = 0 +CHANGE_INTERNAL = 1 + +class PlaylistModel(QAbstractListModel): + def __init__(self, soloTool, *args, **kwargs): + super(PlaylistModel, self).__init__(*args, **kwargs) + self.soloTool = soloTool + + def data(self, index, role): + if role == Qt.DisplayRole: + from pathlib import Path + path = Path(self.soloTool.getSongs()[index.row()]) + return path.name + + def rowCount(self, index): + return len(self.soloTool.getSongs()) + +class ABListModel(QAbstractListModel): + def __init__(self, soloTool, *args, **kwargs): + super(ABListModel, self).__init__(*args, **kwargs) + self.soloTool = soloTool + + def data(self, index, role): + if role == Qt.DisplayRole: + ab = self.soloTool.getStoredAbLimits()[index.row()] + return f"{ab[0]} - {ab[1]}" + + def rowCount(self, index): + return len(self.soloTool.getStoredAbLimits()) + +class MainWindow(QMainWindow, Ui_MainWindow): + songChangeSignal = pyqtSignal(int) + abLimitsChangeSignal = pyqtSignal(int) + + def __init__(self, *args, **kwargs): + super(MainWindow, self).__init__(*args, **kwargs) + + self.setupUi(self) + + self.timer = QTimer(self) + self.timer.setInterval(UI_REFRESH_PERIOD_MS) + self.timer.timeout.connect(self.timerCallback) + + self.soloTool = SoloTool() + self.midiController = MidiController(self.soloTool) + + self.playlistModel = PlaylistModel(self.soloTool) + self.songListView.setModel(self.playlistModel) + self.songListView.selectionModel().selectionChanged.connect(self.playlistSelectionChanged) + self.songChangePending = None + self.songChangeSignal.connect(self.currentSongChanged) + self.soloTool.registerCurrentSongCallback(self.songChangeSignal.emit) + + self.abListModel = ABListModel(self.soloTool) + self.abListView.setModel(self.abListModel) + self.abListView.selectionModel().selectionChanged.connect(self.abListSelectionChanged) + self.abLimitsChangePending = None + self.abLimitsChangeSignal.connect(self.currentAbLimitsChanged) + self.soloTool.registerCurrentAbLimitsCallback(self.abLimitsChangeSignal.emit) + + self.songSlider.setMaximum(POSITION_FACTOR) + self.songSlider.sliderPressed.connect(self.songSliderPressed) + self.songSlider.sliderReleased.connect(self.songSliderReleased) + + self.aSlider.setMaximum(POSITION_FACTOR) + self.aSlider.sliderReleased.connect(self.abSliderReleased) + self.bSlider.setMaximum(POSITION_FACTOR) + self.bSlider.sliderReleased.connect(self.abSliderReleased) + + self.rateSlider.setRange(int(0.5 * RATE_FACTOR), int(1.2 * RATE_FACTOR)) + self.rateSlider.setSingleStep(int(0.1 * RATE_FACTOR)) + self.rateSlider.setValue(int(1.0 * RATE_FACTOR)) + self.rateSlider.sliderReleased.connect(self.rateSliderReleased) + + self.playButton.pressed.connect(self.soloTool.play) + self.pauseButton.pressed.connect(self.soloTool.pause) + self.storeAbButton.pressed.connect(self.storeAbLimits) + self.setAButton.pressed.connect(self.setA) + self.setBButton.pressed.connect(self.setB) + self.saveSessionButton.pressed.connect(self.saveSession) + self.loadSessionButton.pressed.connect(self.loadSession) + self.addSongButton.pressed.connect(self.addSong) + self.abRepeatCheckBox.clicked.connect(self.toggleAbRepeat) + self.initMidiButton.pressed.connect(self.initMidi) + + self.timer.start() + + if len(sys.argv) > 1: + self.loadSession(sys.argv[1]) + + self.show() + + def timerCallback(self): + position = self.soloTool.getPlaybackPosition() * POSITION_FACTOR + self.songSlider.setValue(int(position)) + self.soloTool.tick() + + def addSong(self): + path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "mp3 Audio (*.mp3);FLAC audio (*.flac);All files (*.*)") + if path: + self.soloTool.addSong(path) + self.playlistModel.layoutChanged.emit() + + def storeAbLimits(self): + a = self.aSlider.value() / float(POSITION_FACTOR) + b = self.bSlider.value() / float(POSITION_FACTOR) + self.soloTool.storeAbLimits(a, b) + self.abListModel.layoutChanged.emit() + + def setA(self): + position = self.songSlider.value() + self.aSlider.setValue(position) + self.abSliderReleased() + + def setB(self): + position = self.songSlider.value() + self.bSlider.setValue(position) + self.abSliderReleased() + + def toggleAbRepeat(self): + enable = self.abRepeatCheckBox.isChecked() + self.soloTool.setAbLimitEnable(enable) + + def saveSession(self): + path, _ = QFileDialog.getSaveFileName(self, "Open file", "", "session file (*.json)") + if path: + self.soloTool.saveSession(path) + + def loadSession(self, path=None): + if path is None: + path, _ = QFileDialog.getOpenFileName(self, "Open file", "", "session file (*.json)") + if path is not None: + self.soloTool.loadSession(path) + self.playlistModel.layoutChanged.emit() + self.abListModel.layoutChanged.emit() + + def songSliderPressed(self): + self.timer.stop() + + def songSliderReleased(self): + position = self.songSlider.value() / float(POSITION_FACTOR) + self.soloTool.setPlaybackPosition(position) + self.timer.start() + + def clearListViewSelection(self, listView): + i = listView.selectionModel().currentIndex() + listView.selectionModel().select(i, QItemSelectionModel.Deselect) + + def abSliderReleased(self): + a = self.aSlider.value() / float(POSITION_FACTOR) + b = self.bSlider.value() / float(POSITION_FACTOR) + self.soloTool.setAbLimits(a, b) + self.clearListViewSelection(self.abListView) + + def rateSliderReleased(self): + rate = self.rateSlider.value() / float(RATE_FACTOR) + self.soloTool.setPlaybackRate(rate) + + def playlistSelectionChanged(self, i): + if self.songChangePending == CHANGE_INTERNAL: + self.songChangePending = None + else: + assert self.songChangePending is None + self.songChangePending = CHANGE_GUI + index = i.indexes()[0].row() + self.soloTool.setSong(index) + + self.clearListViewSelection(self.abListView) + self.abListModel.layoutChanged.emit() + + def currentSongChanged(self, songIndex): + if self.songChangePending == CHANGE_GUI: + self.songChangePending = None + else: + assert self.songChangePending is None + self.songChangePending = CHANGE_INTERNAL + i = self.playlistModel.createIndex(songIndex, 0) + self.songListView.selectionModel().select(i, QItemSelectionModel.ClearAndSelect) + + def abListSelectionChanged(self, i): + if self.abLimitsChangePending == CHANGE_INTERNAL: + print("Ack internal change") + self.abLimitsChangePending = None + else: + assert self.abLimitsChangePending is None + if i is not None and not i.isEmpty(): + print("Processing GUI change") + self.abLimitsChangePending = CHANGE_GUI + index = i.indexes()[0].row() + ab = self.soloTool.getStoredAbLimits()[index] + self.soloTool.loadAbLimits(index) + self.aSlider.setValue(int(ab[0] * POSITION_FACTOR)) + self.bSlider.setValue(int(ab[1] * POSITION_FACTOR)) + + def currentAbLimitsChanged(self, abIndex): + if self.abLimitsChangePending == CHANGE_GUI: + print("Ack GUI change") + self.abLimitsChangePending = None + else: + assert self.abLimitsChangePending is None + print("Processing internal change") + self.abLimitsChangePending = CHANGE_INTERNAL + i = self.abListModel.createIndex(abIndex, 0) + self.abListView.selectionModel().select(i, QItemSelectionModel.ClearAndSelect) + ab = self.soloTool.getStoredAbLimits()[abIndex] + self.aSlider.setValue(int(ab[0] * POSITION_FACTOR)) + self.bSlider.setValue(int(ab[1] * POSITION_FACTOR)) + + def initMidi(self): + try: + self.midiController.connect() + except Exception as e: + print("Error: could not connect to MIDI controller") + print(e) + + def keyPressEvent(self, event): + if event.key() == Qt.Key_Super_L: + self.soloTool.jumpToA() + +def 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_() + +if __name__ == '__main__': + main() |