Browse Source

More apcmini functions

master
Nils 1 year 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)
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():
"""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"""
@ -586,9 +599,30 @@ def changeCurrentTrack(trackId):
"""This is for communication between the GUI and APCmini controller. The engine has no concept
of a current track."""
track = session.data.trackById(trackId)
assert track
assert track, trackId
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):
if scale:
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 = {
64: "arrow_up", #Move the pattern-viewing area
65: "arrow_down", #Move the pattern-viewing area
66: "arrow_left", #Move the pattern-viewing area
67: "arrow_right", #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. +Shift: Move Track Down
66: "arrow_left", #Move the pattern-viewing area. +Shift: Measure Left
67: "arrow_right", #Move the pattern-viewing area. +Shift: Measure Right
#Fader Controls
68: "volume",
@ -52,14 +52,14 @@ APCmini_MAP = {
71: "device",
#Scene Launch
82: "clip_stop",
83: "solo",
82: "clip_stop", #Start / Pause
83: "solo", #Loop
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)
86: "select",
87: "unlabeled_1",
88: "unlabeled_2",
89: "stop_all_clips",
87: "unlabeled_upper", #Undo. Redo with shift
88: "unlabeled_lower", #Reset pattern-viewing area to 0,0. Clear current pattern with shift.
89: "stop_all_clips", #Rewind and stop
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.subdivisionsChanged.append(self.callback_subdivisionsChanged)
api.callbacks.exportCacheChanged.append(self.callback_cacheExportDict)
api.callbacks.playbackStatusChanged.append(self.callback_playbackStatusChanged)
api.callbacks.loopChanged.append(self.callback_loopChanged)
#Prepare various patterns.
#"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)
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"]:
self.viewAreaUpDown(-1)
if self.shiftIsActive:
api.currentTrackBy(self.currentTrack["id"], 1)
else:
self.viewAreaUpDown(-1)
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"]:
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:
logging.info(f"Unhandled APCmini noteOn. Pitch {pitch}, Velocity {velocity}")
@ -391,7 +433,7 @@ class ApcMiniInput(MidiInput):
if self.viewAreaUpDownOffset > 0:
self.viewAreaUpDownOffset = 0
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
return # there is no need for further scrolling.
@ -412,7 +454,7 @@ class ApcMiniInput(MidiInput):
if self.viewAreaLeftRightOffset > 0:
self.viewAreaLeftRightOffset = 0
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
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.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

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.
#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
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)
self.ui.gridView.horizontalScrollBar().setSliderPosition(0)
@ -240,7 +240,7 @@ class MainWindow(TemplateMainWindow):
api.setLoopMeasureFactor(self.ui.loopMeasureFactorSpinBox.value())
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.
newCurrentTrack is a backend track ID
@ -259,7 +259,6 @@ class MainWindow(TemplateMainWindow):
"""
if self._blockCurrentTrackSignal:
return
self._blockCurrentTrackSignal = True
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!
self.currentTrackId = newCurrentTrackId
if sendChangeToApi:
self._blockCurrentTrackSignal = True
api.changeCurrentTrack(newCurrentTrackId)
self._blockCurrentTrackSignal = False
api.changeCurrentTrack(newCurrentTrackId)
self._blockCurrentTrackSignal = False
def addPatternTrack(self):
"""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.
newTrackId = api.addTrack(scale)
self.chooseCurrentTrack(self.songEditor.tracks[newTrackId].exportDict)
self.chooseCurrentTrack(self.songEditor.tracks[newTrackId].exportDict, sendChangeToApi=True)
def cloneSelectedTrack(self):
"""Add a new track via the template option. This is the best track add function"""
newTrackExporDict = api.createSiblingTrack(self.currentTrackId)
self.chooseCurrentTrack(newTrackExporDict)
self.chooseCurrentTrack(newTrackExporDict, sendChangeToApi=True)
def _populatePatternToolbar(self):
"""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:
self.parentScene.parentView.parentMainWindow.patternGrid.createShadow(self.exportDict)
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
assert not self._mousePressOn
position = self.scenePos2switchPosition(event.scenePos().x()) #measure number 0 based
@ -539,7 +539,7 @@ class TrackStructure(QtWidgets.QGraphicsRectItem):
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.
#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):
self.statusMessage("")
@ -947,7 +947,7 @@ class TrackLabelEditor(QtWidgets.QGraphicsScene):
del trackLabel
if toDelete and self.parentView.parentMainWindow.currentTrackId in toDelete: #toDelete still exist if tracks were deleted above
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.setSceneRect(0,0,width-1,self.cachedCombinedTrackHeight + 3*SIZE_TOP_OFFSET)
@ -1131,7 +1131,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.parentTrackLabel.statusMessage("")
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())
@ -1150,7 +1150,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def mousePressEvent(self, event):
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()
colorDialog = QtWidgets.QColorDialog()
color = colorDialog.getColor(self.brush().color()) #blocks
@ -1240,7 +1240,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
def mousePressEvent(self,event):
"""We also need to force this track as active"""
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
#super().mousePressEvent(event)
@ -1279,7 +1279,7 @@ class TrackLabel(QtWidgets.QGraphicsRectItem):
self.lengthMultiplicatorSpinBox.spinBox.blockSignals(False)
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
#def mouseReleaseEvent(self, event):

3
template/engine/api.py

@ -381,6 +381,9 @@ def playPause():
cbox.Transport.play()
#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:
return cbox.Transport.status().pos_ppqn

Loading…
Cancel
Save