Browse Source

Show and use keyswitches from the GUI

master
Nils 2 years 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. #Inject the _instrumentMidiNoteOnActivity callback into session.data for access.
session.data.instrumentMidiNoteOnActivity = callbacks._instrumentMidiNoteOnActivity session.data.instrumentMidiNoteOnActivity = callbacks._instrumentMidiNoteOnActivity
callbacks.instrumentMidiNoteOnActivity.append(_checkForKeySwitch)
callbacks._instrumentListMetadata() #Relatively quick. Happens only once in the program 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. #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(): 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. atexit.register(unloadAllInstrumentSamples) #this will handle all python exceptions, but not segfaults of C modules.
logger.info("Tembro api startEngine complete") logger.info("Tembro api startEngine complete")
def loadAllInstrumentSamples(): def loadAllInstrumentSamples():
"""Actually load all instrument samples""" """Actually load all instrument samples"""
logger.info(f"Loading all instruments.") logger.info(f"Loading all instruments.")
@ -208,14 +209,34 @@ def setInstrumentMixerVolume(idkey:tuple, value:float):
Default is -3.0 """ Default is -3.0 """
instrument = Instrument.allInstruments[idkey] instrument = Instrument.allInstruments[idkey]
instrument.mixerLevel = value instrument.mixerLevel = value
libraryId, instrumentId = idkey callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._instrumentStatusChanged(libraryId, instrumentId)
def setInstrumentMixerEnabled(idkey:tuple, state:bool): def setInstrumentMixerEnabled(idkey:tuple, state:bool):
instrument = Instrument.allInstruments[idkey] instrument = Instrument.allInstruments[idkey]
instrument.setMixerEnabled(state) instrument.setMixerEnabled(state)
libraryId, instrumentId = idkey callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._instrumentStatusChanged(libraryId, instrumentId)
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): def auditionerInstrument(idkey:tuple):
"""Load an indendepent instance of an instrument into the auditioner port""" """Load an indendepent instance of an instrument into the auditioner port"""
@ -227,7 +248,8 @@ def auditionerInstrument(idkey:tuple):
var = originalInstrument.currentVariant var = originalInstrument.currentVariant
else: else:
var = originalInstrument.defaultVariant 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) callbacks._auditionerInstrumentChanged(libraryId, instrumentId)
def getAvailableAuditionerPorts()->dict: def getAvailableAuditionerPorts()->dict:

13
engine/auditioner.py

@ -101,10 +101,11 @@ class Auditioner(object):
#Dynamic data #Dynamic data
result["currentVariant"] = self.currentVariant # str result["currentVariant"] = self.currentVariant # str
result["state"] = self.enabled #bool result["state"] = self.enabled #bool
result["currentKeySwitch"] = self.currentKeySwitch #int or None
return result 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 """load_patch_from_tar is blocking. This function will return when the instrument is ready
to play. to play.
@ -113,11 +114,17 @@ class Auditioner(object):
logger.info(f"Start loading samples for auditioner {variantSfzFileName}") logger.info(f"Start loading samples for auditioner {variantSfzFileName}")
#help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc. #help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc.
#newProgramNumber = self.instrumentLayer.engine.get_unused_program() #newProgramNumber = self.instrumentLayer.engine.get_unused_program()
programNumber = 1 programNumber = 0
name = variantSfzFileName name = variantSfzFileName
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, tarFilePath, rootPrefixPath+variantSfzFileName, name) 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 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}") 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 from calfbox import cbox
#Template Modules #Template Modules
from template.engine.pitch import midiName2midiPitch #dict
from template.engine.input_midi import MidiProcessor from template.engine.input_midi import MidiProcessor
class Instrument(object): class Instrument(object):
"""Literally one instrument. """Literally one instrument.
It might exists in different versions that are all loaded here and can be switched in the GUI. It might exists in different variants and version that are all loaded here and can be
For that we identify different .sfz files by a minor version number (see below). switched in the GUI.
All data is provided by the parsed metadata dict, except the filepath of the tar which calfbox All data is provided by the parsed metadata dict, except the filepath of the tar which calfbox
needs again here. needs again here.
@ -73,7 +74,7 @@ class Instrument(object):
For example they use a different control scheme (different CC maps) 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 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 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. 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 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. 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. #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: def exportStatus(self)->dict:
"""The call-often function to get the instrument status. Includes only data that can """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["state"] = self.enabled #bool
result["mixerEnabled"] = self.mixerEnabled #bool result["mixerEnabled"] = self.mixerEnabled #bool
result["mixerLevel"] = self.mixerLevel #float. 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 return result
@ -202,26 +207,203 @@ class Instrument(object):
if not self.enabled: if not self.enabled:
raise RuntimeError(f"{self.name} tried to load variant {variantSfzFileName} but was not yet enabled") raise RuntimeError(f"{self.name} tried to load variant {variantSfzFileName} but was not yet enabled")
if not variantSfzFileName in self.metadata["variants"]: if not variantSfzFileName in self.variants:
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, self.metadata["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}") 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. #help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc.
#newProgramNumber = self.instrumentLayer.engine.get_unused_program() #newProgramNumber = self.instrumentLayer.engine.get_unused_program()
programNumber = self.metadata["variants"].index(variantSfzFileName) #counts from 1 #programNumber = self.variants.index(variantSfzFileName)
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, self.tarFilePath, self.rootPrefixPath+variantSfzFileName, self.metadata["name"]) #tar_name, sfz_name, display_name #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.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 ? 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 is type DocInstrument
#self.instrumentLayer.engine is type SamplerEngine #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)} #self.instrumentLayer.engine.get_patches returns a dict like
#Only ever index 0 is used because we have one patch per port # {0: ('Harpsichord.sfz', <calfbox.cbox.SamplerProgram object at 0x7fd324226a60>, 16)}
#self.instrumentLayer.engine.get_patches()[0][1] is the cbox.SamplerProgram. That should have been self.program, but isn't! # This is similar to program.status() which returns
self.program = self.instrumentLayer.engine.get_patches()[0][1] # SamplerProgram:in_use=16 name=CelloEns-KS.sfz program_no=0 sample_dir=./Strings/Cello Section/pizzT/ source_file=./CelloEns-KS.sfz
logger.info(self.program.status()) # 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.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}") 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): def enable(self):
"""While the instrument ini was already parsed on program start we only create """While the instrument ini was already parsed on program start we only create
@ -365,7 +547,7 @@ class Instrument(object):
def serialize(self)->dict: def serialize(self)->dict:
return { return {
"id" : self.id, #for convenience access "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 "mixerLevel" : self.mixerLevel, #float
"mixerEnabled" : self.mixerEnabled, #bool "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. #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: 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.") 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._createGlobalPorts() #in its own function for readability
self._createCachedJackMetadataSorting() self._createCachedJackMetadataSorting()

2
qtgui/auditioner.py

@ -71,7 +71,7 @@ class AuditionerMidiInputComboController(object):
self.currentInstrumentLabel.setText(t) self.currentInstrumentLabel.setText(t)
def callback__auditionerVolumeChanged(self, value:float): 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)) self.parentMainWindow.statusBar().showMessage(QtCore.QCoreApplication.translate("Auditioner", "Auditioner Volume: {}").format(value))
def _sendVolumeChangeToEngine(self, newValue): def _sendVolumeChangeToEngine(self, newValue):

19
qtgui/designer/mainwindow.py

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

20
qtgui/designer/mainwindow.ui

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

4
qtgui/instrument.py

@ -421,7 +421,9 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
if self.state: if self.state:
#either reload or load for the first time #either reload or load for the first time
self.mixSendDial.setEnabled(True) 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"]: if instrumentStatus["mixerEnabled"]:
muteText = "" muteText = ""
else: 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.details_scrollArea.hide() #until the first instrument was selected
self.ui.variants_comboBox.activated.connect(self._newVariantChosen) self.ui.variants_comboBox.activated.connect(self._newVariantChosen)
self.ui.keySwitch_comboBox.activated.connect(self._newKeySwitchChosen)
#Callbacks #Callbacks
api.callbacks.instrumentListMetadata.append(self.react_initialInstrumentList) api.callbacks.instrumentListMetadata.append(self.react_initialInstrumentList)
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged) api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
def directLibrary(self, idkey:tuple): def directLibrary(self, idkey:tuple):
"""User clicked on a library treeItem""" """User clicked on a library treeItem"""
libraryId, instrumentId = idkey libraryId, instrumentId = idkey
@ -66,6 +64,9 @@ class SelectedInstrumentController(object):
self.ui.variants_comboBox.hide() self.ui.variants_comboBox.hide()
self.ui.variant_label.hide() self.ui.variant_label.hide()
self.ui.keySwitch_label.hide()
self.ui.keySwitch_comboBox.hide()
metadata = self.engineData[libraryId]["library"] metadata = self.engineData[libraryId]["library"]
self.ui.details_groupBox.setTitle(metadata["name"]) self.ui.details_groupBox.setTitle(metadata["name"])
@ -88,6 +89,41 @@ class SelectedInstrumentController(object):
assert self.currentIdKey assert self.currentIdKey
api.chooseVariantByIndex(self.currentIdKey, index) 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): def instrumentChanged(self, idkey:tuple):
"""This is a GUI-internal function. The user selected a different instrument from """This is a GUI-internal function. The user selected a different instrument from
the list. Single click, arrow keys etc. the list. Single click, arrow keys etc.
@ -100,10 +136,17 @@ class SelectedInstrumentController(object):
self.ui.details_scrollArea.show() self.ui.details_scrollArea.show()
#Cached
instrumentStatus = self.statusUpdates[libraryId][instrumentId]
#Static #Static
instrumentData = self.engineData[libraryId][instrumentId] instrumentData = self.engineData[libraryId][instrumentId]
self.ui.details_groupBox.setTitle(instrumentData["name"]) 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.variant_label.show()
self.ui.variants_comboBox.show() self.ui.variants_comboBox.show()
self.ui.variants_comboBox.clear() self.ui.variants_comboBox.clear()
@ -117,6 +160,9 @@ class SelectedInstrumentController(object):
def react_instrumentStatusChanged(self, instrumentStatus:dict): def react_instrumentStatusChanged(self, instrumentStatus:dict):
"""Callback from the api. Has nothing to do with any GUI state or selection. """Callback from the api. Has nothing to do with any GUI state or selection.
Happens if the user loads a variant.
Data: Data:
#Static ids #Static ids
result["id"] = self.metadata["id"] result["id"] = self.metadata["id"]
@ -125,6 +171,14 @@ class SelectedInstrumentController(object):
#Dynamic data #Dynamic data
result["currentVariant"] = self.currentVariant # str result["currentVariant"] = self.currentVariant # str
result["state"] = self.enabled #bool 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"] idkey = instrumentStatus["id-key"]
libraryId, instrumentId = idkey libraryId, instrumentId = idkey
@ -133,6 +187,10 @@ class SelectedInstrumentController(object):
self.statusUpdates[libraryId] = {} #empty library. status dict self.statusUpdates[libraryId] = {} #empty library. status dict
self.statusUpdates[libraryId][instrumentId] = instrumentStatus #create or overwrite / keep up to date 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"] loadState = instrumentStatus["state"]
instrumentData = self.engineData[libraryId][instrumentId] instrumentData = self.engineData[libraryId][instrumentId]
@ -145,8 +203,7 @@ class SelectedInstrumentController(object):
self.ui.variants_comboBox.setEnabled(False) self.ui.variants_comboBox.setEnabled(False)
defaultVariantIndex = instrumentData["variantsWithoutSfzExtension"].index(instrumentData["defaultVariantWithoutSfzExtension"]) defaultVariantIndex = instrumentData["variantsWithoutSfzExtension"].index(instrumentData["defaultVariantWithoutSfzExtension"])
self.ui.variants_comboBox.setCurrentIndex(defaultVariantIndex) self.ui.variants_comboBox.setCurrentIndex(defaultVariantIndex)
self._populateKeySwitchComboBox(instrumentStatus)
def react_initialInstrumentList(self, data:dict): def react_initialInstrumentList(self, data:dict):
@ -162,5 +219,3 @@ class SelectedInstrumentController(object):
self.react_instrumentStatusChanged self.react_instrumentStatusChanged
""" """
self.engineData = data self.engineData = data

Loading…
Cancel
Save