Nils
4 years ago
11 changed files with 861 additions and 16151 deletions
@ -0,0 +1,143 @@ |
|||||
|
#! /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; logger = logging.getLogger(__name__); logger.info("import") |
||||
|
|
||||
|
#Python Standard Lib |
||||
|
|
||||
|
#Third Party |
||||
|
from calfbox import cbox |
||||
|
|
||||
|
#Template Modules |
||||
|
|
||||
|
class Auditioner(object): |
||||
|
""" |
||||
|
A special instrument class. |
||||
|
|
||||
|
Has access to all libraries and can quickly change its sounds. |
||||
|
It has one midi input and stereo output. |
||||
|
Music output does not contribute to global mixer summing ports. |
||||
|
|
||||
|
This is another instance of the instrument library. It will not touch any CC settings |
||||
|
or filters of the real instrument, because there is a chance that this will change the sound |
||||
|
of the real instrument by accident. |
||||
|
""" |
||||
|
|
||||
|
def __init__(self, parentData): |
||||
|
self.parentData = parentData |
||||
|
self.cboxMidiPortUid = None |
||||
|
self.midiInputPortName = "Auditioner" |
||||
|
self.cboxPortname = cbox.JackIO.status().client_name + ":" + self.midiInputPortName |
||||
|
|
||||
|
#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() |
||||
|
if self.parentData.parentSession.standaloneMode: |
||||
|
jackAudioOutLeft = cbox.JackIO.create_audio_output(self.midiInputPortName+"_L", "#1") #add "#1" as second parameter for auto-connection to system out 1 |
||||
|
jackAudioOutRight = cbox.JackIO.create_audio_output(self.midiInputPortName+"_R", "#2") #add "#1" as second parameter for auto-connection to system out 2 |
||||
|
else: |
||||
|
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? |
||||
|
|
||||
|
#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) |
||||
|
|
||||
|
def exportStatus(self): |
||||
|
"""The call-often function to get the instrument status. Includes only data that can |
||||
|
actually change during runtime.""" |
||||
|
result = {} |
||||
|
|
||||
|
#Static ids |
||||
|
result["id"] = self.metadata["id"] |
||||
|
result["id-key"] = self.idKey #redundancy for convenience. |
||||
|
|
||||
|
#Dynamic data |
||||
|
result["currentVariant"] = self.currentVariant # str |
||||
|
result["state"] = self.enabled #bool |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
def loadInstrument(self, tarFilePath, variantSfzFileName:str): |
||||
|
"""load_patch_from_tar is blocking. This function will return when the instrument is ready |
||||
|
to play. |
||||
|
|
||||
|
The function will do nothing when the instrument is not enabled. |
||||
|
""" |
||||
|
logger.info(f"Start loading samples for auditioner {variantSfzFileName}") |
||||
|
#help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc. |
||||
|
#newProgramNumber = self.instrumentLayer.engine.get_unused_program() |
||||
|
programNumber = 1 |
||||
|
name = variantSfzFileName |
||||
|
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, tarFilePath, variantSfzFileName, name) |
||||
|
self.instrumentLayer.engine.set_patch(1, programNumber) #1 is the channel, counting from 1. #TODO: we want this to be on all channels. |
||||
|
self.currentVariant = variantSfzFileName |
||||
|
logger.info(f"Finished loading samples for auditioner {variantSfzFileName}") |
||||
|
|
||||
|
|
||||
|
def getAvailablePorts(self)->dict: |
||||
|
"""This function queries JACK each time it is called. |
||||
|
It returns a dict with two lists. |
||||
|
Keys "hardware" and "software" for the type of port. |
||||
|
""" |
||||
|
result = {} |
||||
|
hardware = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE | cbox.JackIO.PORT_IS_PHYSICAL)) |
||||
|
allPorts = set(cbox.JackIO.get_ports(".*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SOURCE)) |
||||
|
software = allPorts.difference(hardware) |
||||
|
result["hardware"] = sorted(list(hardware)) |
||||
|
result["software"] = sorted(list(software)) |
||||
|
return result |
||||
|
|
||||
|
|
||||
|
def connectMidiInputPort(self, externalPort:str): |
||||
|
"""externalPort is in the Client:Port JACK format |
||||
|
If "" False or None disconnect all ports.""" |
||||
|
|
||||
|
try: |
||||
|
currentConnectedList = cbox.JackIO.get_connected_ports(self.cboxMidiPortUid) |
||||
|
except: #port not found. |
||||
|
currentConnectedList = [] |
||||
|
|
||||
|
for port in currentConnectedList: |
||||
|
cbox.JackIO.port_disconnect(port, self.cboxPortname) |
||||
|
|
||||
|
if externalPort: |
||||
|
availablePorts = self.getAvailablePorts() |
||||
|
if not (externalPort in availablePorts["hardware"] or externalPort in availablePorts["software"]): |
||||
|
raise RuntimeError(f"Auditioner was instructed to connect to port {externalPort}, which does not exist") |
||||
|
|
||||
|
cbox.JackIO.port_connect(externalPort, self.cboxPortname) |
@ -0,0 +1,214 @@ |
|||||
|
#! /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; logger = logging.getLogger(__name__); logger.info("import") |
||||
|
|
||||
|
#Python Standard Lib |
||||
|
|
||||
|
#Third Party |
||||
|
from calfbox import cbox |
||||
|
|
||||
|
#Template Modules |
||||
|
|
||||
|
class Instrument(object): |
||||
|
"""Literally one instrument. |
||||
|
|
||||
|
It might exists in different versions that are all loaded here and can be switched in the GUI. |
||||
|
For that we identify different .sfz files by a minor version number (see below). |
||||
|
|
||||
|
All data is provided by the parsed metadata dict, except the filepath of the tar which calfbox |
||||
|
needs again here. |
||||
|
The metadata dict contains a list of all available sfz files describing variants of the same file, |
||||
|
eg. sharing most of the sample data. The variants filename will be used directly as "preset" |
||||
|
name in a GUI etc. |
||||
|
The variants are case sensitive filenames ending in .sfz |
||||
|
|
||||
|
The order of variants in the config file will never change, it can only get appended. |
||||
|
This way indexing will remain consistent over time. |
||||
|
|
||||
|
The default variant after the first start (no save file) is the a special entry in metadata. |
||||
|
It can change with new versions, so new projects will start with the newer file. |
||||
|
|
||||
|
Examples: |
||||
|
SalamanderPiano1.2.sfz |
||||
|
SalamanderPiano1.3.sfz |
||||
|
SalamanderPiano1.6.sfz |
||||
|
|
||||
|
Here we have versions 1.2, 1.3 and 1.6. 4 and 5 were never released. A dropdown in a GUI |
||||
|
would show these four entries. |
||||
|
|
||||
|
Patches are differentiated by the MINOR version as int. MINOR versions slightly change the sound. |
||||
|
Typical reasons are retuning, filter changes etc. |
||||
|
The chosen MINOR version stays active until changed by the user. All MINOR versions variant of |
||||
|
an instrument must be available in all future file-releases. |
||||
|
|
||||
|
PATCH version levels are just increased, as they are defined to not change the sound outcome. |
||||
|
For example they fix obvious bugs nobody could have wanted, extend the range of an instrument |
||||
|
or introduce new CC controlers for parameters previously not available. |
||||
|
PATCH versions are automatically upgraded. You cannot go back programatically. |
||||
|
The PATCH number is not included in the sfz file name, while major and minor are. |
||||
|
|
||||
|
A MAJOR version must be an entirely different file. These are incompatible with older versions. |
||||
|
For example they use a different control scheme (different CC maps) |
||||
|
|
||||
|
Besides version there is also the option to just name the sfz file anything you want, as a |
||||
|
special variant. Which is problematic: |
||||
|
What constitues as "Instrument Variant" and what as "New Instrument" must be decided on a case |
||||
|
by case basis. For example a different piano than the salamander is surely a new instrument. |
||||
|
But putting a blanket over the strings (prepared piano) to muffle the sound is the same physical |
||||
|
instrument, but is this a variant? Different microphone or mic position maybe? |
||||
|
This is mostly important when we are not talking about upgrades, which can just use version |
||||
|
numbers, but true "side-grades". I guess the main argument is that you never would want both |
||||
|
variants at the same time. And even if the muffled blanket sound is the same instrument, |
||||
|
this could be integrated as CC switch or fader. Same for the different microphones and positions. |
||||
|
At the time of writing the author was not able to come up with a "sidegrade" usecase, |
||||
|
that isn't either an sfz controller or a different instrument (I personally consider the |
||||
|
blanket-piano a different instrument)""" |
||||
|
|
||||
|
allInstruments = {} # (libraryId, instrumentId):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"]) |
||||
|
Instrument.allInstruments[self.idKey] = self |
||||
|
|
||||
|
self.cboxMidiPortUid = None |
||||
|
|
||||
|
self.metadata = metadata #parsed from the ini file. See self.exportMetadata for the pythonic dict |
||||
|
self.tarFilePath = tarFilePath |
||||
|
self.name = metadata["name"] |
||||
|
self.midiInputPortName = metadata["name"] |
||||
|
self.variants = metadata["variants"].split(",") |
||||
|
self.defaultVariant = metadata["defaultVariant"] |
||||
|
self.enabled = True #means loaded. |
||||
|
|
||||
|
|
||||
|
#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) |
||||
|
|
||||
|
#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): |
||||
|
"""The call-often function to get the instrument status. Includes only data that can |
||||
|
actually change during runtime.""" |
||||
|
result = {} |
||||
|
|
||||
|
#Static ids |
||||
|
result["id"] = self.metadata["id"] |
||||
|
result["id-key"] = self.idKey #redundancy for convenience. |
||||
|
|
||||
|
#Dynamic data |
||||
|
result["currentVariant"] = self.currentVariant # str |
||||
|
result["state"] = self.enabled #bool |
||||
|
|
||||
|
return result |
||||
|
|
||||
|
|
||||
|
def exportMetadata(self): |
||||
|
"""This gets called before the samples are loaded. |
||||
|
Only static data, that does not get changed during runtime, is included here. |
||||
|
|
||||
|
Please note that we don't add the default variant here. It is only important for the |
||||
|
external world to know what the current variant is. Which is handled by self.exportStatus() |
||||
|
""" |
||||
|
result = {} |
||||
|
result["id"] = self.metadata["id"] #int |
||||
|
result["id-key"] = self.idKey # tuple (int, int) redundancy for convenience. |
||||
|
result["name"] = self.metadata["name"] #str |
||||
|
result["variants"] = self.variants #list of str |
||||
|
result["defaultVariant"] = self.metadata["defaultVariant"] #str |
||||
|
result["tags"] = self.metadata["tags"].split(",") # list of str |
||||
|
|
||||
|
#Optional Tags. |
||||
|
result["license"] = self.metadata["license"] if "license" in self.metadata else "" #str |
||||
|
result["vendor"] = self.metadata["vendor"] if "vendor" in self.metadata else "" #str |
||||
|
result["group"] = self.metadata["group"] if "group" in self.metadata else "" #str |
||||
|
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""" |
||||
|
if self.startVariantSfzFilename: |
||||
|
self.chooseVariant(self.startVariantSfzFilename) |
||||
|
else: |
||||
|
self.chooseVariant(self.metadata["defaultVariant"]) |
||||
|
|
||||
|
def chooseVariant(self, variantSfzFileName:str): |
||||
|
"""load_patch_from_tar is blocking. This function will return when the instrument is ready |
||||
|
to play. |
||||
|
|
||||
|
The function will do nothing when the instrument is not enabled. |
||||
|
""" |
||||
|
|
||||
|
if not self.enabled: |
||||
|
return |
||||
|
|
||||
|
if not variantSfzFileName in self.metadata["variants"]: |
||||
|
raise ValueError("Variant not in list: {} {}".format(variantSfzFileName, self.metadata["variants"])) |
||||
|
|
||||
|
logger.info(f"Start loading samples for instrument {variantSfzFileName} with id key {self.idKey}") |
||||
|
#help (self.allInstrumentLayers[self.defaultPortUid].engine) #shows the functions to load programs into channels etc. |
||||
|
#newProgramNumber = self.instrumentLayer.engine.get_unused_program() |
||||
|
programNumber = self.metadata["variants"].index(variantSfzFileName) #counts from 1 |
||||
|
self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, self.tarFilePath, variantSfzFileName, self.metadata["name"]) |
||||
|
self.instrumentLayer.engine.set_patch(1, programNumber) #1 is the channel, counting from 1. #TODO: we want this to be on all channels. |
||||
|
self.currentVariant = variantSfzFileName |
||||
|
logger.info(f"Finished loading samples for instrument {variantSfzFileName} with id key {self.idKey}") |
||||
|
|
||||
|
def enable(self): |
||||
|
self.enabled = True |
||||
|
|
||||
|
def disable(self): |
||||
|
"""Jack midi port and audio ports will disappear. Impact on RAM and CPU usage unknown.""" |
||||
|
self.enabled = False |
File diff suppressed because it is too large
Loading…
Reference in new issue