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.

222 lines
9.0 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 ),
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
#Engine
import engine.api as api
class SelectedInstrumentController(object):
"""Not a qt class. We externally control a collection of widgets.
There is only one set of widgets. We change their contents dynamically.
"""
def __init__(self, parentMainWindow):
self.parentMainWindow = parentMainWindow
self.currentIdKey = None
self.engineData = {} # idKey tuple : engine library metadata dict.
self.statusUpdates = {} # same as engineData, but with incremental status updates. One is guranteed to exist at startup
#Our Widgets
self.ui = parentMainWindow.ui
self.ui.details_groupBox.setTitle("")
self.ui.details_scrollArea.hide() #until the first instrument was selected
self.ui.variants_comboBox.activated.connect(self._newVariantChosen)
self.ui.keySwitch_comboBox.activated.connect(self._newKeySwitchChosen)
#Callbacks
api.callbacks.instrumentListMetadata.append(self.react_initialInstrumentList)
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
def directLibrary(self, idkey:tuple):
"""User clicked on a library treeItem"""
libraryId, instrumentId = idkey
self.currentIdKey = None
self.ui.details_scrollArea.show()
self.ui.variants_comboBox.hide()
self.ui.variant_label.hide()
self.ui.keySwitch_label.hide()
self.ui.keySwitch_comboBox.hide()
metadata = self.engineData[libraryId]["library"]
self.ui.details_groupBox.setTitle(metadata["name"])
self.ui.info_label.setText(self._metadataToDescriptionLabel(metadata))
def _metadataToDescriptionLabel(self, metadata:dict)->str:
"""Can work with instruments and libraries alike"""
if "variants" in metadata: #this is an instrument
fullText = metadata["description"] + "\n\nVendor: " + metadata["vendor"] + "\n\nLicense: " + metadata["license"]
else:
fullText = metadata["description"] + "\n\nVendor: " + metadata["vendor"]
return fullText
def _newVariantChosen(self, index:int):
"""User chose a new variant through the combo box"""
assert self.ui.variants_comboBox.currentIndex() == index
assert self.currentIdKey
api.chooseVariantByIndex(self.currentIdKey, index)
def _newKeySwitchChosen(self, index:int):
"""User chose a new keyswitch through the combo box.
Answer comes back via react_instrumentStatusChanged"""
assert self.ui.keySwitch_comboBox.currentIndex() == index
assert self.currentIdKey
#Send back the midi pitch, not the index
api.setInstrumentKeySwitch(self.currentIdKey, self.ui.keySwitch_comboBox.itemData(index))
def _populateKeySwitchComboBox(self, instrumentStatus):
"""Convert engine format from callback to qt combobox string.
This is called when activating an instrument, switching the variant or simply
coming back to an already loaded instrument in the GUI.
This might come in from a midi callback when we are not currently selected.
We test if this message is really for us.
"""
#TODO: distinguish between momentary keyswitches sw_up sw_down and permanent sw_last. We only want sw_last as selection but the rest must be shown as info somewhere.
self.ui.keySwitch_comboBox.clear()
if not instrumentStatus or not "keySwitches" in instrumentStatus: #not all instruments have keyswitches
self.ui.keySwitch_comboBox.setEnabled(False)
return
engineKeySwitchDict = instrumentStatus["keySwitches"]
self.ui.keySwitch_comboBox.setEnabled(True)
for midiPitch, (opcode, label) in sorted(engineKeySwitchDict.items()):
self.ui.keySwitch_comboBox.addItem(f"[{midiPitch}]: {label}", userData=midiPitch)
#set current one, if any. If not the engine-dict is None and we set to an empty entry, even if there are keyswitches. which is fine.
curIdx = self.ui.keySwitch_comboBox.findData(instrumentStatus["currentKeySwitch"])
self.ui.keySwitch_comboBox.setCurrentIndex(curIdx)
def instrumentChanged(self, idkey:tuple):
"""This is a GUI-internal function. The user selected a different instrument from
the list. Single click, arrow keys etc.
We combine static metadata, which we saved ourselves, with the current instrument status
(e.g. which variant was chosen).
"""
libraryId, instrumentId = idkey
self.currentIdKey = idkey
self.ui.details_scrollArea.show()
#Cached
instrumentStatus = self.statusUpdates[libraryId][instrumentId]
#Static
instrumentData = self.engineData[libraryId][instrumentId]
self.ui.details_groupBox.setTitle(instrumentData["name"])
self.ui.keySwitch_label.show()
self.ui.keySwitch_comboBox.show()
self._populateKeySwitchComboBox(instrumentStatus["keySwitches"]) #clears
self.ui.variant_label.show()
self.ui.variants_comboBox.show()
self.ui.variants_comboBox.clear()
self.ui.variants_comboBox.addItems(instrumentData["variantsWithoutSfzExtension"])
self.ui.info_label.setText(self._metadataToDescriptionLabel(instrumentData))
#Dynamic
self.react_instrumentStatusChanged(self.statusUpdates[libraryId][instrumentId])
def react_instrumentStatusChanged(self, instrumentStatus:dict):
"""Callback from the api. Has nothing to do with any GUI state or selection.
Happens if the user loads a variant.
Data:
#Static ids
result["id"] = self.metadata["id"]
result["idKey"] = self.idKey #redundancy for convenience.
#Dynamic data
result["currentVariant"] = self.currentVariant # str
result["state"] = self.enabled #bool
It is possible that this callback comes in from a midi trigger, so we have no guarantee
that this matches the currently selected instrument in the GUI. We will cache the updated
status but check if this is our message before changing any GUI fields.
This callback is called again by the GUI directly when switching the instrument with a
mouseclick in instrumentChanged and the GUI will use the cached data.
"""
idkey = instrumentStatus["idKey"]
libraryId, instrumentId = idkey
if not libraryId in self.statusUpdates:
self.statusUpdates[libraryId] = {} #empty library. status dict
self.statusUpdates[libraryId][instrumentId] = instrumentStatus #create or overwrite / keep up to date
if not self.currentIdKey == idkey:
#Callback for an instrument currently not selected
return
loadState = instrumentStatus["state"]
instrumentData = self.engineData[libraryId][instrumentId]
instrumentStatus = self.statusUpdates[libraryId][instrumentId]
if loadState: #None if not loaded
self.ui.variants_comboBox.setEnabled(True)
currentVariantIndex = instrumentData["variantsWithoutSfzExtension"].index(instrumentStatus["currentVariantWithoutSfzExtension"])
self.ui.variants_comboBox.setCurrentIndex(currentVariantIndex)
else:
self.ui.variants_comboBox.setEnabled(False)
defaultVariantIndex = instrumentData["variantsWithoutSfzExtension"].index(instrumentData["defaultVariantWithoutSfzExtension"])
self.ui.variants_comboBox.setCurrentIndex(defaultVariantIndex)
self._populateKeySwitchComboBox(instrumentStatus)
def react_initialInstrumentList(self, data:dict):
"""For data form see docstring of instrument.py buildTree()
Summary:
libraryid : dict ->
instrumentid : metadatadict
Dict-Keys are always the same. Some always have data, some can be empty.
We receive this once at program start and build our permanent GUI widgets from it.
The additional status update callback for dynamic data is handled in
self.react_instrumentStatusChanged
"""
self.engineData = data