Browse Source

New generic midi in quick connector widget

master
Nils 1 year ago
parent
commit
225945dce2
  1. 9
      template/engine/api.py
  2. 11
      template/engine/data.py
  3. 21
      template/engine/sampler_sf2.py
  4. 146
      template/qtgui/midiinquickwidget.py

9
template/engine/api.py

@ -271,6 +271,7 @@ class Callbacks(object):
func(channel, exportDict)
def _ignoreProgramChangesChanged(self):
#TODO: this only checks the first layer. There might be a second one as well. fine.jpg
state = session.data.midiInput.scene.status().layers[0].status().ignore_program_changes
for func in self.ignoreProgramChangesChanged:
func(state)
@ -280,6 +281,7 @@ class Callbacks(object):
for func in self.channelActivity:
func(channel)
def startEngine(nsmClient):
"""
This function gets called after initializing the GUI, calfbox
@ -342,6 +344,8 @@ def getUndoLists():
#undoHistory, redoHistory = session.history.asList()
return session.history.asList()
def getMidiInputNameAndUuid():
return session.data.getMidiInputNameAndUuid() #tuple name:str, uuid
#Calfbox Sequencer Controls
def playbackStatus()->bool:
@ -443,8 +447,9 @@ def loadSoundfont(filePath):
def setIgnoreProgramAndBankChanges(state):
state = bool(state)
#there is no session wrapper function. we use cbox directly. Save file and callbacks will fetch the current value on its own
session.data.midiInput.scene.status().layers[0].set_ignore_program_changes(state)
assert session.data.midiInput.scene.status().layers[0].status().ignore_program_changes == state
for layer in session.data.midiInput.scene.status().layers: #midi in[0] and real time midi thru[1]
layer.set_ignore_program_changes(state)
assert layer.status().ignore_program_changes == state
callbacks._ignoreProgramChangesChanged()
callbacks._dataChanged()

11
template/engine/data.py

@ -36,6 +36,17 @@ class Data(object):
def updateJackMetadataSorting(self):
pass
def getMidiInputNameAndUuid(self):
"""
Return name and cboxMidiPortUid.
name is Client:Port JACK format
Reimplement this in your actual data classes like sf2_sampler and sfz_sampler and
sequencers. Used by the quick connect midi input widget.
If double None as return the widget in the GUI might hide and deactivate itself."""
return None, None
#Save / Load / Export
def serialize(self)->dict:
return {

21
template/engine/sampler_sf2.py

@ -54,7 +54,8 @@ class Sampler_sf2(Data):
self.buffer_ignoreProgramChanges = ignoreProgramChanges
self.midiInput = MidiInput(session=parentSession, portName="all")
cbox.JackIO.Metadata.set_port_order(f"{cbox.JackIO.status().client_name}:all", 0) #first one. The other port order later start at 1 because we loop 1-based for channels.
self.midiInputJackName = f"{cbox.JackIO.status().client_name}:all"
cbox.JackIO.Metadata.set_port_order(self.midiInputJackName, 0) #first one. The other port order later start at 1 because we loop 1-based for channels.
#Set libfluidsynth! to 16 output pairs. We prepared 32 jack ports in the session start. "soundfont" is our given name, in the line below. This is a prepared config which will be looked up by add_new_instrument
cbox.Config.set("instrument:soundfont", "engine", "fluidsynth")
@ -112,7 +113,9 @@ class Sampler_sf2(Data):
logger.error(e)
#If demanded create 16 routing midi channels, basically a jack port -> midi channel merger.
if True:
#TODO: This is not working. Better leave that to JACK itself and an outside program for now.
#Also deactivated the pretty name metadata update call in updateChannelMidiInJackMetadaPrettyname. It is called by an api callback and by our "all" function
if False:
for midiRouterPortNum in range(1,17):
portName = f"channel_{str(midiRouterPortNum).zfill(2)}"
router_cboxMidiPortUid = cbox.JackIO.create_midi_input(portName)
@ -289,6 +292,7 @@ class Sampler_sf2(Data):
def updateChannelMidiInJackMetadaPrettyname(self, channel:int):
"""Midi in single channels 1-16"""
return #TODO: Until this works.
bank, program, name = self.activePatches()[channel] #activePatches returns a dict, not a list. channel is a 1-based key, not a 0-based index.
portName = f"channel_{str(channel).zfill(2)}"
@ -326,6 +330,19 @@ class Sampler_sf2(Data):
cbox.JackIO.Metadata.set_pretty_name(portnameR, f"{str(channel).zfill(2)}-R : {name}")
"""
def getMidiInputNameAndUuid(self):
"""
Return name and cboxMidiPortUid.
name is Client:Port JACK format
Reimplement this in your actual data classes like sf2_sampler and sfz_sampler and
sequencers. Used by the quick connect midi input widget.
If double None as return the widget in the GUI might hide and deactivate itself."""
return self.midiInputJackName, self.midiInput.cboxMidiPortUid
#Save / Load / Export
def serialize(self)->dict:
self.unlinkUnusedSoundfonts() #We do not want them to be in the save file.

146
template/qtgui/midiinquickwidget.py

@ -0,0 +1,146 @@
#! /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 <http://www.gnu.org/licenses/>.
"""
import logging; logging.info("import {}".format(__file__))
#Standard Library Modules
#Third Party Modules
from PyQt5 import QtWidgets, QtCore, QtGui
from calfbox import cbox
#Template Modules
#Our modules
import engine.api as api
class QuickMidiInputComboController(QtWidgets.QWidget):
"""This widget breaks a bit with the convention of engine/gui. However, it is a pure convenience
fire-and-forget function with no callbacks."""
def __init__(self, parentWidget):
super().__init__(parentWidget)
self.parentWidget = parentWidget
self.layout = QtWidgets.QHBoxLayout()
self.setLayout(self.layout)
self.comboBox = QtWidgets.QComboBox(self)
self.layout.addWidget(self.comboBox)
"""
self.volumeDial = parentWidget.ui.auditionerVolumeDial
self.volumeDial.setMaximum(0)
self.volumeDial.setMinimum(-21)
self.volumeDial.valueChanged.connect(self._sendVolumeChangeToEngine)
self.volumeDial.enterEvent = lambda ev: self.parentWidget.statusBar().showMessage((QtCore.QCoreApplication.translate("Auditioner", "Use mousewheel to change the Auditioner Volume")))
self.volumeDial.leaveEvent = lambda ev: self.parentWidget.statusBar().showMessage("")
self.wholePanel = parentWidget.ui.auditionerWidget
"""
self.label = QtWidgets.QLabel(self)
self.label.setText(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "Midi Input. Use JACK to connect multiple inputs."))
self.layout.addWidget(self.label)
#if not api.isStandaloneMode():
#self.wholePanel.hide()
#return
##self.wholePanel.show() #explicit is better than implicit
self.originalShowPopup = self.comboBox.showPopup
self.comboBox.showPopup = self.showPopup
self.comboBox.activated.connect(self._newPortChosen)
self.cboxPortname, self.cboxMidiPortUid = api.getMidiInputNameAndUuid() #these don't change during runtime.
self.layout.addStretch()
#api.callbacks.startLoadingAuditionerInstrument.append(self.callback_startLoadingAuditionerInstrument)
#api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged)
#api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged)
def callback_startLoadingAuditionerInstrument(self, idkey):
self.parentWidget.qtApp.setOverrideCursor(QtCore.Qt.WaitCursor) #reset in self.callback_auditionerInstrumentChanged
self.label.setText(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "…loading…"))
self.parentWidget.qtApp.processEvents() #actually show the label and cursor
def callback_auditionerInstrumentChanged(self, exportMetadata:dict):
self.parentWidget.qtApp.restoreOverrideCursor() #We assume the cursor was set to a loading animation
key = exportMetadata["id-key"]
t = f"➜ [{key[0]}-{key[1]}] {exportMetadata['name']}"
self.label.setText(t)
def callback__auditionerVolumeChanged(self, value:float):
self.volumeDial.setValue(value)
self.parentWidget.statusBar().showMessage(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "Volume: {}").format(value))
def _sendVolumeChangeToEngine(self, newValue):
self.volumeDial.blockSignals(True)
api.setAuditionerVolume(newValue)
self.volumeDial.blockSignals(False)
def _newPortChosen(self, index:int):
assert self.comboBox.currentIndex() == index
self.connectMidiInputPort(self.comboBox.currentText())
def showPopup(self):
"""When the combobox is opened quickly update the port list before showing it"""
self._fill()
self.originalShowPopup()
def _fill(self):
self.comboBox.clear()
self.comboBox.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents)
availablePorts = self.getAvailablePorts()
self.comboBox.addItem("") # Not only a more visible seaparator than the Qt one, but also doubles as "disconnect"
self.comboBox.addItems(availablePorts["hardware"])
#self.comboBox.insertSeparator(len(availablePorts["hardware"])+1)
self.comboBox.addItem("") # Not only a more visible seaparator than the Qt one, but also doubles as "disconnect"
self.comboBox.addItems(availablePorts["software"])
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"QuickMidiInput was instructed to connect to port {externalPort}, which does not exist")
cbox.JackIO.port_connect(externalPort, self.cboxPortname)
Loading…
Cancel
Save