#! /usr/bin/env python3 # -*- coding: utf-8 -*- """ Copyright 2022, 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 #Template Modules from template.calfbox import cbox from template.engine.input_midi import MidiProcessor 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 self.idKey = None #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") 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(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) #Always on self.midiProcessor = MidiProcessor(parentInput = self) #works through self.cboxMidiPortUid self.midiProcessor.register_NoteOn(self.triggerNoteOnCallback) self.midiProcessor.register_NoteOff(self.triggerNoteOffCallback) #self.midiProcessor.notePrinter(True) self.parentData.parentSession.eventLoop.fastConnect(self.midiProcessor.processEvents) @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 loadInstrument(self, idKey, tarFilePath, rootPrefixPath:str, variantSfzFileName:str, keySwitchMidiPitch:int): """load_patch_from_tar is blocking. This function will return when the instrument is ready to play. """ 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 = 0 name = variantSfzFileName self.program = self.instrumentLayer.engine.load_patch_from_tar(programNumber, tarFilePath, rootPrefixPath+variantSfzFileName, name) self.currentVariant = variantSfzFileName self.idKey = idKey if keySwitchMidiPitch is None: self.currentKeySwitch = None else: self.currentKeySwitch = keySwitchMidiPitch self.scene.send_midi_event(0x90, keySwitchMidiPitch, 64) logger.info(f"Finished loading samples for auditioner {variantSfzFileName}") def unloadInstrument(self): """Unlike instruments disable this will not remove the midi and audio ports. But it will remove the loaded instruments, if any, from ram""" self.scene.status().layers[0].get_instrument().engine.load_patch_from_string(0, "", "", "") #fill with null instruments, hopefully replacing the loaded sfz data. self.currentVariant = None self.currentKeySwitch = None self.idKey = None 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) def triggerNoteOnCallback(self, timestamp, channel, pitch, velocity): """args are: timestamp, channel, note, velocity. consider to change eventloop.slowConnect to fastConnect. And also disconnect in self.disable()""" if self.idKey and self.instrumentLayer: self.parentData.instrumentMidiNoteOnActivity(self.idKey, pitch, velocity) def triggerNoteOffCallback(self, timestamp, channel, pitch, velocity): """args are: timestamp, channel, note, velocity. consider to change eventloop.slowConnect to fastConnect. And also disconnect in self.disable()""" if self.idKey and self.instrumentLayer: self.parentData.instrumentMidiNoteOffActivity(self.idKey, pitch, velocity)