Browse Source

More apcmini functions

master
Nils 2 years ago
parent
commit
a1bff529ca
  1. 36
      engine/api.py
  2. 85
      engine/input_apcmini.py
  3. 15
      qtgui/mainwindow.py
  4. 14
      qtgui/songeditor.py
  5. 3
      template/engine/api.py

36
engine/api.py

@ -383,6 +383,19 @@ def seek(value):
cbox.Transport.seek_ppqn(value) cbox.Transport.seek_ppqn(value)
def seekMeasureLeft():
"""This skips one base measure, not the multiplicator one"""
now = cbox.Transport.status().pos_ppqn
oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions
seek(now - oneMeasureInTicks)
def seekMeasureRight():
"""This skips one base measure, not the multiplicator one"""
now = cbox.Transport.status().pos_ppqn
oneMeasureInTicks = (session.data.howManyUnits * session.data.whatTypeOfUnit) / session.data.subdivisions
seek(now + oneMeasureInTicks)
def getGlobalOffset(): def getGlobalOffset():
"""Return the current offsets in full measures + free tick value 3rd: Cached abolute tick value """Return the current offsets in full measures + free tick value 3rd: Cached abolute tick value
gets updated everytime the time signature changes or setGlobalOffset is called""" gets updated everytime the time signature changes or setGlobalOffset is called"""
@ -586,9 +599,30 @@ def changeCurrentTrack(trackId):
"""This is for communication between the GUI and APCmini controller. The engine has no concept """This is for communication between the GUI and APCmini controller. The engine has no concept
of a current track.""" of a current track."""
track = session.data.trackById(trackId) track = session.data.trackById(trackId)
assert track assert track, trackId
callbacks._currentTrackChanged(track) callbacks._currentTrackChanged(track)
def currentTrackBy(currentTrackId, value:int):
"""Convenience for the apcMiniController or a GUI that wants shortcuts.
Only use +1 and -1 for value for stepping.
We do NOT test for other values if the overshoot the session.data.tracks index!!
We need to know what the current track is because the engine doesn't know it.
Ignores invisible tracks, aka tracks in a GUI-folded group.
"""
assert value in (-1, 1), value
currentTrack = session.data.trackById(currentTrackId)
assert currentTrack, currentTrackId
currentIndex = session.data.tracks.index(currentTrack)
if value == -1 and currentIndex == 0: return #already first track
elif value == 1 and len(session.data.tracks) == currentIndex+1: return #already last track
newCurrentTrack = session.data.tracks[currentIndex+value]
changeCurrentTrack(id(newCurrentTrack))
def addTrack(scale=None): def addTrack(scale=None):
if scale: if scale:
assert type(scale) == tuple assert type(scale) == tuple

85
engine/input_apcmini.py

@ -40,10 +40,10 @@ that uses the api directly, which in turn triggers the GUI.
""" """
APCmini_MAP = { APCmini_MAP = {
64: "arrow_up", #Move the pattern-viewing area 64: "arrow_up", #Move the pattern-viewing area. +Shift: Move Track Up
65: "arrow_down", #Move the pattern-viewing area 65: "arrow_down", #Move the pattern-viewing area. +Shift: Move Track Down
66: "arrow_left", #Move the pattern-viewing area 66: "arrow_left", #Move the pattern-viewing area. +Shift: Measure Left
67: "arrow_right", #Move the pattern-viewing area 67: "arrow_right", #Move the pattern-viewing area. +Shift: Measure Right
#Fader Controls #Fader Controls
68: "volume", 68: "volume",
@ -52,14 +52,14 @@ APCmini_MAP = {
71: "device", 71: "device",
#Scene Launch #Scene Launch
82: "clip_stop", 82: "clip_stop", #Start / Pause
83: "solo", 83: "solo", #Loop
84: "rec_arm", #Send Buttons to Patroneo. Default on. 84: "rec_arm", #Send Buttons to Patroneo. Default on.
85: "mute", #Don't make sounds to midi-thru when you press a button. Default on (no sound) 85: "mute", #Don't make sounds to midi-thru when you press a button. Default on (no sound)
86: "select", 86: "select",
87: "unlabeled_1", 87: "unlabeled_upper", #Undo. Redo with shift
88: "unlabeled_2", 88: "unlabeled_lower", #Reset pattern-viewing area to 0,0. Clear current pattern with shift.
89: "stop_all_clips", 89: "stop_all_clips", #Rewind and stop
98: "shift", #This is just a button. There is no shift layer. 98: "shift", #This is just a button. There is no shift layer.
} }
@ -147,6 +147,8 @@ class ApcMiniInput(MidiInput):
api.callbacks.currentTrackChanged.append(self.callback_currentTrackChanged) api.callbacks.currentTrackChanged.append(self.callback_currentTrackChanged)
api.callbacks.subdivisionsChanged.append(self.callback_subdivisionsChanged) api.callbacks.subdivisionsChanged.append(self.callback_subdivisionsChanged)
api.callbacks.exportCacheChanged.append(self.callback_cacheExportDict) api.callbacks.exportCacheChanged.append(self.callback_cacheExportDict)
api.callbacks.playbackStatusChanged.append(self.callback_playbackStatusChanged)
api.callbacks.loopChanged.append(self.callback_loopChanged)
#Prepare various patterns. #Prepare various patterns.
#"clear music button leds" pattern: #"clear music button leds" pattern:
@ -374,13 +376,53 @@ class ApcMiniInput(MidiInput):
cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
elif pitch == APCmini_MAP["arrow_up"]: elif pitch == APCmini_MAP["arrow_up"]:
self.viewAreaUpDown(+1) if self.shiftIsActive:
api.currentTrackBy(self.currentTrack["id"], -1)
else:
self.viewAreaUpDown(+1)
elif pitch == APCmini_MAP["arrow_down"]: elif pitch == APCmini_MAP["arrow_down"]:
self.viewAreaUpDown(-1) if self.shiftIsActive:
api.currentTrackBy(self.currentTrack["id"], 1)
else:
self.viewAreaUpDown(-1)
elif pitch == APCmini_MAP["arrow_left"]: elif pitch == APCmini_MAP["arrow_left"]:
self.viewAreaLeftRight(+1) #Yes, it is inverted scrolling if self.shiftIsActive:
api.seekMeasureLeft()
else:
self.viewAreaLeftRight(+1) #Yes, it is inverted scrolling
elif pitch == APCmini_MAP["arrow_right"]: elif pitch == APCmini_MAP["arrow_right"]:
self.viewAreaLeftRight(-1) if self.shiftIsActive:
api.seekMeasureRight()
else:
self.viewAreaLeftRight(-1)
elif pitch == APCmini_MAP["unlabeled_upper"]: #Undo
if self.shiftIsActive: #redo
api.redo()
else:
api.undo()
elif pitch == APCmini_MAP["unlabeled_lower"]: #Reset View Area and Reset Pattern(!) with shift
if self.shiftIsActive: #Clear current pattern:
api.patternOffAllSteps(self.currentTrack["id"])
else:
self.viewAreaUpDownOffset = 0
self.viewAreaLeftRightOffset = 0
self.sendApcNotePattern(self.currentTrack)
cbox.send_midi_event(0x90, APCmini_MAP["arrow_up"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
cbox.send_midi_event(0x90, APCmini_MAP["arrow_down"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
cbox.send_midi_event(0x90, APCmini_MAP["arrow_left"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
cbox.send_midi_event(0x90, APCmini_MAP["arrow_right"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
elif pitch == APCmini_MAP["clip_stop"]:
api.playPause()
elif pitch == APCmini_MAP["solo"]:
api.toggleLoop()
elif pitch == APCmini_MAP["stop_all_clips"]:
api.stop()
api.rewind()
else: else:
logging.info(f"Unhandled APCmini noteOn. Pitch {pitch}, Velocity {velocity}") logging.info(f"Unhandled APCmini noteOn. Pitch {pitch}, Velocity {velocity}")
@ -391,7 +433,7 @@ class ApcMiniInput(MidiInput):
if self.viewAreaUpDownOffset > 0: if self.viewAreaUpDownOffset > 0:
self.viewAreaUpDownOffset = 0 self.viewAreaUpDownOffset = 0
if wasZero: return #no visible change. User tried to go too far. if wasZero: return #no visible change. User tried to go too far.
elif abs(self.viewAreaUpDownOffset - 8) > self.currentTrack["numberOfSteps"]: elif value < 0 and abs(self.viewAreaUpDownOffset - 8) > self.currentTrack["numberOfSteps"]:
self.viewAreaUpDownOffset -= value #take it back self.viewAreaUpDownOffset -= value #take it back
return # there is no need for further scrolling. return # there is no need for further scrolling.
@ -412,7 +454,7 @@ class ApcMiniInput(MidiInput):
if self.viewAreaLeftRightOffset > 0: if self.viewAreaLeftRightOffset > 0:
self.viewAreaLeftRightOffset = 0 self.viewAreaLeftRightOffset = 0
if wasZero: return #no visible change. User tried to go too far. if wasZero: return #no visible change. User tried to go too far.
elif abs(self.viewAreaLeftRightOffset - 8) > self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]: elif value < 0 and abs(self.viewAreaLeftRightOffset - 8) > self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]:
self.viewAreaLeftRightOffset -= value #take it back self.viewAreaLeftRightOffset -= value #take it back
return # there is no need for further scrolling. return # there is no need for further scrolling.
@ -516,4 +558,17 @@ class ApcMiniInput(MidiInput):
self.mainStepMap_subdivisions = [not stepNo % subdivisions for stepNo in range(self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]) ] self.mainStepMap_subdivisions = [not stepNo % subdivisions for stepNo in range(self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]) ]
self.sendApcNotePattern(self.currentTrack) self.sendApcNotePattern(self.currentTrack)
def callback_playbackStatusChanged(self, status:bool):
if status:
cbox.send_midi_event(0x90, APCmini_MAP["clip_stop"], APCmini_COLOR["green_blink"], output=self.cboxMidiOutUuid)
else:
cbox.send_midi_event(0x90, APCmini_MAP["clip_stop"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
def callback_loopChanged(self, measureNumber:int):
if measureNumber is None:
cbox.send_midi_event(0x90, APCmini_MAP["solo"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid)
else:
cbox.send_midi_event(0x90, APCmini_MAP["solo"], APCmini_COLOR["green_blink"], output=self.cboxMidiOutUuid)
apcMiniInput = ApcMiniInput() #global to use in other parts of the program apcMiniInput = ApcMiniInput() #global to use in other parts of the program

15
qtgui/mainwindow.py

@ -225,7 +225,7 @@ class MainWindow(TemplateMainWindow):
#There is always a track. Forcing that to be active is better than having to hide all the pattern widgets, or to disable them. #There is always a track. Forcing that to be active is better than having to hide all the pattern widgets, or to disable them.
#However, we need the engine to be ready. #However, we need the engine to be ready.
#Now in 2021-12-13 this is still necessary because of regressions! Eventhough the API sends a current track on startup as convenience for the GUI and hardware controllers #Now in 2021-12-13 this is still necessary because of regressions! Eventhough the API sends a current track on startup as convenience for the GUI and hardware controllers
self.chooseCurrentTrack(api.session.data.tracks[0].export()) #By Grabthar's hammer, by the suns of Worvan, what a hack! self.chooseCurrentTrack(api.session.data.tracks[0].export(), sendChangeToApi=False) #By Grabthar's hammer, by the suns of Worvan, what a hack!
api.callbacks.currentTrackChanged.append(self.chooseCurrentTrack) api.callbacks.currentTrackChanged.append(self.chooseCurrentTrack)
self.ui.gridView.horizontalScrollBar().setSliderPosition(0) self.ui.gridView.horizontalScrollBar().setSliderPosition(0)
@ -240,7 +240,7 @@ class MainWindow(TemplateMainWindow):
api.setLoopMeasureFactor(self.ui.loopMeasureFactorSpinBox.value()) api.setLoopMeasureFactor(self.ui.loopMeasureFactorSpinBox.value())
self.ui.loopMeasureFactorSpinBox.blockSignals(False) self.ui.loopMeasureFactorSpinBox.blockSignals(False)
def chooseCurrentTrack(self, exportDict): def chooseCurrentTrack(self, exportDict, sendChangeToApi=False):
"""This is in mainWindow because we need access to different sections of the program. """This is in mainWindow because we need access to different sections of the program.
newCurrentTrack is a backend track ID newCurrentTrack is a backend track ID
@ -259,7 +259,6 @@ class MainWindow(TemplateMainWindow):
""" """
if self._blockCurrentTrackSignal: if self._blockCurrentTrackSignal:
return return
self._blockCurrentTrackSignal = True
newCurrentTrackId = exportDict["id"] newCurrentTrackId = exportDict["id"]
@ -282,20 +281,22 @@ class MainWindow(TemplateMainWindow):
#Functions depend on getting set after getting called. They need to know the old track! #Functions depend on getting set after getting called. They need to know the old track!
self.currentTrackId = newCurrentTrackId self.currentTrackId = newCurrentTrackId
if sendChangeToApi:
self._blockCurrentTrackSignal = True
api.changeCurrentTrack(newCurrentTrackId)
self._blockCurrentTrackSignal = False
api.changeCurrentTrack(newCurrentTrackId)
self._blockCurrentTrackSignal = False
def addPatternTrack(self): def addPatternTrack(self):
"""Add a new track and initialize it with some data from the current one""" """Add a new track and initialize it with some data from the current one"""
scale = api.session.data.trackById(self.currentTrackId).pattern.scale #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner. scale = api.session.data.trackById(self.currentTrackId).pattern.scale #TODO: Access to the sessions data structure directly instead of api. Not good. Getter function or api @property is cleaner.
newTrackId = api.addTrack(scale) newTrackId = api.addTrack(scale)
self.chooseCurrentTrack(self.songEditor.tracks[newTrackId].exportDict) self.chooseCurrentTrack(self.songEditor.tracks[newTrackId].exportDict, sendChangeToApi=True)
def cloneSelectedTrack(self): def cloneSelectedTrack(self):
"""Add a new track via the template option. This is the best track add function""" """Add a new track via the template option. This is the best track add function"""
newTrackExporDict = api.createSiblingTrack(self.currentTrackId) newTrackExporDict = api.createSiblingTrack(self.currentTrackId)
self.chooseCurrentTrack(newTrackExporDict) self.chooseCurrentTrack(newTrackExporDict, sendChangeToApi=True)
def _populatePatternToolbar(self): def _populatePatternToolbar(self):
"""Called once at the creation of the GUI""" """Called once at the creation of the GUI"""

14
qtgui/songeditor.py

@ -485,7 +485,7 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
if event.button() == QtCore.Qt.MiddleButton and not self._mousePressOn: if event.button() == QtCore.Qt.MiddleButton and not self._mousePressOn:
self.parentScene.parentView.parentMainWindow.patternGrid.createShadow(self.exportDict) self.parentScene.parentView.parentMainWindow.patternGrid.createShadow(self.exportDict)
else: else:
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict) self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
if event.button() == QtCore.Qt.LeftButton: #Create a switch or continue to hold down mouse button and drag to draw -> mouseMoveEvent if event.button() == QtCore.Qt.LeftButton: #Create a switch or continue to hold down mouse button and drag to draw -> mouseMoveEvent
assert not self._mousePressOn assert not self._mousePressOn
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
@ -539,7 +539,7 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
self._highlightSwitch.show() self._highlightSwitch.show()
self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Empty Measure: Left click to activate. Middle click to show as shadows in current pattern. Right click for measure group options.")) #Yes, this is the track. Empty measures are not objects. self.statusMessage(QtCore.QCoreApplication.translate("Statusbar", "Empty Measure: Left click to activate. Middle click to show as shadows in current pattern. Right click for measure group options.")) #Yes, this is the track. Empty measures are not objects.
#This seemed to be a good idea but horrible UX. If you move the mouse down to edit a pattern you end up choosing the last track #This seemed to be a good idea but horrible UX. If you move the mouse down to edit a pattern you end up choosing the last track
#self.scene().parentView.parentMainWindow.chooseCurrentTrack(self.exportDict) #self.scene().parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
def hoverLeaveEvent(self, event): def hoverLeaveEvent(self, event):
self.statusMessage("") self.statusMessage("")
@ -947,7 +947,7 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
del trackLabel del trackLabel
if toDelete and self.parentView.parentMainWindow.currentTrackId in toDelete: #toDelete still exist if tracks were deleted above if toDelete and self.parentView.parentMainWindow.currentTrackId in toDelete: #toDelete still exist if tracks were deleted above
anyExistingTrack = next(iter(self.tracks.values())) anyExistingTrack = next(iter(self.tracks.values()))
self.parentView.parentMainWindow.chooseCurrentTrack(anyExistingTrack.exportDict) self.parentView.parentMainWindow.chooseCurrentTrack(anyExistingTrack.exportDict, sendChangeToApi=True)
self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT + groupOffset - hiddenOffsetCounter self.cachedCombinedTrackHeight = len(self.tracks) * SIZE_UNIT + groupOffset - hiddenOffsetCounter
self.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET) self.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET)
@ -1131,7 +1131,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.parentTrackLabel.statusMessage("") self.parentTrackLabel.statusMessage("")
def spinBoxValueChanged(self): def spinBoxValueChanged(self):
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict) self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict, sendChangeToApi=True)
api.setTrackPatternLengthMultiplicator(self.parentTrackLabel.exportDict["id"], self.spinBox.value()) api.setTrackPatternLengthMultiplicator(self.parentTrackLabel.exportDict["id"], self.spinBox.value())
@ -1150,7 +1150,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def mousePressEvent(self, event): def mousePressEvent(self, event):
if event.button() == QtCore.Qt.LeftButton: if event.button() == QtCore.Qt.LeftButton:
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict) self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict, sendChangeToApi=True)
event.accept() event.accept()
colorDialog = QtWidgets.QColorDialog() colorDialog = QtWidgets.QColorDialog()
color = colorDialog.getColor(self.brush().color()) #blocks color = colorDialog.getColor(self.brush().color()) #blocks
@ -1240,7 +1240,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def mousePressEvent(self,event): def mousePressEvent(self,event):
"""We also need to force this track as active""" """We also need to force this track as active"""
event.accept() #we need this for doubleClick event.accept() #we need this for doubleClick
self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict) self.parentTrackLabel.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.parentTrackLabel.exportDict, sendChangeToApi=True)
#event.ignore() #send to parent instead #event.ignore() #send to parent instead
#super().mousePressEvent(event) #super().mousePressEvent(event)
@ -1279,7 +1279,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.lengthMultiplicatorSpinBox.spinBox.blockSignals(False) self.lengthMultiplicatorSpinBox.spinBox.blockSignals(False)
def mousePressEvent(self,event): def mousePressEvent(self,event):
self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict) self.parentScene.parentView.parentMainWindow.chooseCurrentTrack(self.exportDict, sendChangeToApi=True)
#event.ignore() #don't add that here. Will destroy mouseMove and Release events of child widgets #event.ignore() #don't add that here. Will destroy mouseMove and Release events of child widgets
#def mouseReleaseEvent(self, event): #def mouseReleaseEvent(self, event):

3
template/engine/api.py

@ -381,6 +381,9 @@ def playPause():
cbox.Transport.play() cbox.Transport.play()
#It takes a few ms for playbackStatus to catch up. If you call it here again it will not be updated. #It takes a few ms for playbackStatus to catch up. If you call it here again it will not be updated.
def stop():
cbox.Transport.stop()
def getPlaybackTicks()->int: def getPlaybackTicks()->int:
return cbox.Transport.status().pos_ppqn return cbox.Transport.status().pos_ppqn

Loading…
Cancel
Save