Nils
3 years ago
6 changed files with 662 additions and 19 deletions
@ -0,0 +1,519 @@ |
|||
#! /usr/bin/env python3 |
|||
# -*- coding: utf-8 -*- |
|||
""" |
|||
Copyright 2021, Nils Hilbricht, Germany ( https://www.hilbricht.net ) |
|||
|
|||
This file is part of the Laborejo Software Suite ( https://www.laborejo.org ), |
|||
more specifically its template base application. |
|||
|
|||
This application is free software: you can redistribute it and/or modify |
|||
it under the terms of the GNU General Public License as published by |
|||
the Free Software Foundation, either version 3 of the License, or |
|||
(at your option) any later version. |
|||
|
|||
This program is distributed in the hope that it will be useful, |
|||
but WITHOUT ANY WARRANTY; without even the implied warranty of |
|||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|||
GNU General Public License for more details. |
|||
|
|||
You should have received a copy of the GNU General Public License |
|||
along with this program. If not, see <http://www.gnu.org/licenses/>. |
|||
""" |
|||
|
|||
import logging; logging.info("import {}".format(__file__)) |
|||
|
|||
#Python Standard Library |
|||
import atexit |
|||
|
|||
#Third Party Modules |
|||
from calfbox import cbox |
|||
|
|||
#Template Modules |
|||
from template.engine.input_midi import MidiInput |
|||
|
|||
#Our Modules |
|||
import engine.api as api |
|||
|
|||
""" |
|||
This file is used directly. It is a global part of the engine (see bottom of the file) |
|||
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 |
|||
|
|||
#Fader Controls |
|||
68: "volume", |
|||
69: "pan", |
|||
70: "send", |
|||
71: "device", |
|||
|
|||
#Scene Launch |
|||
82: "clip_stop", |
|||
83: "solo", |
|||
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", |
|||
|
|||
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()}) |
|||
|
|||
|
|||
#APC Color Mappings are velocity: |
|||
#The colors are only for the note buttons |
|||
#The horizontal buttons are only red and controlled by 0,1,2 |
|||
#The vertical buttons are only green and controlled by 0,1,2 |
|||
#The shift button has no color |
|||
APCmini_COLOR = { |
|||
0:"off", |
|||
1:"green", |
|||
2:"green_blink", |
|||
3:"red", |
|||
4:"red_blink", |
|||
5:"yellow", |
|||
6:"yellow_blink", |
|||
#All higher are green |
|||
} |
|||
#Also put in the reverse: |
|||
APCmini_COLOR.update({value:key for key,value in APCmini_COLOR.items()}) |
|||
|
|||
class ApcMiniInput(MidiInput): |
|||
"""We initialize with everything switched off. This makes it easier for a complex system |
|||
of engine, GUI and midiinput to start up and get the order of operations right |
|||
|
|||
The apcmini is controlled by note on and note off. |
|||
|
|||
Musical Notes are an 8x8 grid: |
|||
56 57 58 59 60 61 62 63 |
|||
48 49 50 51 52 53 54 55 |
|||
40 ... |
|||
32 |
|||
24 |
|||
16 |
|||
8 |
|||
0 |
|||
|
|||
For all other buttons please see APCmini_MAP |
|||
|
|||
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. |
|||
""" |
|||
|
|||
def __init__(self): |
|||
#No super init in here! This is delayed until self.start |
|||
pass |
|||
|
|||
def start(self): |
|||
"""Call this manually after the engine and and event loop have started. |
|||
For example from the GUI. It is currently started by api.startEngine() |
|||
But it could be started from a simple command line interface as well.""" |
|||
portname = "APCmini" |
|||
logging.info(f"Creating APC midi input port") |
|||
super().__init__(session=api.session, portName=portname) |
|||
self.connectToHardware("APC MINI MIDI*") |
|||
self.midiProcessor.active = False #This is just internal. We need it, but not used in this file |
|||
|
|||
self.mainStepMap_subdivisions = None #List of bools |
|||
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.shiftIsActive = None #For shift momentary press |
|||
self.armRecord = None #Toggle |
|||
self.mute = None #Make music when you press buttons |
|||
|
|||
self.viewAreaLeftRightOffset = 0 |
|||
self.viewAreaUpDownOffset = 0 |
|||
|
|||
#Connect the template midi input with api calls. |
|||
#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) |
|||
|
|||
#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 |
|||
|
|||
api.callbacks.patternChanged.append(self.callback_patternChanged) |
|||
api.callbacks.stepChanged.append(self.callback_stepChanged) |
|||
api.callbacks.removeStep.append(self.callback_removeStep) |
|||
api.callbacks.currentTrackChanged.append(self.callback_currentTrackChanged) |
|||
api.callbacks.subdivisionsChanged.append(self.callback_subdivisionsChanged) |
|||
api.callbacks.exportCacheChanged.append(self.callback_cacheExportDict) |
|||
|
|||
#Prepare various patterns. |
|||
#"clear music button leds" pattern: |
|||
# Create a binary blob that contains the MIDI events |
|||
pblob = bytes() |
|||
for pitch in range(0,64): |
|||
# note on |
|||
pblob += cbox.Pattern.serialize_event(1, 0x90, pitch, 0) #tick in pattern, midi, pitch, velocity |
|||
# Create a new pattern object using events from the blob |
|||
self.resetMusicLEDPattern = cbox.Document.get_song().pattern_from_blob(pblob, 2) #2 ticks. |
|||
|
|||
# Clear all non-music buttons - pattern: |
|||
# Create a binary blob that contains the MIDI events |
|||
pblob = bytes() |
|||
for pitch in range(64,72): |
|||
# note on |
|||
pblob += cbox.Pattern.serialize_event(1, 0x90, pitch, 0) #tick in pattern, midi, pitch, velocity |
|||
# Create a new pattern object using events from the blob |
|||
self.resetNonMusicLEDPattern_Horizontal = cbox.Document.get_song().pattern_from_blob(pblob, 2) #2 ticks. |
|||
|
|||
pblob = bytes() |
|||
for pitch in range(82,90): |
|||
# note on |
|||
pblob += cbox.Pattern.serialize_event(1, 0x90, pitch, 0) #tick in pattern, midi, pitch, velocity |
|||
# Create a new pattern object using events from the blob |
|||
self.resetNonMusicLEDPattern_Vertical = cbox.Document.get_song().pattern_from_blob(pblob, 2) #2 ticks. |
|||
|
|||
#Create Midi Out and Connect |
|||
logging.info(f"Creating APC midi output port") |
|||
outportname = "APCmini Feedback" |
|||
self.cboxMidiOutUuid = cbox.JackIO.create_midi_output(outportname) |
|||
cbox.JackIO.rename_midi_output(self.cboxMidiOutUuid, outportname) |
|||
self.outputScene = cbox.Document.get_engine().new_scene() |
|||
self.outputScene.clear() |
|||
self.outputScene.add_new_midi_layer(self.cboxMidiOutUuid) |
|||
|
|||
potentialHardwareOuts = set(cbox.JackIO.get_ports("APC MINI MIDI*", cbox.JackIO.MIDI_TYPE, cbox.JackIO.PORT_IS_SINK | cbox.JackIO.PORT_IS_PHYSICAL)) |
|||
for hp in potentialHardwareOuts: |
|||
cbox.JackIO.port_connect(cbox.JackIO.status().client_name + ":" + outportname, hp) |
|||
|
|||
self.sendDefaultConfig() |
|||
|
|||
atexit.register(self.sendPatternClear) |
|||
atexit.register(self.sendOtherButtonsClear) |
|||
|
|||
|
|||
#We need the following three files for the interface, but they are not used in this file except switching on once. |
|||
@property |
|||
def midiInIsActive(self): |
|||
try: |
|||
return self.midiProcessor.active |
|||
except AttributeError: #during startup |
|||
return False |
|||
def setMidiInputActive(self, state:bool): |
|||
self.midiProcessor.active = state |
|||
def toggleMidiIn(self): |
|||
self.setMidiInputActive(not self.midiInIsActive) |
|||
|
|||
|
|||
def sendPatternClear(self): |
|||
"""The APCmini has no "clear all" function. We need to loop over all buttons and send |
|||
velocity 0. |
|||
Here we only clear the pattern, not other status buttons.""" |
|||
self.outputScene.play_pattern(self.resetMusicLEDPattern, 150.0) #150 tempo |
|||
|
|||
def sendOtherButtonsClear(self): |
|||
"""There is a bug in the device. You cannot send the vertical control buttons at the same |
|||
time as the other buttons. The device will lock up or ignore your buttons. |
|||
We send in multiple passes.""" |
|||
self.outputScene.play_pattern(self.resetNonMusicLEDPattern_Horizontal, 150.0) #150 tempo |
|||
self.outputScene.play_pattern(self.resetNonMusicLEDPattern_Vertical, 150.0) #150 tempo |
|||
|
|||
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["rec_arm"], 1) |
|||
self.armRecord = True |
|||
|
|||
pat = cbox.Document.get_song().pattern_from_blob(pblob, 0) #0 ticks. |
|||
self.outputScene.play_pattern(pat, 150.0) #150 tempo |
|||
|
|||
def sendCompleteState(self, velocities:list): |
|||
"""Reset the device entirely. Actively clear unused buttons, activate others. |
|||
This is not only a last-resort function but useful for various scenarios. |
|||
|
|||
#BUG BUG BUG! We cannot send the buttons higher than 82 at the same time as the lower values. |
|||
#Either only the lower part works or it shuts down completely and nothing happens. |
|||
#This is a timing problem in the APCmini. We need to send it in multiple passes. |
|||
""" |
|||
|
|||
#Here comes the C loop: |
|||
pblobNotes = bytes() |
|||
for i in range(64): |
|||
pblobNotes += cbox.Pattern.serialize_event(1, 0x90, i, velocities[i] ) #tick in pattern, midi, pitch, velocity |
|||
|
|||
# Create a new pattern object using events from the blob |
|||
patNotes = cbox.Document.get_song().pattern_from_blob(pblobNotes, 0) #0 ticks. |
|||
self.outputScene.play_pattern(patNotes, 150.0) #150 tempo |
|||
|
|||
pblobHorizontalControls = bytes() |
|||
for j in range(64, 72): |
|||
pblobHorizontalControls += cbox.Pattern.serialize_event(1, 0x90, j, velocities[j] ) #tick in pattern, midi, pitch, velocity |
|||
patHorizontalControls = cbox.Document.get_song().pattern_from_blob(pblobHorizontalControls, 0) #0 ticks. |
|||
self.outputScene.play_pattern(patHorizontalControls, 150.0) #150 tempo |
|||
|
|||
pblobVerticalControls = bytes() |
|||
for j in range(82, 90): |
|||
pblobHorizontalControls += cbox.Pattern.serialize_event(1, 0x90, j, velocities[j] ) #tic-k in pattern, midi, pitch, velocity |
|||
patVerticalControls = cbox.Document.get_song().pattern_from_blob(pblobVerticalControls, 0) #0 ticks. |
|||
self.outputScene.play_pattern(patVerticalControls, 150.0) #150 tempo |
|||
|
|||
def sendApcNotePattern(self, trackExport:dict): |
|||
"""Convert the notes from a trackExport to an APC midi pattern and send it. |
|||
This is the important "send the current state" function. |
|||
|
|||
It takes into account our local offsets of the pattern grid because patroneo is n*n but |
|||
APCmini is only 8x8 and must be shifted with it's button keys. |
|||
|
|||
trackExport["numberOfSteps"] is NOT the index but how many pitches we have in a pattern. |
|||
""" |
|||
if not self.currentTrack: return |
|||
if not trackExport["id"] == self.currentTrack["id"]: return |
|||
if not self.mainStepMap_subdivisions: return |
|||
|
|||
self.sendPatternClear() |
|||
|
|||
pblobNotes = bytes() |
|||
for stepDict in trackExport["pattern"]: |
|||
apcPitch = self.patroneoCoord2APCPitch(stepDict) |
|||
if not apcPitch is None: #outside the current viewArea? |
|||
pblobNotes += cbox.Pattern.serialize_event(1, 0x90, apcPitch, self.getColor(stepDict) ) #tick in pattern, midi, pitch, velocity |
|||
# Create a new pattern object using events from the blob |
|||
patNotes = cbox.Document.get_song().pattern_from_blob(pblobNotes, 0) #0 ticks. |
|||
self.outputScene.play_pattern(patNotes, 150.0) #150 tempo |
|||
|
|||
|
|||
def getColor(self, stepDict:dict): |
|||
try: |
|||
if self.mainStepMap_subdivisions[stepDict["index"]]: #list of bools. True if on a main beat. |
|||
return APCmini_COLOR["red"] |
|||
else: |
|||
return APCmini_COLOR["green"] |
|||
except IndexError: |
|||
logging.error(f"mainStepMap is too short for index {stepDict['index']} to get color. Is: {self.mainStepMap_subdivisions}. Pattern length is {self.currentTrack['patternBaseLength']} with multiplier {self.currentTrack['patternLengthMultiplicator']} ") |
|||
raise IndexError |
|||
|
|||
def patroneoCoord2APCPitch(self, stepDict:dict)->int: |
|||
"""Patroneo 'Index' is the column, the time axis: X |
|||
Patroneo 'Pitch' is the row, the tonal axis: Y |
|||
|
|||
The APC Mini has an 8x8 button matrix. |
|||
It starts with pitch 56 in the top left, incrementing 57, 58, 59... to 63. |
|||
Second row is lower! Matrix: |
|||
|
|||
56 57 58 59 60 61 62 63 |
|||
48 49 50 51 52 53 54 55 |
|||
40 ... |
|||
32 |
|||
24 |
|||
16 |
|||
8 |
|||
0 |
|||
|
|||
So it is a simple 2d->1d transformation except we substract from 56. |
|||
""" |
|||
|
|||
x = stepDict["index"] + self.viewAreaLeftRightOffset |
|||
if x < 0 or x > 7: return None |
|||
|
|||
y = 56 - (stepDict["pitch"] + self.viewAreaUpDownOffset) * 8 |
|||
|
|||
#assert x+y < 64, (x, y, x+y, stepDict) |
|||
#If the index is > 7 the pattern has more than 8 columns. That is perfectly fine in Patroneo. |
|||
#However we cannot display y+x > 63, after taking the arrow-shifting into consideration. |
|||
#The user just clicked in a field that is outside of our current APC-Viewarea. |
|||
if x+y > 63: |
|||
return None |
|||
else: |
|||
return y + x |
|||
|
|||
def APCPitch2patroneoCoord(self, apcPitch:int): |
|||
"""apc pitch looks like a midi note from 0 (bottom left) to 63 (top right). |
|||
See also docstring of patroneoCoord2APCPitch. |
|||
|
|||
We do a 1d->2d transformation for patroneo row/pitch and column/index |
|||
|
|||
Patroneo 'Index' is the column, the time axis: X |
|||
Patroneo 'Pitch' is the row, the tonal axis: Y |
|||
The rows begin at 0 at the top and increment down. The highest possible note in a pattern |
|||
is pitch 0. The earlist one index 0. |
|||
""" |
|||
|
|||
index = apcPitch % 8 - self.viewAreaLeftRightOffset #because the apcmini has a fixed width of 8 buttons |
|||
pitch = 7 - int(apcPitch / 8) - self.viewAreaUpDownOffset |
|||
|
|||
return index, pitch |
|||
|
|||
|
|||
#Receive midi from the device |
|||
def receiveNoteOn(self, timestamp, channel, pitch, velocity): |
|||
if pitch <= 63 and self.midiInIsActive: #actual musical notes |
|||
factor = 1 |
|||
patroneoIndex, patroneoPitch = self.APCPitch2patroneoCoord(pitch) |
|||
#Is the scale 7 notes or less? Did we received the 8th pitch from the APC? |
|||
inRange = patroneoPitch < self.currentTrack["numberOfSteps"]# and patroneoIndex < self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"] |
|||
if self.armRecord and inRange: |
|||
api.toggleStep(self.currentTrack["id"], patroneoIndex, patroneoPitch, factor, self.prevailingVelocity) |
|||
if not self.mute: |
|||
api.noteOn(self.currentTrack["id"], patroneoPitch) |
|||
elif pitch == APCmini_MAP["shift"]: |
|||
self.shiftIsActive = True #The shift button has no color. We just remember the momentary state. |
|||
elif pitch == APCmini_MAP["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) |
|||
else: |
|||
cbox.send_midi_event(0x90, APCmini_MAP["rec_arm"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|||
elif pitch == APCmini_MAP["mute"]: |
|||
self.mute = not self.mute |
|||
if self.mute: |
|||
cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["green"], output=self.cboxMidiOutUuid) |
|||
else: |
|||
cbox.send_midi_event(0x90, APCmini_MAP["mute"], APCmini_COLOR["off"], output=self.cboxMidiOutUuid) |
|||
|
|||
elif pitch == APCmini_MAP["arrow_up"]: |
|||
self.viewAreaUpDown(+1) |
|||
elif pitch == APCmini_MAP["arrow_down"]: |
|||
self.viewAreaUpDown(-1) |
|||
elif pitch == APCmini_MAP["arrow_left"]: |
|||
self.viewAreaLeftRight(+1) #Yes, it is inverted scrolling |
|||
elif pitch == APCmini_MAP["arrow_right"]: |
|||
self.viewAreaLeftRight(-1) |
|||
else: |
|||
logging.info(f"Unhandled APCmini noteOn. Pitch {pitch}, Velocity {velocity}") |
|||
|
|||
def viewAreaUpDown(self, value:int): |
|||
if value == 0: return |
|||
wasZero = self.viewAreaUpDownOffset == 0 |
|||
self.viewAreaUpDownOffset += value |
|||
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"]: |
|||
self.viewAreaUpDownOffset -= value #take it back |
|||
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) |
|||
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) |
|||
|
|||
self.sendApcNotePattern(self.currentTrack) |
|||
|
|||
def viewAreaLeftRight(self, value:int): |
|||
if value == 0: return |
|||
wasZero = self.viewAreaLeftRightOffset == 0 |
|||
self.viewAreaLeftRightOffset += value |
|||
|
|||
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"]: |
|||
self.viewAreaLeftRightOffset -= value #take it back |
|||
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) |
|||
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) |
|||
|
|||
self.sendApcNotePattern(self.currentTrack) |
|||
|
|||
|
|||
|
|||
def receiveNoteOff(self, timestamp, channel, pitch, velocity): |
|||
if pitch <= 63 and self.midiInIsActive: #actual musical notes |
|||
#if not self.mute: Potential of hanging notes. |
|||
patroneoIndex, patroneoPitch = self.APCPitch2patroneoCoord(pitch) |
|||
api.noteOff(self.currentTrack["id"], patroneoPitch) |
|||
elif pitch == APCmini_MAP["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 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) |
|||
|
|||
#Receive from callbacks and send to the device to make its LED light up |
|||
|
|||
def callback_cacheExportDict(self, exportTrack:dict): |
|||
if self.currentTrack and exportTrack["id"] == self.currentTrack["id"]: |
|||
self.currentTrack = exportTrack |
|||
|
|||
def callback_patternChanged(self, exportTrack:dict): |
|||
"""We receive the whole track as exportDict. |
|||
exportDict["pattern"] is the data structure example in the class docstring. |
|||
|
|||
This happens on track changes that are not pitch active/deactive. |
|||
|
|||
For example invert row, copy pattern etc. So: actual musical changes. |
|||
|
|||
We also receive this for every track, no matter if this our current working track. |
|||
So we check if we are the current track. However, that prevents setting up or steps |
|||
on a track change because during the track change the currenTrackId value is still the old |
|||
track. There we introduce a force switch that enables self.guicallback_chooseCurrentTrack |
|||
to trigger a redraw even during the track change. |
|||
""" |
|||
if self.mainStepMap_subdivisions: |
|||
self.mainStepMap_subdivisions = [not stepNo % self._cacheSubdivisonValue for stepNo in range(self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]) ] |
|||
if self.currentTrack and exportTrack["id"] == self.currentTrack["id"]: |
|||
self.currentTrack = exportTrack |
|||
if self.mainStepMap_subdivisions and self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"] == len(self.mainStepMap_subdivisions): |
|||
#If not the redraw command will arrive shortly through subdivisions callback |
|||
self.sendApcNotePattern(exportTrack) |
|||
|
|||
def callback_stepChanged(self, stepDict): |
|||
"""Patroneo step activated or velocity changed. |
|||
|
|||
APC Color Mappings are velocity: |
|||
0 - off |
|||
01 - green |
|||
02 - green blink |
|||
03 - red |
|||
04 - red blink |
|||
05 - yellow |
|||
06 - yellow blink |
|||
""" |
|||
if self.currentTrack: |
|||
apcPitch = self.patroneoCoord2APCPitch(stepDict) |
|||
if not apcPitch is None: #could be None, which is outside the current viewArea |
|||
cbox.send_midi_event(0x90, apcPitch, self.getColor(stepDict), output=self.cboxMidiOutUuid) |
|||
|
|||
def callback_removeStep(self, stepDict): |
|||
"""LED off is note on 0x90 with velocity 0. |
|||
See docstring for callbac_stepChanged""" |
|||
apcPitch = self.patroneoCoord2APCPitch(stepDict) |
|||
if not apcPitch is None: #could be None, which is outside the current viewArea |
|||
cbox.send_midi_event(0x90, apcPitch, 0, output=self.cboxMidiOutUuid) |
|||
|
|||
def callback_currentTrackChanged(self, exportTrack:dict): |
|||
if self._blockCurrentTrackSignal: |
|||
return |
|||
self._blockCurrentTrackSignal = True |
|||
|
|||
self.chooseCurrentTrack(exportTrack) |
|||
|
|||
self._blockCurrentTrackSignal = False |
|||
|
|||
def callback_subdivisionsChanged(self, subdivisions): |
|||
"""On program start and load and on every change.""" |
|||
if not self.currentTrack: return #program start |
|||
if self._cacheSubdivisonValue == subdivisions: return |
|||
|
|||
self._cacheSubdivisonValue = subdivisions |
|||
self.mainStepMap_subdivisions = [not stepNo % subdivisions for stepNo in range(self.currentTrack["patternBaseLength"] * self.currentTrack["patternLengthMultiplicator"]) ] |
|||
self.sendApcNotePattern(self.currentTrack) |
|||
|
|||
apcMiniInput = ApcMiniInput() #global to use in other parts of the program |
Loading…
Reference in new issue