You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
8.4 KiB
175 lines
8.4 KiB
#! /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 <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
|
|
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()
|
|
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")
|
|
|
|
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:
|
|
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:
|
|
self.parentData.instrumentMidiNoteOffActivity(self.idKey, pitch, velocity)
|
|
|