Browse Source

more CODE!

master
Nils 3 years ago
parent
commit
39badd034b
  1. 469
      engine/api.py
  2. 52
      engine/main.py
  3. 2
      qtgui/constantsAndConfigs.py
  4. 16
      qtgui/designer/mainwindow.py
  5. 35
      qtgui/designer/mainwindow.ui
  6. 80
      qtgui/inputcursor.py
  7. 121
      qtgui/items.py
  8. 172
      qtgui/mainwindow.py
  9. 116
      qtgui/score.py
  10. 19
      qtgui/scoreview.py

469
engine/api.py

@ -31,10 +31,10 @@ from template.engine.input_midi import MidiInput
#New callbacks
class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
def __init__(self):
super().__init__()
super().__init__()
self.stepEntryNoteOn = []
self.stepEntryNoteOff = []
self.newEvent = []
self.newEvent = []
self.deleteEvent = []
self.eventPositionChanged = []
self.eventByteOneChanged = []
@ -43,72 +43,86 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
self.activeLayerChanged = []
self.layerChanged = []
self.layerColorChanged = []
self.layerMidiChannelChanged = []
self.songDurationChanged = []
self.pastedEvents = []
def _stepEntryNoteOn(self, pitch, velocity):
for func in self.stepEntryNoteOn:
def _stepEntryNoteOn(self, pitch, velocity):
for func in self.stepEntryNoteOn:
func(pitch, velocity)
self._dataChanged()
def _stepEntryNoteOff(self, pitch, velocity):
for func in self.stepEntryNoteOff:
func(pitch, velocity)
for func in self.stepEntryNoteOff:
func(pitch, velocity)
self._dataChanged()
def _newEvent(self, eventDict):
"""Incremental update for all other event """
for func in self.newEvent:
for func in self.newEvent:
func(eventDict)
self._dataChanged()
def _deleteEvent(self, eventDict):
for func in self.deleteEvent:
for func in self.deleteEvent:
func(eventDict)
self._dataChanged()
def _eventPositionChanged(self, eventDict):
for func in self.eventPositionChanged:
for func in self.eventPositionChanged:
func(eventDict)
self._dataChanged()
def _eventByteOneChanged(self, eventDict):
for func in self.eventByteOneChanged:
for func in self.eventByteOneChanged:
func(eventDict)
self._dataChanged()
def _eventByteTwoChanged(self, eventDict):
for func in self.eventByteTwoChanged:
for func in self.eventByteTwoChanged:
func(eventDict)
self._dataChanged()
def _eventFreeTextChanged(self, eventDict):
for func in self.eventFreeTextChanged:
func(eventDict)
self._dataChanged()
def _activeLayerChanged(self):
self._dataChanged()
def _activeLayerChanged(self):
"""We order 0 before 1 but the keyboard layout trumps programming logic.
First key is 1, therefore default layer is 1"""
layer = session.data.track.layers[session.data.track.activeLayer]
midiInput.setMidiThruChannel(layer.midiChannel)
for func in self.activeLayerChanged:
func(session.data.track.activeLayer)
self._dataChanged()
def _layerChanged(self, layer):
def _layerChanged(self, layerIndex):
"""Send a complete layer to redraw. Used 10x when loading a file or after
heavy operations."""
export = session.data.track.layers[layer].export() #side-effect: cbox midi update
export = session.data.track.layers[layerIndex].export() #side-effect: cbox midi update
for func in self.layerChanged:
func(layer, export)
func(layerIndex, export)
self._dataChanged()
def _layerColorChanged(self, layer):
def _layerColorChanged(self, layerIndex):
"""while included in _layerChanged, this callback only changes the color"""
export = session.data.track.layers[layer].color
for func in self.layerColorChanged:
func(layer, export)
export = session.data.track.layers[layerIndex].color
for func in self.layerColorChanged:
func(layerIndex, export)
self._dataChanged()
def _layerMidiChannelChanged(self, layerIndex):
"""while included in _layerChanged, this callback only changes the color"""
layer = session.data.track.layers[session.data.track.activeLayer]
if layerIndex == session.data.track.activeLayer:
midiInput.setMidiThruChannel(layer.midiChannel)
export = layer.midiChannel
for func in self.layerMidiChannelChanged:
func(layerIndex, export)
self._dataChanged()
def _songDurationChanged(self):
"""Intended to tell the GUI how far to draw their scene. Also includes the first event
to make navigation more convenient."""
@ -117,10 +131,10 @@ class ClientCallbacks(Callbacks): #inherits from the templates api callbacks
for func in self.songDurationChanged:
func(first, last)
self._dataChanged()
def _pastedEvents(self, listOfEventIdsAndLayers):
"""Inform which events just have been pasted so they can be separated from the original
items which are still on the exact position (if not cut or paste to different layer"""
"""Inform which events just have been pasted so they can be separated from the original
items which are still on the exact position (if not cut or paste to different layer"""
for func in self.pastedEvents:
func(listOfEventIdsAndLayers)
@ -132,52 +146,55 @@ _templateStartEngine = startEngine
midiInput = None #set in startEngine
def startEngine(nsmClient):
callbacks.playbackStatusChanged.append(finalizeRecording)
def startEngine(nsmClient):
callbacks.playbackStatusChanged.append(finalizeRecording)
callbacks.playbackStatusChanged.append(toggleUndoRecordingCollector)
callbacks.recordingModeChanged.append(checkIfPlaybackIsRunningWhileRecordinChanged)
callbacks.recordingModeChanged.append(checkIfPlaybackIsRunningWhileRecordinChanged)
_templateStartEngine(nsmClient)
#Setup Midi Input
global midiInput
midiInput = MidiInput(session, portName="in") #MidiInput is a standalone class that communicates purely with callbacks and injections.
midiInput.setMidiThru(session.data.track.sequencerInterface.cboxMidiOutUuid)
midiInput.midiProcessor.register_NoteOn(noteOn)
midiInput.midiProcessor.register_NoteOff(noteOff)
midiInput.midiProcessor.register_NoteOff(noteOff)
midiInput.midiProcessor.register_CC(cc)
midiInput.midiProcessor.register_PolyphonicAftertouch(polyphonicAftertouch)
midiInput.midiProcessor.register_ProgramChange(progamChange)
#Send initial Data, and callbacks to create the first GUI state.
for layerIndex in session.data.track.layers.keys():
midiInput.midiProcessor.register_ChannelPressure(channelPressure)
midiInput.midiProcessor.register_PitchBend(pitchBend)
#Send initial Data, and callbacks to create the first GUI state.
for layerIndex in session.data.track.layers.keys():
callbacks._layerColorChanged(layerIndex) #Send color BEFORE the rest so activeLayer below can already use the color
callbacks._layerChanged(layerIndex)
callbacks._songDurationChanged()
callbacks._activeLayerChanged()
callbacks._activeLayerChanged()
def setLayerColor(layer:int, color:str):
session.data.track.layers[layer].color = color
callbacks._layerColorChanged(layer) #grabs the color itself
#Midi Input functions. Called by the midiInput module callbacks
def noteOn(tickindex:int, channel:int, note:int, velocity:int):
if session.recordingEnabled and not tickindex is None:
#if velocity == 0: noteOff(tickindex, channel, note, velocity). INVALID in Jack
def noteOn(tickindex:int, channel:int, note:int, velocity:int):
if session.recordingEnabled and not tickindex is None:
#if velocity == 0: noteOff(tickindex, channel, note, velocity). INVALID in Jack
tickindex = int(tickindex)
event = session.data.liveNoteOn(tickindex, note, velocity) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
else: #non-recording
callbacks._stepEntryNoteOn(note, velocity)
def noteOff(tickindex:int, channel:int, note:int, velocity:int):
if session.recordingEnabled and not tickindex is None:
else: #non-recording
callbacks._stepEntryNoteOn(note, velocity)
def noteOff(tickindex:int, channel:int, note:int, velocity:int):
if session.recordingEnabled and not tickindex is None:
tickindex = int(tickindex)
event = session.data.liveNoteOff(tickindex, note, velocity) #Forward to engine, whichs knows the activeLayer already.
event = session.data.liveNoteOff(tickindex, note, velocity) #Forward to engine, whichs knows the activeLayer already.
if event: #it is possible that we received a note-off without a previous note-on. Ignore.
exp = event.export()
callbacks._newEvent(exp)
@ -190,35 +207,71 @@ def finalizeRecording(state:bool):
forcedNoteOffs = session.data.finalizeRecording(state) #Cbox already knows about the note offs but we need to tell the GUI.
for noteOffEvent in forcedNoteOffs:
callbacks._newEvent(noteOffEvent.export())
if session.data.eventCollectorForUndo: #if either None or empty list will not generate new cbox data
session.history.register(lambda l=session.data.eventCollectorForUndo: _deleteEvents(l), descriptionString="Record Events")
session.data.eventCollectorForUndo = None
if session.data.eventCollectorForUndo: #if either None or empty list will not generate new cbox data
session.history.register(lambda l=session.data.eventCollectorForUndo: _deleteEvents(l), descriptionString="Record Events")
session.data.eventCollectorForUndo = None
session.data.track.generateCalfboxMidi() #atomic live note event callbacks are enough for the gui. And while it is playing the RT midi through is used. After recording we saved everything as proper notes but cbox still needs update.
callbacks._songDurationChanged()
def cc(tickindex:int, channel:int, type:int, value:int):
if session.recordingEnabled and not tickindex is None:
def cc(tickindex:int, channel:int, type:int, value:int):
"""0xC0"""
if session.recordingEnabled and not tickindex is None:
tickindex = int(tickindex)
event = session.data.liveCC(tickindex, type, value) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
def polyphonicAftertouch(tickindex:int, channel:int, note:int, value:int):
"""0xA0"""
if session.recordingEnabled and not tickindex is None:
tickindex = int(tickindex)
event = session.data.liveCC(tickindex, type, value) #Forward to engine, whichs knows the activeLayer already.
event = session.data.livePolyphonicAftertouch(tickindex, note, value) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
callbacks._songDurationChanged()
_lastPitchBendCoarse = None
def pitchBend(tickindex:int, channel:int, fine:int, coarse:int):
"""Pitchbend 0xE0 is a 14 bit value. Byte2 is coarse/MSB, byte1 is fine/LSB.
Many keyboards leave byte1 as 0 and use only byte2, making PitchBend 128steps.
There is also a CC to specify pitchbend musical range and sensitivity,
but that is for a synth, we just send the CC without knowing its implications
As a specific vico "feature" we always set the fine bit to 0, to have any chance
to display it in a meaningful way in our Qt GUI.
A special case. Pitch bend is 14bit but it is very hard to react to that many messages
with a standard GUI or similar. We therefore discard the fine/LSB byte1 already here
and only trigger when the coarse value is different from before.
"""
if session.recordingEnabled and not tickindex is None:
global _lastPitchBendCoarse
if not _lastPitchBendCoarse == coarse:
tickindex = int(tickindex)
event = session.data.livePitchBend(tickindex, fine, coarse) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
_lastPitchBendCoarse = coarse
def progamChange(tickindex:int, channel:int, type:int, value:int):
"""Value is byte1. There is no byte2"""
if session.recordingEnabled and not tickindex is None:
def progamChange(tickindex:int, channel:int, value:int):
"""0xB0
Value is byte1. There is no byte2"""
if session.recordingEnabled and not tickindex is None:
tickindex = int(tickindex)
event = session.data.liveProgramChange(tickindex, value) #Forward to engine, whichs knows the activeLayer already.
event = session.data.liveProgramChange(tickindex, value) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
callbacks._songDurationChanged()
def channelPressure(tickindex:int, channel:int, type:int, value:int):
"""Value is byte1. There is no byte2"""
if session.recordingEnabled and not tickindex is None:
def channelPressure(tickindex:int, channel:int, value:int):
"""0xD0
Like Program Change, has only byte1. There is no byte2"""
if session.recordingEnabled and not tickindex is None:
tickindex = int(tickindex)
event = session.data.liveChannelPressure(tickindex, value) #Forward to engine, whichs knows the activeLayer already.
event = session.data.liveChannelPressure(tickindex, value) #Forward to engine, whichs knows the activeLayer already.
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
callbacks._songDurationChanged()
#General Purpose Functions
@ -228,12 +281,12 @@ def _deleteEvents(listOfEvents):
"""
if not listOfEvents:
return
session.history.register(lambda l=listOfEvents: _insertEvents(l), descriptionString="Delete Events")
session.history.register(lambda l=listOfEvents: _insertEvents(l), descriptionString="Delete Events")
#changedLayers = set()
for event in listOfEvents:
for event in listOfEvents:
#changedLayers.add(event.layer)
session.data.track.deleteEvent(event)
callbacks._deleteEvent(event.export())
@ -242,22 +295,22 @@ def _deleteEvents(listOfEvents):
callbacks._songDurationChanged()
def deleteEventsById(listOfEventIds):
"""We receive 2 events per note: on and off. All others are single"""
_deleteEvents([session.data.allEventsById[evid] for evid in listOfEventIds])
"""We receive 2 events per note: on and off. All others are single"""
_deleteEvents([session.data.allEventsById[evid] for evid in listOfEventIds])
def _insertEvents(listOfEvents):
"""complementary to _deleteEvents. Can be used for circular undo/redo"""
if not listOfEvents:
return
session.history.register(lambda l=listOfEvents: _deleteEvents(l), descriptionString="Insert Events")
return
session.history.register(lambda l=listOfEvents: _deleteEvents(l), descriptionString="Insert Events")
#changedLayers = set()
for event in listOfEvents:
for event in listOfEvents:
#changedLayers.add(event.layer)
session.data.track.insertEvent(event)
callbacks._newEvent(event.export())
callbacks._newEvent(event.export())
session.data.track.generateCalfboxMidi()
callbacks._songDurationChanged()
@ -265,17 +318,17 @@ def _copy(listOfEvents):
"""Ctrl+C. Copy a list of events into an interal buffer, ready to paste.
We copy twice: First on copy, so the original event can be deleted.
Then on paste, because paste can happen multiple times in a row
Old copy buffers will be replaced.
Sends no callbacks.
"""
"""
if not listOfEvents: #empty copy should not delete the existing copy buffer
return
return
session.data.copyBuffer = []
for event in listOfEvents:
session.data.copyBuffer.append(event.copy())
session.data.copyBuffer.append(event.copy())
def copyById(listOfEventIds):
"""Wrapper for _copy"""
@ -290,16 +343,16 @@ def cutById(listOfEventIds):
_cut([session.data.allEventsById[evid] for evid in listOfEventIds])
def paste():
"""Create a copy of our copy-buffer and insert them at the same position as the originals,
"""Create a copy of our copy-buffer and insert them at the same position as the originals,
but on the current layer.
Then send a callback with a list of new event IDs, so a GUI can react by keeping them selected
for further movement.
"""
for further movement.
"""
listOfEvents = session.data.getCopyBufferCopy(getActiveLayer())
_insertEvents(listOfEvents)
callbacks._pastedEvents([(id(event), event.layer) for event in listOfEvents])
statusToName = {
statusToName = {
0xA0: "Aftertouch",
0xB0: "Control Change",
0xC0: "Program Change",
@ -312,95 +365,95 @@ statusToName = {
def createEvent(tickindex:int, status:int, byte1:int, byte2:int):
"""For all events excepts notes"""
if status == 0x90 or status == 0x80:
logging.info("createEvent is not for notes")
logging.info("createEvent is not for notes")
layer = session.data.track.layers[session.data.track.activeLayer]
tickindex = int(tickindex)
event = layer.newEvent(tickindex, status, byte1, byte2, "")
event = layer.newEvent(tickindex, status, byte1, byte2, "")
name = statusToName[status]
session.history.register(lambda l=[event]: _deleteEvents(l), descriptionString=f"Create {name}")
session.history.register(lambda l=[event]: _deleteEvents(l), descriptionString=f"Create {name}")
session.data.track.generateCalfboxMidi()
callbacks._newEvent(event.export())
callbacks._songDurationChanged()
callbacks._songDurationChanged()
def createNote(tickindex:int, pitch:int, velocity:int, duration:int):
"""Creates two engine events for note on/off"""
"""Creates two engine events for note on/off"""
layer = session.data.track.layers[session.data.track.activeLayer]
tickindex = int(tickindex)
evOn = layer.newEvent(tickindex, 0x90, pitch, velocity, "")
evOff = layer.newEvent(tickindex+duration, 0x80, pitch, velocity, "")
evOn = layer.newEvent(tickindex, 0x90, pitch, velocity, "")
evOff = layer.newEvent(tickindex+duration, 0x80, pitch, velocity, "")
#newEvent already added the events to their layers. We want to call _insertEvents for convenience (history, callbacks etc.)
#So we need to remove the events temporarily. This is a cheap and quick operation.
layer.deleteEvent(evOn)
layer.deleteEvent(evOff)
_insertEvents([evOn, evOff])
layer.deleteEvent(evOff)
_insertEvents([evOn, evOff])
def _changeBytesSeparateLists(listForOne, listForTwo, differenceInSteps):
"""
Convenience function to have all changes in one go.
It is possible to go below 0 and above 127. However, these values do not get exported to
It is possible to go below 0 and above 127. However, these values do not get exported to
midi but will trigger a logger-info instead. This means we can go down again without loosing
information.
information.
"""
if not listForOne+listForTwo:
return
return
changedLayers = set()
session.history.register(lambda l1=listForOne, l2=listForTwo, d=-1*differenceInSteps: changeBytesByIdSeparateLists(l1, l2, d), descriptionString="Move Events")
for event in listForOne:
event.byte1 += differenceInSteps
for event in listForOne:
event.byte1 += differenceInSteps
callbacks._eventByteOneChanged(event.export())
changedLayers.add(event.layer)
for event in listForTwo:
event.byte2 += differenceInSteps
callbacks._eventByteTwoChanged(event.export())
for event in listForTwo:
event.byte2 += differenceInSteps
callbacks._eventByteTwoChanged(event.export())
changedLayers.add(event.layer)
for layer in changedLayers:
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
def changeBytesByIdSeparateLists(listForOne, listForTwo, differenceInSteps):
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
def changeBytesByIdSeparateLists(listForOne, listForTwo, differenceInSteps):
_changeBytesSeparateLists([session.data.allEventsById[evid] for evid in listForOne], [session.data.allEventsById[evid] for evid in listForTwo], differenceInSteps)
def repositionItemsRelative(listOfEventIds, differenceAsTicks):
if not listOfEventIds:
return
changedLayers = set()
session.history.register(lambda l=listOfEventIds, d=-1*differenceAsTicks: repositionSelectedItemsRelative(l, d), descriptionString="Reposition Events")
for evid in listOfEventIds:
for evid in listOfEventIds:
event = session.data.allEventsById[evid]
event.position += differenceAsTicks
event.position += differenceAsTicks
callbacks._eventPositionChanged(event.export())
changedLayers.add(event.layer)
for layer in changedLayers:
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
session.data.track.generateCalfboxMidi()
def moveEvents(dictOfTuples):
def moveEvents(dictOfTuples):
"""Change tickindex and byte1 at the same time (same callback round). This is meant for a GUI
that moves pitch and position at the same time in a 2D canvas.
We also change byte2 because in some cases, like CC, "moving" up and down is performed on the
second byte.
Parameter is id:(newPos, newByte1, newByte2)
note on and note off arrive as two events
"""
"""
if not dictOfTuples:
return
undoDataSet = {} #same as parameter. Holds only primitive, immutable types so we can just assign values into it.
changedLayers = set()
for eventId, (tickposition, byte1, byte2) in dictOfTuples.items():
for eventId, (tickposition, byte1, byte2) in dictOfTuples.items():
tickposition = int(tickposition)
event = session.data.allEventsById[eventId]
undoDataSet[eventId] = (event.position, event.byte1, event.byte2)
@ -410,126 +463,138 @@ def moveEvents(dictOfTuples):
changedLayers.add(event.layer)
ex = event.export()
callbacks._eventByteOneChanged(ex)
callbacks._eventByteTwoChanged(ex)
callbacks._eventByteTwoChanged(ex)
callbacks._eventPositionChanged(ex)
for layer in changedLayers:
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
session.history.register(lambda d=undoDataSet: moveEvents(d), descriptionString="Move Events")
session.data.track.generateCalfboxMidi()
session.history.register(lambda d=undoDataSet: moveEvents(d), descriptionString="Move Events")
def _changeByte2(data:dict):
"""data contains id:absoluteVelocityValue.
We allow velocities outside the boundaries 0-127 to make undo and relative movement possible
without compressing data. The engine will warn and compress midi output.
Notes arrive as only note-on!!
Notes arrive as only note-on!!
"""
if not data:
return
undoDataSet = {} #same as parameter. Holds only primitive, immutable types so we can just assign values into it.
changedLayers = set()
undoDataSet = {} #same as parameter. Holds only primitive, immutable types so we can just assign values into it.
changedLayers = set()
for eventId, byte2 in data.items():
event = session.data.allEventsById[eventId]
undoDataSet[eventId] = (event.byte2)
event.byte2 = byte2
changedLayers.add(event.layer)
ex = event.export()
callbacks._eventByteTwoChanged(ex)
callbacks._eventByteTwoChanged(ex)
for layer in changedLayers:
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
session.history.register(lambda d=undoDataSet: _changeByte2(d), descriptionString="Change Byte2")
def changeVelocitiesRelative(listOfEventIds, differenceInSteps):
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
session.data.track.generateCalfboxMidi()
session.history.register(lambda d=undoDataSet: _changeByte2(d), descriptionString="Change Byte2")
def changeVelocitiesRelative(listOfEventIds, differenceInSteps):
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
#data = {}
#for evid in listOfEventIds:
# event = session.data.allEventsById[evid]
# data[evid] = event.byte2 + differenceInSteps
data = { evid : session.data.allEventsById[evid].byte2+differenceInSteps for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
# data[evid] = event.byte2 + differenceInSteps
data = { evid : session.data.allEventsById[evid].byte2+differenceInSteps for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
_changeByte2(data)
def setVelocities(listOfEventIds, absoluteValue):
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
data = { evid : absoluteValue for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
data = { evid : absoluteValue for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
_changeByte2(data)
def compressVelocities(listOfEventIds, lowerBound, upperBound):
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
c = lambda input: int(compress(input, 0, 127, outputLowest=lowerBound, outputHighest=upperBound))
data = { evid : c(session.data.allEventsById[evid].byte2) for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
def compressVelocities(listOfEventIds, lowerBound, upperBound):
"""A wrapper around _changeByte2 which only works on velocities of note-ons"""
c = lambda input: int(compress(input, 0, 127, outputLowest=lowerBound, outputHighest=upperBound))
data = { evid : c(session.data.allEventsById[evid].byte2) for evid in listOfEventIds if session.data.allEventsById[evid].status == 0x90}
_changeByte2(data)
def setFreeText(eventId:int, text:str):
if not session.data.allEventsById[eventId].freeText == text:
event = session.data.allEventsById[eventId]
session.history.register(lambda t=event.freeText: setFreeText(eventId, t), descriptionString="Free Text")
session.history.register(lambda t=event.freeText: setFreeText(eventId, t), descriptionString="Free Text")
event.freeText = text
callbacks._eventFreeTextChanged(event.export())
def toggleUndoRecordingCollector(state:bool):
"""The span of one playback session is what gets covered by undo.
Called by the api itself.
Works in tandem with checkIfPlaybackIsRunningWhileRecordinChanged because the user
can start recording while playback is already running.
However, we don't care if recording is toggled off (and on again?... ) while playback is running.
can start recording while playback is already running.
However, we don't care if recording is toggled off (and on again?... ) while playback is running.
"""
if state and session.recordingEnabled: #recording started while recording
if state and session.recordingEnabled: #recording started while recording
if session.data.eventCollectorForUndo is None:
session.data.eventCollectorForUndo = []
session.data.eventCollectorForUndo = []
def checkIfPlaybackIsRunningWhileRecordinChanged(state:bool):
"""Called by the template api.
Works in tandem with toggleUndoRecordingCollector because the user
can toggle recording while playback is already/still running.
can toggle recording while playback is already/still running.
"""
pb = playbackStatus()
if state and pb: #recording was switch on during playback running
if session.data.eventCollectorForUndo is None:
session.data.eventCollectorForUndo = []
def chooseActiveLayer(value:int):
session.data.eventCollectorForUndo = []
def chooseActiveLayer(value:int):
if value < 0 or value > 9:
raise ValueError("There are only layers from 0 to 9")
session.data.track.activeLayer = value
callbacks._activeLayerChanged()
def getActiveLayer()->int:
return session.data.track.activeLayer
def getActiveColor()->str:
return session.data.track.layers[getActiveLayer()].color
def getActiveColor()->str:
return session.data.track.layers[getActiveLayer()].color
def getActiveMedianVelocity()->str:
def setLayerMidiChannel(layer:int, channel:int):
if channel < 1 or channel > 16:
raise ValueError("Midi Channel must been between 1 and 16 inclusive, not " + str(channel))
session.data.track.layers[layer].midiChannel = channel
session.data.track.layers[layer].dirty = True
session.data.track.generateCalfboxMidi()
callbacks._layerMidiChannelChanged(layer) #grabs the channel itself
def getActiveMidiChannel()->int:
return session.data.track.layers[getActiveLayer()].midiChannel
def getActiveMedianVelocity()->str:
return session.data.track.layers[getActiveLayer()].cachedMedianVelocity
def layerFilterAndMove(sourceLayerIndex:int, targetLayerIndex:int, statusByte:int, byte1RangeMinimum:int, byte1RangeMaximum:int, byte2RangeMinimum:int, byte2RangeMaximum:int, callback=True):
"""send all events from one layer to another, under certain conditions.
"""send all events from one layer to another, under certain conditions.
All ranges are inclusive at both ends.
If statusByte is either note on or note off it will move BOTH.
If statusByte is either note on or note off it will move BOTH.
"""
if sourceLayerIndex == targetLayerIndex:
return
sourceLayer = session.data.track.layers[sourceLayerIndex]
targetLayer = session.data.track.layers[targetLayerIndex]
targetLayer = session.data.track.layers[targetLayerIndex]
if statusByte == 0x90 or statusByte == 0x80:
statusBytes = (0x90, 0x80)
else:
statusBytes = (statusByte,) #tuple-comma
for statusType, listOfEvents in sourceLayer.events.items():
for event in listOfEvents[:]:
if event.status in statusBytes:
@ -540,34 +605,34 @@ def layerFilterAndMove(sourceLayerIndex:int, targetLayerIndex:int, statusByte:in
byte2InRange = True
if byte1InRange and byte2InRange:
listOfEvents.remove(event) #we iterate over a shallow copy, we delete from the original.
targetLayer.events[event.status].append(event)
targetLayer.events[event.status].append(event)
if callback:
callbacks._layerChanged(sourceLayerIndex) #includes cbox update
callbacks._layerChanged(targetLayerIndex) #includes cbox update
callbacks._layerChanged(sourceLayerIndex) #includes cbox update
callbacks._layerChanged(targetLayerIndex) #includes cbox update
#song duration does not change
def filterAndMoveAllLayers(targetLayerIndex:int, statusByte:int, byte1RangeMinimum:int, byte1RangeMaximum:int, byte2RangeMinimum:int, byte2RangeMaximum:int):
"""like layerFilterAndMove, but for all layers as source layers, except the targetLayer itself"""
for sourceLayerIndex in session.data.track.layers.keys():
layerFilterAndMove(sourceLayerIndex, targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum, callback=False)
callbacks._layerChanged(sourceLayerIndex)
callbacks._layerChanged(sourceLayerIndex)
callbacks._layerChanged(targetLayerIndex)
def multiple_layerFilterAndMove(listOfInstructions):
"""Calls layerFilterAndMove multiple times, but only callbacks once in the end"""
for (sourceLayerIndex, targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum) in listOfInstructions:
for (sourceLayerIndex, targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum) in listOfInstructions:
layerFilterAndMove(sourceLayerIndex, targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum, callback=False)
for layerIndex in session.data.track.layers.keys():
for layerIndex in session.data.track.layers.keys():
callbacks._layerChanged(layerIndex)
def mulitple_filterAndMoveAllLayers(listOfInstructions):
"""Calls filterAndMoveAllLayers multiple times, but only callbacks once in the end"""
for (targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum) in listOfInstructions:
for sourceLayerIndex in session.data.track.layers.keys():
layerFilterAndMove(sourceLayerIndex, targetLayerIndex, statusByte, byte1RangeMinimum, byte1RangeMaximum, byte2RangeMinimum, byte2RangeMaximum, callback=False)
for layerIndex in session.data.track.layers.keys():
for layerIndex in session.data.track.layers.keys():
callbacks._layerChanged(layerIndex)
def filterTestCC():
@ -577,17 +642,17 @@ def filterTestCC():
(3, 0xC0, 0, 127, 0, 127), #move all Program Changes to layer 3
]
mulitple_filterAndMoveAllLayers(instructions)
def sendNoteOnToCbox(midipitch):
"""Not Realtime!
Caller is responsible to shut off the note"""
Caller is responsible to shut off the note"""
v = getActiveMedianVelocity()
callbacks._stepEntryNoteOn(midipitch, v)
callbacks._stepEntryNoteOn(midipitch, v)
cbox.send_midi_event(0x90, midipitch, v, output=session.data.track.sequencerInterface.cboxMidiOutUuid)
def sendNoteOffToCbox(midipitch):
"""Not Realtime!"""
callbacks._stepEntryNoteOff(midipitch, 0)
cbox.send_midi_event(0x80, midipitch, 0, output=session.data.track.sequencerInterface.cboxMidiOutUuid)
cbox.send_midi_event(0x80, midipitch, 0, output=session.data.track.sequencerInterface.cboxMidiOutUuid)

52
engine/main.py

@ -76,6 +76,7 @@ class Data(template.engine.sequencer.Score):
for event in self.copyBuffer:
ev = event.copy()
ev.layer = layerIndex
ev.parentLayer = self.track.layers[layerIndex]
assert not ev in self.allEventsById
self.allEventsById[id(ev)] = ev
result.append(ev)
@ -90,7 +91,7 @@ class Data(template.engine.sequencer.Score):
assert not self.eventCollectorForUndo is None
assert velocity > 0, velocity
self.noteOnsInProgress[self.track.activeLayer].add(note)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x90, note, velocity)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x90, note, velocity, "")
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
@ -103,32 +104,51 @@ class Data(template.engine.sequencer.Score):
if self.parentSession.recordingEnabled and not tickindex is None:
assert not self.eventCollectorForUndo is None
self.noteOnsInProgress[self.track.activeLayer].remove(note)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, note, velocity)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, note, velocity, "")
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def livePolyphonicAftertouch(self, tickindex:int, note:int, value:int):
"""0xA0 Connected via the api, which sends further callbacks to the GUI"""
if self.parentSession.recordingEnabled and not tickindex is None:
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xA0, note, value, "")
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def liveCC(self, tickindex:int, type:int, value:int):
"""Connected via the api, which sends further callbacks to the GUI"""
"""0xB0 Connected via the api, which sends further callbacks to the GUI"""
if self.parentSession.recordingEnabled and not tickindex is None:
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xB0, type, value)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xB0, type, value, "")
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def liveProgramChange(self, tickindex:int, value:int):
"""Connected via the api, which sends further callbacks to the GUI"""
if self.parentSession.recordingEnabled and not tickindex is None:
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xB0, value, 0) #Byte2 gets ignored
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xC0, value, 0, "") #Byte2 gets ignored
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def liveChannelPressure(self, tickindex:int, value:int):
"""Connected via the api, which sends further callbacks to the GUI"""
if self.parentSession.recordingEnabled and not tickindex is None:
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xD0, value, 0) #Byte2 gets ignored
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xD0, value, 0, "") #Byte2 gets ignored
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
def livePitchBend(self, tickindex:int, fine:int, coarse:int):
"""Pitchbend 0xE0 is a 14 bit value. Byte2 is coarse/MSB, byte1 is fine/LSB.
Many keyboards leave byte1 as 0 and use only byte2, making PitchBend 128steps.
There is also a CC to specify pitchbend musical range and sensitivity,
but that is for a synth, we just send the CC without knowing its implications"""
if self.parentSession.recordingEnabled and not tickindex is None:
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0xE0, fine, coarse, "")
self.eventCollectorForUndo.append(resultEvent)
return resultEvent
#def playbackStatusChanged(self, state:bool):
# """auto-triggered via api callback"""
@ -152,7 +172,7 @@ class Data(template.engine.sequencer.Score):
for layerIndex, noteOnBuffer in self.noteOnsInProgress.items():
for noteOnPitch in noteOnBuffer:
cbox.send_midi_event(0x80, noteOnPitch, 0, output=self.track.sequencerInterface.cboxMidiOutUuid)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, noteOnPitch, 0)
resultEvent = self.track.layers[self.track.activeLayer].newEvent(tickindex, 0x80, noteOnPitch, 0, "")
forcedNoteOffs.append(resultEvent)
self.eventCollectorForUndo.append(resultEvent)
noteOnBuffer.clear()
@ -262,7 +282,7 @@ class Layer(object):
self.color = "cyan"
self.events = defaultdict(list) # statusType: [Event]
self.dirty = False # indicates wether this needs re-export. Obviously not saved.
self.midiChannel = 1 #1-16 inclusive.
self._processAfterInit()
def _processAfterInit(self):
@ -275,8 +295,9 @@ class Layer(object):
"""The only place in the program where events get created, except Score.getCopyBufferCopy
and where self.parentTrack.parentData.allEventsById gets populated. """
ev = Event(tickindex, statusType, byte1, byte2, self.index, freeText)
ev.parentLayer = self
self.parentTrack.parentData.allEventsById[id(ev)] = ev
self.events[statusType].append(ev)
self.events[statusType].append(ev)
self.dirty = True
return ev
@ -284,6 +305,7 @@ class Layer(object):
"""Insert an existing Event object, that was at one time created through newEvent"""
assert not event in self.events[event.status]
self.events[event.status].append(event)
event.parentLayer = self
self.dirty = True
def deleteEvent(self, event):
@ -307,6 +329,7 @@ class Layer(object):
"index" : self.index,
"color" : self.color,
"events": events,
"midiChannel": self.midiChannel,
}
@classmethod
@ -315,6 +338,7 @@ class Layer(object):
self.parentTrack = parentTrack
self.color = serializedData["color"]
self.index = serializedData["index"]
self.midiChannel = serializedData["midiChannel"]
self.events = defaultdict(list) # statusType: [Event]
for seriEvent in serializedData["events"]: #tuples or list
self.newEvent(*seriEvent)
@ -389,8 +413,9 @@ class Event:
self.status = status # e.g. 0x90 for note-on or 0xE0 for pitchbend
self.byte1 = byte1 # e.g. 60 for middle c
self.byte2 = byte2 #eg. 100 for a rather loud note. Can be None for Program Changes.
self.layer = layer #0-9 incl. . Events need to know their layer for undo.
self.layer = layer #0-9 incl. . Events need to know their layer for undo.
self.freeText = freeText
#self.parentLayer = Injected and changed by the Layer itself.
def serialize(self)->tuple:
return (int(self.position), self.status, self.byte1, self.byte2, self.freeText)
@ -406,15 +431,16 @@ class Event:
"freeText" : self.freeText,
}
def toCboxBytes(self)->bytes:
byte1 = pitch.midiPitchLimiter(self.byte1, 0)
byte2 = pitch.midiPitchLimiter(self.byte2, 0)
status = self.status + self.parentLayer.midiChannel - 1 #we index channels from 1 to 16, so -1 here because status is itself already chan 1
if self.position >= 0:
return cbox.Pattern.serialize_event(self.position, self.status, byte1, byte2)
return cbox.Pattern.serialize_event(self.position, status, byte1, byte2)
else:
logging.warning(f"Event {self.byte1},{self.byte2} has position less than 0. Limiting to 0 in midi output. Please fix manually")
return cbox.Pattern.serialize_event(0, self.status, byte1, byte2)
return cbox.Pattern.serialize_event(0, status, byte1, byte2)
def __repr__(self):
return str(self.export())

2
qtgui/constantsAndConfigs.py

@ -30,5 +30,7 @@ class ConstantsAndConfigs(TemplateConstantsAndConfigs):
self.ticksToPixelRatio = 16 #D4 is 840 ticks. 840/16 = one quarter notes in pixel
self.stafflineGap = 10 #this is not only for Laborejo but also the grids vertical line spacing. Cannot be changed in runtime
self.scoreHeight = self.stafflineGap * 128 #notes
self.snapToDot = None #set during mainWindow init
self.snapToGrid = None #set during mainWindow init
constantsAndConfigs = ConstantsAndConfigs() #singleton

16
qtgui/designer/mainwindow.py

@ -13,17 +13,19 @@ from PyQt5 import QtCore, QtGui, QtWidgets
class Ui_MainWindow(object):
def setupUi(self, MainWindow):
MainWindow.setObjectName("MainWindow")
MainWindow.resize(800, 791)
MainWindow.resize(800, 604)
self.centralwidget = QtWidgets.QWidget(MainWindow)
self.centralwidget.setObjectName("centralwidget")
self.gridLayout = QtWidgets.QGridLayout(self.centralwidget)
self.gridLayout.setContentsMargins(0, 0, 0, 0)
self.gridLayout.setSpacing(0)
self.gridLayout.setObjectName("gridLayout")
self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget)
self.verticalLayout.setContentsMargins(3, 3, 3, 3)
self.verticalLayout.setSpacing(0)
self.verticalLayout.setObjectName("verticalLayout")
self.grid = QtWidgets.QGridLayout()
self.grid.setSpacing(0)
self.grid.setVerticalSpacing(0)
self.grid.setObjectName("grid")
self.gridLayout.addLayout(self.grid, 0, 0, 1, 1)
self.verticalLayout.addLayout(self.grid)
spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding)
self.verticalLayout.addItem(spacerItem)
MainWindow.setCentralWidget(self.centralwidget)
self.menubar = QtWidgets.QMenuBar(MainWindow)
self.menubar.setGeometry(QtCore.QRect(0, 0, 800, 20))

35
qtgui/designer/mainwindow.ui

@ -7,36 +7,49 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>791</height>
<height>604</height>
</rect>
</property>
<property name="windowTitle">
<string>Vico</string>
</property>
<widget class="QWidget" name="centralwidget">
<layout class="QGridLayout" name="gridLayout">
<property name="leftMargin">
<layout class="QVBoxLayout" name="verticalLayout">
<property name="spacing">
<number>0</number>
</property>
<property name="leftMargin">
<number>3</number>
</property>
<property name="topMargin">
<number>0</number>
<number>3</number>
</property>
<property name="rightMargin">
<number>0</number>
<number>3</number>
</property>
<property name="bottomMargin">
<number>0</number>
<number>3</number>
</property>
<property name="spacing">
<number>0</number>
</property>
<item row="0" column="0">
<item>
<layout class="QGridLayout" name="grid">
<property name="spacing">
<property name="verticalSpacing">
<number>0</number>
</property>
</layout>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<widget class="QMenuBar" name="menubar">

80
qtgui/inputcursor.py

@ -28,12 +28,14 @@ from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.helper import stretchRect, invertColor
from template.engine import pitch
from template.engine.pitch import simpleNoteNames
from template.engine.midi import programList
#User modules
from .constantsAndConfigs import constantsAndConfigs
import engine.api as api
from .items import CC as CCItem
from .items import PolyphonicAftertouch as PolyphonicAftertouchItem
class InputCursor(QtWidgets.QGraphicsItem):
"""A singleton instance that gets attached to the cursor while it is on the screen.
@ -66,7 +68,20 @@ class InputCursor(QtWidgets.QGraphicsItem):
self.ccStamp = QtWidgets.QGraphicsPolygonItem(CCItem.triangle)
self.ccStamp.setPen(pen)
self.ccStamp.setParentItem(self)
self.ccStamp.setParentItem(self)
self.polyphonicAftertouchStamp = QtWidgets.QGraphicsPolygonItem(PolyphonicAftertouchItem.triangle)
self.polyphonicAftertouchStamp.setPen(pen)
self.polyphonicAftertouchStamp.setParentItem(self)
self.channelPressureStamp = QtWidgets.QGraphicsSimpleTextItem("x")
self.channelPressureStamp.setPos(0,-4)
self.channelPressureStamp.setPen(pen)
self.channelPressureStamp.setParentItem(self)
self.pitchbendStamp = QtWidgets.QGraphicsEllipseItem(0,0,constantsAndConfigs.stafflineGap,constantsAndConfigs.stafflineGap) #x, y, w, h, same as rect
self.pitchbendStamp.setPen(pen)
self.pitchbendStamp.setParentItem(self)
self.programChangeStamp = QtWidgets.QGraphicsRectItem(0,-2, constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap) #x, y, w, h
self.programChangeStamp.setPen(pen)
@ -120,14 +135,20 @@ class InputCursor(QtWidgets.QGraphicsItem):
self.label.hide()
self.noteStamp.hide()
self.ccStamp.hide()
self.polyphonicAftertouchStamp.hide()
self.programChangeStamp.hide()
self.pitchbendStamp.hide()
self.channelPressureStamp.hide()
#TODO: the others
def _setStampColors(self, layerIndex:int):
c = self.layerColors[layerIndex]
self.noteStamp.setBrush(c)
self.ccStamp.setBrush(c)
self.polyphonicAftertouchStamp.setBrush(c)
self.programChangeStamp.setBrush(c)
self.pitchbendStamp.setBrush(c)
self.channelPressureStamp.setBrush(c)
def setNoteStampDuration(self, value:int):
"""Automatically converts to pixel. This is not an engine object so no api calls are made"""
@ -143,9 +164,16 @@ class InputCursor(QtWidgets.QGraphicsItem):
def _updateLabel(self):
if self.pitch and self.pitch > 0 and self.pitch <= 127:
if self.currentMode == "note":
labelInfo = pitch.midi_notenames_english[self.pitch] + " (" + str(self.pitch) + ")"
#labelInfo = pitch.midi_notenames_english[self.pitch] + " (" + str(self.pitch) + ")"
labelInfo = simpleNoteNames["English"][self.pitch] + " (" + str(self.pitch) + ")"
elif self.currentMode == "cc":
labelInfo = "CC " + str(api.session.guiSharedDataToSave["lastCCtype"]) +": " + str(self.pitch)
elif self.currentMode == "polyphonicaftertouch":
labelInfo = "PA " + str(api.session.guiSharedDataToSave["lastPolyphonicAftertouchNote"]) +": " + str(self.pitch)
elif self.currentMode == "pitchbend":
labelInfo = "Pitch Bend (64=off)" +": " + str(self.pitch)
elif self.currentMode == "program":
labelInfo = "Program Change to" + str(self.pitch) + ": " + programList[self.pitch]
else:
labelInfo = str(self.pitch)
self.label.setText(labelInfo)
@ -164,15 +192,8 @@ class InputCursor(QtWidgets.QGraphicsItem):
self.parentScene.parentView.setCursor(QtCore.Qt.BlankCursor)
self.show() #We are the cursor now
# X Position in Time
if self.currentMode == "note" and self.currentDuration and self._cachedBBT and self._cachedBBT["denominator"]:
#checking for note and duration takes out freehand
denomi = self.currentDuration / constantsAndConfigs.ticksToPixelRatio
x = round(scenePos.x() / denomi ) * denomi #snap to grid and duration
x = int(x)
elif self.duringFreehandDrawing:
# X Position in Time
if self.duringFreehandDrawing:
#x = self.noteStamp.scenePos().x() #we need the absolute scenePos. No even better to rely on our own data, not qt inteference. See next line
assert not self.duringFreehandDrawing is None
x = self.duringFreehandDrawing
@ -185,6 +206,8 @@ class InputCursor(QtWidgets.QGraphicsItem):
else:
x = scenePos.x()
x = round(x / constantsAndConfigs.snapToGrid ) * constantsAndConfigs.snapToGrid #snap to grid and duration
if self.duringFreehandDrawing:
super().setX(x)
else:
@ -214,12 +237,22 @@ class InputCursor(QtWidgets.QGraphicsItem):
elif self.currentMode == "note" and self.currentDuration == None:
self.startFreeHandDrawing()
elif self.currentMode == "polyphonicaftertouch":
api.createEvent(tickposition, 0xA0, api.session.guiSharedDataToSave["lastPolyphonicAftertouchNote"], byte1) #Yes, we reverse the bytes! See user manual.
elif self.currentMode == "cc":
api.createEvent(tickposition, 0xB0, api.session.guiSharedDataToSave["lastCCtype"], byte1) #Yes, we reverse the bytes! See user manual.
elif self.currentMode == "program":
api.createEvent(tickposition, 0xC0, byte1, 0) #Byte 2 is ignored
elif self.currentMode == "pitchbend":
api.createEvent(tickposition, 0xE0, 0, byte1) #Byte 1 is ignored in Vico
elif self.currentMode == "channelpressure":
api.createEvent(tickposition, 0xD0, byte1, 0) #Byte 2 is ignored
else:
print (tickposition, status, byte1)
@ -302,11 +335,24 @@ class InputCursor(QtWidgets.QGraphicsItem):
r.setWidth(4)
self.noteStamp.setRect(r)
self.noteStamp.show()
elif mode == "actionSetInsertCC":
self.currentMode = "cc"
self.currentDuration = None
self.currentStamp = self.ccStamp
self.ccStamp.show()
self.ccStamp.show()
elif mode == "actionSetPolyphonicAftertouch":
self.currentMode = "polyphonicaftertouch"
self.currentDuration = None
self.currentStamp = self.polyphonicAftertouchStamp
self.polyphonicAftertouchStamp.show()
elif mode == "actionSetInsertPitchBend":
self.currentMode = "pitchbend"
self.currentDuration = None
self.currentStamp = self.pitchbendStamp
self.pitchbendStamp.show()
elif mode == "actionSetInsertProgramChange":
self.currentMode = "program"
@ -314,6 +360,12 @@ class InputCursor(QtWidgets.QGraphicsItem):
self.currentStamp = self.programChangeStamp
self.programChangeStamp.show()
elif mode == "actionSetInsertChannelPressure":
self.currentMode = "channelpressure"
self.currentDuration = None
self.currentStamp = self.channelPressureStamp
self.channelPressureStamp.show()
elif mode == "actionSetInserToggleDot":
pass
else:

121
qtgui/items.py

@ -28,6 +28,8 @@ from PyQt5 import QtWidgets, QtCore, QtGui
#Template Modules
from template.qtgui.helper import stretchRect, invertColor
from template.engine.midi import programList
#User modules
from .constantsAndConfigs import constantsAndConfigs
@ -355,6 +357,88 @@ class CC(_EventTraits, QtWidgets.QGraphicsPolygonItem): #resolution order is imp
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class PolyphonicAftertouch(_EventTraits, QtWidgets.QGraphicsPolygonItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
"""See CC. Same system. We flip the bytes for easier intuitive editing."""
triangle = QtGui.QPolygonF()
#Upside Down
triangle.append(QtCore.QPointF(0,0))
triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap/2, constantsAndConfigs.stafflineGap))
triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, 0))
triangle.append(QtCore.QPointF(0,0))
#triangle.append(QtCore.QPointF(0,0))
#triangle.append(QtCore.QPointF(0, constantsAndConfigs.stafflineGap))
#triangle.append(QtCore.QPointF(constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap/2))
#triangle.append(QtCore.QPointF(0,0))
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(PolyphonicAftertouch.triangle)
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
self.label = QtWidgets.QGraphicsSimpleTextItem("PA " + str(byte1))
self.label.setEnabled(False) #prevents the child item from ending up in the selection
self.label.setParentItem(self)
#self.label.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations). Prevents zoom
self.label.setScale(0.5)
self.label.setPos(1.1*constantsAndConfigs.stafflineGap, 0)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte2 = pitch
def callbackByteOne(self, value):
self.byte1 = value
def callbackByteTwo(self, value):
self.byte2 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class PitchBend(_EventTraits, QtWidgets.QGraphicsEllipseItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
"""Byte1 is always zero in Vico."""
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__(0, 0, constantsAndConfigs.stafflineGap, constantsAndConfigs.stafflineGap) #x, y, w, h, like a Rect
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText)
#self.label = QtWidgets.QGraphicsSimpleTextItem("CC " + str(byte1))
#self.label.setEnabled(False) #prevents the child item from ending up in the selection
#self.label.setParentItem(self)
#self.label.setFlag(QtWidgets.QGraphicsItem.ItemIgnoresTransformations). Prevents zoom
#self.label.setScale(0.5)
#self.label.setPos(1.1*constantsAndConfigs.stafflineGap, 0)
def setPianoRollPitch(self, pitch):
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte2 = pitch
def callbackByteOne(self, value):
self.byte1 = value
def callbackByteTwo(self, value):
self.byte2 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
class ProgramChange(_EventTraits, QtWidgets.QGraphicsRectItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
@ -374,7 +458,7 @@ class ProgramChange(_EventTraits, QtWidgets.QGraphicsRectItem): #resolution orde
self.freeText.setRotation(-45) #from otherinit
self.label = QtWidgets.QGraphicsSimpleTextItem("Program Change to " + str(self.byte1))
self.label = QtWidgets.QGraphicsSimpleTextItem("Program Change to " + str(self.byte1) + ": " + programList[self.byte1])
self.label.setEnabled(False) #prevents the child item from ending up in the selection
self.label.setParentItem(self)
self.label.setRotation(-45)
@ -386,13 +470,44 @@ class ProgramChange(_EventTraits, QtWidgets.QGraphicsRectItem): #resolution orde
"""This is a true engine value where 0 is lowest and 127 highest. Not the inverted
GraphicsScene coordinates"""
self.byte1 = pitch
self.label.setText("Program Change to " + str(self.byte1))
self.label.setText("Program Change to " + str(self.byte1) + ": " + programList[self.byte1])
def callbackByteOne(self, value):
self.byte1 = value
y = (127-value) * constantsAndConfigs.stafflineGap
self.setY(y)
self.label.setText("Program Change to " + str(self.byte1))
self.label.setText("Program Change to " + str(self.byte1) + ": " + programList[self.byte1])
def callbackByteTwo(self, value):
self.byte2 = value
class ChannelPressure(_EventTraits, QtWidgets.QGraphicsSimpleTextItem): #resolution order is important. _EventTraits needs to come first, otherwise class _EventTraits will not find super()
def __init__(self, parentLayer, parentLayerIndex:int, byte1:int, byte2:int, color, freeText:str):
"""Position in the layer/scene is not calculated in the item itself but outside"""
super().__init__("x")
assert byte2 == 0
self.setParentItem(parentLayer)
self.parentLayer = parentLayer
self.parentLayerIndex = parentLayerIndex
self.byte1 = byte1
self.byte2 = byte2
self.otherinit(color, freeText<