aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEddy Pedroni <eddy@0xf7.com>2021-11-02 21:56:30 +0100
committerEddy Pedroni <eddy@0xf7.com>2021-11-02 21:56:30 +0100
commit27f908340a5b8d3c8a9f3354f99712be5a0f739c (patch)
tree34fc2b08800d97d4edb13713ca8234482ae13721
parent6a74b090b13a9e1ff37338332627eb5f16ed7d40 (diff)
Implemented basic features
-rw-r--r--MainWindow.py81
-rw-r--r--mainwindow.ui112
-rw-r--r--requirements.txt2
-rw-r--r--solo-tool.py196
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_()