Browse Source

Full support for multi-audio-out instruments. Port names, sfz labels, port sorting, internal mixer and auditioner routing

master
Nils 2 years ago
parent
commit
5a4974cb21
  1. 4
      engine/api.py
  2. 2
      engine/auditioner.py
  3. 227
      engine/instrument.py
  4. 56
      engine/main.py
  5. 3
      template/calfbox/cbox.py

4
engine/api.py

@ -283,7 +283,7 @@ def chooseVariantByIndex(idKey:tuple, variantIndex:int):
def setInstrumentMixerVolume(idKey:tuple, value:float): def setInstrumentMixerVolume(idKey:tuple, value:float):
"""From 0 to -21. """From 0 to -21.
Default is -3.0 """ Default is -1.0 """
instrument = _instr(idKey) instrument = _instr(idKey)
instrument.mixerLevel = value instrument.mixerLevel = value
callbacks._instrumentStatusChanged(*instrument.idKey) callbacks._instrumentStatusChanged(*instrument.idKey)
@ -344,7 +344,7 @@ def connectAuditionerPort(externalPort:str):
def setAuditionerVolume(value:float): def setAuditionerVolume(value:float):
"""From 0 to -21. """From 0 to -21.
Default is -3.0 """ Default is -1.0 """
session.data.auditioner.volume = value session.data.auditioner.volume = value
callbacks._auditionerVolumeChanged() callbacks._auditionerVolumeChanged()

2
engine/auditioner.py

@ -65,7 +65,7 @@ class Auditioner(object):
jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R") jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R")
self.outputMergerRouter = cbox.JackIO.create_audio_output_router(jackAudioOutLeft, jackAudioOutRight) 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 = 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? 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?

227
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. The order of variants in the config file will never change, it can only get appended.
This way indexing will remain consistent over time. 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 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
@ -120,7 +87,7 @@ class Instrument(object):
self.rootPrefixPath = "" 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. 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.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. 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 <control> self.controlLabels = {} #CC int:str opcode label_cc# in <control>
self.keyLabels = {} #Pitch int:str opcode label_key# in <control> self.keyLabels = {} #Pitch int:str opcode label_key# in <control>
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: 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
@ -150,6 +126,7 @@ class Instrument(object):
result["playableKeys"] = self.playableKeys result["playableKeys"] = self.playableKeys
result["keyLabels"] = self.keyLabels result["keyLabels"] = self.keyLabels
result["controlLabels"] = self.controlLabels #CCs result["controlLabels"] = self.controlLabels #CCs
result["outputLabels"] = self.outputLabels
return result return result
def exportMetadata(self)->dict: def exportMetadata(self)->dict:
@ -221,7 +198,7 @@ class Instrument(object):
if not variantSfzFileName in self.variants: if not variantSfzFileName in self.variants:
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, 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. #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.variants.index(variantSfzFileName) #programNumber = self.variants.index(variantSfzFileName)
@ -253,7 +230,25 @@ class Instrument(object):
else: else:
self.currentKeySwitch = None 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): def _parseKeyInfoAndLabels(self):
""" """
@ -361,10 +356,16 @@ class Instrument(object):
for notePitch in range(lower, higher+1): for notePitch in range(lower, higher+1):
writeInResult.add(notePitch) 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}") logger.info(f"Start parsing possible keyswitches in the current variant/cbox-program for {self.name} {self.currentVariant}")
result = {} # int:tuple(opcode, keyswitch-label) result = {} # int:tuple(opcode, keyswitch-label)
others = {} # var:var 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() hierarchy = self.program.get_hierarchy() #starts with global and dicts down with get_children(). First single entry layer is get_global()
allKeys = set() allKeys = set()
@ -377,22 +378,26 @@ class Instrument(object):
k1AsDict = k1.as_dict() k1AsDict = k1.as_dict()
findPlayableKeys(k1AsDict, allKeys) findPlayableKeys(k1AsDict, allKeys)
findKS(k1AsDict, result, others) findKS(k1AsDict, result, others)
findOutputPairs(k1AsDict, outputPairsResultSet)
if v1: if v1:
for k2,v2 in v1.items(): #Group for k2,v2 in v1.items(): #Group
k2AsDict = k2.as_dict() k2AsDict = k2.as_dict()
findPlayableKeys(k2AsDict, allKeys) findPlayableKeys(k2AsDict, allKeys)
findKS(k2AsDict, result, others) findKS(k2AsDict, result, others)
findOutputPairs(k2AsDict, outputPairsResultSet)
if v2: if v2:
for k3,v3 in v2.items(): #Regions for k3,v3 in v2.items(): #Regions
k3AsDict = k3.as_dict() k3AsDict = k3.as_dict()
findPlayableKeys(k3AsDict, allKeys) findPlayableKeys(k3AsDict, allKeys)
findKS(k3AsDict, result, others) 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.playableKeys = tuple(sorted(allKeys))
self.controlLabels = self.program.get_control_labels() #opcode label_cc# in <control> self.controlLabels = self.program.get_control_labels() #opcode label_cc# in <control>
self.outputLabels = self.program.get_output_labels() #opcode label_output# in <control>
self.keyLabels = self.program.get_key_labels() #opcode label_cc# in <control> self.keyLabels = self.program.get_key_labels() #opcode label_cc# in <control>
#Add some defaults. #Add some default key labels
for k,v in {60:"Middle C", 53:"𝄢", 67:"𝄞"}.items(): for k,v in {60:"Middle C", 53:"𝄢", 67:"𝄞"}.items():
if not k in self.keyLabels: if not k in self.keyLabels:
self.keyLabels[k] = v self.keyLabels[k] = v
@ -459,17 +464,21 @@ class Instrument(object):
return changed, 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
the jack port and load samples when requested. the jack port and load samples when requested.
Creating the jack ports takes a non-trivial amount of time, which produces an unacceptably Creating the jack ports takes a non-trivial amount of time, which produces an unacceptably
slow startup. 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 After this step an instrument variant must still be loaded. The api and GUI combine this
process by auto-loading the standard variant. process by auto-loading the standard variant.
""" """
logger.info(f"Start enabling instrument {self.midiInputPortName}.")
if self.enabled: 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.") 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. #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 = cbox.Document.get_engine().new_scene() #We need an individual scene for each instrument. Midi Routing is based on scenes.
self.scene.clear() 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.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.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.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) #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) #Create Stereo Audio Ouput Ports
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?
self.routerToGlobalSummingStereoMixer = cbox.JackIO.create_audio_output_router(self.parentLibrary.parentData.lmixUuid, self.parentLibrary.parentData.rmixUuid) self.routerToGlobalSummingStereoMixer = cbox.JackIO.create_audio_output_router(self.parentLibrary.parentData.lmixUuid, self.parentLibrary.parentData.rmixUuid)
self.routerToGlobalSummingStereoMixer.set_gain(-3.0) self.routerToGlobalSummingStereoMixer.set_gain(-1.0)
instrument.get_output_slot(0).rec_wet.attach(self.routerToGlobalSummingStereoMixer)
#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) self.setMixerEnabled(True)
#Create Midi Input Port #Create Midi Input Port
self.cboxMidiPortUid = cbox.JackIO.create_midi_input(self.midiInputPortName) 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.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. 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.cboxPortname = cbox.JackIO.status().client_name + ":" + self.midiInputPortName
self.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid self.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid
self.midiProcessor.register_NoteOn(self.triggerNoteOnCallback) self.midiProcessor.register_NoteOn(self.triggerNoteOnCallback)
self.midiProcessor.register_NoteOff(self.triggerNoteOffCallback) self.midiProcessor.register_NoteOff(self.triggerNoteOffCallback)
self.midiProcessor.register_CC(self.triggerCCCallback) self.midiProcessor.register_CC(self.triggerCCCallback)
#self.midiProcessor.notePrinter(True) #self.midiProcessor.notePrinter(True)
self.parentLibrary.parentData.parentSession.eventLoop.fastConnect(self.midiProcessor.processEvents) self.parentLibrary.parentData.parentSession.eventLoop.fastConnect(self.midiProcessor.processEvents)
self.parentLibrary.parentData.updateJackMetadataSorting() self.parentLibrary.parentData.updateJackMetadataSorting()
logger.info(f"Finished enabling instrument {self.midiInputPortName}. Loading a variant comes next")
@property @property
def mixerLevel(self)->float: 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: if self.enabled:
return self.routerToGlobalSummingStereoMixer.status().gain return self.routerToGlobalSummingStereoMixers[0].status().gain
else: else:
return None return None
@ -529,12 +584,16 @@ class Instrument(object):
"""0 is the default instrument level, as the sample files were recorded. """0 is the default instrument level, as the sample files were recorded.
Negative numbers reduce volume, as it is custom in digital audio. 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- 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: if self.enabled:
self.routerToGlobalSummingStereoMixer.set_gain(value) for router in self.routerToGlobalSummingStereoMixers:
router.set_gain(value)
else: else:
raise ValueError("Tried to set mixer level while instrument is disabled") 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 If it is None the instrument is currently not loaded. Either because it was deactivated or
because it was never loaded. because it was never loaded.
""" """
instrument = self.sfzSamplerLayer.get_instrument() instrument = self.instrumentLayer
self.mixerEnabled = state self.mixerEnabled = state
try:
if state: for outputSlot, summingRouter in zip(instrument.output_slots, self.routerToGlobalSummingStereoMixers):
instrument.get_output_slot(0).rec_wet.attach(self.routerToGlobalSummingStereoMixer) try:
else: #output_slot means a pair. Most sfz instrument have only one stereo pair.
instrument.get_output_slot(0).rec_wet.detach(self.routerToGlobalSummingStereoMixer) if state:
except: #"Router already attached" outputSlot.rec_wet.attach(summingRouter)
pass 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): def triggerNoteOnCallback(self, timestamp, channel, pitch, velocity):
"""args are: timestamp, channel, note, 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. 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 = self.instrumentLayer
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
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) for audioOutput in self.audioOutputs:
cbox.JackIO.delete_audio_output(self.jackAudioOutRight) cbox.JackIO.delete_audio_output(audioOutput)
self.audioOutputs = []
cbox.JackIO.delete_midi_input(self.cboxMidiPortUid) 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.parentLibrary.parentData.parentSession.eventLoop.slowDisconnect(self.midiProcessor.processEvents)
self.scene = None self.scene = None
self.sfzSamplerLayer = None
self.cboxMidiPortUid = None self.cboxMidiPortUid = None
self.instrumentLayer = None self.instrumentLayer = None
self.program = None self.program = None
self.enabled = False self.enabled = False
self.jackAudioOutLeft = None
self.jackAudioOutRight = None
self.currentVariant = "" self.currentVariant = ""
self.midiProcessor = None self.midiProcessor = None
self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable. self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable.

56
engine/main.py

@ -65,7 +65,7 @@ class Data(TemplateData):
self.cachedSerializedDataForStartEngine = None self.cachedSerializedDataForStartEngine = None
def allInstr(self): 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(): for instrId, instr in lib.instruments.items():
yield instr yield instr
@ -189,8 +189,8 @@ class Data(TemplateData):
the Auditioner. the Auditioner.
""" """
self.lmixUuid = cbox.JackIO.create_audio_output('left_mix') self.lmixUuid = cbox.JackIO.create_audio_output('Stereo Mix L')
self.rmixUuid = cbox.JackIO.create_audio_output('right_mix') self.rmixUuid = cbox.JackIO.create_audio_output('Stereo Mix R')
self.auditioner = Auditioner(self) 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, 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 clientName = cbox.JackIO.status().client_name
mix_l = f"{clientName}:left_mix" mix_l = f"{clientName}:Stereo Mix L"
mix_r = f"{clientName}:right_mix" mix_r = f"{clientName}:Stereo Mix R"
aud_l = f"{clientName}:{self.auditioner.midiInputPortName}_L" aud_l = f"{clientName}:{self.auditioner.midiInputPortName}_L"
aud_r = f"{clientName}:{self.auditioner.midiInputPortName}_R" aud_r = f"{clientName}:{self.auditioner.midiInputPortName}_R"
@ -236,17 +236,25 @@ class Data(TemplateData):
clientName = cbox.JackIO.status().client_name clientName = cbox.JackIO.status().client_name
order = {} order = {}
orderCounter = 0 orderCounter = 0
for instr in self.allInstr():
L = clientName + ":" + instr.midiInputPortName + "_L" for instr in self.allInstr(): #this is sorted alphabetically, which is the library id
R = clientName + ":" + instr.midiInputPortName + "_R" #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
order[L] = (orderCounter, instr) for n in range(instr.numberOfOutputsPairs):
orderCounter += 1 L = clientName + ":" + instr.midiInputPortName+"_"+str(n)+"_L"
order[R] = (orderCounter, instr) order[L] = (orderCounter, instr)
orderCounter += 1 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 order[clientName + ":" + instr.midiInputPortName] = (orderCounter, instr) #midi port
orderCounter +=1 orderCounter +=1
#print (3* len(list(self.allInstr()))) #without multi-out instruments this is the orderCounter in the end.
self._cachedJackMedataPortOrder = order self._cachedJackMedataPortOrder = order
def updateJackMetadataSorting(self): def updateJackMetadataSorting(self):
@ -256,11 +264,29 @@ class Data(TemplateData):
jack ports. jack ports.
Luckily our data never changes. We can just prepare one order, cache it, filter it 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: try:
cbox.JackIO.Metadata.set_all_port_order(cleanedOrder) #wants a dict with {complete jack portname : sortIndex} 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. except Exception as e: #No Jack Meta Data or Error with ports.

3
template/calfbox/cbox.py

@ -879,6 +879,7 @@ class DocInstrument(DocObj):
self.output_slots.append(io) self.output_slots.append(io)
def move_to(self, target_scene, pos = 0): def move_to(self, target_scene, pos = 0):
return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1) return self.cmd_makeobj("/move_to", target_scene.uuid, pos + 1)
def get_output_slot(self, slot): def get_output_slot(self, slot):
return self.output_slots[slot] return self.output_slots[slot]
Document.classmap['cbox_instrument'] = DocInstrument Document.classmap['cbox_instrument'] = DocInstrument
@ -1125,6 +1126,8 @@ class SamplerProgram(DocObj):
return self.get_thing("/control_labels", '/control_label', {int : str}) return self.get_thing("/control_labels", '/control_label', {int : str})
def get_key_labels(self): def get_key_labels(self):
return self.get_thing("/key_labels", '/key_label', {int : str}) 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): def get_keyswitch_groups(self):
return self.get_thing("/keyswitch_groups", '/key_range', [(int, int)]) return self.get_thing("/keyswitch_groups", '/key_range', [(int, int)])
def new_group(self): def new_group(self):

Loading…
Cancel
Save