#! /usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Copyright 2022 , 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
#Template Modules
from template . calfbox import cbox
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_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
67 : " arrow_right " , #Move the pattern-viewing area. +Shift: Measure Right
#Fader Controls
68 : " volume " ,
69 : " pan " ,
70 : " send " ,
71 : " device " ,
#Scene Launch
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_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.
}
#Also put in the reverse:
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
#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 8 x8 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_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.
"""
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 = 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
self . activeNoteOns = set ( ) # midiPitchInt
self . stepsNotAvailableBecauseOverlayedByFactor = set ( )
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 )
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
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 )
api . callbacks . playbackStatusChanged . append ( self . callback_playbackStatusChanged )
api . callbacks . loopChanged . append ( self . callback_loopChanged )
#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
self . stepsNotAvailableBecauseOverlayedByFactor = set ( )
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_NOTES [ " mute " ] , 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.
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 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 max 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 " ] :
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
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 ) :
"""
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 .
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 2 d - > 1 d transformation except we substract from 56.
"""
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 not result :
return None , None
else :
#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).
See also docstring of patroneoCoord2APCPitch .
We do a 1 d - > 2 d 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
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 :
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_NOTES [ " shift " ] :
self . shiftIsActive = True #The shift button has no color. We just remember the momentary state.
elif pitch == APCmini_MAP_NOTES [ " rec_arm " ] :
self . armRecord = not self . armRecord
if self . armRecord :
cbox . send_midi_event ( 0x90 , APCmini_MAP_NOTES [ " rec_arm " ] , APCmini_COLOR [ " green " ] , output = self . cboxMidiOutUuid )
else :
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_NOTES [ " mute " ] , APCmini_COLOR [ " green " ] , output = self . cboxMidiOutUuid )
else :
cbox . send_midi_event ( 0x90 , APCmini_MAP_NOTES [ " mute " ] , APCmini_COLOR [ " off " ] , output = self . cboxMidiOutUuid )
elif pitch == APCmini_MAP_NOTES [ " arrow_up " ] :
if self . shiftIsActive :
api . currentTrackBy ( self . currentTrack [ " id " ] , - 1 )
else :
self . viewAreaUpDown ( + 1 )
elif pitch == APCmini_MAP_NOTES [ " arrow_down " ] :
if self . shiftIsActive :
api . currentTrackBy ( self . currentTrack [ " id " ] , 1 )
else :
self . viewAreaUpDown ( - 1 )
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_NOTES [ " arrow_right " ] :
if self . shiftIsActive :
api . seekMeasureRight ( )
else :
self . viewAreaLeftRight ( - 1 )
elif pitch == APCmini_MAP_NOTES [ " unlabeled_upper " ] : #Undo
if self . shiftIsActive : #redo
api . redo ( )
else :
api . undo ( )
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_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_NOTES [ " clip_stop " ] :
api . playPause ( )
elif pitch == APCmini_MAP_NOTES [ " solo " ] :
api . toggleLoop ( )
elif pitch == APCmini_MAP_NOTES [ " stop_all_clips " ] :
api . stop ( )
api . rewind ( )
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_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 . sendApcNotePattern ( exportTrack )
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 value < 0 and 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_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_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 )
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 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.
if self . viewAreaLeftRightOffset :
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_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 )
#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 :
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 """
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 :
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 )
def callback_playbackStatusChanged ( self , status : bool ) :
if status :
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_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_NOTES [ " solo " ] , APCmini_COLOR [ " off " ] , output = self . cboxMidiOutUuid )
else :
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