Sampled Instrument Player with static and monolithic design. All instruments are built-in.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

198 lines
7.9 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
Laborejo2 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 <http://www.gnu.org/licenses/>.
"""
import logging; logger = logging.getLogger(__name__); logger.info("import")
from template.calfbox import cbox
from typing import Iterable, Callable, Tuple
from PyQt5 import QtCore, QtGui, QtWidgets
import engine.api as api
from qtgui.constantsAndConfigs import constantsAndConfigs #Client constantsAndConfigs!
"""
There are two types of submenus in this file. The majority is created in menu.py during start up.
Like Clef, KeySig etc. These don't need to ask for any dynamic values.
The other is like SecondaryTempoChangeMenu. In menu.py this is bound with a lambda construct so a
new instance gets created each time the action is called by the user. Thats why this function has
self.__call__ in its init.
"""
class Submenu(QtWidgets.QDialog):
def __init__(self, mainWindow, labelString, hasOkCancelButtons=False):
super().__init__(mainWindow) #if you don't set the parent to the main window the whole screen will be the root and the dialog pops up in the middle of it.
#self.setModal(True) #we don't need this when called with self.exec() instead of self.show()
self.layout = QtWidgets.QFormLayout()
#self.layout = QtWidgets.QVBoxLayout()
self.setLayout(self.layout)
label = QtWidgets.QLabel(labelString) #"Choose a clef" or so.
self.layout.addWidget(label)
#Second Label that can be changed on every call with self.dynamicLabel.setText()
self.dynamicLabel = QtWidgets.QLabel("")
self.layout.addWidget(self.dynamicLabel)
#self.setFocus(); #self.grabKeyboard(); #redundant for a proper modal dialog. Leave here for documentation reasons.
if hasOkCancelButtons == 1: #or true
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Ok | QtWidgets.QDialogButtonBox.Cancel)
self.buttonBox.accepted.connect(self.process)
self.buttonBox.rejected.connect(self.reject)
elif hasOkCancelButtons == 2: #only cancel. #TODO: unpythonic.
self.buttonBox = QtWidgets.QDialogButtonBox(QtWidgets.QDialogButtonBox.Cancel)
self.buttonBox.rejected.connect(self.reject)
else:
self.buttonBox = None
def keyPressEvent(self, event):
"""Escape closes the dialog by default.
We want Enter as "accept value"
All other methods of mixing editing, window focus and signals
results in strange qt behaviour, triggering the api function twice or more.
Especially unitbox.editingFinished is too easy to trigger.
The key-event method turned out to be the most straightforward way."""
try:
getattr(self, "process")
k = event.key() #49=1, 50=2 etc.
if k == 0x01000004 or k == 0x01000005: #normal enter or keypad enter
event.ignore()
self.process()
else: #Pressed Esc
self.abortHandler()
super().keyPressEvent(event)
except AttributeError:
super().keyPressEvent(event)
def showEvent(self, event):
#TODO: not optimal but better than nothing.
super().showEvent(event)
#self.resize(self.layout.geometry().width(), self.layout.geometry().height())
self.resize(self.childrenRect().height(), self.childrenRect().width())
self.updateGeometry()
def abortHandler(self):
pass
def process(self):
"""Careful! Calling this eats python errors without notice. Make sure your objects exists
and your syntax is correct"""
raise NotImplementedError()
self.done(True)
def __call__(self):
"""This instance can be called like a function.
Subclasses can subclass this as well to create non-static functionality."""
if self.buttonBox:
self.layout.addWidget(self.buttonBox)
self.setFixedSize(self.layout.geometry().size())
self.exec() #blocks until the dialog gets closed
"""
Most submenus have the line "lambda, r, value=value"...
the r is the return value we get automatically from the Qt buttons which need to be handled.
"""
class ChooseOne(Submenu):
"""A generic submenu that presents a list of options to the users.
Only supports up to ten entries, for number shortcuts"""
def __init__(self, mainWindow, title:str, lst:Iterable[Tuple[str, Callable]]):
if len(lst) > 9:
raise ValueError(f"ChooseOne submenu supports up to nine entries. You have {len(lst)}")
super().__init__(mainWindow, title)
for number, (prettyname, function) in enumerate(lst):
button = QtWidgets.QPushButton(f"[{number+1}] {prettyname}")
button.setShortcut(QtGui.QKeySequence(str(number+1)))
button.setStyleSheet("Text-align:left; padding: 5px;");
self.layout.addWidget(button)
button.clicked.connect(function)
button.clicked.connect(self.done)
#TODO: This breaks backend/frontend division. We ask calfbox/jack directly.
def nestedJackPortsMenu(parseOtherOutputs:bool=True, midi:bool=True):
"""
This function queries JACK each time it is called. No cache.
The jack graph has a fixed depth of 1.
Each client gets a submenu.
The default shows all output midi ports of other programs.
If we are a sampler this enables a context menu on an instrument to connect another programs
sequencer port, or a hardware controller, to us.
set parseOtherOutputs to False to get other input ports instead. Use this if we are the sequencer
and click on a track to connect it to an external synth.
switch midi to False for Audio ports. Use this to connect our sample audio outputs (mono)
to external effects or DAW recording tracks.
ourClientPortName is the full jack name, such as:
Patroneo:BassC
"""
menu = QtWidgets.QMenu()
result = {}
hardware = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL))
allPorts = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE))
software = allPorts.difference(hardware)
result["hardware"] = sorted(list(hardware))
result["software"] = sorted(list(software))
action = menu.addAction("Disconnect")
action.setData(-1)
hardwareMenu = menu.addMenu("Hardware")
for clientAndPortString in result["hardware"]:
action = hardwareMenu.addAction(clientAndPortString)
action.setData(clientAndPortString)
softwareClean = {} #clientName:portName
clientMenus = {} #clientName:QMenu
for fullString in result["software"]:
if "a2j" in fullString:
continue
client, port = fullString.split(":", 1)
if not client in softwareClean:
softwareClean[client] = list()
clientMenus[client] = menu.addMenu(client)
softwareClean[client].append(port)
action = clientMenus[client].addAction(port)
action.setData(fullString)
pos = QtGui.QCursor.pos()
pos.setY(pos.y() + 5)
result = menu.exec_(pos)
if result: #None or QAction
return result.data() #text is just the port. data is the full string, we set this ourselves above.