@ -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 <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 :
""" 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 varia nt { 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 <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>
#Add some defaults.
#Add some default key label s
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.