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.
 
 

330 lines
14 KiB

#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This application 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")
#Standard Library
#Third Party
from PyQt5 import QtCore, QtGui, QtWidgets
#Our Qt
from template.qtgui.helper import ToggleSwitch
#Engine
import engine.api as api
COLUMNS = ("state", "id-key", "name", "loaded", "group", "tags" ) #Loaded = Variant
class InstrumentTreeController(object):
"""Not a qt class. We externally controls the QTreeWidget
Why is this not a QTableWidget? As in Agordejo, a TableWidget is a complex item, and inconvenient
to use. You need to add an Item to each cell. While in TreeWidget you just create one item.
And we might use the TreeView to group by manufacturer etc. Eventhough we have a Filter.
"""
def __init__(self, parentMainWindow):
self.parentMainWindow = parentMainWindow
self.treeWidget = self.parentMainWindow.ui.instruments_treeWidget
self.guiInstruments = {} # id-key : GuiInstrument
self.headerLabels = [
" ",
QtCore.QCoreApplication.translate("InstrumentTreeController", "ID"),
QtCore.QCoreApplication.translate("InstrumentTreeController", "Name"),
QtCore.QCoreApplication.translate("InstrumentTreeController", "Loaded"),
QtCore.QCoreApplication.translate("InstrumentTreeController", "Group"),
QtCore.QCoreApplication.translate("InstrumentTreeController", "Tags"),
]
self.treeWidget.setColumnCount(len(self.headerLabels))
self.treeWidget.setHeaderLabels(self.headerLabels)
self.treeWidget.setSortingEnabled(True)
self.treeWidget.setAlternatingRowColors(True)
self.treeWidget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
self.treeWidget.header().setSortIndicator(0,0)
self.treeWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged)
self.sortByColumnValue = 1 #by instrId
self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending
api.callbacks.instrumentListMetadata.append(self.buildTree)
api.callbacks.startLoadingSamples.append(self.react_startLoadingSamples)
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
def itemSelectionChanged(self):
"""Only one instrument can be selected at the same time.
This function mostly informs other widgets that a different instrument was selected
"""
selItems = self.treeWidget.selectedItems()
assert len(selItems) == 1, selItems #because our selection is set to single
item = selItems[0]
if type(item) is GuiInstrument:
self.parentMainWindow.selectedInstrumentController.instrumentChanged(item.idkey)
else:
self.parentMainWindow.selectedInstrumentController.directLibrary(item.idkey)
def itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int):
if type(item) is GuiInstrument:
api.auditionerInstrument(item.idkey)
def buildTree(self, data:dict):
"""Data is a dict of dicts and has a hierarchy.
data[libId] = dictOfInstruments
dictOfInstrument[instrId] = pythonDataDict
There is one special "library" key dictOfInstrument["library"] that has metadata for
the lib itself. We use that to construct the top level item and sort all others inside
Example data
{'0': {'0': {'group': 'strings',
'id': '0',
'id-key': ('0', '0'),
'license': 'https://unlicense.org/',
'name': 'Sine Wave',
'tags': ['sine', 'basic'],
'variants': ['Sine.sfz', 'Sine With 5th Slide.sfz'],
'vendor': 'Test entry to provide more vendor information'},
'1': {'group': 'strings',
'id': '1',
'id-key': ('0', '1'),
'license': '',
'name': 'Square Wave',
'tags': ['square', 'basic'],
'variants': ['Square.sfz', 'Square With 5th Slide.sfz'],
'vendor': ''},
'2': {'group': 'brass',
'id': '2',
'id-key': ('0', '2'),
'license': '',
'name': 'Saw Wave',
'tags': ['saw', 'basic'],
'variants': ['Saw.sfz', 'Saw With 5th Slide.sfz'],
'vendor': ''},
'3': {'group': '',
'id': '3',
'id-key': ('0', '3'),
'license': '',
'name': 'Triangle Wave',
'tags': ['triangle', 'complex'],
'variants': ['Triangle.sfz', 'Triangle With 5th Slide.sfz'],
'vendor': ''},
'library': {'description': 'Basic waveforms. Actual non-looping '
'samples, created by sox. No sfz '
'synth-engine involved. There is a variant '
'with an additional sound-blip for each wave '
'form, which are purely there as technical '
'example. They are not a guideline what '
'constitues a variant and what a different '
'instrument.',
'id': '0',
'license': 'https://creativecommons.org/publicdomain/zero/1.0/',
'name': 'Tembro Test Instruments',
'tarFilePath': PosixPath('/home/nils/lss/test-data.tar'),
'vendor': 'Hilbricht Nils 2021, Laborejo Software Suite '
'https://www.laborejo.org info@laborejo.org'}}}
"""
for libraryId, libraryDict in data.items():
parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"])
self.treeWidget.addTopLevelItem(parentLibraryWidget)
for instrumentdId, instrumentDict in libraryDict.items():
if instrumentdId == "library":
#Top level item was already created. Ignore here.
pass
else:
self.newInstrument(parentLibraryWidget, instrumentDict)
parentLibraryWidget.setExpanded(True)
self._adjustColumnSize()
def newInstrument(self, parentLibraryWidget, instrumentDict):
gi = GuiInstrument(parentTreeController=self, instrumentDict=instrumentDict)
#self.treeWidget.addTopLevelItem(gi)
parentLibraryWidget.addChild(gi)
gi.injectToggleSwitch() #only possible after gi.init was done and item inserted.
self._adjustColumnSize()
self.guiInstruments[instrumentDict["id-key"]] = gi
def _adjustColumnSize(self):
self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue)
stateIndex = COLUMNS.index("state")
for index in range(self.treeWidget.columnCount()):
if not index == stateIndex:
self.treeWidget.resizeColumnToContents(index)
self.treeWidget.setColumnWidth(index, self.treeWidget.columnWidth(index)+15) #add padding
#Fixed width for the switch
self.treeWidget.setColumnWidth(stateIndex, 80)
def react_instrumentStatusChanged(self, instrumentStatus:dict):
gi = self.guiInstruments[instrumentStatus["id-key"]]
gi.updateStatus(instrumentStatus)
def react_startLoadingSamples(self, idkey:tuple):
"""Will be overriden by instrument status change / variant chosen"""
text = QtCore.QCoreApplication.translate("InstrumentTreeController", "…loading…")
loadedIndex = COLUMNS.index("loaded")
instr = self.guiInstruments[idkey]
instr.setText(loadedIndex, text)
self.parentMainWindow.qtApp.processEvents() #actually show the label
class GuiLibrary(QtWidgets.QTreeWidgetItem):
"""The top level library item. All instruments are in a library."""
def __init__(self, parentTreeController, libraryDict):
super().__init__([], type=1000) #type 0 is default qt type. 1000 is subclassed user type)
self.idkey = (libraryDict["id"], 0) #fake it for compatibility
#No dynamic data here. Everything gets created once.
self.setText(COLUMNS.index("id-key"), str(libraryDict["id"]))
self.setText(COLUMNS.index("name"), str(libraryDict["name"]))
self.setText(COLUMNS.index("tags"), str(libraryDict["description"])[:42]+"")
class GuiInstrument(QtWidgets.QTreeWidgetItem):
"""
Why is this not a QTableWidget? As in Agordejo, a TableWidget is a complex item, and inconvenient
to use. You need to add an Item to each cell. While in TreeWidget you just create one item.
All data is received at program start. No new items will be created, none will get deleted.
All instruments in Tembro are static.
Most parameters we receive are read only, like instrId, name and version
Not all parameters are used in the TreeWidgetItem. e.g. Description, Vendor and License are only
for the details view.
"""
allItems = {} # instrId : GuiInstrument
def __init__(self, parentTreeController, instrumentDict):
GuiInstrument.allItems[instrumentDict["id-key"]] = self
self.parentTreeController = parentTreeController
self.idkey = instrumentDict["id-key"]
#Start with empty columns. We fill in later in _writeColumns
super().__init__([], type=1000) #type 0 is default qt type. 1000 is subclassed user type)
self.columns = COLUMNS
#Use with:
#nameColumnIndex = self.columns.index("prettyName")
#self.setText(nameColumnIndex, "hello")
self.setTextAlignment(self.columns.index("id-key"), QtCore.Qt.AlignHCenter)
self.state = None #by self.switch()
self.instrumentDict = None
self._writeColumns(instrumentDict)
self.toggleSwitch = ToggleSwitch()
self.toggleSwitch.setAutoFillBackground(True) #otherwise conflicts with setItemWidget
#self.toggleSwitch.toggled.connect(lambda c: print('toggled', c)) #triggered by engine callback as well
self.toggleSwitch.clicked.connect(self.instrumentSwitchOnViaGui)
#self.toggleSwitch.pressed.connect(lambda: print('pressed')) #literal mouse down.
#self.toggleSwitch.released.connect(lambda: print('released'))
#We cannot add the ToggleSwitch Widget here.
#It must be inserted after self was added to the Tree. Use self.injectToggleSwitch from parent
def instrumentSwitchOnViaGui(self, state):
"""Only GUI clicks. Does not react to the engine callback that switches on instruments. For
example one that arrives through "load group" or "load all" """
if state:
api.loadInstrumentSamples(self.idkey)
else:
api.unloadInstrumentSamples(self.idkey)
def updateStatus(self, instrumentStatus:dict):
variantColumnIndex = self.columns.index("loaded")
self.currentVariant = instrumentStatus["currentVariant"]
if instrumentStatus["currentVariant"]: #None if not loaded or not enabled anymore
self.setText(variantColumnIndex, instrumentStatus["currentVariant"].rstrip(".sfz"))
self.state = instrumentStatus["state"]
self.toggleSwitch.setChecked(instrumentStatus["state"])
def injectToggleSwitch(self):
"""Call this after the item was added to the tree"""
stateColumnIndex = self.columns.index("state")
self.parentTreeController.treeWidget.setItemWidget(self, stateColumnIndex, self.toggleSwitch)
def _writeColumns(self, instrumentDict):
"""This is used to construct the columns when the program starts. There is an
update callback for dynamic values as well"""
self.instrumentDict = instrumentDict
for index, key in enumerate(self.columns):
QtCore.QCoreApplication.translate("OpenSession", "not saved")
if key == "state" or key == "loaded":
pass #this arrives through a different api.callback
elif type(instrumentDict[key]) is str:
self.setText(index, str(instrumentDict[key]))
elif key == "tags": #list
t = ", ".join(instrumentDict[key])
self.setText(index, t)
elif key == "id-key": #tuple
libId, instrId = instrumentDict[key]
self.setText(index, f"{libId}-{instrId}")
"""
elif key == "state": #use parameter for initial value. loaded from file or default = False.
state = instrumentDict[key]
assert type(state) is bool, state
self.switch(state)
"""
def switch(self, state:bool):
"""This is not the Qt function but if an instrument is enabled, loaded to RAM and ready to
receive midi data.
Function will mimic Qt disabled behaviour by greying things out and deactivating individual
sub-widgets. But some, like the GUI switch itself, will always stay enabled."""
self.state = state
def toggleSwitchState(self):
self.switch(not self.state)