diff --git a/engine/api.py b/engine/api.py index f171f14..c74d0ef 100644 --- a/engine/api.py +++ b/engine/api.py @@ -283,7 +283,7 @@ def chooseVariantByIndex(idKey:tuple, variantIndex:int): def setInstrumentMixerVolume(idKey:tuple, value:float): """From 0 to -21. - Default is -3.0 """ + Default is -1.0 """ instrument = _instr(idKey) instrument.mixerLevel = value callbacks._instrumentStatusChanged(*instrument.idKey) @@ -344,7 +344,7 @@ def connectAuditionerPort(externalPort:str): def setAuditionerVolume(value:float): """From 0 to -21. - Default is -3.0 """ + Default is -1.0 """ session.data.auditioner.volume = value callbacks._auditionerVolumeChanged() diff --git a/engine/auditioner.py b/engine/auditioner.py index aea811b..9f7a44e 100644 --- a/engine/auditioner.py +++ b/engine/auditioner.py @@ -65,7 +65,7 @@ class Auditioner(object): jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R") self.outputMergerRouter = cbox.JackIO.create_audio_output_router(jackAudioOutLeft, jackAudioOutRight) - self.outputMergerRouter.set_gain(-3.0) + self.outputMergerRouter.set_gain(-1.0) instrument = layer.get_instrument() instrument.get_output_slot(0).rec_wet.attach(self.outputMergerRouter) #output_slot is 0 based and means a pair. Most sfz instrument have only one stereo pair. #TODO: And what if not? diff --git a/engine/instrument.py b/engine/instrument.py index d693736..3235505 100644 --- a/engine/instrument.py +++ b/engine/instrument.py @@ -47,39 +47,6 @@ class Instrument(object): The order of variants in the config file will never change, it can only get appended. This way indexing will remain consistent over time. - NOTE CONCERNING THE BELOW: - The instruments are not really described by semantic versioning. That was the initial plan - but it showed that versioning of third party instruments is difficult or impossible. - But it also is not needed nor practical. New versions are so rare that one can easily - find individual schemes to name and differentiate variants. - - The default variant after the first start (no save file) is the a special entry in metadata. - It can change with new versions, so new projects will start with the newer file. - - Examples: - SalamanderPiano1.2.sfz - SalamanderPiano1.3.sfz - SalamanderPiano1.6.sfz - - Here we have versions 1.2, 1.3 and 1.6. 4 and 5 were never released. A dropdown in a GUI - would show these entries. - - Patches are differentiated by the MINOR version as int. MINOR versions slightly change the sound. - Typical reasons are retuning, filter changes etc. - The chosen MINOR version stays active until changed by the user. All MINOR versions variant of - an instrument must be available in all future file-releases. - - PATCH version levels are just increased, as they are defined to not change the sound outcome. - For example they fix obvious bugs nobody could have wanted, extend the range of an instrument - or introduce new CC controlers for parameters previously not available. - PATCH versions are automatically upgraded. You cannot go back programatically. - The PATCH number is not included in the sfz file name, while major and minor are. - - A MAJOR version must be an entirely different file. These are incompatible with older versions. - 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. 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 @@ -120,7 +87,7 @@ class Instrument(object): self.rootPrefixPath = "" 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.numberOfOutputsPairs:int = int(self.metadata["outputPairs"]) if "outputPairs" in self.metadata else 1 self.currentVariantKeySwitches = None #set by _parseKeys through chooseVariant . Ttuple: dict, sw_lokey, sw_highkey . See docstring of parseKeySwitches self.currentKeySwitch:int = None # Midi pitch. Default is set on load. @@ -129,6 +96,15 @@ class Instrument(object): self.controlLabels = {} #CC int:str opcode label_cc# in self.keyLabels = {} #Pitch int:str opcode label_key# in + self.outputLabels = {} # self._parseKeyInfoAndLabels() + self.audioOutputs = [] #jack audio output ports uuids, compatible with cbox. Multiple of 2 because stereo pairs. They get created on enable and deleted on disable. Between these points they are static. All variants have the same number of outputs. + + #Set in self.enable() + self.outputMergerRouters = [] #use index as slot index + self.routerToGlobalSummingStereoMixers = [] #use index as slot index + self.monoOutputPortsNames = [] #without jack client name. Empty if not enabled + + #We could call self.loadSamples() now, but we delay that for the user experience. See docstring. def exportStatus(self)->dict: """The call-often function to get the instrument status. Includes only data that can @@ -150,6 +126,7 @@ class Instrument(object): result["playableKeys"] = self.playableKeys result["keyLabels"] = self.keyLabels result["controlLabels"] = self.controlLabels #CCs + result["outputLabels"] = self.outputLabels return result def exportMetadata(self)->dict: @@ -221,7 +198,7 @@ class Instrument(object): 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}") + logger.info(f"Start loading instrument variant {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.variants.index(variantSfzFileName) @@ -253,7 +230,25 @@ class Instrument(object): else: self.currentKeySwitch = None - logger.info(f"Finished loading samples for instrument {variantSfzFileName} with id key {self.idKey}") + #Set pretty names for out jack audio outputs + #If there is an output label we use that instead of the pair-number + #If there are no labels but just one pair (most instruments) we remove the number + #For multi-output instruments we use a label were present, and fall back to a number if not. + if self.numberOfOutputsPairs == 1 and not 0 in self.outputLabels: + assert not self.outputLabels, self.outputLabels + n = 0 + cbox.JackIO.Metadata.set_pretty_name(self.cboxPortname+"_"+str(n)+"_L", self.midiInputPortName+" L") + cbox.JackIO.Metadata.set_pretty_name(self.cboxPortname+"_"+str(n)+"_R", self.midiInputPortName+" R") + else: + for outputNum in range(self.numberOfOutputsPairs): + if outputNum in self.outputLabels: #dict + #print (self.outputLabels[outputNum], outputNum) + cbox.JackIO.Metadata.set_pretty_name(self.cboxPortname+"_"+str(outputNum)+"_L", self.midiInputPortName+ " " + self.outputLabels[outputNum] + " L") + cbox.JackIO.Metadata.set_pretty_name(self.cboxPortname+"_"+str(outputNum)+"_R", self.midiInputPortName+ " " + self.outputLabels[outputNum] + " R") + #else: + # print ("no label", outputNum) + + logger.info(f"Finished loading instrument variant {variantSfzFileName} with id key {self.idKey}") def _parseKeyInfoAndLabels(self): """ @@ -361,10 +356,16 @@ class Instrument(object): for notePitch in range(lower, higher+1): writeInResult.add(notePitch) + def findOutputPairs(data:dict, writeInResult:set): + if "output" in data: + writeInResult.add(int(data["output"])) + logger.info(f"Start parsing possible keyswitches in the current variant/cbox-program for {self.name} {self.currentVariant}") result = {} # int:tuple(opcode, keyswitch-label) others = {} # var:var + outputPairsResultSet = set() + outputPairsResultSet.add(0) hierarchy = self.program.get_hierarchy() #starts with global and dicts down with get_children(). First single entry layer is get_global() allKeys = set() @@ -377,22 +378,26 @@ class Instrument(object): k1AsDict = k1.as_dict() findPlayableKeys(k1AsDict, allKeys) findKS(k1AsDict, result, others) + findOutputPairs(k1AsDict, outputPairsResultSet) if v1: for k2,v2 in v1.items(): #Group k2AsDict = k2.as_dict() findPlayableKeys(k2AsDict, allKeys) findKS(k2AsDict, result, others) + findOutputPairs(k2AsDict, outputPairsResultSet) if v2: for k3,v3 in v2.items(): #Regions k3AsDict = k3.as_dict() findPlayableKeys(k3AsDict, allKeys) findKS(k3AsDict, result, others) + findOutputPairs(k3AsDict, outputPairsResultSet) + #Setup labels and string descriptions, most of which will be used when actually loading the instrument self.playableKeys = tuple(sorted(allKeys)) self.controlLabels = self.program.get_control_labels() #opcode label_cc# in - + self.outputLabels = self.program.get_output_labels() #opcode label_output# in self.keyLabels = self.program.get_key_labels() #opcode label_cc# in - #Add some defaults. + #Add some default key labels for k,v in {60:"Middle C", 53:"𝄢", 67:"𝄞"}.items(): if not k in self.keyLabels: self.keyLabels[k] = v @@ -459,17 +464,21 @@ class Instrument(object): return changed, new - - def enable(self): """While the instrument ini was already parsed on program start we only create the jack port and load samples when requested. Creating the jack ports takes a non-trivial amount of time, which produces an unacceptably slow startup. + At this point there is no knowledge about any of the sfz variants of this instrument, + we only know the ini metadata. For example we don't know anything about keyswitches or + output pairs number and labels. + After this step an instrument variant must still be loaded. The api and GUI combine this process by auto-loading the standard variant. """ + logger.info(f"Start enabling instrument {self.midiInputPortName}.") + if self.enabled: raise RuntimeError(f"{self.name} tried to switch to enabled, but it already was. This is not a trivial error. Please make sure no user-function can reach this state.") @@ -478,49 +487,95 @@ class Instrument(object): #Calfbox. The JACK ports are constructed without samples at first. self.scene = cbox.Document.get_engine().new_scene() #We need an individual scene for each instrument. Midi Routing is based on scenes. self.scene.clear() - self.sfzSamplerLayer = self.scene.add_new_instrument_layer(self.midiInputPortName, "sampler") #"sampler" is the cbox sfz engine + + #We set temporary config settings before creating the instrument. + #In the past with only stereo outputs and self.scene.add_new_instrument_layer this was not needed + #but now we want multi outputs and need this little work around + instrumentName = str(self.idKey) #the instrument name is not visible anywhere. It is an internal name only. + cbox.Config.set("instrument:" + instrumentName, "engine", "sampler") + cbox.Config.set("instrument:" + instrumentName , "output_pairs", self.numberOfOutputsPairs) + + self.instrumentLayer = self.scene.add_instrument_layer(instrumentName).get_instrument() + instrument = self.instrumentLayer + + #self.sfzSamplerLayer = self.scene.add_new_instrument_layer(self.midiInputPortName, "sampler") #"sampler" is the cbox sfz engine self.scene.status().layers[0].get_instrument().engine.load_patch_from_string(0, "", "", "") #fill with null instruments - self.instrumentLayer = self.scene.status().layers[0].get_instrument() + #self.instrumentLayer = self.scene.status().layers[0].get_instrument() self.program = None #return object from self.instrumentLayer.engine.load_patch_from_tar #self.scene.status().layers[0].set_ignore_program_changes(1) #TODO: ignore different channels. We only want one channel per scene/instrument/port. #TODO: Add generic filters to filter out redundant tasks like mixing and panning, which should be done in an audio mixer. #self.instrumentLayer.engine.set_polyphony(int) - #Create Stereo Audio Ouput Ports - #Connect to our own pair but also to a generic mixer port that is in Data() - self.jackAudioOutLeft = cbox.JackIO.create_audio_output(self.midiInputPortName+"_L") - self.jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R") - self.outputMergerRouter = cbox.JackIO.create_audio_output_router(self.jackAudioOutLeft, self.jackAudioOutRight) - self.outputMergerRouter.set_gain(-3.0) - instrument = self.sfzSamplerLayer.get_instrument() - instrument.get_output_slot(0).rec_wet.attach(self.outputMergerRouter) #output_slot is 0 based and means a pair. Most sfz instrument have only one stereo pair. #TODO: And what if not? + #Create Stereo Audio Ouput Ports self.routerToGlobalSummingStereoMixer = cbox.JackIO.create_audio_output_router(self.parentLibrary.parentData.lmixUuid, self.parentLibrary.parentData.rmixUuid) - self.routerToGlobalSummingStereoMixer.set_gain(-3.0) - instrument.get_output_slot(0).rec_wet.attach(self.routerToGlobalSummingStereoMixer) + self.routerToGlobalSummingStereoMixer.set_gain(-1.0) + + #We will create the audio outputs now. They will get pretty names in chooseVariant. + #Most instruments are stereo, they have one output pair + #They are always stereo pairs, first L then R. + #Both SFZ and cbox outputs are index 0 based. + + self.outputMergerRouters = [] #use index as slot index + self.routerToGlobalSummingStereoMixers = [] #use index as slot index + self.monoOutputPortsNames = [] #without jack client name + + for n in range(self.numberOfOutputsPairs): + + #Create two ports per output-pair. They get generic names based on the midi input and a number. + #Pretty names with output-labels or simplifications (no number for just one stereo pair) are set in ChooseVariant + outPortL = cbox.JackIO.create_audio_output(self.midiInputPortName+"_"+str(n)+"_L") + outPortR = cbox.JackIO.create_audio_output(self.midiInputPortName+"_"+ str(n)+"_R") + + self.monoOutputPortsNames.append(outPortL) + self.monoOutputPortsNames.append(outPortR) + + self.audioOutputs.append(outPortL) + self.audioOutputs.append(outPortR) + + outputMergerRouter = cbox.JackIO.create_audio_output_router(outPortL, outPortR) + outputMergerRouter.set_gain(-1.0) + instrument.get_output_slot(n).rec_wet.attach(outputMergerRouter) #output_slot is 0 based and means a pair. + self.outputMergerRouters.append(outputMergerRouter) + + globalSumMerger = cbox.JackIO.create_audio_output_router(self.parentLibrary.parentData.lmixUuid, self.parentLibrary.parentData.rmixUuid) + globalSumMerger.set_gain(-1.0) + #instrument.get_output_slot(n).rec_wet.attach(globalSumMerger) #this happens in setMixerEnabled + self.routerToGlobalSummingStereoMixers.append(globalSumMerger) + + + #self.jackAudioOutLeft = cbox.JackIO.create_audio_output(self.midiInputPortName+"_L") + #self.jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R") + + #self.outputMergerRouter = cbox.JackIO.create_audio_output_router(self.jackAudioOutLeft, self.jackAudioOutRight) + #self.outputMergerRouter.set_gain(-1.0) + #instrument.get_output_slot(0).rec_wet.attach(self.outputMergerRouter) #output_slot is 0 based and means a pair. + self.setMixerEnabled(True) #Create Midi Input Port self.cboxMidiPortUid = cbox.JackIO.create_midi_input(self.midiInputPortName) cbox.JackIO.set_appsink_for_midi_input(self.cboxMidiPortUid, True) #This sounds like a program wide sink, but it is needed for every port. cbox.JackIO.route_midi_input(self.cboxMidiPortUid, self.scene.uuid) #Route midi input to the scene. Without this we have no sound, but the python processor would still work. - self.cboxPortname = cbox.JackIO.status().client_name + ":" + self.midiInputPortName - - self.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid self.midiProcessor.register_NoteOn(self.triggerNoteOnCallback) self.midiProcessor.register_NoteOff(self.triggerNoteOffCallback) self.midiProcessor.register_CC(self.triggerCCCallback) + #self.midiProcessor.notePrinter(True) self.parentLibrary.parentData.parentSession.eventLoop.fastConnect(self.midiProcessor.processEvents) self.parentLibrary.parentData.updateJackMetadataSorting() + logger.info(f"Finished enabling instrument {self.midiInputPortName}. Loading a variant comes next") + @property def mixerLevel(self)->float: + """We do have a list of router-mixers, but they all have the same gain value. + we just return the first one here""" if self.enabled: - return self.routerToGlobalSummingStereoMixer.status().gain + return self.routerToGlobalSummingStereoMixers[0].status().gain else: return None @@ -529,12 +584,16 @@ class Instrument(object): """0 is the default instrument level, as the sample files were recorded. Negative numbers reduce volume, as it is custom in digital audio. - Default is -3.0. + Default is -1.0. To completely mute use self.mute = True. The mixerLevel will be preserved over this- + + All router-mixers are set to the same level. We receive one and apply it to the list. + """ if self.enabled: - self.routerToGlobalSummingStereoMixer.set_gain(value) + for router in self.routerToGlobalSummingStereoMixers: + router.set_gain(value) else: raise ValueError("Tried to set mixer level while instrument is disabled") @@ -548,15 +607,20 @@ class Instrument(object): If it is None the instrument is currently not loaded. Either because it was deactivated or because it was never loaded. """ - instrument = self.sfzSamplerLayer.get_instrument() + instrument = self.instrumentLayer self.mixerEnabled = state - try: - if state: - instrument.get_output_slot(0).rec_wet.attach(self.routerToGlobalSummingStereoMixer) - else: - instrument.get_output_slot(0).rec_wet.detach(self.routerToGlobalSummingStereoMixer) - except: #"Router already attached" - pass + + for outputSlot, summingRouter in zip(instrument.output_slots, self.routerToGlobalSummingStereoMixers): + try: + #output_slot means a pair. Most sfz instrument have only one stereo pair. + if state: + outputSlot.rec_wet.attach(summingRouter) + else: + outputSlot.rec_wet.detach(summingRouter) + #but don't delete + except Exception as e: #"Router already attached" or " Recorder is not attached to this source" + #print (e) + pass def triggerNoteOnCallback(self, timestamp, channel, pitch, velocity): """args are: timestamp, channel, note, velocity. @@ -613,30 +677,35 @@ class Instrument(object): self.scene.status().layers[0].get_instrument().engine.load_patch_from_string(0, "", "", "") #fill with null instruments, hopefully replacing the loaded sfz data. - instrument = self.sfzSamplerLayer.get_instrument() - instrument.get_output_slot(0).rec_wet.detach(self.outputMergerRouter) #output_slot is 0 based and means a pair. Most sfz instrument have only one stereo pair. #TODO: And what if not? - self.setMixerEnabled(False) # instrument.get_output_slot(0).rec_wet.detach(self.routerToGlobalSummingStereoMixer) - self.routerToGlobalSummingStereoMixer.delete() - self.outputMergerRouter.delete() - self.routerToGlobalSummingStereoMixer = None - self.outputMergerRouter = None + instrument = self.instrumentLayer - self.scene.clear() + for router in self.routerToGlobalSummingStereoMixers: + router.delete() + self.routerToGlobalSummingStereoMixers = [] + + for outputSlot, outputMerger in zip(instrument.output_slots, self.outputMergerRouters): + try: + outputSlot.rec_wet.detach(outputMerger) #output_slot means a pair. Most sfz instrument have only one stereo pair. + except: #"Recorder is not attached to this source" + pass + outputMerger.delete() + self.outputMergerRouters = [] #use index as slot index - cbox.JackIO.delete_audio_output(self.jackAudioOutLeft) - cbox.JackIO.delete_audio_output(self.jackAudioOutRight) + for audioOutput in self.audioOutputs: + cbox.JackIO.delete_audio_output(audioOutput) + self.audioOutputs = [] cbox.JackIO.delete_midi_input(self.cboxMidiPortUid) + self.setMixerEnabled(False) # Already deleted. Just in case? + self.scene.clear() + self.parentLibrary.parentData.parentSession.eventLoop.slowDisconnect(self.midiProcessor.processEvents) self.scene = None - self.sfzSamplerLayer = None self.cboxMidiPortUid = None self.instrumentLayer = None self.program = None self.enabled = False - self.jackAudioOutLeft = None - self.jackAudioOutRight = None self.currentVariant = "" self.midiProcessor = None self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable. diff --git a/engine/main.py b/engine/main.py index 3aa7afe..6b23107 100644 --- a/engine/main.py +++ b/engine/main.py @@ -65,7 +65,7 @@ class Data(TemplateData): self.cachedSerializedDataForStartEngine = None def allInstr(self): - for libId, lib in self.libraries.items(): + for libId, lib in sorted(self.libraries.items()): for instrId, instr in lib.instruments.items(): yield instr @@ -189,8 +189,8 @@ class Data(TemplateData): the Auditioner. """ - self.lmixUuid = cbox.JackIO.create_audio_output('left_mix') - self.rmixUuid = cbox.JackIO.create_audio_output('right_mix') + self.lmixUuid = cbox.JackIO.create_audio_output('Stereo Mix L') + self.rmixUuid = cbox.JackIO.create_audio_output('Stereo Mix R') self.auditioner = Auditioner(self) @@ -199,8 +199,8 @@ class Data(TemplateData): hardwareAudioPorts = cbox.JackIO.get_ports("system*", cbox.JackIO.AUDIO_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL) #don't sort. This is correctly sorted. Another sorted will do 1, 10, 11, clientName = cbox.JackIO.status().client_name - mix_l = f"{clientName}:left_mix" - mix_r = f"{clientName}:right_mix" + mix_l = f"{clientName}:Stereo Mix L" + mix_r = f"{clientName}:Stereo Mix R" aud_l = f"{clientName}:{self.auditioner.midiInputPortName}_L" aud_r = f"{clientName}:{self.auditioner.midiInputPortName}_R" @@ -236,17 +236,25 @@ class Data(TemplateData): clientName = cbox.JackIO.status().client_name order = {} orderCounter = 0 - for instr in self.allInstr(): - L = clientName + ":" + instr.midiInputPortName + "_L" - R = clientName + ":" + instr.midiInputPortName + "_R" - - order[L] = (orderCounter, instr) - orderCounter += 1 - order[R] = (orderCounter, instr) - orderCounter += 1 + + for instr in self.allInstr(): #this is sorted alphabetically, which is the library id + #Use the real names, not the pretty metadata names + #We save the instrument object so that the real function updateJackMetadataSorting below can quickly check if an instrument is currently enabled and send this to jack metadata, or not + for n in range(instr.numberOfOutputsPairs): + L = clientName + ":" + instr.midiInputPortName+"_"+str(n)+"_L" + order[L] = (orderCounter, instr) + orderCounter += 1 + + R = clientName + ":" + instr.midiInputPortName+"_"+str(n)+"_R" + order[R] = (orderCounter, instr) + orderCounter += 1 + + #Also send the midi portname. It doesn't matter that they all get very high numbers. + #Sure, we could do a midi counter as well, but it just works fine this way. The order matters, not small numbers. order[clientName + ":" + instr.midiInputPortName] = (orderCounter, instr) #midi port orderCounter +=1 + #print (3* len(list(self.allInstr()))) #without multi-out instruments this is the orderCounter in the end. self._cachedJackMedataPortOrder = order def updateJackMetadataSorting(self): @@ -256,11 +264,29 @@ class Data(TemplateData): jack ports. Luckily our data never changes. We can just prepare one order, cache it, filter it - and send that again and again. + and send that again and again when instruments get disabled and enabled. """ - cleanedOrder = { fullportname : index for fullportname, (index, instrObj) in self._cachedJackMedataPortOrder.items() if instrObj.enabled} + logger.info("Calculating new port sorting/order based on permanent general list and currently enabled instruments") + #Add static ports + clientName = cbox.JackIO.status().client_name + mix_l = f"{clientName}:Stereo Mix L" + mix_r = f"{clientName}:Stereo Mix R" + aud_midi = f"{clientName}:{self.auditioner.midiInputPortName}" + aud_l = f"{clientName}:{self.auditioner.midiInputPortName}_L" + aud_r = f"{clientName}:{self.auditioner.midiInputPortName}_R" + staticOrder = {} + staticOrder[mix_l] = 0 + staticOrder[mix_r] = 1 + staticOrder[aud_l] = 2 + staticOrder[aud_r] = 3 + staticOrder[aud_midi] = 4 + offset = len(staticOrder.keys()) + + #Now the dynamic ports with static offset + cleanedOrder = { fullportname : index+offset for fullportname, (index, instrObj) in self._cachedJackMedataPortOrder.items() if instrObj.enabled} + cleanedOrder.update(staticOrder) try: cbox.JackIO.Metadata.set_all_port_order(cleanedOrder) #wants a dict with {complete jack portname : sortIndex} except Exception as e: #No Jack Meta Data or Error with ports. diff --git a/template/calfbox/cbox.py b/template/calfbox/cbox.py index 1b2fbff..0f78339 100644 --- a/template/calfbox/cbox.py +++ b/template/calfbox/cbox.py @@ -879,6 +879,7 @@ class DocInstrument(DocObj): self.output_slots.append(io) def move_to(self, target_scene, pos = 0): return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1) + def get_output_slot(self, slot): return self.output_slots[slot] Document.classmap['cbox_instrument'] = DocInstrument @@ -1125,6 +1126,8 @@ class SamplerProgram(DocObj): return self.get_thing("/control_labels", '/control_label', {int : str}) def get_key_labels(self): return self.get_thing("/key_labels", '/key_label', {int : str}) + def get_output_labels(self): + return self.get_thing("/output_labels", '/output_label', {int : str}) def get_keyswitch_groups(self): return self.get_thing("/keyswitch_groups", '/key_range', [(int, int)]) def new_group(self):