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.
defexportStatus(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]ifself.currentVariantKeySwitcheselse{}#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
returnresult
@ -202,26 +207,203 @@ class Instrument(object):
ifnotself.enabled:
raiseRuntimeError(f"{self.name} tried to load variant {variantSfzFileName} but was not yet enabled")
#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!
logger.warning(f"Something tried to set the keyswitch but this instrument {self.name}{self.currentVariant} is currently not enabled. Nothing was changed.")
return
ifnotself.currentVariant:
logger.warning(f"Something tried to parse keyswitches but this instrument {self.name}{self.currentVariant} currently has no variant loaded. Nothing was changed.")
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"indata:
midiPitch=midiName2midiPitch[data["sw_last"]]
writeInResult[midiPitch]="sw_last",label
elif"sw_down"indata:
midiPitch=midiName2midiPitch[data["sw_down"]]
writeInResult[midiPitch]="sw_down",label
elif"sw_up"indata:
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}")
logger.info(f"Finished parsing possible keyswitches in the current variant/cbox-program for {self.name}{self.currentVariant}. Found: {len(result)} keyswitches.")
logger.warning(f"Tried to set the keyswitch but this instrument {self.name} is currently not enabled. Nothing was changed.")
return
ifnotself.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]
ifnotkeySwitchMidiPitchinkeySwitchDict:
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
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.")
returnself.updateCurrentKeySwitch()
defupdateCurrentKeySwitch(self,force=None):
"""This is either called directly by setKeySwitch after a user change
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
returnchanged,new
defenable(self):
"""While the instrument ini was already parsed on program start we only create
@ -365,7 +547,7 @@ class Instrument(object):
defserialize(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.
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
#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()
ifnotinstrumentStatusornot"keySwitches"ininstrumentStatus:#not all instruments have keyswitches