#! /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
#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 )