Browse Source

Just a pre-release code dump

master
Nils 3 years 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.guiSharedDataToSave["libraryIsExpanded"][libraryId]) #only possible after gi.init() was done and item inserted.
#parentLibraryWidget.setHidden(True) #only possible after insert
for instrumentdId, instrumentDict in libraryDict.items():
if instrumentdId == "library":
#Top level item was already created. Ignore here.
pass
else:
self.newInstrument(parentLibraryWidget, instrumentDict)
parentLibraryWidget.setExpanded(True)
gi = GuiInstrument(parentTreeController=self, instrumentDict=instrumentDict)
if nested:
parentLibraryWidget.addChild(gi)
else:
self.treeWidget.addTopLevelItem(gi)
gi.injectToggleSwitch() #only possible after gi.init() was done and item inserted.
self.guiInstruments[instrumentDict["id-key"]] = gi
if instrumentDict["id-key"] in self._cachedLastInstrumentStatus:
gi.updateStatus(self._cachedLastInstrumentStatus[instrumentDict["id-key"]])
self._adjustColumnSize()
def toggleNestedFlat(self, newCheckStateIsNested):
"""We receive newCheckState as automatic parameter from Qt from the calling menu action"""
self.buildTree(data=None, nested=newCheckStateIsNested) #with data=None it will used the cache data we received once, at startup
def newInstrument(self, parentLibraryWidget, instrumentDict):
gi = GuiInstrument(parentTreeController=self, instrumentDict=instrumentDict)
#self.treeWidget.addTopLevelItem(gi)
parentLibraryWidget.addChild(gi)
gi.injectToggleSwitch() #only possible after gi.init was done and item inserted.
self._adjustColumnSize()
self.guiInstruments[instrumentDict["id-key"]] = gi
def setAllExpanded(self, state:bool):
"""We do not use the qt function collapseAll and expandAll because they do not trigger
the signal"""
if self.currentlyNested:
for libid, guiLib in self.guiLibraries.items():
guiLib.setExpanded(state) #triggers signal which will trigger self.toggleNestedFlat
def _adjustColumnSize(self):
self.treeWidget.sortItems(self.sortByColumnValue, self.sortDescendingValue)
@ -192,17 +253,29 @@ class InstrumentTreeController(object):
def react_instrumentStatusChanged(self, instrumentStatus:dict):
self.parentMainWindow.qtApp.restoreOverrideCursor() #Sometimes the instrument was loaded with a cursor animation
gi = self.guiInstruments[instrumentStatus["id-key"]]
gi.updateStatus(instrumentStatus)
self._adjustColumnSize()
#We also cache the last status, as we cache the initial data. This way we can delete and recreate TreeItems without requesting new status data from the engine
self._cachedLastInstrumentStatus[instrumentStatus["id-key"]] = instrumentStatus
def react_startLoadingSamples(self, idkey:tuple):
"""Will be overriden by instrument status change / variant chosen"""
self.parentMainWindow.qtApp.setOverrideCursor(QtCore.Qt.WaitCursor) #reset in self.react_instrumentStatusChanged
text = QtCore.QCoreApplication.translate("InstrumentTreeController", "…loading…")
loadedIndex = COLUMNS.index("loaded")
instr = self.guiInstruments[idkey]
instr.setText(loadedIndex, text)
self.parentMainWindow.qtApp.processEvents() #actually show the label
self.parentMainWindow.qtApp.processEvents() #actually show the label and cursor
def react_instrumentMidiNoteOnActivity(self, idkey:tuple):
#First figure out which instrument has activity
gi = self.guiInstruments[idkey]
gi.activity()
class GuiLibrary(QtWidgets.QTreeWidgetItem):
"""The top level library item. All instruments are in a library."""
@ -210,13 +283,28 @@ class GuiLibrary(QtWidgets.QTreeWidgetItem):
def __init__(self, parentTreeController, libraryDict):
super().__init__([], type=1000) #type 0 is default qt type. 1000 is subclassed user type)
self.id = libraryDict["id"]
self.idkey = (libraryDict["id"], 0) #fake it for compatibility
self.name = libraryDict["name"]
#No dynamic data here. Everything gets created once.
self.setText(COLUMNS.index("id-key"), str(libraryDict["id"]))
#self.setText(COLUMNS.index("id-key"), str(libraryDict["id"]).zfill(leadingZeroesForZfill))
self.setData(COLUMNS.index("id-key"), 0, int(libraryDict["id"])) #set data allows sorting by actual numbers. 0 is the data role, which is just "display text".
self.setText(COLUMNS.index("name"), str(libraryDict["name"]))
self.setText(COLUMNS.index("tags"), str(libraryDict["description"])[:42]+"")
#Hack the row height through an unused column.
#self.setText(COLUMNS.index("loaded"), "")
#We cannot call setExpanded here. The item must first be inserted into the parent tree.
#We placed this call in InstrumentTreeController.buildTree
#self.setExpanded(False)
icon = parentTreeController.parentMainWindow.style().standardIcon(getattr(QtWidgets.QStyle, "SP_DirIcon"))
self.setIcon(COLUMNS.index("name"), icon)
class GuiInstrument(QtWidgets.QTreeWidgetItem):
"""
@ -235,6 +323,8 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
allItems = {} # instrId : GuiInstrument
def __init__(self, parentTreeController, instrumentDict):
GuiInstrument.allItems[instrumentDict["id-key"]] = self
self.parentTreeController = parentTreeController
@ -251,21 +341,41 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
self.setTextAlignment(self.columns.index("id-key"), QtCore.Qt.AlignHCenter)
self.state = None #by self.switch()
self.state = None #by self.update...
self.instrumentDict = None
self._writeColumns(instrumentDict)
self.toggleSwitch = ToggleSwitch()
self.toggleSwitch.setAutoFillBackground(True) #otherwise conflicts with setItemWidget
self.toggleSwitch = ToggleSwitch(track_radius=8,thumb_radius=7) #radius is the actual size, not the rounded corners.
#self.toggleSwitch.toggled.connect(lambda c: print('toggled', c)) #triggered by engine callback as well
self.toggleSwitch.setAutoFillBackground(True) #otherwise conflicts with setItemWidget
self.toggleSwitch.clicked.connect(self.instrumentSwitchOnViaGui)
#self.toggleSwitch.pressed.connect(lambda: print('pressed')) #literal mouse down.
#self.toggleSwitch.released.connect(lambda: print('released'))
#We cannot add the ToggleSwitch Widget here.
#It must be inserted after self was added to the Tree. Use self.injectToggleSwitch from parent
#icon = parentTreeController.parentMainWindow.style().standardIcon(getattr(QtWidgets.QStyle, "SP_ComputerIcon"))
#px = QtGui.QPixmap(32,32) #TODO: get size from standard icon above. but how? Apparently it does not matter how big this is.
#px.fill(QtGui.QColor(255,255,255,0)) #transparent icon)
#icon = QtGui.QIcon(px)
#self.setIcon(COLUMNS.index("name"), icon)
#Create icons for midi in status
on = QtGui.QPixmap(32,32)
oncolor = QtGui.QColor("cyan")
on.fill(oncolor)
self.onIcon = QtGui.QIcon(on)
off = QtGui.QPixmap(32,32)
#offcolor = QtGui.QColor(50, 50, 50) #dark grey
offcolor = QtGui.QColor(255,255,255,0) #transparent
off.fill(offcolor)
self.offIcon = QtGui.QIcon(off)
self.setIcon(COLUMNS.index("name"), self.offIcon)
self.activeFlag = False #midi indicator
def instrumentSwitchOnViaGui(self, state):
"""Only GUI clicks. Does not react to the engine callback that switches on instruments. For
@ -276,13 +386,22 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
api.unloadInstrumentSamples(self.idkey)
def updateStatus(self, instrumentStatus:dict):
#Before we set the state permanently we use the opportunity to see if this is program state (state == None) or unloading
firstLoad = self.state is None
variantColumnIndex = self.columns.index("loaded")
self.currentVariant = instrumentStatus["currentVariant"]
if instrumentStatus["currentVariant"]: #None if not loaded or not enabled anymore
self.setText(variantColumnIndex, instrumentStatus["currentVariant"].rstrip(".sfz"))
#if instrumentStatus["currentVariant"]: #None if not loaded or not enabled anymore
self.setText(variantColumnIndex, instrumentStatus["currentVariant"].rstrip(".sfz")) #either "" or a variant
self.state = instrumentStatus["state"]
self.toggleSwitch.setChecked(instrumentStatus["state"])
if self.state:
#either reload or load for the first time
api.session.eventLoop.verySlowConnect(self._activityOff)
elif not firstLoad: #and not self.state
#the instrument was once loaded and is currently connected
api.session.eventLoop.verySlowDisconnect(self._activityOff)
def injectToggleSwitch(self):
"""Call this after the item was added to the tree"""
stateColumnIndex = self.columns.index("state")
@ -309,9 +428,15 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
elif key == "id-key": #tuple
libId, instrId = instrumentDict[key]
#self.setText(index, f"{libId}-{str(instrId).zfill(3)}")
zeros = int(instrumentDict["instrumentsInLibraryCount"]/10)+1
self.setText(index, str(instrId).zfill(zeros))
instIdZFilled = str(instrId).zfill(zeros)
if self.parentTreeController.currentlyNested:
self.setText(index, instIdZFilled)
else: #full id
#self.setText(index, f"{libId}-{str(instrId).zfill(zeros)}")
self.setData(index, 0, float(libId + "." + instIdZFilled)) #0 is the data role, just stnadard display text. We combine both IDs to a float number for sorting. If used with setData instead of setText Qt will know how to sort 11 before 1000
"""
elif key == "state": #use parameter for initial value. loaded from file or default = False.
@ -320,13 +445,13 @@ class GuiInstrument(QtWidgets.QTreeWidgetItem):
self.switch(state)
"""
def switch(self, state:bool):
"""This is not the Qt function but if an instrument is enabled, loaded to RAM and ready to
receive midi data.
Function will mimic Qt disabled behaviour by greying things out and deactivating individual
sub-widgets. But some, like the GUI switch itself, will always stay enabled."""
self.state = state
def _activityOff(self):
"""Called by a timer"""
if self.activeFlag:
self.activeFlag = False
self.setIcon(COLUMNS.index("name"), self.offIcon)
def toggleSwitchState(self):
self.switch(not self.state)
def activity(self):
"""Show midi note ons as flashing light."""
self.activeFlag = True
self.setIcon(COLUMNS.index("name"), self.onIcon)

24
qtgui/mainwindow.py

@ -70,22 +70,38 @@ class MainWindow(TemplateMainWindow):
self.ui.details_groupBox.setMinimumSize(1, 50)
self.ui.splitter.setSizes([1,1]) #just a forced update
self.setupMenu()
self.auditionerMidiInputComboController = AuditionerMidiInputComboController(parentMainWindow=self)
self.instrumentTreeController = InstrumentTreeController(parentMainWindow=self)
self.selectedInstrumentController = SelectedInstrumentController(parentMainWindow=self)
self.setupMenu()
self.start() #This shows the GUI, or not, depends on the NSM gui save setting. We need to call that after the menu, otherwise the about dialog will block and then we get new menu entries, which looks strange.
#Statusbar will show possible actions, such as "use scrollwheel to transpose"
#self.statusBar().showMessage(QtCore.QCoreApplication.translate("Statusbar", ""))
self.statusBar().showMessage("")
def setupMenu(self):
"""In its own function purely for readability"""
#New menu entries and template-menu overrides
#self.menu.connectMenuEntry("actionAbout", lambda: print("About Dialog Menu deactivated")) #deactivates the original function
self.menu.addMenuEntry("menuEdit", "actionNils", "Nils", lambda: print("Merle"))
self.menu.addMenuEntry("menuEdit", "actionLoadSamples", "Load all Instrument Samples", api.loadAllInstrumentSamples)
#self.menu.addMenuEntry("menuEdit", "actionNils", "Nils", lambda: print("Merle"))
self.menu.addMenuEntry("menuEdit", "actionLoadSamples", QtCore.QCoreApplication.translate("Menu", "Load all Instrument Samples (slow!)"), api.loadAllInstrumentSamples)
#self.menu.connectMenuEntry("actionNils", lambda: print("Override"))
self.menu.addSubmenu("menuView", QtCore.QCoreApplication.translate("Menu", "View"))
self.menu.addMenuEntry("menuView", "actionExpandAll", QtCore.QCoreApplication.translate("Menu", "Expand all Libraries"), lambda: self.instrumentTreeController.setAllExpanded(True))
self.menu.addMenuEntry("menuView", "actionCollapseAll", QtCore.QCoreApplication.translate("Menu", "Collapse all Libraries"), lambda: self.instrumentTreeController.setAllExpanded(False))
if "nestedView" in api.session.guiSharedDataToSave:
nested = api.session.guiSharedDataToSave["nestedView"]
else:
nested = True
self.menu.addMenuEntry("menuView", "actionFlatNested", QtCore.QCoreApplication.translate("Menu", "Nested Instrument List"), self.instrumentTreeController.toggleNestedFlat, checkable=True, startChecked=nested) #function receives check state as automatic parameter
self.menu.orderSubmenus(["menuFile", "menuEdit", "menuView", "menuHelp"])
def zoom(self, scaleFactor:float):
pass

41
qtgui/mixer.py

@ -0,0 +1,41 @@
#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net )
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ),
This application is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
#Standard Library Modules
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
#Our modules
import engine.api as api
class MixerController(object):
def __init__(self, parentMainWindow):
self.parentMainWindow = parentMainWindow
self.mixerAreaWidget = parentMainWindow.ui.mixerAreaWidget

2
template/calfbox/py/cbox.py

@ -1041,6 +1041,8 @@ class DocScene(DocObj):
def move_layer(self, old_pos, new_pos):
self.cmd("/move_layer", None, int(old_pos + 1), int(new_pos + 1))
#Layer positions are 0 for "append" and other positions are 1...n which need to be unique
def add_layer(self, aux, pos = None):
if pos is None:
return self.cmd_makeobj("/add_layer", 0, aux)

14
template/calfbox/sfzloader.c

@ -128,7 +128,7 @@ static gboolean load_sfz_group(struct sfz_parser_client *client)
static gboolean load_sfz_region(struct sfz_parser_client *client)
{
struct sfz_load_state *ls = client->user_data;
ls->target = ls->region = sampler_layer_new(ls->m, ls->program, ls->group);
// g_warning("-- start region");
return TRUE;
@ -154,7 +154,7 @@ static gboolean load_sfz_curve(struct sfz_parser_client *client)
static gboolean load_sfz_key_value(struct sfz_parser_client *client, const char *key, const char *value)
{
struct sfz_load_state *ls = client->user_data;
if (ls->section_type == slst_curve)
{
if (key[0] == 'v' && isdigit(key[1]))
@ -218,14 +218,14 @@ static gboolean load_sfz_key_value(struct sfz_parser_client *client, const char
g_warning("Unrecognized SFZ key in control section: %s", key);
return TRUE;
}
struct sampler_layer *l = ls->target;
if (!ls->target)
{
g_warning("Parameter '%s' entered outside of global, master, region or group", key);
return TRUE;
}
if (!sampler_layer_apply_param(l, key, value, ls->error))
return FALSE;
@ -275,7 +275,9 @@ gboolean sampler_module_load_program_sfz(struct sampler_module *m, struct sample
if (is_from_string)
status = load_sfz_from_string(sfz, strlen(sfz), &c, error);
else
status = load_sfz(sfz, prg->tarfile, &c, error);
{
status = load_sfz(sfz, prg->tarfile, &c, error); //Loads the audio files but also sets fields, like prg->sample_dir. After this we cannot modify any values anymore.
}
if (!status)
{
if (ls.region)
@ -284,7 +286,7 @@ gboolean sampler_module_load_program_sfz(struct sampler_module *m, struct sample
}
end_token(&c);
prg->all_layers = g_slist_reverse(prg->all_layers);
sampler_program_update_layers(prg);
return TRUE;

15
template/calfbox/sfzparser.c

@ -434,6 +434,11 @@ restore:
return ok;
}
/*
* This is not only called when literally constructing a sfz string
* but also when loading a null instrument e.g. to first create the jack ports and only later
* actually load the sfz and samples, which can be costly.
*/
gboolean load_sfz_from_string(const char *buf, int len, struct sfz_parser_client *c, GError **error)
{
struct sfz_parser_state s;
@ -449,13 +454,17 @@ gboolean load_sfz_from_string(const char *buf, int len, struct sfz_parser_client
return result;
}
/*
* Called once per sfz.
* Does not load samples, but only the sfz file.
*/
gboolean load_sfz_into_state(struct sfz_parser_state *s, const char *name)
{
g_clear_error(s->error);
FILE *f;
int len = -1;
if (s->tarfile)
{
{ //This only extracts the .sfz file itself and will not attempt to load any sample waveforms, eventhough cbox_tarfile_get_item_by_name will later be used to extract the sample as well.
struct cbox_taritem *item = cbox_tarfile_get_item_by_name(s->tarfile, name, TRUE);
if (!item)
{
@ -479,14 +488,14 @@ gboolean load_sfz_into_state(struct sfz_parser_state *s, const char *name)
g_set_error(s->error, G_FILE_ERROR, g_file_error_from_errno (errno), "Cannot open '%s'", name);
return FALSE;
}
if (len == -1)
{
fseek(f, 0, SEEK_END);
len = ftell(f);
fseek(f, 0, SEEK_SET);
}
unsigned char *buf = malloc(len + 1);
buf[len] = '\0';
if (fread(buf, 1, len, f) != (size_t)len)

72
template/calfbox/wavebank.c

@ -70,7 +70,7 @@ static void my_fft_main(complex float output[STD_WAVEFORM_FRAMES])
int invi = STD_WAVEFORM_BITS - i - 1;
int disp = 1 << i;
int mask = disp - 1;
for (int j = 0; j < STD_WAVEFORM_FRAMES / 2; j++)
{
int jj1 = (j & mask) + ((j & ~mask) << 1); // insert 0 at i'th bit to get the left arm of the butterfly
@ -93,9 +93,9 @@ static void my_fft_r2c(complex float output[STD_WAVEFORM_FRAMES], int16_t input[
// Copy + bit reversal addressing
for (int i = 0; i < STD_WAVEFORM_FRAMES; i++)
output[i] = input[map_table[i]] * (1.0 / STD_WAVEFORM_FRAMES);
my_fft_main(output);
}
static void my_ifft_c2r(int16_t output[STD_WAVEFORM_FRAMES], complex float input[STD_WAVEFORM_FRAMES])
@ -116,7 +116,7 @@ static void my_ifft_c2r(int16_t output[STD_WAVEFORM_FRAMES], complex float input
if (fabs(value) > maxv)
maxv = fabs(value);
output[i] = (int16_t)value;
}
}
}
struct wave_bank
@ -168,7 +168,7 @@ void cbox_waveform_generate_levels(struct cbox_waveform *waveform, int levels, d
complex float output[STD_WAVEFORM_FRAMES], bandlimited[STD_WAVEFORM_FRAMES];
my_fft_r2c(output, waveform->data);
int N = STD_WAVEFORM_FRAMES;
waveform->levels = calloc(levels, sizeof(struct cbox_waveform_level));
double rate = 65536.0 * 65536.0; // / waveform->info.frames;
double orig_rate = 65536.0 * 65536.0; // / waveform->info.frames;
@ -176,7 +176,7 @@ void cbox_waveform_generate_levels(struct cbox_waveform *waveform, int levels, d
{
int harmonics = N / 2 / (rate / orig_rate);
bandlimited[0] = 0;
if (harmonics > 0)
{
for (int j = 1; j <= harmonics; j++)
@ -187,7 +187,7 @@ void cbox_waveform_generate_levels(struct cbox_waveform *waveform, int levels, d
for (int j = harmonics; j <= N / 2; j++)
bandlimited[j] = bandlimited [N - j] = 0;
}
waveform->levels[i].data = calloc(N + MAX_INTERPOLATION_ORDER, sizeof(int16_t));
my_ifft_c2r(waveform->levels[i].data, bandlimited);
memcpy(waveform->levels[i].data + N, waveform->levels[i].data, MAX_INTERPOLATION_ORDER * sizeof(int16_t));
@ -204,11 +204,11 @@ void cbox_wavebank_add_std_waveform(const char *name, float (*getfunc)(float v,
for (int i = 0; i < nsize; i++)
{
float v = getfunc(i * 1.0 / nsize, user_data);
if (fabs(v) > 1)
if (fabs(v) > 1)
v = (v < 0) ? -1 : 1;
// cannot use full scale here, because bandlimiting will introduce
// some degree of overshoot
wave[i] = (int16_t)(25000 * v);
wave[i] = (int16_t)(25000 * v);
}
struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform));
waveform->data = wave;
@ -225,10 +225,10 @@ void cbox_wavebank_add_std_waveform(const char *name, float (*getfunc)(float v,
waveform->loop_end = nsize;
waveform->levels = NULL;
waveform->level_count = 0;
if (levels)
cbox_waveform_generate_levels(waveform, levels, 2);
g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform);
g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform);
bank.std_waveforms = g_slist_prepend(bank.std_waveforms, waveform);
@ -247,7 +247,7 @@ void cbox_wavebank_init()
bank.waveforms_by_id = g_hash_table_new(g_int_hash, g_int_equal);
bank.std_waveforms = NULL;
bank.streaming_prefetch_size = cbox_config_get_int("streaming", "prefetch_size", 65536);
cbox_wavebank_add_std_waveform("*sine", func_sine, NULL, 0);
// XXXKF this should not be a real waveform
cbox_wavebank_add_std_waveform("*silence", func_silence, NULL, 0);
@ -263,7 +263,7 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED, "%s: no filename specified", context_name);
return NULL;
}
// Built in waveforms don't go through path canonicalization
if (filename[0] == '*')
{
@ -275,7 +275,7 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
return waveform;
}
}
gchar *value_copy = g_strdup(filename);
for (int i = 0; value_copy[i]; i++)
{
@ -309,17 +309,35 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
{
g_free(pathname);
g_free(canonical);
struct cbox_waveform *waveform = value;
cbox_waveform_ref(waveform);
return waveform;
}
struct cbox_waveform *waveform = calloc(1, sizeof(struct cbox_waveform));
SNDFILE *sndfile = NULL;
struct cbox_taritem *taritem = NULL;
if (tarfile)
{
if (strcmp(sample_dir, ".") == 0)
{
//Potential path lookup problem:
//This is a sample without sfz default_path opcode inside a tar.
//We need to set the sample dir to the position of the .sfz file within the .tar
//because we also assume that the sample paths in regions are relative to the .sfz path.
//If the sfz is in the tar root this is a redundant action, but if the sfz is itself
//in a subdirectoy we need to adjust the path now.
// XXXNH sample_dir will not be updated in the struct itself and thus reported as "." in python etc.
//context_name is the sfz file, filename the sample file without leading ./ and sample_dir just a dot.
gchar *sfz_dir = g_path_get_dirname(context_name); //take the path of the sfz file...
pathname = g_build_filename(sfz_dir, filename, NULL); //... and prefix the sample filename with it.
g_free(sfz_dir);
}
taritem = cbox_tarfile_get_item_by_name(tarfile, pathname, TRUE);
if (taritem)
sndfile = cbox_tarfile_opensndfile(tarfile, taritem, &waveform->sndstream, &waveform->info);
@ -339,7 +357,7 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
uint32_t nshorts;
if (waveform->info.channels != 1 && waveform->info.channels != 2)
{
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED,
g_set_error(error, CBOX_WAVEFORM_ERROR, CBOX_WAVEFORM_ERROR_FAILED,
"%s: cannot open file '%s': unsupported channel count %d", context_name, pathname, (int)waveform->info.channels);
sf_close(sndfile);
free(canonical);
@ -364,7 +382,7 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
waveform->preloaded_frames = preloaded_frames;
waveform->tarfile = tarfile;
waveform->taritem = taritem;
if (sf_command(sndfile, SFC_GET_INSTRUMENT, &instrument, sizeof(SF_INSTRUMENT)))
{
for (int i = 0; i < instrument.loop_count; i++)
@ -389,7 +407,7 @@ struct cbox_waveform *cbox_wavebank_get_waveform(const char *context_name, struc
bank.maxbytes = bank.bytes;
g_hash_table_insert(bank.waveforms_by_name, waveform->canonical_name, waveform);
g_hash_table_insert(bank.waveforms_by_id, &waveform->id, waveform);
return waveform;
}
@ -419,10 +437,10 @@ void cbox_wavebank_foreach(void (*cb)(void *, struct cbox_waveform *), void *use
gpointer key, value;
g_hash_table_iter_init (&iter, bank.waveforms_by_id);
while (g_hash_table_iter_next (&iter, &key, &value))
while (g_hash_table_iter_next (&iter, &key, &value))
{
(*cb)(user_data, value);
}
}
}
void cbox_wavebank_close()
@ -451,7 +469,7 @@ void cbox_waveform_unref(struct cbox_waveform *waveform)
{
if (--waveform->refcount > 0)
return;
g_hash_table_remove(bank.waveforms_by_name, waveform->canonical_name);
g_hash_table_remove(bank.waveforms_by_id, &waveform->id);
bank.bytes -= waveform->bytes;
@ -463,7 +481,7 @@ void cbox_waveform_unref(struct cbox_waveform *waveform)
free(waveform->levels);
free(waveform->data);
free(waveform);
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@ -478,7 +496,7 @@ struct waves_foreach_data
void wave_list_cb(void *user_data, struct cbox_waveform *waveform)
{
struct waves_foreach_data *wfd = user_data;
wfd->success = wfd->success && cbox_execute_on(wfd->fb, NULL, "/waveform", "i", wfd->error, (int)waveform->id);
}
@ -488,7 +506,7 @@ static gboolean waves_process_cmd(struct cbox_command_target *ct, struct cbox_co
{
if (!cbox_check_fb_channel(fb, cmd->command, error))
return FALSE;
// XXXKF this only supports 4GB - not a big deal for now yet?
return cbox_execute_on(fb, NULL, "/bytes", "i", error, (int)cbox_wavebank_get_bytes()) &&
cbox_execute_on(fb, NULL, "/max_bytes", "i", error, (int)cbox_wavebank_get_maxbytes()) &&
@ -499,7 +517,7 @@ static gboolean waves_process_cmd(struct cbox_command_target *ct, struct cbox_co
{
if (!cbox_check_fb_channel(fb, cmd->command, error))
return FALSE;
struct waves_foreach_data wfd = { fb, error, TRUE };
cbox_wavebank_foreach(wave_list_cb, &wfd);
return wfd.success;
@ -508,7 +526,7 @@ static gboolean waves_process_cmd(struct cbox_command_target *ct, struct cbox_co
{
if (!cbox_check_fb_channel(fb, cmd->command, error))
return FALSE;
int id = CBOX_ARG_I(cmd, 0);
struct cbox_waveform *waveform = cbox_wavebank_peek_waveform_by_id(id);
if (waveform == NULL)

8
template/engine/input_midi.py

@ -104,6 +104,10 @@ class MidiInput(object):
class MidiProcessor(object):
"""
The parameter parentInput MUST be a an object that has the attribute "cboxMidiPortUid" of
type cbox.JackIO.create_midi_input(portName)
There are two principal modes: Step Entry and Live Recording.
Add your function to callbacks3 for notes and CC or callbacks2 for program change and Channel Pressure.
@ -129,6 +133,10 @@ class MidiProcessor(object):
def __init__(self, parentInput):
self.parentInput = parentInput
if not hasattr(parentInput, "cboxMidiPortUid"):
raise ValueError("argument 'parentInput' must be an object with the attribute cboxMidiPortUid, returned from cbox.JackIO.create_midi_input(portName)")
self.callbacks3 = {} #keys are tuples
self.callbacks2 = {} #keys are tuples
self.active = True

11
template/qtgui/helper.py

@ -214,10 +214,11 @@ class ToggleSwitch(QtWidgets.QAbstractButton):
self._offset = self._base_offset
palette = self.palette()
if self._thumb_radius > self._track_radius:
self._track_color = {
True: palette.highlight(),
False: palette.dark(),
False: palette.mid(),
}
self._thumb_color = {
True: palette.highlight(),
@ -225,7 +226,7 @@ class ToggleSwitch(QtWidgets.QAbstractButton):
}
self._text_color = {
True: palette.highlightedText().color(),
False: palette.dark().color(),
False: palette.mid().color(),
}
self._thumb_text = {
True: '',
@ -239,17 +240,17 @@ class ToggleSwitch(QtWidgets.QAbstractButton):
}
self._track_color = {
True: palette.highlight(),
False: palette.dark(),
False: palette.mid(),
}
self._text_color = {
True: palette.highlight().color(),
False: palette.dark().color(),
False: palette.mid().color(),
}
self._thumb_text = {
True: '',
False: '',
}
self._track_opacity = 1
self._track_opacity = 0.7
@QtCore.pyqtProperty(int)
def offset(self):

3
template/qtgui/menu.py

@ -213,7 +213,7 @@ class Menu(object):
menuAction = getattr(self.ui, submenu).menuAction()
raise NotImplementedError #TODO
def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False):
def addMenuEntry(self, submenu, actionAsString:str, text:str, connectedFunction=None, shortcut:str="", tooltip:str="", iconResource:str="", checkable=False, startChecked=False):
"""
parameterstrings must already be translated.
@ -250,6 +250,7 @@ class Menu(object):
action.setIcon(ico)
if checkable:
action.setCheckable(True)
action.setChecked(startChecked)
self.connectMenuEntry(actionAsString, connectedFunction, shortcut)

Loading…
Cancel
Save