+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
+import string
+
+import rtmidi
+
+
+def _clean_port_names(ports):
+ """
+ Removes port ID numbers from rtmidi’s names to keep them human friendly.
+
+ Note: this may induce name collisions if multiple interfaces with the same
+ name are available.
+ """
+ clean_ports = []
+ for port in ports:
+ clean_ports.append(port.rstrip(string.digits).rstrip(string.whitespace))
+ return sorted(clean_ports)
+
+
+class MidiPorts:
+ _inputs = rtmidi.MidiIn()
+ _outputs = rtmidi.MidiOut()
+
+ input_ports = _clean_port_names(_inputs.get_ports())
+ output_ports = _clean_port_names(_outputs.get_ports())
+
+ @staticmethod
+ def get_midi_inputs():
+ return MidiPorts.input_ports
+
+ @staticmethod
+ def get_midi_outputs():
+ return MidiPorts.output_ports
+
+ @staticmethod
+ def get_default_midi_input():
+ return MidiPorts.input_ports[0]
+
+ @staticmethod
+ def get_default_midi_output():
+ return MidiPorts.output_ports[0]
+
+ @staticmethod
+ def refresh_ports():
+ MidiPorts.input_ports = _clean_port_names(MidiPorts._inputs.get_ports())
+ MidiPorts.output_ports = _clean_port_names(MidiPorts._outputs.get_ports())
diff --git a/python_mcu/PythonMcu/Tools/AboutDialog.py b/python_mcu/PythonMcu/Tools/AboutDialog.py
index 767ff59..49857f9 100644
--- a/python_mcu/PythonMcu/Tools/AboutDialog.py
+++ b/python_mcu/PythonMcu/Tools/AboutDialog.py
@@ -26,15 +26,11 @@
import sys
-if __name__ == "__main__":
- # allow "PythonMcu" package imports when executing this module
- sys.path.append('../../')
+from PySide6.QtCore import Qt
+from PySide6.QtGui import QFont, QTextCharFormat, QFontMetrics
+from PySide6.QtWidgets import QDialog, QVBoxLayout, QTextBrowser, QHBoxLayout, QLabel, QPushButton, QApplication
-from PythonMcu.Tools.ApplicationConfiguration import ApplicationConfiguration
-
-from PySide2.QtCore import Qt
-from PySide2.QtGui import QFont, QTextCharFormat, QFontMetrics
-from PySide2.QtWidgets import QDialog, QVBoxLayout, QTextBrowser, QHBoxLayout, QLabel, QPushButton, QApplication
+from .ApplicationConfiguration import ApplicationConfiguration
# noinspection PyArgumentList
@@ -55,7 +51,8 @@ def __init__(self, parent=None):
char_format = QTextCharFormat()
char_format.setFontFamily(font.defaultFamily())
- text_width = QFontMetrics(char_format.font()).width('*') * 83
+ #text_width = QFontMetrics(char_format.font()).width('*') * 83
+ text_width = QFontMetrics(char_format.font()).horizontalAdvance('*' * 83)
text_height = QFontMetrics(char_format.font()).height() * 40
self._edit_license = QTextBrowser()
diff --git a/python_mcu/PythonMcu/Tools/ApplicationAbout.py b/python_mcu/PythonMcu/Tools/ApplicationAbout.py
index 65c00e9..76d9341 100644
--- a/python_mcu/PythonMcu/Tools/ApplicationAbout.py
+++ b/python_mcu/PythonMcu/Tools/ApplicationAbout.py
@@ -1504,9 +1504,9 @@ def __repr__(self):
if setting in ('license_plain', 'license_html'):
short_setting = '\n'.join(self._about[setting].split('\n')[:5])
short_setting += '\n[...]'
- output += '[%s]\n%s\n\n' % (setting, short_setting)
+ output += f'[{setting}]\n{short_setting}\n\n'
else:
- output += '[%s]\n%s\n\n' % (setting, self._about[setting])
+ output += f'[{setting}]\n{self._about[setting]}\n\n'
output = output.strip('\n')
# dump the whole thing
@@ -1538,10 +1538,7 @@ def get_copyrights(self):
Formatted string containing application copyrights
"""
- return '(c) %(copyright_years)s %(authors)s' % {
- 'copyright_years': self.get('copyright_years'),
- 'authors': self.get('authors')
- }
+ return f'(c) {self.get("copyright_years")} {self.get("authors")}'
def get_license(self, selection):
"""Return application license or its terms as string.
@@ -1560,7 +1557,7 @@ def get_license(self, selection):
"""
if selection in ('selected', 'name', 'short', 'plain', 'html'):
- return self.get('license_%s' % selection)
+ return self.get(f'license_{selection}')
return None
@@ -1577,10 +1574,7 @@ def get_version(self, long):
"""
if long:
- return '%(application)s %(version)s' % {
- 'application': self.get('application'),
- 'version': self.get('version')
- }
+ return f'{self.get("application")} {self.get("version")}'
return self.get('version')
@@ -1616,17 +1610,14 @@ def get_full_description(self, text_format='plain'):
"""
if text_format == 'html':
- output = '%s
%s
%s
' % (
- self.get_version(True),
- self.get_copyrights(),
- self.get_description(False)
- )
+ output = f'{self.get_version(True)}
'
+ output += f'{self.get_copyrights()}
{self.get_description(False)}
'
- output += 'License
%s
' % self.get_license('short')
+ output += f'License
{self.get_license("short")}'
contributors = self.get('contributors')
if contributors:
- output += 'Contributors
%s
' % contributors
+ output += f'Contributors
{contributors}
'
return output
diff --git a/python_mcu/PythonMcu/Tools/ApplicationConfiguration.py b/python_mcu/PythonMcu/Tools/ApplicationConfiguration.py
index 234d62f..d10fe4c 100644
--- a/python_mcu/PythonMcu/Tools/ApplicationConfiguration.py
+++ b/python_mcu/PythonMcu/Tools/ApplicationConfiguration.py
@@ -27,7 +27,7 @@
import configparser
import os
-from PythonMcu.Tools import ApplicationAbout
+from .ApplicationAbout import ApplicationAbout
class SortedDict(dict):
@@ -81,7 +81,7 @@ def __init__(self):
"""
# initialise application information
- self._about = ApplicationAbout.ApplicationAbout()
+ self._about = ApplicationAbout()
# ascertain compatibility with the class "ApplicationAbout"
assert self.get_application_information('about_class_incarnation') == 3
@@ -112,10 +112,10 @@ def __repr__(self):
output += '\n\n\nConfiguration file\n=================='
# append sorted sections
for section in self.get_sections():
- output += '\n[%s]\n' % section
+ output += f'\n{section}\n'
# append sorted options
for item in self.get_items(section):
- output += '%s: %s\n' % (item[0], item[1])
+ output += f'{item[0]}: {item[1]}\n'
# dump the whole thing
return output.strip('\n')
diff --git a/python_mcu/__init__.py b/python_mcu/__init__.py
new file mode 100644
index 0000000..6e59cc5
--- /dev/null
+++ b/python_mcu/__init__.py
@@ -0,0 +1,22 @@
+# -*- coding: utf-8 -*-
+
+"""
+PythonMcu
+=========
+Mackie Host Controller written in Python
+Copyright (c) 2011 Martin Zuther (http://www.mzuther.de/)
+Copyright (c) 2021 Raphaël Doursenaud
+
+This program is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program. If not, see .
+"""
diff --git a/python_mcu/python_mcu.py b/python_mcu/python_mcu.py
index fc4b567..dee6506 100644
--- a/python_mcu/python_mcu.py
+++ b/python_mcu/python_mcu.py
@@ -24,27 +24,27 @@
Thank you for using free software!
"""
-
+import importlib
import platform
import sys
-import PySide2
-import pygame.version
-from PySide2.QtCore import QTimer, Qt
-from PySide2.QtGui import QFont, QFontMetrics, QTextCharFormat, QTextCursor
-from PySide2.QtWidgets import QFrame, QApplication, QPlainTextEdit, QStyle, QHBoxLayout, QVBoxLayout, QGridLayout, \
- QLabel, QComboBox, QPushButton
+import PySide6
+import rtmidi.version
+from PySide6.QtCore import QTimer, Qt
+from PySide6.QtGui import QFont, QFontMetrics, QTextCharFormat, QTextCursor
+from PySide6.QtWidgets import QFrame, QApplication, QPlainTextEdit, QStyle, QHBoxLayout, QVBoxLayout, QGridLayout, \
+ QLabel, QComboBox, QPushButton, QCheckBox
-# noinspection PyUnresolvedReferences
-from PythonMcu.Hardware import *
from PythonMcu.MackieControl.MackieHostControl import MackieHostControl
from PythonMcu.McuInterconnector.McuInterconnector import McuInterconnector
-from PythonMcu.Midi.MidiConnection import MidiConnection
+from PythonMcu.Midi.MidiPorts import MidiPorts
from PythonMcu.Tools.AboutDialog import AboutDialog
from PythonMcu.Tools.ApplicationConfiguration import ApplicationConfiguration
configuration = ApplicationConfiguration()
+DEBUG = True
+
# noinspection PyArgumentList
class PythonMcuApp(QFrame):
@@ -87,9 +87,10 @@ def __init__(self, parent=None):
self.callback_log('')
self.callback_log('Version numbers')
self.callback_log('===============')
- self.callback_log('Python: %s (%s)' % (platform.python_version(), platform.python_implementation()))
- self.callback_log('PySide: %s' % PySide2.__version__)
- self.callback_log('pygame: %s' % pygame.version.ver)
+ self.callback_log('')
+ self.callback_log(f'Python: {platform.python_version()} ({platform.python_implementation()})')
+ self.callback_log(f'PySide6: {PySide6.__version__}')
+ self.callback_log(f'rtmidi: {rtmidi.version.version}')
self.callback_log('')
self.callback_log('')
@@ -114,6 +115,8 @@ def __init__(self, parent=None):
'Novation ZeRO SL MkII',
'Novation ZeRO SL MkII (MIDI)'
]
+ if DEBUG:
+ hardware_controllers.append('_Midi Controller Template')
self.setWindowTitle(configuration.get_version(True))
@@ -153,14 +156,16 @@ def __init__(self, parent=None):
'Connection:', connection_types
)
+ # TODO: create automatically and only show name
self._combo_mcu_midi_input = self._create_combo_box(
self.grid_layout_mcu, self._mcu_midi_input,
- 'MIDI In:', MidiConnection.get_midi_inputs()
+ 'MIDI In:', MidiPorts.get_midi_inputs()
)
+ # TODO: create automatically and only show name
self._combo_mcu_midi_output = self._create_combo_box(
self.grid_layout_mcu, self._mcu_midi_output,
- 'MIDI Out:', MidiConnection.get_midi_outputs()
+ 'MIDI Out:', MidiPorts.get_midi_outputs()
)
self._combo_hardware_controller = self._create_combo_box(
@@ -170,14 +175,18 @@ def __init__(self, parent=None):
self._combo_controller_midi_input = self._create_combo_box(
self.grid_layout_controller, self._controller_midi_input,
- 'MIDI In:', MidiConnection.get_midi_inputs()
+ 'MIDI In:', MidiPorts.get_midi_inputs()
)
-
self._combo_controller_midi_output = self._create_combo_box(
self.grid_layout_controller, self._controller_midi_output,
- 'MIDI Out:', MidiConnection.get_midi_outputs()
+ 'MIDI Out:', MidiPorts.get_midi_outputs()
)
+ self.button_controller_midi_refresh = QPushButton('Refresh MIDI ports')
+ row = self.grid_layout_controller.rowCount()
+ self.grid_layout_controller.addWidget(self.button_controller_midi_refresh, row, 1)
+ self.button_controller_midi_refresh.clicked.connect(self.midiports_refresh)
+
self.grid_layout_controller.addWidget(
self._edit_usage_hint, self.grid_layout_controller.rowCount(),
0, 1, 2
@@ -194,9 +203,14 @@ def __init__(self, parent=None):
self.button_start_stop.setFocus()
self.button_start_stop.clicked.connect(self.interconnector_start_stop)
+ # TODO: add autostart checkbox and configuration
+ self.checkbox_autostart = QCheckBox('Autostart')
+ self.bottom_layout.addWidget(self.checkbox_autostart)
+
self.button_close = QPushButton('&Close')
self.bottom_layout.addWidget(self.button_close)
self.button_close.clicked.connect(self.close_application)
+ self.closeEvent = self.close_event # Hide PySide's non pythonic method name
self.button_about = QPushButton('A&bout')
self.bottom_layout.addWidget(self.button_about)
@@ -297,16 +311,15 @@ def _initialise_hardware_controller(self):
self._hardware_controller_class = self._hardware_controller_class.replace('[', '').replace(']', '')
self._hardware_controller_class = self._hardware_controller_class.replace('{', '').replace('}', '')
- # get hardware controller's preferred MIDI ports
- eval_controller_midi_input = '{0!s}.{0!s}.get_preferred_midi_input()'.format(self._hardware_controller_class)
- eval_controller_midi_output = '{0!s}.{0!s}.get_preferred_midi_output()'.format(self._hardware_controller_class)
+ # FIXME: factorize into factory method
+ module = importlib.import_module('.' + self._hardware_controller_class, package='PythonMcu.Hardware')
+ hw_class = getattr(module, self._hardware_controller_class)
- controller_midi_input_default = eval(eval_controller_midi_input)
- controller_midi_output_default = eval(eval_controller_midi_output)
+ controller_midi_input_default = hw_class.get_preferred_midi_input()
+ controller_midi_output_default = hw_class.get_preferred_midi_output()
# show controller's usage hint
- usage_hint = '{0!s}.{0!s}.get_usage_hint()'.format(self._hardware_controller_class)
- self._edit_usage_hint.setPlainText(eval(usage_hint))
+ self._edit_usage_hint.setPlainText(hw_class.get_usage_hint())
return controller_midi_input_default, controller_midi_output_default
@@ -317,6 +330,15 @@ def callback_log(self, message, repaint=False):
print(message)
self._edit_logger.appendPlainText(message)
+ def midiports_refresh(self):
+ MidiPorts.refresh_ports()
+
+ self._combo_controller_midi_input.clear()
+ self._combo_controller_midi_input.addItems(MidiPorts.get_midi_inputs())
+
+ self._combo_controller_midi_output.clear()
+ self._combo_controller_midi_output.addItems(MidiPorts.get_midi_outputs())
+
def combobox_item_selected(self):
widget = self.sender()
selected_text = widget.currentText()
@@ -381,7 +403,7 @@ def combobox_item_selected(self):
self._mcu_connection
)
else:
- self.callback_log('QComboBox not handled ("%s").' % selected_text)
+ self.callback_log(f'QComboBox not handled ("{selected_text}").')
def process_midi_input(self):
self._interconnector.process_midi_input()
@@ -391,21 +413,21 @@ def display_about(self):
def interconnector_start_stop(self):
if not self._interconnector:
+ self.button_start_stop.setText('&Starting...')
self._enable_controls(False)
- self.button_start_stop.setText('&Stop')
self.callback_log('Settings')
self.callback_log('========')
- self.callback_log('MCU emulation: %s' % self._mcu_emulated_model)
- self.callback_log('Connection: %s' % self._mcu_connection)
- self.callback_log('MIDI input: %s' % self._mcu_midi_input)
- self.callback_log('MIDI output: %s' % self._mcu_midi_output)
+ self.callback_log(f'MCU emulation: {self._mcu_emulated_model}')
+ self.callback_log(f'Connection: {self._mcu_connection}')
+ self.callback_log(f'MIDI input: {self._mcu_midi_input}')
+ self.callback_log(f'MIDI output: {self._mcu_midi_output}')
self.callback_log('')
- self.callback_log('Controller: %s' % self._hardware_controller)
- self.callback_log('MIDI input: %s' % self._controller_midi_input)
- self.callback_log('MIDI output: %s' % self._controller_midi_output)
+ self.callback_log(f'Controller: {self._hardware_controller}')
+ self.callback_log(f'MIDI input: {self._controller_midi_input}')
+ self.callback_log(f'MIDI output: {self._controller_midi_output}')
self.callback_log('')
- self.callback_log('MIDI latency: %s ms' % self._midi_latency)
+ self.callback_log(f'MIDI latency: {self._midi_latency} ms')
self.callback_log('')
self.callback_log('')
@@ -430,13 +452,29 @@ def interconnector_start_stop(self):
self._controller_midi_output,
self.callback_log
)
- self._interconnector.connect()
+ try:
+ self._interconnector.connect()
+ except ValueError as e:
+ self.callback_log(f'Connecting failed with the following error: {e!r}')
+ self._interconnector.disconnect()
+ self. _interconnector = None
+ self._enable_controls(True)
+ self.button_start_stop.setText('&Start')
+ return
+
+ # We set the button after making sure we can connect
+ self._enable_controls(False)
+ self.button_start_stop.setText('&Stop')
self._timer.start()
else:
+ self.button_start_stop.setText('&Stopping')
+
+ self._interconnector_stop()
+
+ # We set the button after making sure we can properly stop
self._enable_controls(True)
self.button_start_stop.setText('&Start')
- self._interconnector_stop()
def _interconnector_stop(self):
self._timer.stop()
@@ -453,7 +491,7 @@ def _interconnector_stop(self):
def close_application(self):
self.close()
- def closeEvent(self, event):
+ def close_event(self, _):
if self._interconnector:
self._interconnector_stop()
@@ -470,4 +508,4 @@ def closeEvent(self, event):
python_mcu.show()
# Run the main Qt loop
- sys.exit(app.exec_())
+ sys.exit(app.exec())
diff --git a/requirements.txt b/requirements.txt
index deffc2b..be03b49 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
-pygame
-pyside2
\ No newline at end of file
+python-rtmidi
+pyside6
\ No newline at end of file
diff --git a/test-requirements.txt b/test-requirements.txt
new file mode 100644
index 0000000..18f9fdc
--- /dev/null
+++ b/test-requirements.txt
@@ -0,0 +1,3 @@
+-r requirements.txt
+pylint
+flake8
\ No newline at end of file