Sampled Instrument Player with static and monolithic design. All instruments are built-in.
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.

170 lines
8.1 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
#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)