Browse Source

Just a pre-release code dump

master
Nils 1 year ago
parent
commit
a3a4f3cfe5
  1. 49
      engine/api.py
  2. 18
      engine/auditioner.py
  3. 233
      engine/instrument.py
  4. 101
      engine/main.py
  5. 24
      qtgui/auditioner.py
  6. 247
      qtgui/designer/mainwindow.bak
  7. 74
      qtgui/designer/mainwindow.py
  8. 344
      qtgui/designer/mainwindow.ui
  9. 201
      qtgui/instrument.py
  10. 24
      qtgui/mainwindow.py
  11. 41
      qtgui/mixer.py
  12. 2
      template/calfbox/py/cbox.py
  13. 14
      template/calfbox/sfzloader.c
  14. 15
      template/calfbox/sfzparser.c
  15. 72
      template/calfbox/wavebank.c
  16. 8
      template/engine/input_midi.py
  17. 11
      template/qtgui/helper.py
  18. 3
      template/qtgui/menu.py

49
engine/api.py

@ -46,7 +46,10 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
self.instrumentListMetadata = []
self.startLoadingSamples = []
self.instrumentStatusChanged = []
self.startLoadingAuditionerInstrument = []
self.auditionerInstrumentChanged = []
self.auditionerVolumeChanged = []
self.instrumentMidiNoteOnActivity = []
def _tempCallback(self):
"""Just for copy paste during development"""
@ -82,12 +85,31 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
for func in self.startLoadingSamples:
func(key)
def _startLoadingAuditionerInstrument(self, libraryId:int, instrumentId:int):
"""The sample loading of the auditioner has started. Start flashing an LED or so. The
end of loading is signaled by a _auditionerInstrumentChanged callback with the current
variant included.
"""
key = (libraryId, instrumentId)
for func in self.startLoadingAuditionerInstrument:
func(key)
def _auditionerInstrumentChanged(self, libraryId:int, instrumentId:int):
"""We just send the key. It is assumed that the receiver already has a key:metadata database"""
export = session.data.libraries[libraryId].instruments[instrumentId].exportMetadata()
for func in self.auditionerInstrumentChanged:
func(export)
def _auditionerVolumeChanged(self):
export = session.data.auditioner.volume
for func in self.auditionerVolumeChanged:
func(export)
def _instrumentMidiNoteOnActivity(self, libraryId:int, instrumentId:int):
key = (libraryId, instrumentId)
for func in self.instrumentMidiNoteOnActivity:
func(key)
#Inject our derived Callbacks into the parent module
template.engine.api.callbacks = ClientCallbacks()
from template.engine.api import callbacks
@ -102,11 +124,22 @@ def startEngine(nsmClient):
#For example it is important that the tracks get created first and only then the number of measures
logger.info("Sending initial callbacks to GUI")
#In opposite to other LSS programs loading is delayed because load times are so big.
#Here we go, when everything is already in place and we have callbacks
if session.data.cachedSerializedDataForStartEngine: #We loaded a save file
logger.info("We started from a save-file. Restoring saved state now:")
session.data.loadCachedSerializedData()
#Inject the _instrumentMidiNoteOnActivity callback into session.data for access.
session.data.instrumentMidiNoteOnActivity = callbacks._instrumentMidiNoteOnActivity
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.
for instrument in Instrument.allInstruments.values():
callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._auditionerVolumeChanged()
logger.info("Tembro api startEngine complete")
@ -119,14 +152,20 @@ def loadAllInstrumentSamples():
callbacks._dataChanged
def unloadInstrumentSamples(idkey:tuple):
print ("unloading is not implemented yet")
instrument = Instrument.allInstruments[idkey]
instrument.disable()
callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._dataChanged
def loadInstrumentSamples(idkey:tuple):
"""Load one .sfz from a library."""
instrument = Instrument.allInstruments[idkey]
callbacks._startLoadingSamples(*instrument.idKey)
if not instrument.enabled:
instrument.enable()
instrument.loadSamples()
callbacks._instrumentStatusChanged(*instrument.idKey)
callbacks._dataChanged
@ -142,6 +181,7 @@ def chooseVariantByIndex(idkey:tuple, variantIndex:int):
def auditionerInstrument(idkey:tuple):
"""Load an indendepent instance of an instrument into the auditioner port"""
libraryId, instrumentId = idkey
callbacks._startLoadingAuditionerInstrument(libraryId, instrumentId)
originalInstrument = Instrument.allInstruments[idkey]
#It does not matter if the originalInstrument has its samples loaded or not. We just want the path
if originalInstrument.currentVariant:
@ -155,8 +195,13 @@ def getAvailableAuditionerPorts()->dict:
"""Fetches a new port list each time it is called. No cache."""
return session.data.auditioner.getAvailablePorts()
def connectAuditionerPort(externalPort:str):
"""externalPort is in the Client:Port JACK format.
If externalPort evaluates to False it will disconnect any port."""
session.data.auditioner.connectMidiInputPort(externalPort)
def setAuditionerVolume(value:float):
"""From 0 to -21.
Default is -3.0 """
session.data.auditioner.volume = value
callbacks._auditionerVolumeChanged()

18
engine/auditioner.py

@ -67,16 +67,28 @@ class Auditioner(object):
jackAudioOutLeft = cbox.JackIO.create_audio_output(self.midiInputPortName+"_L")
jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R")
outputMergerRouter = cbox.JackIO.create_audio_output_router(jackAudioOutLeft, jackAudioOutRight)
outputMergerRouter.set_gain(-3.0)
self.outputMergerRouter = cbox.JackIO.create_audio_output_router(jackAudioOutLeft, jackAudioOutRight)
self.outputMergerRouter.set_gain(-3.0)
instrument = layer.get_instrument()
instrument.get_output_slot(0).rec_wet.attach(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?
#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)
@property
def volume(self)->float:
return self.outputMergerRouter.status().gain
@volume.setter
def volume(self, value:float):
if value > 0:
value = 0
elif value < -21: #-21 was determined by ear.
value = -21
self.outputMergerRouter.set_gain(value)
def exportStatus(self):
"""The call-often function to get the instrument status. Includes only data that can
actually change during runtime."""

233
engine/instrument.py

@ -28,6 +28,8 @@ import logging; logger = logging.getLogger(__name__); logger.info("import")
from calfbox import cbox
#Template Modules
from template.engine.input_midi import MidiProcessor
class Instrument(object):
"""Literally one instrument.
@ -88,66 +90,36 @@ class Instrument(object):
def __init__(self, parentLibrary, libraryId:int, metadata:dict, tarFilePath:str, startVariantSfzFilename:str=None):
self.parentLibrary = parentLibrary
self.id = metadata["id"]
self.idKey = (libraryId, metadata["id"])
self.metadata = metadata #parsed from the ini file. See self.exportMetadata for the pythonic dict
self.tarFilePath = tarFilePath
self.libraryId = libraryId
self.startVariantSfzFilename = startVariantSfzFilename
self.idKey = (self.libraryId, self.metadata["id"])
Instrument.allInstruments[self.idKey] = self
self.id = self.metadata["id"]
self.cboxMidiPortUid = None
self.enabled = False #At startup no samples and no jack-ports. But we already have all metadata ready so a GUI can build a database and offer Auditioner choices.
self.mixerEnabled = None #not only is the mixer disabled, but it is unavailable.
self.metadata = metadata #parsed from the ini file. See self.exportMetadata for the pythonic dict
self.instrumentsInLibraryCount = None #injected by the creating process. Counted live in the program, not included in the ini file.
self.tarFilePath = tarFilePath
self.name = metadata["name"]
self.midiInputPortName = f"[{libraryId}-{self.id}] " + metadata["name"]
self.variants = metadata["variants"].split(",")
self.defaultVariant = metadata["defaultVariant"]
self.enabled = False #means loaded.
if "root" in metadata:
if metadata["root"].endswith("/"):
self.rootPrefixPath = metadata["root"]
self.name = self.metadata["name"]
self.cboxMidiPortUid = None
self.midiInputPortName = f"[{self.libraryId}-{self.id}] " + self.metadata["name"]
self.variants = self.metadata["variants"].split(";")
self.defaultVariant = self.metadata["defaultVariant"]
if "root" in self.metadata:
if self.metadata["root"].endswith("/"):
self.rootPrefixPath = self.metadata["root"]
else:
self.rootPrefixPath = metadata["root"] + "/"
self.rootPrefixPath = self.metadata["root"] + "/"
else:
self.rootPrefixPath = ""
#Calfbox. The JACK ports are constructed without samples at first.
self.scene = cbox.Document.get_engine().new_scene()
self.scene.clear()
layer = 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.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()
jackAudioOutLeft = cbox.JackIO.create_audio_output(self.midiInputPortName+"_L")
jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R")
outputMergerRouter = cbox.JackIO.create_audio_output_router(jackAudioOutLeft, jackAudioOutRight)
outputMergerRouter.set_gain(-3.0)
instrument = layer.get_instrument()
instrument.get_output_slot(0).rec_wet.attach(outputMergerRouter) #output_slot is 0 based and means a pair. Most sfz instrument have only one stereo pair. #TODO: And what if not?
routerToGlobalSummingStereoMixer = cbox.JackIO.create_audio_output_router(self.parentLibrary.parentData.lmixUuid, self.parentLibrary.parentData.rmixUuid)
routerToGlobalSummingStereoMixer.set_gain(-3.0)
instrument.get_output_slot(0).rec_wet.attach(routerToGlobalSummingStereoMixer)
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.
#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)
self.startVariantSfzFilename = startVariantSfzFilename
self.currentVariant:str = None #set by self.chooseVariant()
#We could call self.load() now, but we delay that for the user experience. See docstring.
def exportStatus(self):
def exportStatus(self)->dict:
"""The call-often function to get the instrument status. Includes only data that can
actually change during runtime."""
result = {}
@ -160,11 +132,12 @@ class Instrument(object):
result["currentVariant"] = self.currentVariant # str
result["currentVariantWithoutSfzExtension"] = self.currentVariant.rstrip(".sfz") if self.currentVariant else ""# str
result["state"] = self.enabled #bool
result["mixerEnabled"] = self.mixerEnabled #bool
#result["mixerLevel"] = self.mixerLevel #float.
return result
def exportMetadata(self):
def exportMetadata(self)->dict:
"""This gets called before the samples are loaded.
Only static data, that does not get changed during runtime, is included here.
@ -185,7 +158,6 @@ class Instrument(object):
result["tags"] = self.metadata["tags"].split(",") # list of str
result["instrumentsInLibraryCount"] = self.instrumentsInLibraryCount # int
#Optional Tags.
result["group"] = self.metadata["group"] if "group" in self.metadata else "" #str
@ -202,10 +174,12 @@ class Instrument(object):
return result
def loadSamples(self):
"""Instrument is constructed without loading the sample data. But the JACK Port and
all Python objects exist. The API can instruct the loading when everything is ready,
so that the callbacks can receive load-progress messages"""
self.enable()
"""
Convenience starter. Use this.
"""
if not self.enabled:
self.enable()
if self.startVariantSfzFilename:
self.chooseVariant(self.startVariantSfzFilename)
else:
@ -225,7 +199,7 @@ class Instrument(object):
"""
if not self.enabled:
return
raise RuntimeError(f"{self.name} tried to load variant {variantSfzFileName} but was not yet enabled")
if not variantSfzFileName in self.metadata["variants"]:
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, self.metadata["variants"]))
@ -243,14 +217,153 @@ class Instrument(object):
#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!
self.program = self.instrumentLayer.engine.get_patches()[0][1]
print (self.program.status())
logger.info(self.program.status())
self.currentVariant = variantSfzFileName
logger.info(f"Finished loading samples for instrument {variantSfzFileName} with id key {self.idKey}")
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.
After this step an instrument variant must still be loaded. The api and GUI combine this
process by auto-loading the standard variant.
"""
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.")
self.enabled = True
#Calfbox. The JACK ports are constructed without samples at first.
self.scene = cbox.Document.get_engine().new_scene()
self.scene.clear()
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.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?
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.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.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid
self.midiProcessor.register_NoteOn(self.triggerActivityCallback)
#self.midiProcessor.notePrinter(True)
self.parentLibrary.parentData.parentSession.eventLoop.slowConnect(self.midiProcessor.processEvents)
@property
def mixerLevel(self)->float:
if self.enabled:
return self.routerToGlobalSummingStereoMixer.status().gain
else:
return None
@mixerLevel.setter
def mixerLevel(self, value:float):
"""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.
To completely mute use self.mute = True. The mixerLevel will be preserved over this-
"""
if self.enabled:
self.routerToGlobalSummingStereoMixer.set_gain(value)
else:
raise ValueError("Tried to set mixer level while instrument is disabled")
def setMixerEnabled(self, state:bool):
"""Connect or disconnect the instrument from the summing mixer.
We need to track the connection state on our own.
The mixer level is preserved.
self.mixerEnabled can be True or False, this is always a user value.
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()
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
def triggerActivityCallback(self, *args):
"""args are: timestamp, channel, note, velocity.
Which we all don't need at the moment.
If in the future we need these for a more important task than blinking an LED:
consider to change eventloop.slowConnect to fastConnect. And also disconnect in self.disable()"""
self.parentLibrary.parentData.instrumentMidiNoteOnActivity(*self.idKey)
def disable(self):
"""Jack midi port and audio ports will disappear. Impact on RAM and CPU usage unknown."""
if not self.enabled:
raise RuntimeError(f"{self.name} tried to switch to disabled, but it already was. This is not a trivial error. Please make sure no user-function can reach this state.")
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
self.scene.clear()
cbox.JackIO.delete_audio_output(self.jackAudioOutLeft)
cbox.JackIO.delete_audio_output(self.jackAudioOutRight)
cbox.JackIO.delete_midi_input(self.cboxMidiPortUid)
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.
#Save
def serialize(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
"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.
}
#Loading is done externally by main/Data directly

101
engine/main.py

@ -56,45 +56,103 @@ class Data(TemplateData):
def __init__(self, parentSession): #Program start.
super().__init__(parentSession)
session = self.parentSession #self.parentSession is already defined in template.data. We just want to work conveniently in init with it by creating a local var.
self._processAfterInit()
session = self.parentSession
def _processAfterInit(self):
session = self.parentSession #We just want to work conveniently in init with it by creating a local var.
#Create two mixer ports, for stereo. Each instrument will not only create their own jack out ports
#but also connect to these left/right.
assert not session.standaloneMode is None
if session.standaloneMode:
self.lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1
self.rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2
else:
self.lmixUuid = cbox.JackIO.create_audio_output('left_mix')
self.rmixUuid = cbox.JackIO.create_audio_output('right_mix')
#Auditioner: Create an additional stereo port pair to pre-listen to on sample instrument alone
self.auditioner = Auditioner(self)
self.cachedSerializedDataForStartEngine = None
#Parse all tar libraries and load them all. ALL OF THEM!
#for each library in ...
self.libraries = {} # libraryId:int : Library-object
basePath = pathlib.Path("/home/nils/samples/Tembro/out/")
basePath = pathlib.Path("/home/nils/samples/Tembro/out/") #TODO: replace with system-finder /home/xy or /usr/local/share or /usr/share etc.
for f in basePath.glob('*.tar'):
if f.is_file() and f.suffix == ".tar":
lib = Library(parentData=self, tarFilePath=f)
self.libraries[lib.id] = lib
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._createGlobalPorts() #in its own function for readability
def _createGlobalPorts(self):
"""Create two mixer ports, for stereo. Each instrument will not only create their own jack
out ports but also connect to these left/right.
If we are not in an NSM Session auto-connect them to the system ports for convenience.
Also create an additional stereo port pair to pre-listen to on sample instrument alone,
the Auditioner.
"""
assert not self.parentSession.standaloneMode is None
if self.parentSession.standaloneMode:
self.lmixUuid = cbox.JackIO.create_audio_output('left_mix', "#1") #add "#1" as second parameter for auto-connection to system out 1
self.rmixUuid = cbox.JackIO.create_audio_output('right_mix', "#2") #add "#2" as second parameter for auto-connection to system out 2
else:
self.lmixUuid = cbox.JackIO.create_audio_output('left_mix')
self.rmixUuid = cbox.JackIO.create_audio_output('right_mix')
self.auditioner = Auditioner(self)
def exportMetadata(self)->dict:
"""Data we sent in callbacks. This is the initial 'build-the-instrument-database' function.
Each first level dict contains another dict with instruments, but also a special key
"library" that holds the metadata for the lib itself.
"""
result = {}
for libId, libObj in self.libraries.items():
result[libId] = libObj.exportMetadata() #also a dict. Contains a special key "library" which holds the library metadata itself
return result
#Save / Load
def serialize(self)->dict:
return {
"libraries" : [libObj.serialize() for libObj in self.libraries.values()],
}
@classmethod
def instanceFromSerializedData(cls, parentSession, serializedData):
"""As an experiment we try to load everything from this function alone and not create a
function hirarchy. Save is in a hirarchy though.
This differs from other LSS programs that most data is just static stuff.
Instead we delay loading until everything is setup and just deposit saved data here
for the startEngine call to use."""
self = cls.__new__(cls)
self.session = parentSession
self.parentSession = parentSession
self._processAfterInit()
self.cachedSerializedDataForStartEngine = serializedData
return self
def loadCachedSerializedData(self):
"""Called by api.startEngine after all static data libraries are loaded, without jack ports
or instrument samples loaded. This is the same as a the empty session without a save file.
This way the callbacks have a chance to load instrument with feedback status"""
assert self.cachedSerializedDataForStartEngine
serializedData = self.cachedSerializedDataForStartEngine
for libSerialized in serializedData["libraries"]:
libObj = self.libraries[libSerialized["id"]]
for instrSerialized in libSerialized["instruments"]:
instObj = libObj.instruments[instrSerialized["id"]]
instObj.startVariantSfzFilename = instrSerialized["currentVariant"]
if instrSerialized["currentVariant"]:
instObj.loadSamples() #will use startVariantSfzFilename to set the currentVariant
if not instrSerialized["mixerEnabled"] is None: #can be True/False. None for "never touched" or "instrument not loaded"
assert instrSerialized["currentVariant"] #mixer is auto-disabled when instrument deactivated
instObj.setMixerEnabled(instrSerialized["mixerEnabled"])
if not instrSerialized["mixerLevel"] is None: #could be 0. None for "never touched" or "instrument not loaded"
assert instrSerialized["currentVariant"] #mixerLevel is None when no instrument is loaded. mixerEnabled can be False though.
instObj.mixerLevel = instrSerialized["mixerLevel"]
class Library(object):
"""Open a .tar library and extract information without actually loading any samples.
This is for GUI data etc.
@ -116,6 +174,8 @@ class Library(object):
raise RuntimeError(f"Wrong file {tarFilePath}")
logger.info(f"Parsing {tarFilePath}")
needTarData = False #TODO: If we have images etc. in the future.
if needTarData:
with tarfile.open(name=tarFilePath, mode='r:') as opentarfile:
@ -170,3 +230,14 @@ class Library(object):
return result
#Save
def serialize(self)->dict:
"""The library-obj is already constructed from static default values.
We only save what differs from the default, most important the state of the instruments.
If a library gets a new instrument in between tembro-runs it will just use default values.
"""
return {
"id" : self.id, #for convenience access
"instruments" : [instr.serialize() for instr in self.instruments.values()]
}

24
qtgui/auditioner.py

@ -36,10 +36,17 @@ class AuditionerMidiInputComboController(object):
def __init__(self, parentMainWindow):
self.parentMainWindow = parentMainWindow
self.comboBox = parentMainWindow.ui.auditionerMidiInputComboBox
self.volumeDial = parentMainWindow.ui.auditionerVolumeDial
self.volumeDial.setMaximum(0)
self.volumeDial.setMinimum(-21)
self.volumeDial.valueChanged.connect(self._sendVolumeChangeToEngine)
#self.volumeDial.setAttribute(QtCore.Qt.WA_Hover)
self.volumeDial.leaveEvent = lambda ev: self.parentMainWindow.statusBar().showMessage("")
self.wholePanel = parentMainWindow.ui.auditionerWidget
self.currentInstrumentLabel = parentMainWindow.ui.auditionerCurrentInstrument_label
self.currentInstrumentLabel.setText("")
#if not api.isStandaloneMode():
#self.wholePanel.hide()
#return
@ -49,13 +56,30 @@ class AuditionerMidiInputComboController(object):
self.comboBox.showPopup = self.showPopup
self.comboBox.activated.connect(self._newPortChosen)
api.callbacks.startLoadingAuditionerInstrument.append(self.callback_startLoadingAuditionerInstrument)
api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged)
api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged)
def _sendVolumeChangeToEngine(self, newValue):
self.volumeDial.blockSignals(True)
api.setAuditionerVolume(newValue)
self.volumeDial.blockSignals(False)
def callback_startLoadingAuditionerInstrument(self, idkey):
self.parentMainWindow.qtApp.setOverrideCursor(QtCore.Qt.WaitCursor) #reset in self.callback_auditionerInstrumentChanged
self.currentInstrumentLabel.setText(QtCore.QCoreApplication.translate("Auditioner", "…loading…"))
self.parentMainWindow.qtApp.processEvents() #actually show the label and cursor
def callback_auditionerInstrumentChanged(self, exportMetadata:dict):
self.parentMainWindow.qtApp.restoreOverrideCursor() #We assume the cursor was set to a loading animation
key = exportMetadata["id-key"]
t = f"➜ [{key[0]}-{key[1]}] {exportMetadata['name']}"
self.currentInstrumentLabel.setText(t)
def callback__auditionerVolumeChanged(self, value:float):
self.volumeDial.setValue(value)
self.parentMainWindow.statusBar().showMessage(QtCore.QCoreApplication.translate("Auditioner", "Auditioner Volume: {}").format(value))
def _newPortChosen(self, index:int):
assert self.comboBox.currentIndex() == index
api.connectAuditionerPort(self.comboBox.currentText())

247
qtgui/designer/mainwindow.bak

@ -0,0 +1,247 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>MainWindow</class>
<widget class="QMainWindow" name="MainWindow">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1087</width>
<height>752</height>
</rect>
</property>
<property name="windowTitle">
<string>MainWindow</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QSplitter" name="splitter_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QGroupBox" name="search_groupBox">
<property name="title">
<string>Search</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QListWidget" name="search_listWidget"/>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QGroupBox" name="iinstruments_groupBox">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>Instruments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="auditionerWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Auditioner MIDI Input</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="auditionerMidiInputComboBox">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="auditionerCurrentInstrument_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="instruments_treeWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<column>
<property name="text">
<string/>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Variant</string>
</property>
</column>
<column>
<property name="text">
<string>Tags</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="details_groupBox">
<property name="title">
<string>NamePlaceholder</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QScrollArea" name="details_scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>598</width>
<height>158</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">
<item row="0" column="0">
<widget class="QLabel" name="variant_label">
<property name="text">
<string>Variants</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="variants_comboBox"/>
</item>
<item row="1" column="1">
<widget class="QLabel" name="info_label">
<property name="text">
<string>TextLabel</string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>1087</width>
<height>20</height>
</rect>
</property>
</widget>
<widget class="QStatusBar" name="statusbar"/>
</widget>
<resources/>
<connections/>
</ui>

74
qtgui/designer/mainwindow.py

@ -34,17 +34,31 @@ class Ui_MainWindow(object):
self.splitter = QtWidgets.QSplitter(self.splitter_2)
self.splitter.setOrientation(QtCore.Qt.Vertical)
self.splitter.setObjectName("splitter")
self.iinstruments_groupBox = QtWidgets.QGroupBox(self.splitter)
self.iinstruments_groupBox.setEnabled(True)
self.iinstruments_groupBox.setObjectName("iinstruments_groupBox")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.iinstruments_groupBox)
self.iinstruments_tabWidget = QtWidgets.QTabWidget(self.splitter)
self.iinstruments_tabWidget.setEnabled(True)
self.iinstruments_tabWidget.setObjectName("iinstruments_tabWidget")
self.Instruments = QtWidgets.QWidget()
self.Instruments.setObjectName("Instruments")
self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.Instruments)
self.verticalLayout_2.setContentsMargins(0, 0, 0, 0)
self.verticalLayout_2.setSpacing(0)
self.verticalLayout_2.setObjectName("verticalLayout_2")
self.auditionerWidget = QtWidgets.QWidget(self.iinstruments_groupBox)
self.auditionerWidget = QtWidgets.QWidget(self.Instruments)
self.auditionerWidget.setObjectName("auditionerWidget")
self.horizontalLayout = QtWidgets.QHBoxLayout(self.auditionerWidget)
self.horizontalLayout.setObjectName("horizontalLayout")
self.auditionerVolumeDial = QtWidgets.QDial(self.auditionerWidget)
self.auditionerVolumeDial.setMaximumSize(QtCore.QSize(32, 32))
self.auditionerVolumeDial.setSizeIncrement(QtCore.QSize(3, 0))
self.auditionerVolumeDial.setMinimum(-40)
self.auditionerVolumeDial.setMaximum(0)
self.auditionerVolumeDial.setPageStep(3)
self.auditionerVolumeDial.setProperty("value", -3)
self.auditionerVolumeDial.setWrapping(False)
self.auditionerVolumeDial.setNotchTarget(3.0)
self.auditionerVolumeDial.setNotchesVisible(True)
self.auditionerVolumeDial.setObjectName("auditionerVolumeDial")
self.horizontalLayout.addWidget(self.auditionerVolumeDial)
self.label = QtWidgets.QLabel(self.auditionerWidget)
self.label.setObjectName("label")
self.horizontalLayout.addWidget(self.label)
@ -58,7 +72,7 @@ class Ui_MainWindow(object):
spacerItem = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout.addItem(spacerItem)
self.verticalLayout_2.addWidget(self.auditionerWidget)
self.instruments_treeWidget = QtWidgets.QTreeWidget(self.iinstruments_groupBox)
self.instruments_treeWidget = QtWidgets.QTreeWidget(self.Instruments)
self.instruments_treeWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
self.instruments_treeWidget.setProperty("showDropIndicator", False)
self.instruments_treeWidget.setAlternatingRowColors(True)
@ -67,6 +81,47 @@ class Ui_MainWindow(object):
self.instruments_treeWidget.setHorizontalScrollMode(QtWidgets.QAbstractItemView.ScrollPerPixel)
self.instruments_treeWidget.setObjectName("instruments_treeWidget")
self.verticalLayout_2.addWidget(self.instruments_treeWidget)
self.iinstruments_tabWidget.addTab(self.Instruments, "")
self.Mixer = QtWidgets.QWidget()
self.Mixer.setObjectName("Mixer")
self.mixerVerticalLayout = QtWidgets.QVBoxLayout(self.Mixer)
self.mixerVerticalLayout.setContentsMargins(0, 0, 0, 0)
self.mixerVerticalLayout.setSpacing(0)
self.mixerVerticalLayout.setObjectName("mixerVerticalLayout")
self.mixerInstructionLabel = QtWidgets.QLabel(self.Mixer)
sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed)
sizePolicy.setHorizontalStretch(0)
sizePolicy.setVerticalStretch(0)
sizePolicy.setHeightForWidth(self.mixerInstructionLabel.sizePolicy().hasHeightForWidth())
self.mixerInstructionLabel.setSizePolicy(sizePolicy)
self.mixerInstructionLabel.setWordWrap(True)
self.mixerInstructionLabel.setObjectName("mixerInstructionLabel")
self.mixerVerticalLayout.addWidget(self.mixerInstructionLabel)
self.scrollArea = QtWidgets.QScrollArea(self.Mixer)
self.scrollArea.setWidgetResizable(True)
self.scrollArea.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
self.scrollArea.setObjectName("scrollArea")
self.mixerAreaWidget = QtWidgets.QWidget()
self.mixerAreaWidget.setGeometry(QtCore.QRect(0, 0, 599, 447))
self.mixerAreaWidget.setObjectName("mixerAreaWidget")
self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.mixerAreaWidget)
self.horizontalLayout_2.setObjectName("horizontalLayout_2")
self.progressBar = QtWidgets.QProgressBar(self.mixerAreaWidget)
self.progressBar.setProperty("value", 24)
self.progressBar.setOrientation(QtCore.Qt.Vertical)
self.progressBar.setTextDirection(QtWidgets.QProgressBar.TopToBottom)
self.progressBar.setObjectName("progressBar")
self.horizontalLayout_2.addWidget(self.progressBar)
self.progressBar_2 = QtWidgets.QProgressBar(self.mixerAreaWidget)
self.progressBar_2.setProperty("value", 24)
self.progressBar_2.setOrientation(QtCore.Qt.Vertical)
self.progressBar_2.setObjectName("progressBar_2")
self.horizontalLayout_2.addWidget(self.progressBar_2)
spacerItem1 = QtWidgets.QSpacerItem(40, 20, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)
self.horizontalLayout_2.addItem(spacerItem1)
self.scrollArea.setWidget(self.mixerAreaWidget)
self.mixerVerticalLayout.addWidget(self.scrollArea)
self.iinstruments_tabWidget.addTab(self.Mixer, "")
self.details_groupBox = QtWidgets.QGroupBox(self.splitter)
self.details_groupBox.setObjectName("details_groupBox")
self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.details_groupBox)
@ -77,7 +132,7 @@ class Ui_MainWindow(object):
self.details_scrollArea.setWidgetResizable(True)
self.details_scrollArea.setObjectName("details_scrollArea")
self.scrollAreaWidgetContents = QtWidgets.QWidget()
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 598, 158))
self.scrollAreaWidgetContents.setGeometry(QtCore.QRect(0, 0, 597, 155))
self.scrollAreaWidgetContents.setObjectName("scrollAreaWidgetContents")
self.formLayout = QtWidgets.QFormLayout(self.scrollAreaWidgetContents)
self.formLayout.setObjectName("formLayout")
@ -105,19 +160,22 @@ class Ui_MainWindow(object):
MainWindow.setStatusBar(self.statusbar)
self.retranslateUi(MainWindow)
self.iinstruments_tabWidget.setCurrentIndex(0)
QtCore.QMetaObject.connectSlotsByName(MainWindow)
def retranslateUi(self, MainWindow):
_translate = QtCore.QCoreApplication.translate
MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
self.search_groupBox.setTitle(_translate("MainWindow", "Search"))
self.iinstruments_groupBox.setTitle(_translate("MainWindow", "Instruments"))
self.label.setText(_translate("MainWindow", "Auditioner MIDI Input"))
self.auditionerCurrentInstrument_label.setText(_translate("MainWindow", "TextLabel"))
self.instruments_treeWidget.headerItem().setText(1, _translate("MainWindow", "ID"))
self.instruments_treeWidget.headerItem().setText(2, _translate("MainWindow", "Name"))
self.instruments_treeWidget.headerItem().setText(3, _translate("MainWindow", "Variant"))
self.instruments_treeWidget.headerItem().setText(4, _translate("MainWindow", "Tags"))
self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Instruments), _translate("MainWindow", "Instruments"))
self.mixerInstructionLabel.setText(_translate("MainWindow", "This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected."))
self.iinstruments_tabWidget.setTabText(self.iinstruments_tabWidget.indexOf(self.Mixer), _translate("MainWindow", "Mixer"))
self.details_groupBox.setTitle(_translate("MainWindow", "NamePlaceholder"))
self.variant_label.setText(_translate("MainWindow", "Variants"))
self.info_label.setText(_translate("MainWindow", "TextLabel"))

344
qtgui/designer/mainwindow.ui

@ -49,117 +49,255 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<widget class="QGroupBox" name="iinstruments_groupBox">
<widget class="QTabWidget" name="iinstruments_tabWidget">
<property name="enabled">
<bool>true</bool>
</property>
<property name="title">
<string>Instruments</string>
<property name="currentIndex">
<number>0</number>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="auditionerWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Auditioner MIDI Input</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="auditionerMidiInputComboBox">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="auditionerCurrentInstrument_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="instruments_treeWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<column>
<property name="text">
<string/>
<widget class="QWidget" name="Instruments">
<attribute name="title">
<string>Instruments</string>
</attribute>
<layout class="QVBoxLayout" name="verticalLayout_2">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QWidget" name="auditionerWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QDial" name="auditionerVolumeDial">
<property name="maximumSize">
<size>
<width>32</width>
<height>32</height>
</size>
</property>
<property name="sizeIncrement">
<size>
<width>3</width>
<height>0</height>
</size>
</property>
<property name="minimum">
<number>-40</number>
</property>
<property name="maximum">
<number>0</number>
</property>
<property name="pageStep">
<number>3</number>
</property>
<property name="value">
<number>-3</number>
</property>
<property name="wrapping">
<bool>false</bool>
</property>
<property name="notchTarget">
<double>3.000000000000000</double>
</property>
<property name="notchesVisible">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Auditioner MIDI Input</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="auditionerMidiInputComboBox">
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToContents</enum>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="auditionerCurrentInstrument_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QTreeWidget" name="instruments_treeWidget">
<property name="editTriggers">
<set>QAbstractItemView::NoEditTriggers</set>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
</column>
<column>
<property name="text">
<string>Variant</string>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="verticalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<property name="horizontalScrollMode">
<enum>QAbstractItemView::ScrollPerPixel</enum>
</property>
<column>
<property name="text">
<string/>
</property>
</column>
<column>
<property name="text">
<string>ID</string>
</property>
</column>
<column>
<property name="text">
<string>Name</string>
</property>
</column>
<column>
<property name="text">
<string>Variant</string>
</property>
</column>
<column>
<property name="text">
<string>Tags</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="Mixer">
<attribute name="title">
<string>Mixer</string>
</attribute>
<layout class="QVBoxLayout" name="mixerVerticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QLabel" name="mixerInstructionLabel">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</column>
<column>
<property name="text">
<string>Tags</string>
<string>This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected.</string>
</property>
</column>
</widget>
</item>
</layout>
<property name="wordWrap">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QScrollArea" name="scrollArea">
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="mixerAreaWidget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>599</width>
<height>447</height>
</rect>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="textDirection">
<enum>QProgressBar::TopToBottom</enum>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar_2">
<property name="value">
<number>24</number>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
</widget>
</widget>
<widget class="QGroupBox" name="details_groupBox">
<property name="title">
@ -191,8 +329,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>598</width>
<height>158</height>
<width>597</width>
<height>155</height>
</rect>
</property>
<layout class="QFormLayout" name="formLayout">

201
qtgui/instrument.py

@ -37,7 +37,10 @@ COLUMNS = ("state", "id-key", "name", "loaded", "group", "tags" ) #Loaded = Vari
class InstrumentTreeController(object):
"""Not a qt class. We externally controls the QTreeWidget
"""
Shows the list of instruments, so they can be clicked upon :)
Not a qt class. We externally controls the QTreeWidget
Why is this not a QTableWidget? As in Agordejo, a TableWidget is a complex item, and inconvenient
to use. You need to add an Item to each cell. While in TreeWidget you just create one item.
@ -49,8 +52,11 @@ class InstrumentTreeController(object):
self.parentMainWindow = parentMainWindow
self.treeWidget = self.parentMainWindow.ui.instruments_treeWidget
self._cachedData = None
self._cachedLastInstrumentStatus = {} # instrument idkey : status Dict
self.guiLibraries = {} # id-key : GuiLibrary
self.guiInstruments = {} # id-key : GuiInstrument
self.currentlyNested = None #is the view nested in libraries or just all instruments?
self.headerLabels = [
@ -70,21 +76,37 @@ class InstrumentTreeController(object):
self.treeWidget.itemDoubleClicked.connect(self.itemDoubleClicked)
self.treeWidget.itemSelectionChanged.connect(self.itemSelectionChanged)
self.treeWidget.itemExpanded.connect(self.itemExpandedOrCollapsed)
self.treeWidget.itemCollapsed.connect(self.itemExpandedOrCollapsed)
self.sortByColumnValue = 1 #by instrId
self.sortDescendingValue = 0 # Qt::SortOrder which is 0 for ascending and 1 for descending
api.callbacks.instrumentListMetadata.append(self.buildTree)
api.callbacks.instrumentListMetadata.append(self.buildTree) #called without arguments it will just create the standard tree. This will only happen once at program start.
api.callbacks.startLoadingSamples.append(self.react_startLoadingSamples)
api.callbacks.instrumentStatusChanged.append(self.react_instrumentStatusChanged)
api.callbacks.instrumentMidiNoteOnActivity.append(self.react_instrumentMidiNoteOnActivity)
#self.treeWidget.setStyleSheet("QTreeWidget::item { border-bottom: 1px solid black;}") #sadly for all items. We want a line below top level items.
if not "libraryIsExpanded" in api.session.guiSharedDataToSave:
api.session.guiSharedDataToSave["libraryIsExpanded"] = {} # libId : bool if expanded or not. Also used when switching from nested to flat and back.
#Default values are used in self.buildTree
def itemExpandedOrCollapsed(self, libraryItem:QtWidgets.QTreeWidgetItem):
#print (libraryItem.name, libraryItem.isExpanded())
api.session.guiSharedDataToSave["libraryIsExpanded"][libraryItem.id] = libraryItem.isExpanded()
def itemSelectionChanged(self):
"""Only one instrument can be selected at the same time.
This function mostly informs other widgets that a different instrument was selected
"""
selItems = self.treeWidget.selectedItems()
assert len(selItems) == 1, selItems #because our selection is set to single
if not selItems:
return
assert len(selItems) == 1, selItems #because our selection is set to single.
item = selItems[0]
if type(item) is GuiInstrument:
self.parentMainWindow.selectedInstrumentController.instrumentChanged(item.idkey)
@ -92,12 +114,19 @@ class InstrumentTreeController(object):
self.parentMainWindow.selectedInstrumentController.directLibrary(item.idkey)
def itemDoubleClicked(self, item:QtWidgets.QTreeWidgetItem, column:int):
"""This chooses the auditioner. Callbacks are handled in the auditioner widget itself"""
if type(item) is GuiInstrument:
api.auditionerInstrument(item.idkey)
def buildTree(self, data:dict):
"""Data is a dict of dicts and has a hierarchy.
def buildTree(self, data:dict, nested:bool=None):
"""
Create the tree. Can be called multiple times and it will re-create itself destructively.
If you call it with data, once at program start, data will get cached. If you call
with data=None it will used the cached variant.
Data is a dict of dicts and has a hierarchy.
data[libId] = dictOfInstruments
dictOfInstrument[instrId] = pythonDataDict
@ -153,31 +182,63 @@ class InstrumentTreeController(object):
'vendor': 'Hilbricht Nils 2021, Laborejo Software Suite '
'https://www.laborejo.org info@laborejo.org'}}}
"""
#Reset everything except our cached data.
self.treeWidget.clear() #will delete the C++ objects. We need to delete the PyQt objects ourselves, like so:
self.guiLibraries = {} # id-key : GuiLibrary
self.guiInstruments = {} # id-key : GuiInstrument
self.currentlyNested = nested
if data:
self._cachedData = data
else:
assert self._cachedData
data = self._cachedData
if nested is None:
if "nestedView" in api.session.guiSharedDataToSave:
nested = api.session.guiSharedDataToSave["nestedView"]
else:
nested = True
api.session.guiSharedDataToSave["nestedView"] = nested
self.currentlyNested = nested
for libraryId, libraryDict in data.items():
parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"])
self.treeWidget.addTopLevelItem(parentLibraryWidget)
if nested:
parentLibraryWidget = GuiLibrary(parentTreeController=self, libraryDict=libraryDict["library"])
self.guiLibraries[libraryId] = parentLibraryWidget
self.treeWidget.addTopLevelItem(parentLibraryWidget)
if libraryId in api.session.guiSharedDataToSave["libraryIsExpanded"]:
parentLibraryWidget.setExpanded(api.session