|
|
@ -39,7 +39,7 @@ This file is used directly. It is a global part of the engine (see bottom of the |
|
|
|
that uses the api directly, which in turn triggers the GUI. |
|
|
|
""" |
|
|
|
|
|
|
|
APCmini_MAP = { |
|
|
|
APCmini_MAP_NOTES = { |
|
|
|
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 |
|
|
@ -64,8 +64,22 @@ APCmini_MAP = { |
|
|
|
98: "shift", #This is just a button. There is no shift layer. |
|
|
|
} |
|
|
|
#Also put in the reverse: |
|
|
|
APCmini_MAP.update({value:key for key,value in APCmini_MAP.items()}) |
|
|
|
|
|
|
|
APCmini_MAP_NOTES.update({value:key for key,value in APCmini_MAP_NOTES.items()}) |
|
|
|
|
|
|
|
|
|
|
|
#The CCs have no labels on the hardware. We use the function as value directly |
|
|
|
APCmini_MAP_CC = { |
|
|
|
48: "", |
|
|
|
49: "", |
|
|
|
50: "", |
|
|
|
51: "", |
|
|
|
52: "volume", #because this is below the button with "volume". |
|
|
|
53: "", |
|
|
|
54: "", |
|
|
|
55: "", |
|
|
|
56: "swing", |
|
|
|
} |
|
|
|
APCmini_MAP_CC.update({value:key for key,value in APCmini_MAP_CC.items()}) |
|
|
|
|
|
|
|
#APC Color Mappings are velocity: |
|
|
|
#The colors are only for the note buttons |
|
|
@ -101,7 +115,7 @@ class ApcMiniInput(MidiInput): |
|
|
|
8 |
|
|
|
0 |
|
|
|
|
|
|
|
For all other buttons please see APCmini_MAP |
|
|
|
For all other buttons please see APCmini_MAP_NOTES |
|
|
|
|
|
|
|
Because we only have 8*8 buttons, but Patroneo has n*n we have a "ViewArea" for the apcMini |
|
|
|
that can be scrolled with it's arrow keys. |
|
|
@ -125,7 +139,7 @@ class ApcMiniInput(MidiInput): |
|
|
|
self._blockCurrentTrackSignal = False # to prevent recursive calls |
|
|
|
self._cacheSubdivisonValue = None #set in callback |
|
|
|
self.currentTrack = None #track export dict. Set on program start and on every GUI track change and also on our track change. |
|
|
|
self.prevailingVelocity = None # defaults to average track velocity. But can also be set by the hardware faders |
|
|
|
self.prevailingVelocity = 90 # defaults to 90 by convention on startup. After that only the hardware fader counts. |
|
|
|
self.shiftIsActive = None #For shift momentary press |
|
|
|
self.armRecord = None #Toggle |
|
|
|
self.mute = None #Make music when you press buttons |
|
|
@ -140,6 +154,7 @@ class ApcMiniInput(MidiInput): |
|
|
|
#self.midiProcessor.notePrinter(True) |
|
|
|
self.midiProcessor.register_NoteOn(self.receiveNoteOn) #this also includes note off, which is 0x90 velocity 0 |
|
|
|
self.midiProcessor.register_NoteOff(self.receiveNoteOff) |
|
|
|
self.midiProcessor.register_CC(self.receiveCC) |
|
|
|
|
|
|
|
#api.callbacks.setCursor.append(self._setMidiThru) #When the track changes re-route cbox RT midi thru |
|
|
|
#api.callbacks._setCursor(destroySelection = False) #Force once to trigger a cursor export which calls our midi thru setter |
|
|
@ -228,9 +243,9 @@ class ApcMiniInput(MidiInput): |
|
|
|
def sendDefaultConfig(self): |
|
|
|
"""Setup our default setup. This is not saved but restored on every program startup.""" |
|
|
|
pblob = bytes() |
|
|
|
pblob += cbox.Pattern.serialize_event(1, 0x90, APCmini_MAP["mute"], 1) |
|
|
|
pblob += cbox.Pattern.serialize_event(1, 0x90, APCmini_MAP_NOTES["mute"], 1) |
|
|
|
|
|
|
|
pblob += cbox.Pattern.serialize_event(1, 0x90, APCmini_MAP["rec_arm"], 1) |
|
|
|
pblob += cbox.Pattern.serialize_event(1, 0x90, APCmini_MAP_NOTES["rec_arm"], 1) |
|
|
|
self.armRecord = True |
|
|
|
|
|
|
|
pat = cbox.Document.get_song().pattern_from_blob(pblob, 0) #0 ticks. |
|
|
@ -411,66 +426,66 @@ class ApcMiniInput(MidiInput): |
|
|
|
|
|
|
|
if not self.mute: |
|
|
|
api.noteOn(self.currentTrack["id"], patroneoPitch) |
|
|
|
elif pitch == APCmini_MAP["shift"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["shift"]: |
|
|
|
self.shiftIsActive = True #The shift button has no color. We just remember the momentary state. |
|
|
|
elif pitch == APCmini_MAP["rec_arm"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["rec_arm"]: |
|
|
|
self.armRecord = not self.armRecord |
|
|
|
if self.armRecord: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["rec_arm"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["rec_arm"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
else: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["rec_arm"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
elif pitch == APCmini_MAP["mute"]: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["rec_arm"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
elif pitch == APCmini_MAP_NOTES["mute"]: |
|
|
|
self.mute = not self.mute |
|
|
|
if self.mute: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["mute"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
else: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["mute"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
|
|
|
|
elif pitch == APCmini_MAP["arrow_up"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["arrow_up"]: |
|
|
|
if self.shiftIsActive: |
|
|
|
api.currentTrackBy(self.currentTrack["id"], -1) |
|
|
|
else: |
|
|
|
self.viewAreaUpDown(+1) |
|
|
|
|
|
|
|
elif pitch == APCmini_MAP["arrow_down"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["arrow_down"]: |
|
|
|
if self.shiftIsActive: |
|
|
|
api.currentTrackBy(self.currentTrack["id"], 1) |
|
|
|
else: |
|
|
|
self.viewAreaUpDown(-1) |
|
|
|
|
|
|
|
elif pitch == APCmini_MAP["arrow_left"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["arrow_left"]: |
|
|
|
if self.shiftIsActive: |
|
|
|
api.seekMeasureLeft() |
|
|
|
else: |
|
|
|
self.viewAreaLeftRight(+1) #Yes, it is inverted scrolling |
|
|
|
elif pitch == APCmini_MAP["arrow_right"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["arrow_right"]: |
|
|
|
if self.shiftIsActive: |
|
|
|
api.seekMeasureRight() |
|
|
|
else: |
|
|
|
self.viewAreaLeftRight(-1) |
|
|
|
elif pitch == APCmini_MAP["unlabeled_upper"]: #Undo |
|
|
|
elif pitch == APCmini_MAP_NOTES["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 |
|
|
|
elif pitch == APCmini_MAP_NOTES["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) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_up"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_down"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_left"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_right"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
|
|
|
|
elif pitch == APCmini_MAP["clip_stop"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["clip_stop"]: |
|
|
|
api.playPause() |
|
|
|
elif pitch == APCmini_MAP["solo"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["solo"]: |
|
|
|
api.toggleLoop() |
|
|
|
elif pitch == APCmini_MAP["stop_all_clips"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["stop_all_clips"]: |
|
|
|
api.stop() |
|
|
|
api.rewind() |
|
|
|
else: |
|
|
@ -494,18 +509,29 @@ class ApcMiniInput(MidiInput): |
|
|
|
self.activeNoteOns.remove(pitch) |
|
|
|
#else: this is just midi hardware. Button was pressed while connecting and then released, or something like that. |
|
|
|
|
|
|
|
elif pitch == APCmini_MAP["shift"]: |
|
|
|
elif pitch == APCmini_MAP_NOTES["shift"]: |
|
|
|
self.shiftIsActive = False #The shift button has no color. We just remember the momentary state. |
|
|
|
else: |
|
|
|
logging.info(f"Unhandled APCmini noteOff. Pitch {pitch}, Velocity {velocity}") |
|
|
|
|
|
|
|
|
|
|
|
def receiveCC(self, timestamp, channel, ccnumber, value): |
|
|
|
if ccnumber == APCmini_MAP_CC["swing"]: |
|
|
|
if value == 0: |
|
|
|
api.setSwingPercent(0) |
|
|
|
else: |
|
|
|
v = int(value/127 * 100) |
|
|
|
api.setSwingPercent(v) |
|
|
|
elif ccnumber == APCmini_MAP_CC["volume"]: |
|
|
|
self.prevailingVelocity = value |
|
|
|
|
|
|
|
|
|
|
|
def chooseCurrentTrack(self, exportTrack): |
|
|
|
"""This gets called either by the callback or by an APCmini button. |
|
|
|
The callback has a signal blocker that prevents recursive calls.""" |
|
|
|
if self.currentTrack and exportTrack["id"] == self.currentTrack["id"]: return #already current track |
|
|
|
self.currentTrack = exportTrack |
|
|
|
api.changeCurrentTrack(exportTrack["id"]) |
|
|
|
self.prevailingVelocity = self.currentTrack["averageVelocity"] |
|
|
|
self.sendApcNotePattern(exportTrack) |
|
|
|
|
|
|
|
|
|
|
@ -521,11 +547,11 @@ class ApcMiniInput(MidiInput): |
|
|
|
return # there is no need for further scrolling. |
|
|
|
|
|
|
|
if self.viewAreaUpDownOffset: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["arrow_up"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["arrow_down"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_up"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_down"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
else: #Indicate that the origin is not 0,0 |
|
|
|
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_NOTES["arrow_up"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_down"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
|
|
|
|
self.sendApcNotePattern(self.currentTrack) |
|
|
|
|
|
|
@ -542,11 +568,11 @@ class ApcMiniInput(MidiInput): |
|
|
|
return # there is no need for further scrolling. |
|
|
|
|
|
|
|
if self.viewAreaLeftRightOffset: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["arrow_left"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["arrow_right"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_left"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_right"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|
|
|
else: #Indicate that the origin is not 0,0 |
|
|
|
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) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_left"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["arrow_right"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
|
|
|
|
self.sendApcNotePattern(self.currentTrack) |
|
|
|
|
|
|
@ -636,15 +662,15 @@ class ApcMiniInput(MidiInput): |
|
|
|
|
|
|
|
def callback_playbackStatusChanged(self, status:bool): |
|
|
|
if status: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["clip_stop"], APCmini_COLOR["green_blink"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["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) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["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) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["solo"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|
|
|
else: |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP["solo"], APCmini_COLOR["green_blink"], output=self.cboxMidiOutUuid) |
|
|
|
cbox.send_midi_event(0x90, APCmini_MAP_NOTES["solo"], APCmini_COLOR["green_blink"], output=self.cboxMidiOutUuid) |
|
|
|
|
|
|
|
|
|
|
|
apcMiniInput = ApcMiniInput() #global to use in other parts of the program |
|
|
|