@ -130,6 +130,9 @@ class ApcMiniInput(MidiInput):
self . armRecord = None #Toggle
self . mute = None #Make music when you press buttons
self . activeNoteOns = set ( ) # midiPitchInt
self . stepsNotAvailableBecauseOverlayedByFactor = set ( )
self . viewAreaLeftRightOffset = 0
self . viewAreaUpDownOffset = 0
@ -213,6 +216,7 @@ class ApcMiniInput(MidiInput):
velocity 0.
Here we only clear the pattern , not other status buttons . """
self . outputScene . play_pattern ( self . resetMusicLEDPattern , 150.0 ) #150 tempo
self . stepsNotAvailableBecauseOverlayedByFactor = set ( )
def sendOtherButtonsClear ( self ) :
""" There is a bug in the device. You cannot send the vertical control buttons at the same
@ -264,12 +268,12 @@ class ApcMiniInput(MidiInput):
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 .
This is the important " send the current state " function that makes the LEDs go bling !
It takes into account our local offsets of the pattern grid because patroneo is n * n but
APCmini is only 8 x8 and must be shifted with it ' s button keys.
trackExport [ " numberOfSteps " ] is NOT the index but how many pitches we have in a pattern .
trackExport [ " numberOfSteps " ] is NOT the max index but how many pitches we have in a pattern .
"""
if not self . currentTrack : return
if not trackExport [ " id " ] == self . currentTrack [ " id " ] : return
@ -279,9 +283,18 @@ class ApcMiniInput(MidiInput):
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
startsAtBeginning , apcPitches = self . patroneoCoord2APCPitch ( stepDict )
if not apcPitches is None : #could be None, which is outside the current viewArea
first = True and startsAtBeginning
for apcPitch in apcPitches : #most of the time this is just len one. We draw factor > 1 as special yellow notes.
if first :
color = self . getColor ( stepDict ) #red or green for main beat or other beat.
first = False
else :
color = APCmini_COLOR [ " yellow " ] #for notes longer than one step / factor > 1
self . stepsNotAvailableBecauseOverlayedByFactor . add ( apcPitch )
pblobNotes + = cbox . Pattern . serialize_event ( 1 , 0x90 , apcPitch , color ) #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
@ -297,8 +310,15 @@ class ApcMiniInput(MidiInput):
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
def patroneoCoord2APCPitch ( self , stepDict : dict ) :
"""
Returns a list of midi pitches for the apcMini .
Most of the time it is of length 1 , just one note .
If there is a note factor > 1 it will return more notes which will be rendered yellow LED .
Or returns None if outside of the current view area .
Patroneo ' Index ' is the column , the time axis : X
Patroneo ' Pitch ' is the row , the tonal axis : Y
The APC Mini has an 8 x8 button matrix .
@ -313,23 +333,42 @@ class ApcMiniInput(MidiInput):
16
8
0
So it is a simple 2 d - > 1 d 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
result = [ ]
startsAtBeginning = True
ra = int ( stepDict [ " factor " ] )
if ra < stepDict [ " factor " ] :
ra + = 1 #round up, but only for non-integer numbers.
first = True
for r in range ( ra ) : #round up. Don't do half steps
x = stepDict [ " index " ] + r + self . viewAreaLeftRightOffset
if x < 0 or x > 7 :
if first :
startsAtBeginning = False
first = False
continue
y = 56 - ( stepDict [ " pitch " ] + self . viewAreaUpDownOffset ) * 8
if x + y > 63 :
if first :
startsAtBeginning = False
first = False
continue
first = False
result . append ( x + y )
#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
if not result :
return None , None
else :
return y + x
#return y + x
return startsAtBeginning , result
def APCPitch2patroneoCoord ( self , apcPitch : int ) :
""" apc pitch looks like a midi note from 0 (bottom left) to 63 (top right).
@ -352,12 +391,24 @@ class ApcMiniInput(MidiInput):
#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 )
d = self . _pitchesToPatroneoCoordDict ( self . activeNoteOns )
if patroneoPitch in d :
patroneoIndexOther , originalApcPitch = d [ patroneoPitch ]
self . activeNoteOns . remove ( originalApcPitch )
difference = patroneoIndex - patroneoIndexOther
if difference > 0 :
api . toggleStep ( self . currentTrack [ " id " ] , patroneoIndexOther , patroneoPitch , difference + 1 , self . prevailingVelocity )
else :
api . toggleStep ( self . currentTrack [ " id " ] , patroneoIndex , patroneoPitch , - 1 * ( difference - 1 ) , self . prevailingVelocity )
else : #Note will be activated on note off
if not pitch in self . stepsNotAvailableBecauseOverlayedByFactor :
self . activeNoteOns . add ( pitch ) #2d. contains index.
if not self . mute :
api . noteOn ( self . currentTrack [ " id " ] , patroneoPitch )
elif pitch == APCmini_MAP [ " shift " ] :
@ -403,7 +454,6 @@ class ApcMiniInput(MidiInput):
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 " ] )
@ -426,6 +476,39 @@ class ApcMiniInput(MidiInput):
else :
logging . info ( f " Unhandled APCmini noteOn. Pitch { pitch } , Velocity { velocity } " )
def _pitchesToPatroneoCoordDict ( self , pitches : set ) :
result = { }
for pitch in pitches :
patroneoIndex , patroneoPitch = self . APCPitch2patroneoCoord ( pitch )
result [ patroneoPitch ] = ( patroneoIndex , pitch )
return result
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 )
if pitch in self . activeNoteOns : #Just a single, standard step on/off
factor = 1
api . toggleStep ( self . currentTrack [ " id " ] , patroneoIndex , patroneoPitch , factor , self . prevailingVelocity )
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 " ] :
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 )
def viewAreaUpDown ( self , value : int ) :
if value == 0 : return
wasZero = self . viewAreaUpDownOffset == 0
@ -468,26 +551,6 @@ class ApcMiniInput(MidiInput):
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 ) :
@ -529,16 +592,29 @@ class ApcMiniInput(MidiInput):
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 )
startsAtBeginning , apcPitches = self . patroneoCoord2APCPitch ( stepDict )
if not apcPitches is None : #could be None, which is outside the current viewArea
first = True and startsAtBeginning
for apcPitch in apcPitches : #most of the time this is just len one. We draw factor > 1 as special yellow notes.
if first :
color = self . getColor ( stepDict ) #red or green for main beat or other beat.
first = False
else :
color = APCmini_COLOR [ " yellow " ] #for notes longer than one step / factor > 1
self . stepsNotAvailableBecauseOverlayedByFactor . add ( apcPitch )
cbox . send_midi_event ( 0x90 , apcPitch , color , 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 )
startsAtBeginning , apcPitches = self . patroneoCoord2APCPitch ( stepDict )
if not apcPitches is None : #could be None, which is outside the current viewArea
first = True and startsAtBeginning
for apcPitch in apcPitches : #most of the time this is just len one. We draw factor > 1 as special yellow notes.
cbox . send_midi_event ( 0x90 , apcPitch , 0 , output = self . cboxMidiOutUuid )
if apcPitch in self . stepsNotAvailableBecauseOverlayedByFactor :
self . stepsNotAvailableBecauseOverlayedByFactor . remove ( apcPitch )
def callback_currentTrackChanged ( self , exportTrack : dict ) :
if self . _blockCurrentTrackSignal :