Browse Source

Show and use keyswitches from the GUI

master
Nils 1 year ago
parent
commit
4e654ff424
  1. 34
      engine/api.py
  2. 13
      engine/auditioner.py
  3. 212
      engine/instrument.py
  4. 2
      engine/main.py
  5. 2
      qtgui/auditioner.py
  6. 19
      qtgui/designer/mainwindow.py
  7. 20
      qtgui/designer/mainwindow.ui
  8. 4
      qtgui/instrument.py
  9. 69
      qtgui/selectedinstrumentcontroller.py

34
engine/api.py

@ -137,6 +137,8 @@ def startEngine(nsmClient, additionalData):
#Inject the _instrumentMidiNoteOnActivity callback into session.data for access.
session.data.instrumentMidiNoteOnActivity = callbacks._instrumentMidiNoteOnActivity
callbacks.instrumentMidiNoteOnActivity.append(_checkForKeySwitch)
callbacks._instrumentListMetadata() #Relatively quick. Happens only once in the program
#One round of status updates for all instruments. Still no samples loaded, but we saved the status of each with our own data.
for instrument in Instrument.allInstruments.values():
@ -147,7 +149,6 @@ def startEngine(nsmClient, additionalData):
atexit.register(unloadAllInstrumentSamples) #this will handle all python exceptions, but not segfaults of C modules.
logger.info("Tembro api startEngine complete")
def loadAllInstrumentSamples():
"""Actually load all instrument samples"""
logger.info(f"Loading all instruments.")
@ -208,14 +209,34 @@ def setInstrumentMixerVolume(idkey:tuple, value:float):
Default is -3.0 """
instrument = Instrument.allInstruments[idkey]
instrument.mixerLevel = value
libraryId, instrumentId = idkey
callbacks._instrumentStatusChanged(libraryId, instrumentId)
callbacks._instrumentStatusChanged(*instrument.idKey)
def setInstrumentMixerEnabled(idkey:tuple, state:bool):
instrument = Instrument.allInstruments[idkey]
instrument.setMixerEnabled(state)
libraryId, instrumentId = idkey
callbacks._instrumentStatusChanged(libraryId, instrumentId)
callbacks._instrumentStatusChanged(*instrument.idKey)
def setInstrumentKeySwitch(idkey:tuple, keySwitchMidiPitch:int):
"""Choose a keyswitch of the currently selected variant of this instrument. Keyswitch does not
exist: The engine will throw a warning if the keyswitch was not in our internal representation,
but nothing bad will happen otherwise.
We use the key-midi-pitch directly.
"""
instrument = Instrument.allInstruments[idkey]
result = instrument.setKeySwitch(keySwitchMidiPitch)
if result: #could be None
changed, nowKeySwitchPitch = result
#Send in any case, no matter if changed or not. Doesn't hurt.
callbacks._instrumentStatusChanged(*instrument.idKey)
def _checkForKeySwitch(idkey:tuple):
"""We added this ourselves to the note-on midi callback.
So this gets called for every note-one."""
instrument = Instrument.allInstruments[idkey]
changed, nowKeySwitchPitch = instrument.updateCurrentKeySwitch()
if changed:
callbacks._instrumentStatusChanged(*instrument.idKey)
def auditionerInstrument(idkey:tuple):
"""Load an indendepent instance of an instrument into the auditioner port"""
@ -227,7 +248,8 @@ def auditionerInstrument(idkey:tuple):
var = originalInstrument.currentVariant
else:
var = originalInstrument.defaultVariant
session.data.auditioner.loadInstrument(originalInstrument.tarFilePath, originalInstrument.rootPrefixPath, var)
session.data.auditioner.loadInstrument(originalInstrument.tarFilePath, originalInstrument.rootPrefixPath, var, originalInstrument.currentKeySwitch)
callbacks._auditionerInstrumentChanged(libraryId, instrumentId)
def getAvailableAuditionerPorts()->dict:

13
engine/auditioner.py

@ -101,10 +101,11 @@ class Auditioner(object):
#Dynamic data
result["currentVariant"] = self.currentVariant # str
result["state"] = self.enabled #bool
result["currentKeySwitch"] = self.currentKeySwitch #int or None
return result
def loadInstrument(self, tarFilePath, rootPrefixPath:str, variantSfzFileName:str):
def loadInstrument(self, tarFilePath, rootPrefixPath:str, variantSfzFileName:str, keySwitchMidiPitch:int):
"""load_patch_from_tar is blocking. This function will return when the instrument is ready
to play.
@ -113,11 +114,17 @@ class Auditioner(object):
logger.info(f"Start loading samples for auditioner {variantSfzFileName}")
#help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc.
#newProgramNumber = self.instrumentLayer.engine.get_unused_program()
programNumber = 1
programNumber = 0
name = variantSfzFileName
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, tarFilePath, rootPrefixPath+variantSfzFileName, name)
self.instrumentLayer.engine.set_patch(1, programNumber) #1 is the channel, counting from 1. #TODO: we want this to be on all channels.
self.currentVariant = variantSfzFileName
if keySwitchMidiPitch is None:
self.currentKeySwitch = None
else:
self.currentKeySwitch = keySwitchMidiPitch
self.scene.send_midi_event(0x90, keySwitchMidiPitch, 64)
logger.info(f"Finished loading samples for auditioner {variantSfzFileName}")

212
engine/instrument.py

@ -28,14 +28,15 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
from calfbox import cbox
#Template Modules
from template.engine.pitch import midiName2midiPitch #dict
from template.engine.input_midi import MidiProcessor
class Instrument(object):
"""Literally one instrument.
It might exists in different versions that are all loaded here and can be switched in the GUI.
For that we identify different .sfz files by a minor version number (see below).
It might exists in different variants and version that are all loaded here and can be
switched in the GUI.
All data is provided by the parsed metadata dict, except the filepath of the tar which calfbox
needs again here.
@ -73,7 +74,7 @@ class Instrument(object):
For example they use a different control scheme (different CC maps)
Besides version there is also the option to just name the sfz file anything you want, as a
special variant. Which is problematic:
special variant.
What constitues as "Instrument Variant" and what as "New Instrument" must be decided on a case
by case basis. For example a different piano than the salamander is surely a new instrument.
But putting a blanket over the strings (prepared piano) to muffle the sound is the same physical
@ -119,6 +120,8 @@ class Instrument(object):
self.currentVariant:str = "" #This is the currently loaded variant. Only set after actual loading samples. That means it is "" even from a savefile and only set later.
#We could call self.loadSamples() now, but we delay that for the user experience. See docstring.
self.currentVariantKeySwitches = None #set by _parseKeySwitches through chooseVariant . Ttuple: dict, sw_lokey, sw_highkey . See docstring of parseKeySwitches
self.currentKeySwitch:int = None # Midi pitch. Default is set on load.
def exportStatus(self)->dict:
"""The call-often function to get the instrument status. Includes only data that can
@ -135,6 +138,8 @@ class Instrument(object):
result["state"] = self.enabled #bool
result["mixerEnabled"] = self.mixerEnabled #bool
result["mixerLevel"] = self.mixerLevel #float.
result["keySwitches"] = self.currentVariantKeySwitches[0] if self.currentVariantKeySwitches else {} #Internally this is a tuple with [0] being a dict: Unordered!! dict with midiPitch: (opcode, label). You need the opcode to see if it is a momentary switch or permanent.
result["currentKeySwitch"] = self.currentKeySwitch
return result
@ -202,26 +207,203 @@ class Instrument(object):
if not self.enabled:
raise RuntimeError(f"{self.name} tried to load variant {variantSfzFileName} but was not yet enabled")
if not variantSfzFileName in self.metadata["variants"]:
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, self.metadata["variants"]))
if not variantSfzFileName in self.variants:
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, self.variants))
logger.info(f"Start loading samples for instrument {variantSfzFileName} with id key {self.idKey}")
#help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc.
#newProgramNumber = self.instrumentLayer.engine.get_unused_program()
programNumber = self.metadata["variants"].index(variantSfzFileName) #counts from 1
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, self.tarFilePath, self.rootPrefixPath+variantSfzFileName, self.metadata["name"]) #tar_name, sfz_name, display_name
self.instrumentLayer.engine.set_patch(1, programNumber) #1 is the channel, counting from 1. #TODO: we want this to be on all channels.
#self.program is always None ?
#programNumber = self.variants.index(variantSfzFileName)
#We do NOT use the program number, that was a mistake in the past. Set programNumber to 0 so it overwrites the current file.
self.program = self.instrumentLayer.engine.load_patch_from_tar(0, self.tarFilePath, self.rootPrefixPath+variantSfzFileName, self.metadata["name"]) #program number fixed to 0, tar_name, sfz_name, display_name
#self.program = self.instrumentLayer.engine.get_patches()[0][1] workaround in the past, when load_patch_from_tar returned None by mistake.
#Turns out we do not even need set_patch.
#self.instrumentLayer.engine.set_patch(1, 0) #1 is supposed to be the channel, 0 is the program. But it listens to all 16 channels anyway.
#self.instrumentLayer is type DocInstrument
#self.instrumentLayer.engine is type SamplerEngine
#self.instrumentLayer.engine.get_patches returns a dict like {0: ('Harpsichord.sfz', <calfbox.cbox.SamplerProgram object at 0x7fd324226a60>, 16)}
#Only ever index 0 is used because we have one patch per port
#self.instrumentLayer.engine.get_patches()[0][1] is the cbox.SamplerProgram. That should have been self.program, but isn't!
self.program = self.instrumentLayer.engine.get_patches()[0][1]
logger.info(self.program.status())
#self.instrumentLayer.engine.get_patches returns a dict like
# {0: ('Harpsichord.sfz', <calfbox.cbox.SamplerProgram object at 0x7fd324226a60>, 16)}
# This is similar to program.status() which returns
# SamplerProgram:in_use=16 name=CelloEns-KS.sfz program_no=0 sample_dir=./Strings/Cello Section/pizzT/ source_file=./CelloEns-KS.sfz
# This is on program number 0 and listens on all 16 channels.
#Only ever index 0 is used because we have one patch per port
status = self.program.status()
logger.info(status)
assert status.name == variantSfzFileName, (status.name, variantSfzFileName)
assert status.in_use == 16, status.SamplerProgram
assert status.program_no == 0, status.program_no
self.currentVariant = variantSfzFileName
self.currentVariantKeySwitches = self._parseKeyswitches()
if self.currentVariantKeySwitches and self.currentVariantKeySwitches[3]: #[3] is sw_default
self.currentKeySwitch = self.currentVariantKeySwitches[3]
else:
self.currentKeySwitch = None
logger.info(f"Finished loading samples for instrument {variantSfzFileName} with id key {self.idKey}")
def _parseKeyswitches(self):
"""
Called only by chooseVariant. This is a function only for readability reasons and for the
docstring.
Returns a tuple: dict, sw_lokey, sw_highkey
dict with key=keystring e.g. c#4
and value=(opcode,label). label can be empty.
keyswitches can only get parsed if the program is actually loaded/enabled.
Only existing keyswitches are included, not every number from 0-127.
Two special keys "sw_lokey" and "sw_hikeys" are returned and show the total range of possible
keyswitches.
This is currently a function to find the most common keyswitches, not every advanced scenario
with sw_previous for context-sensitive-regions (which isn't really a keyswitch),
nor sw_lokey, sw_hikey and multiple parallel switches per key.
Specifically it just searches for sw_last, sw_down and sw_up and assumes that any level (master,
group, region) can only use one of the three.
sw_down and sw_up are implementation-dependent. We must assume that there are instruments that
use these opcodes without specifying sw_lokey and sw_hikey. sw_last requires the range.
For these reasons we cannot do sanity-checking here. We just report all single-key keyswitches.
Finally it assumes that there is one sw_lokey and one sw_hikey in the whole file, and it
must be in global. No actual keyswitches will be in global.
For example:
VSCO Strings - Cello parses as follows:
( #Dict with all keyswitches and labels. Label can be empty string.
{'d6': ('sw_last', 'D6 Spiccato'),
'c6': ('sw_last', 'C6 Sustain Vibrato'),
'd#6': ('sw_last', 'D#6 Pizzicato'),
'c#6': ('sw_last', 'C#6 Tremolo')},
'', #sw_lokey not set
'') #sw_highkey not set
Calfbox with default settings will report keys as string,
no matter if they are entered as pitch-numbers or keystrings in the .sfz file itself.
We convert them here ourselves.
We assume there can only be one sw_default. We allow redundant entries but will just
use the last value we encounter. Logical consistency must be checked with an external tool
or process.
"""
if not self.enabled:
logger.warning(f"Something tried to set the keyswitch but this instrument {self.name} {self.currentVariant} is currently not enabled. Nothing was changed.")
return
if not self.currentVariant:
logger.warning(f"Something tried to parse keyswitches but this instrument {self.name} {self.currentVariant} currently has no variant loaded. Nothing was changed.")
return
def findKS(data, writeInResult, writeInOthers):
if "sw_label" in data:
label = data["sw_label"]
else:
label = ""
if "sw_default" in data:
if "sw_default" in writeInOthers and writeInOthers["sw_default"] and writeInOthers["sw_default"] != data["sw_default"]:
logger.error(f"Instrument {self.name} {self.currentVariant} has multiple different sw_default values. We will use the last one encountered. This conflict: {writeInOthers['sw_default']} vs {data['sw_default']} ")
writeInOthers["sw_default"] = data["sw_default"]
if "sw_last" in data:
midiPitch = midiName2midiPitch[data["sw_last"]]
writeInResult[midiPitch] = "sw_last", label
elif "sw_down" in data:
midiPitch = midiName2midiPitch[data["sw_down"]]
writeInResult[midiPitch] = "sw_down", label
elif "sw_up" in data:
midiPitch = midiName2midiPitch[data["sw_up"]]
writeInResult[midiPitch] = "sw_up", label
logger.info(f"Start parsing possible keyswitches in the current variant/cbox-program for {self.name} {self.currentVariant}")
result = {} # int:tuple(string)
others = {} # var:var
hierarchy = self.program.get_hierarchy()
for k,v in hierarchy.items(): #Global
globalData = k.as_dict()
swlokeyValue = globalData["sw_lokey"] if "sw_lokey" in globalData else ""
swhikeyValue = globalData["sw_hikey"] if "sw_hikey" in globalData else ""
others["sw_default"] = globalData["sw_default"] if "sw_default" in globalData else ""
for k1,v1 in v.items(): #Master
findKS(k1.as_dict(), result, others)
if v1:
for k2,v2 in v1.items(): #Group
findKS(k2.as_dict(), result, others)
if v2:
for k3,v3 in v2.items(): #Regions
findKS(k3.as_dict(), result, others)
logger.info(f"Finished parsing possible keyswitches in the current variant/cbox-program for {self.name} {self.currentVariant}. Found: {len(result)} keyswitches.")
if not result:
return None
else:
return (result, swlokeyValue, swhikeyValue, midiName2midiPitch[others["sw_default"]])
def setKeySwitch(self, keySwitchMidiPitch:int):
"""Set the current variant to it's keySwitch number keySwitchIndex.
We use the key-midi-pitch directly.
We also check against our own parsing on startup if the variant has this keyswitch and
do a logger warning if the keyswitch does not exist, and then do nothing.
Keyswitches get internally reset on variant-switching and use the one specified as default
in the variant-.sfz.
"""
if not self.enabled:
logger.warning(f"Tried to set the keyswitch but this instrument {self.name} is currently not enabled. Nothing was changed.")
return
if not self.currentVariant:
logger.warning(f"Tried to parse keyswitches but this instrument {self.name} currently has no variant loaded. Nothing was changed.")
return
cboxReportedKeySwitchesRange = self.program.get_keyswitch_groups()[0] # This is ALWAYS two values. It is a range!
keySwitchDict = self.currentVariantKeySwitches[0]
if not keySwitchMidiPitch in keySwitchDict:
logger.warning(f"Tried setting instrument {self.name} to key switch {keySwitchMidiPitch} but that switch was not parsed on load. Nothing was changed. We parsed: {keySwitchDict} in the range from/to {cboxReportedKeySwitchesRange}")
return
self.scene.send_midi_event(0x90, keySwitchMidiPitch, 64) #note on with vel 64
currentKeySwitch = self.instrumentLayer.engine.get_keyswitch_state(1, 0) #midi channel indexFrom1, keyswitch group
#Confirm that we changed the switch for both development and release version
assert currentKeySwitch == keySwitchMidiPitch, (currentKeySwitch, keySwitchMidiPitch)
if not currentKeySwitch == keySwitchMidiPitch:
logger.error(f"Tried setting instrument {self.name} to key switch {keySwitchMidiPitch} but afterwards we are switch {currentKeySwitch}. Cause unknown. Not sure if this is a problem or not.")
return self.updateCurrentKeySwitch()
def updateCurrentKeySwitch(self, force=None):
"""This is either called directly by setKeySwitch after a user change
but also from our midi event checker in python.
Returns a tuple (stateChangedSinceLastCheck:bool, keySwitchMidiPitch)
This is called on every(!) note on.
This is only for permanent sw_last, not momentary sw_up and sw_down
"""
if not self.currentVariantKeySwitches:
return #optimisation.
if not force is None:
changed = self.currentKeySwitch != force
new = force
else:
new = self.instrumentLayer.engine.get_keyswitch_state(1, 0) #midi channel indexFrom1, keyswitch group(!! ugly !!. We don't support that at all.)
changed = self.currentKeySwitch != new
self.currentKeySwitch = new
return changed, new
def enable(self):
"""While the instrument ini was already parsed on program start we only create
@ -365,7 +547,7 @@ class Instrument(object):
def serialize(self)->dict:
return {
"id" : self.id, #for convenience access
"currentVariant" : self.currentVariant, #string. Since currentVariant is set to "" when disabling an instrument this is also our marker
"currentVariant" : self.currentVariant, #string. Since currentVariant is set to "" when disabling an instrument this is also our marker for the instrument loaded state.
"mixerLevel" : self.mixerLevel, #float
"mixerEnabled" : self.mixerEnabled, #bool
#Do NOT save "self.enabled". This is just an internal convenience switch. currentVariant is the data that tells us if there was an actively loaded instrument, inluding loaded samples.

2
engine/main.py

@ -90,7 +90,7 @@ class Data(TemplateData):
if not self.libraries:
logger.error("There were no sample libraries to parse! This is correct on the first run, since you still need to choose a sample directory.")
self.instrumentMidiNoteOnActivity = None # the api will inject a callback function here which takes (libId, instrId) as parameter to indicate midi noteOn activity for non-critical information like a GUI LED blinking. The instruments individiual midiprocessor will call this as a parent-call.
self.instrumentMidiNoteOnActivity = None # the api will inject a callback function here which takes (libId, instrId) as parameter to indicate midi noteOn activity for non-critical information like a GUI LED blinking or checking for new keyswitch states. The instruments individiual midiprocessor will call this as a parent-call.
self._createGlobalPorts() #in its own function for readability
self._createCachedJackMetadataSorting()

2
qtgui/auditioner.py

@ -71,7 +71,7 @@ class AuditionerMidiInputComboController(object):
self.currentInstrumentLabel.setText(t)
def callback__auditionerVolumeChanged(self, value:float):
self.volumeDial.setValue(value)
self.volumeDial.setValue(int(value))
self.parentMainWindow.statusBar().showMessage(QtCore.QCoreApplication.translate("Auditioner", "Auditioner Volume: {}").format(value))
def _sendVolumeChangeToEngine(self, newValue):

19
qtgui/designer/mainwindow.py

@ -2,7 +2,7 @@
# Form implementation generated from reading ui file 'mainwindow.ui'
#
# Created by: PyQt5 UI code generator 5.15.4
# 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.
@ -136,17 +136,23 @@ class Ui_MainWindow(object):
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents)
self.formLayout.setObjectName("formLayout")
self.variant_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.variant_label.setObjectName("variant_label")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.LabelRole, self.variant_label)
self.variants_comboBox = QtWidgets.QComboBox(self.scrollAreaWidgetContents)
self.variants_comboBox.setObjectName("variants_comboBox")
self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.variants_comboBox)
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.variants_comboBox)
self.variant_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.variant_label.setObjectName("variant_label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.LabelRole, self.variant_label)
self.info_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.info_label.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.info_label.setWordWrap(True)
self.info_label.setObjectName("info_label")
self.formLayout.setWidget(1, QtWidgets.QFormLayout.FieldRole, self.info_label)
self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.info_label)
self.keySwitch_comboBox = QtWidgets.QComboBox(self.scrollAreaWidgetContents)
self.keySwitch_comboBox.setObjectName("keySwitch_comboBox")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.FieldRole, self.keySwitch_comboBox)
self.keySwitch_label = QtWidgets.QLabel(self.scrollAreaWidgetContents)
self.keySwitch_label.setObjectName("keySwitch_label")
self.formLayout.setWidget(2, QtWidgets.QFormLayout.LabelRole, self.keySwitch_label)
self.details_scrollArea.setWidget(self.scrollAreaWidgetContents)
self.verticalLayout_3.addWidget(self.details_scrollArea)
self.verticalLayout.addWidget(self.splitter_2)
@ -180,3 +186,4 @@ class Ui_MainWindow(object):
self.details_groupBox.setTitle(_translate("MainWindow", "NamePlaceholder"))
self.variant_label.setText(_translate("MainWindow", "Variants"))
self.info_label.setText(_translate("MainWindow", "TextLabel"))
self.keySwitch_label.setText(_translate("MainWindow", "KeySwitch"))

20
qtgui/designer/mainwindow.ui

@ -339,17 +339,17 @@
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<item row="1" column="1">
<widget class="QComboBox" name="variants_comboBox"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="variant_label">
<property name="text">
<string>Variants</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="variants_comboBox"/>
</item>
<item row="1" column="1">
<item row="3" column="1">
<widget class="QLabel" name="info_label">
<property name="text">
<string>TextLabel</string>
@ -362,6 +362,16 @@
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="keySwitch_comboBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="keySwitch_label">
<property name="text">
<string>KeySwitch</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>

4
qtgui/instrument.py

@ -421,7 +421,9 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
if self.state:
#either reload or load for the first time
self.mixSendDial.setEnabled(True)
self.mixSendDial.setValue(instrumentStatus["mixerLevel"])
#Qslider can only have integer values. mixerLevel" is float.
#We simply drop the float part, it doesn't matter for our coarse in-house mixing.
self.mixSendDial.setValue(int(instrumentStatus["mixerLevel"]))
if instrumentStatus["mixerEnabled"]:
muteText = ""
else:

69
qtgui/selectedinstrumentcontroller.py

@ -50,14 +50,12 @@ class SelectedInstrumentController(object):
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
@ -66,6 +64,9 @@ class SelectedInstrumentController(object):
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"])
@ -88,6 +89,41 @@ class SelectedInstrumentController(object):
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.
@ -100,10 +136,17 @@ class SelectedInstrumentController(object):
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()
@ -117,6 +160,9 @@ class SelectedInstrumentController(object):
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"]
@ -125,6 +171,14 @@ class SelectedInstrumentController(object):
#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["id-key"]
libraryId, instrumentId = idkey
@ -133,6 +187,10 @@ class SelectedInstrumentController(object):
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]
@ -145,8 +203,7 @@ class SelectedInstrumentController(object):
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):
@ -162,5 +219,3 @@ class SelectedInstrumentController(object):
self.react_instrumentStatusChanged
"""
self.engineData = data

Loading…
Cancel
Save