diff --git a/engine/api.py b/engine/api.py
index e07f5bb..0bdcc85 100644
--- a/engine/api.py
+++ b/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()
diff --git a/engine/auditioner.py b/engine/auditioner.py
index 1443cb5..a7c6598 100644
--- a/engine/auditioner.py
+++ b/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."""
diff --git a/engine/instrument.py b/engine/instrument.py
index de3b9fa..0e97dab 100644
--- a/engine/instrument.py
+++ b/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
diff --git a/engine/main.py b/engine/main.py
index cadd928..aeb2663 100644
--- a/engine/main.py
+++ b/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()]
+ }
diff --git a/qtgui/auditioner.py b/qtgui/auditioner.py
index e241bf7..8f5d2a1 100644
--- a/qtgui/auditioner.py
+++ b/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())
diff --git a/qtgui/designer/mainwindow.bak b/qtgui/designer/mainwindow.bak
new file mode 100644
index 0000000..3476e0b
--- /dev/null
+++ b/qtgui/designer/mainwindow.bak
@@ -0,0 +1,247 @@
+
+
+ MainWindow
+
+
+
+ 0
+ 0
+ 1087
+ 752
+
+
+
+ MainWindow
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ Search
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
+
+
+
+ Qt::Vertical
+
+
+
+ true
+
+
+ Instruments
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
-
+
+
+ Auditioner MIDI Input
+
+
+
+ -
+
+
+ QComboBox::AdjustToContents
+
+
+
+ -
+
+
+ TextLabel
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QAbstractItemView::NoEditTriggers
+
+
+ false
+
+
+ true
+
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+
+
+
+
+
+
+ ID
+
+
+
+
+ Name
+
+
+
+
+ Variant
+
+
+
+
+ Tags
+
+
+
+
+
+
+
+
+ NamePlaceholder
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+ true
+
+
+
+
+ 0
+ 0
+ 598
+ 158
+
+
+
+
-
+
+
+ Variants
+
+
+
+ -
+
+
+ -
+
+
+ TextLabel
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/qtgui/designer/mainwindow.py b/qtgui/designer/mainwindow.py
index 1851bb7..8ff6d77 100644
--- a/qtgui/designer/mainwindow.py
+++ b/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"))
diff --git a/qtgui/designer/mainwindow.ui b/qtgui/designer/mainwindow.ui
index 3476e0b..df0fce4 100644
--- a/qtgui/designer/mainwindow.ui
+++ b/qtgui/designer/mainwindow.ui
@@ -49,117 +49,255 @@
Qt::Vertical
-
+
true
-
- Instruments
+
+ 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
-
- 0
-
- -
-
-
-
-
-
-
- Auditioner MIDI Input
-
-
-
- -
-
-
- QComboBox::AdjustToContents
-
-
-
- -
-
-
- TextLabel
-
-
-
- -
-
-
- Qt::Horizontal
-
-
-
- 40
- 20
-
-
-
-
-
-
-
- -
-
-
- QAbstractItemView::NoEditTriggers
-
-
- false
-
-
- true
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::ScrollPerPixel
-
-
- QAbstractItemView::ScrollPerPixel
-
-
-
-
+
+
+ Instruments
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
-
+
+
+
-
+
+
+
+ 32
+ 32
+
+
+
+
+ 3
+ 0
+
+
+
+ -40
+
+
+ 0
+
+
+ 3
+
+
+ -3
+
+
+ false
+
+
+ 3.000000000000000
+
+
+ true
+
+
+
+ -
+
+
+ Auditioner MIDI Input
+
+
+
+ -
+
+
+ QComboBox::AdjustToContents
+
+
+
+ -
+
+
+ TextLabel
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+ -
+
+
+ QAbstractItemView::NoEditTriggers
-
-
-
- ID
+
+ false
-
-
-
- Name
+
+ true
-
-
-
- Variant
+
+ QAbstractItemView::SingleSelection
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+
+
+
+
+
+
+ ID
+
+
+
+
+ Name
+
+
+
+
+ Variant
+
+
+
+
+ Tags
+
+
+
+
+
+
+
+
+ Mixer
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+ 0
+
-
-
- Tags
+ This mixer controls the amount of each loaded instrument in the optional mixer output-ports. Idividual instrument outputs are unaffected.
-
-
-
-
+
+ true
+
+
+
+ -
+
+
+ true
+
+
+ Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop
+
+
+
+
+ 0
+ 0
+ 599
+ 447
+
+
+
+
-
+
+
+ 24
+
+
+ Qt::Vertical
+
+
+ QProgressBar::TopToBottom
+
+
+
+ -
+
+
+ 24
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ Qt::Horizontal
+
+
+
+ 40
+ 20
+
+
+
+
+
+
+
+
+
+
@@ -191,8 +329,8 @@
0
0
- 598
- 158
+ 597
+ 155
diff --git a/qtgui/instrument.py b/qtgui/instrument.py
index 3072198..f7f711f 100644
--- a/qtgui/instrument.py
+++ b/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)
diff --git a/qtgui/mainwindow.py b/qtgui/mainwindow.py
index 7e1213e..2baeaee 100644
--- a/qtgui/mainwindow.py
+++ b/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
diff --git a/qtgui/mixer.py b/qtgui/mixer.py
new file mode 100644
index 0000000..e91914b
--- /dev/null
+++ b/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 .
+"""
+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
+
+
+
diff --git a/template/calfbox/py/cbox.py b/template/calfbox/py/cbox.py
index ac93bc6..3684ffe 100644
--- a/template/calfbox/py/cbox.py
+++ b/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)
diff --git a/template/calfbox/sfzloader.c b/template/calfbox/sfzloader.c
index 8b64bbc..0375c77 100644
--- a/template/calfbox/sfzloader.c
+++ b/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;
diff --git a/template/calfbox/sfzparser.c b/template/calfbox/sfzparser.c
index 7515ee2..c848b05 100644
--- a/template/calfbox/sfzparser.c
+++ b/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)
diff --git a/template/calfbox/wavebank.c b/template/calfbox/wavebank.c
index 1b59351..de48e29 100644
--- a/template/calfbox/wavebank.c
+++ b/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)
diff --git a/template/engine/input_midi.py b/template/engine/input_midi.py
index c979977..fb46a39 100644
--- a/template/engine/input_midi.py
+++ b/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
diff --git a/template/qtgui/helper.py b/template/qtgui/helper.py
index 410c1e1..b04c111 100644
--- a/template/qtgui/helper.py
+++ b/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):
diff --git a/template/qtgui/menu.py b/template/qtgui/menu.py
index bf6af9e..5aaa217 100644
--- a/template/qtgui/menu.py
+++ b/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)