Browse Source

Use midi input as cursor pitch when step midi is not active

master
Nils 4 months ago
parent
commit
3b7c3ecccc
  1. 2
      CHANGELOG
  2. 29
      engine/api.py
  3. 3
      engine/cursor.py
  4. 12
      engine/main.py
  5. 47
      engine/midiinput/stepmidiinput.py
  6. 11
      qtgui/menu.py
  7. 5
      template/engine/input_midi.py
  8. 28
      template/qtgui/midiinquickwidget.py

2
CHANGELOG

@ -6,6 +6,8 @@ External contributors notice at the end of the line: (LastName, FirstName / nick
## 2022-07-15 2.1.0
When not in F4-StepInput Mode use midi keyboard as pitch-cursor.
Add midi-in selector drop down, as seen in Tembro and Fluajho.
Rewrite grid, which was a performance drag in the past.
Add functions to move block to start or end of track.
Scroll view when dragging blocks and tracks with the mouse.

29
engine/api.py

@ -331,6 +331,21 @@ def playFromBlockStart():
raise RuntimeError("reached end of blocks without matchin current block index")
def getMidiInputNameAndUuid()->(str, int): #tuple name:str, uuid
"""Override template function. We access the stepMidi directly.
Used by the quick midi input widget
Return name and cboxMidiPortUid.
name is Client:Port JACK format
If not return None, None
"""
from engine.midiinput.stepmidiinput import stepMidiInput #singleton instance #avoid circular dependency. stepMidiInput import api
if stepMidiInput.ready: #startup delay
return stepMidiInput.fullName(), stepMidiInput.cboxMidiPortUid
else:
return None, None
topLevelFunction = None
def simpleCommand(function, autoStepLeft = True, forceReturnToItem = None):
"""
@ -1164,6 +1179,13 @@ def selectToTickindex(trackid, tickindex):
session.data.setSelectionBeginning()
toTickindex(trackid, tickindex, destroySelection = False)
#####Pitches
def toPitch(pitchindex:int):
session.data.cursor.pitchindex = pitchindex
callbacks._setCursor(destroySelection = False) #does not modify the pitch to keysig etc.
def up():
session.data.cursor.up()
callbacks._setCursor(destroySelection = False)
@ -1261,8 +1283,11 @@ def insertChord(baseDuration, pitchToInsert):
def insertCursorChord(baseDuration):
"""Insert a new chord with one note to the track.
The intial note gets its pitch from the cursor position, to scale"""
keysig = session.data.currentTrack().state.keySignature()
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
if session.data.cursor.cursorWasMovedAfterChoosingPitchViaMidi: #we had cursor arrow keys movement
keysig = session.data.currentTrack().state.keySignature()
pitchToInsert = pitchmath.toScale(session.data.cursor.pitchindex, keysig)
else:
pitchToInsert = session.data.cursor.pitchindex
insertChord(baseDuration, pitchToInsert)
def addNoteToChord(pitchToInsert):

3
engine/cursor.py

@ -46,6 +46,7 @@ class Cursor:
self.prevailingBaseDuration = duration.D4
self.persistentPrevailingDot = False #For the sake of simplicity this is only a bool. No "two prevailing dots"
self.oneTimePrevailingDotInverter = False #effects persistentPrevailingDot
self.cursorWasMovedAfterChoosingPitchViaMidi = False #this flag enables the api to check if the cursor was set with cursor commands (=arrow keys) after it was set by midi. If not the api can insert the literal midi pitch
def up(self):
"""Move the cursor/pitchindex up, +50.
@ -74,6 +75,7 @@ class Cursor:
self.correctPitchindex()
def correctPitchindex(self):
self.cursorWasMovedAfterChoosingPitchViaMidi = True
if self.pitchindex >= pitch.MAX:
self.pitchindex = pitch.MAX
elif self.pitchindex <= pitch.MIN:
@ -117,6 +119,7 @@ class Cursor:
item = trackState.track.currentItem()
block = trackState.track.currentBlock()
return {
"type": "Cursor",
"trackIndex": trackState.index(),

12
engine/main.py

@ -1052,6 +1052,18 @@ class Data(template.engine.sequencer.Score):
data = {track:track.lilypond() for track in self.tracks} #processed in the lilypond module
return fromTemplate(session = self.parentSession, templateFile = "default.ly", data = data, meta = self.metaData, tempoStaff = tempoStaff)
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:
dictionary = super().serialize()

47
engine/midiinput/stepmidiinput.py

@ -46,12 +46,17 @@ class StepMidiInput(MidiInput):
which means that as long as one of the chord notes would still be down the chord was still "on".
That is not as robust and convenient as using the starting note, which is counter intuitive,
therefore documented here.
Up until 2022 we used the midi in active toggle just on/off. So the template bypass.
But now we never switch midi processing off, we just reroute input vs. setting the pitch cursor
"""
def __init__(self):
#No super init in here! This is delayed until self.start
self.firstActiveNote = None #for chord entry.
self._currentlyActiveNotes = set()
self.ready = False #Program start
self.inputitemTrueCursorpitchFalse = False #start with just
def start(self):
"""Call this manually after the engine and an event loop have started.
@ -59,7 +64,8 @@ class StepMidiInput(MidiInput):
But it could be started from a simple command line interface as well."""
assert api.laborejoEngineStarted
super().__init__(session=api.session, portName="in")
self.midiProcessor.active = False #specific to Laborejo
self.midiProcessor.active = True #never off.
self.ready = True #Program start
#Connect the template midi input with Laborejo api calls.
#self.midiProcessor.notePrinter(True)
@ -71,35 +77,42 @@ class StepMidiInput(MidiInput):
@property
def midiInIsActive(self):
"""
try:
return self.midiProcessor.active
except AttributeError: #during startupt
except AttributeError: #during startup
return False
"""
return self.inputitemTrueCursorpitchFalse
def _insertMusicItemFromMidi(self, timeStamp, channel, midipitch, velocity):
if self._currentlyActiveNotes: #Chord
api.left()
keysig = api.session.data.currentTrack().state.keySignature()
pitchToInsert = api.pitchmath.fromMidi(midipitch, keysig)
api.addNoteToChord(pitchToInsert)
api.right()
else: #Single note
baseDuration = api.session.data.cursor.prevailingBaseDuration
keysig = api.session.data.currentTrack().state.keySignature()
pitchToInsert = api.pitchmath.fromMidi(midipitch, keysig)
api.insertChord(baseDuration, pitchToInsert)
self._currentlyActiveNotes.add(midipitch)
keysig = api.session.data.currentTrack().state.keySignature()
pitchToInsert = api.pitchmath.fromMidi(midipitch, keysig)
if self.inputitemTrueCursorpitchFalse:
if self._currentlyActiveNotes: #Chord
api.left()
api.addNoteToChord(pitchToInsert)
api.right()
else: #Single note
baseDuration = api.session.data.cursor.prevailingBaseDuration
api.insertChord(baseDuration, pitchToInsert)
else:
api.session.data.cursor.cursorWasMovedAfterChoosingPitchViaMidi = False
api.toPitch(pitchToInsert)
self._currentlyActiveNotes.add(midipitch) #This needs to be at the end of the function for chord-detection to work
def _pop(self, timeStamp, channel, midipitch, velocity):
self._currentlyActiveNotes.remove(midipitch)
def setMidiInputActive(self, state:bool):
self.midiProcessor.active = state
#self.midiProcessor.active = state
self.inputitemTrueCursorpitchFalse = state
api.callbacks._prevailingBaseDurationChanged(api.session.data.cursor.prevailingBaseDuration)
def toggleMidiIn(self):
self.setMidiInputActive(not self.midiInIsActive)
#self.setMidiInputActive(not self.midiInIsActive)
self.setMidiInputActive(not self.inputitemTrueCursorpitchFalse)
def _setMidiThru(self, cursorExport):
"""We don't need to react to deleted tracks because that does reset the cursor.

11
qtgui/menu.py

@ -30,6 +30,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets
translate = QtCore.QCoreApplication.translate
#Template Modules
from template.qtgui.midiinquickwidget import QuickMidiInputComboController
#Our modules
@ -373,7 +374,11 @@ class MenuActionDatabase(object):
#Prepare non-designer widgets. Designer can't put normal widgets in a toolbar, but Qt can.
#Eventhough this is a dict, which has no order, the CREATION has an order. So the first item in the dict will be the first item in the toolBar
midiText = QtCore.QCoreApplication.translate("menu", "Step Midi Input")
self.extraToolBarWidgets = {
"midiInputWidget" : self.mainWindow.ui.toolBar.addWidget(QuickMidiInputComboController(self.mainWindow.ui.toolBar, text=midiText, stretch=False)),
"snapToGrid" : self.mainWindow.ui.toolBar.addWidget(ToolBarSnapToGrid(mainWindow=self.mainWindow)),
"metronome" : self.mainWindow.ui.toolBar.addWidget(ToolBarMetronome(mainWindow=self.mainWindow)),
"playbackSpeed" : self.mainWindow.ui.toolBar.addWidget(ToolBarPlaybackSpeed(mainWindow=self.mainWindow)),
@ -381,9 +386,9 @@ class MenuActionDatabase(object):
}
self.toolbarContexts = {
"notation" : [self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["playbackSpeed"], ],
"cc" : [self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["ccType"], self.extraToolBarWidgets["playbackSpeed"]],
"block": [self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["playbackSpeed"], ],
"notation" : [self.extraToolBarWidgets["midiInputWidget"], self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["playbackSpeed"], ],
"cc" : [self.extraToolBarWidgets["midiInputWidget"], self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["ccType"], self.extraToolBarWidgets["playbackSpeed"]],
"block": [self.extraToolBarWidgets["midiInputWidget"], self.extraToolBarWidgets["snapToGrid"], self.extraToolBarWidgets["metronome"], self.extraToolBarWidgets["playbackSpeed"], ],
}
#Now connect all actions to functions

5
template/engine/input_midi.py

@ -109,6 +109,9 @@ class MidiInput(object):
for hp in hardwareMidiPorts:
cbox.JackIO.port_connect(hp, cbox.JackIO.status().client_name + ":" + self.portName)
def fullName(self)->str:
return cbox.JackIO.status().client_name + ":" + self.portName
class MidiProcessor(object):
"""
@ -165,7 +168,7 @@ class MidiProcessor(object):
This function gets called very often. So every optimisation is good.
"""
events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid)
events = cbox.JackIO.get_new_events(self.parentInput.cboxMidiPortUid) #We get the events even if not active. Otherwise they pile up.
if not self.active:
return
if not events:

28
template/qtgui/midiinquickwidget.py

@ -34,9 +34,11 @@ 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."""
fire-and-forget function with no callbacks.
def __init__(self, parentWidget):
If you give a text it must already be translated."""
def __init__(self, parentWidget, text=None, stretch=True):
super().__init__(parentWidget)
self.parentWidget = parentWidget
self.layout = QtWidgets.QHBoxLayout()
@ -54,7 +56,10 @@ class QuickMidiInputComboController(QtWidgets.QWidget):
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."))
if text:
self.label.setText(text) #already translated by parent.
else:
self.label.setText(QtCore.QCoreApplication.translate("QuickMidiInputComboController", "Midi Input. Use JACK to connect multiple inputs."))
self.layout.addWidget(self.label)
#if not api.isStandaloneMode():
@ -66,9 +71,8 @@ class QuickMidiInputComboController(QtWidgets.QWidget):
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()
if stretch:
self.layout.addStretch()
#api.callbacks.startLoadingAuditionerInstrument.append(self.callback_startLoadingAuditionerInstrument)
#api.callbacks.auditionerInstrumentChanged.append(self.callback_auditionerInstrumentChanged)
#api.callbacks.auditionerVolumeChanged.append(self.callback__auditionerVolumeChanged)
@ -130,17 +134,23 @@ class QuickMidiInputComboController(QtWidgets.QWidget):
"""externalPort is in the Client:Port JACK format
If "" False or None disconnect all ports."""
cboxPortname, cboxMidiPortUid = api.getMidiInputNameAndUuid() #these don't change during runtime. But if the system it not ready yet it returns None, None
if cboxPortname is None and cboxMidiPortUid is None:
logging.info("engine is not ready yet to deliver midi input name and port id. This is normal during startup but a problem during normal runtime.")
return #startup delay
try:
currentConnectedList = cbox.JackIO.get_connected_ports(self.cboxMidiPortUid)
currentConnectedList = cbox.JackIO.get_connected_ports(cboxMidiPortUid)
except: #port not found.
currentConnectedList = []
for port in currentConnectedList:
cbox.JackIO.port_disconnect(port, self.cboxPortname)
cbox.JackIO.port_disconnect(port, 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)
cbox.JackIO.port_connect(externalPort, cboxPortname)

Loading…
Cancel
Save