#! /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; 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, rootPrefixPath:str, 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, rootPrefixPath+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)